From 5b35b296311398441b4ca2683edf072a0b32c144 Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Tue, 29 Sep 2026 00:39:52 +0300 Subject: [PATCH 1/2] Xlsx Reader and Writer : Header/footer image description A header/footer image had no alternative text in Xlsx: the VML writer put only the name into o:title of v:imagedata. Excel keeps the alternative text in the alt attribute of v:shape (ECMA-376 Part 4), so the description is now written there when set, and read back. --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xlsx.php | 1 + src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 4 ++ .../Xlsx/HeaderFooterImageDescriptionTest.php | 52 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/HeaderFooterImageDescriptionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b73bc918f..af4b29c67f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Some earlier branches remain supported and security fixes are applied to them; i - `Document\Properties::setLanguage()`: the language of a spreadsheet as a BCP 47 tag, written and read as `dc:language` in Xlsx and Ods; the Ods writer also gives it to the default cell style, where LibreOffice keeps a spreadsheet's language, as LibreOffice writes it (the Western, Asian or complex slot by script, and `style:rfc-language-tag` for a tag with more than a language and a country), and the Ods reader reads it from there when there is no `dc:language`. Html writes and reads it as the `lang` of the `html` element. [Issue #5040](https://github.com/PHPOffice/PhpSpreadsheet/issues/5040) [PR #5041](https://github.com/PHPOffice/PhpSpreadsheet/pull/5041) - Xlsx Writer and Reader: `Chart::setDescription()` is the alternative text of a chart, written as `descr` of the chart's frame and read back, and the frame takes the chart's name; LibreOffice exports it as `/Alt` of the chart in a tagged PDF. [Issue #5038](https://github.com/PHPOffice/PhpSpreadsheet/issues/5038) [PR #5039](https://github.com/PHPOffice/PhpSpreadsheet/pull/5039) - `BaseDrawing::setDecorative()` marks an image as decorative. Xlsx writes and reads it as Excel and LibreOffice do (`adec:decorative` in the image's `cNvPr`), Ods as LibreOffice does (`loext:decorative` in the frame's graphic style, which the writer now defines; the reader also takes ODF 1.4's `draw:decorative`); LibreOffice then leaves the image out of a tagged PDF as an artifact. Html writes and reads it as an empty `alt`. [Issue #5036](https://github.com/PHPOffice/PhpSpreadsheet/issues/5036) [PR #5037](https://github.com/PHPOffice/PhpSpreadsheet/pull/5037) +- Xlsx Writer and Reader: the description of a header/footer image is its alternative text, `alt` on the VML shape as Excel writes it. ### Removed diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index b551dd14a3..1f6ab3c4e5 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1429,6 +1429,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet if (isset($imageData['title'])) { $hfImages[$shapeId]->setName((string) $imageData['title']); } + $hfImages[$shapeId]->setDescription((string) $shape['alt']); $hfImages[$shapeId]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false, $zip); $hfImages[$shapeId]->setResizeProportional(false); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 9c732dd6a3..bebfb97090 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -550,6 +550,10 @@ private function writeVMLHeaderFooterImage(XMLWriter $objWriter, string $referen $objWriter->writeAttribute('o:spid', '_x0000_s' . $id); $objWriter->writeAttribute('type', '#_x0000_t75'); $objWriter->writeAttribute('style', "position:absolute;margin-left:{$marginLeft}px;margin-top:{$marginTop}px;width:{$width}px;height:{$height}px;z-index:1"); + // The alternative text, as Excel writes it; o:title below is the name + if ($image->getDescription() !== '') { + $objWriter->writeAttribute('alt', $image->getDescription()); + } // v:imagedata $objWriter->startElement('v:imagedata'); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/HeaderFooterImageDescriptionTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/HeaderFooterImageDescriptionTest.php new file mode 100644 index 0000000000..39e26055de --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/HeaderFooterImageDescriptionTest.php @@ -0,0 +1,52 @@ +getActiveSheet()->getHeaderFooter(); + $headerFooter->setOddHeader('&L&G&R&G'); + foreach ([HeaderFooter::IMAGE_HEADER_LEFT => 'Company logo', HeaderFooter::IMAGE_HEADER_RIGHT => ''] as $location => $description) { + $image = new HeaderFooterDrawing(); + $image->setName("Image $location"); + $image->setDescription($description); + $image->setPath('samples/images/blue_square.png'); + $image->setHeight(12); + $headerFooter->addImage($image, $location); + } + + return $spreadsheet; + } + + public function testWrittenAsAlt(): void + { + $spreadsheet = self::spreadsheet(); + $vml = (new XlsxWriter($spreadsheet))->getWriterPartDrawing()->writeVMLHeaderFooterImages($spreadsheet->getActiveSheet()); + self::assertMatchesRegularExpression('/]* alt="Company logo">]*o:title="Image LH"/', $vml); + self::assertSame(1, substr_count($vml, ' alt=')); + $spreadsheet->disconnectWorksheets(); + } + + public function testDescriptionSurvivesTheRoundTrip(): void + { + $spreadsheetOld = self::spreadsheet(); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Xlsx'); + $spreadsheetOld->disconnectWorksheets(); + $images = $spreadsheet->getActiveSheet()->getHeaderFooter()->getImages(); + self::assertSame('Company logo', $images['LH']->getDescription()); + self::assertSame('Image LH', $images['LH']->getName()); + self::assertSame('', $images['RH']->getDescription()); + $spreadsheet->disconnectWorksheets(); + } +} From bfaf69826561edd5730a9a723288fb49ebf9a1c7 Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Sat, 17 Oct 2026 00:04:29 +0300 Subject: [PATCH 2/2] Changelog: link #5057 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af4b29c67f..e00204b186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Some earlier branches remain supported and security fixes are applied to them; i - `Document\Properties::setLanguage()`: the language of a spreadsheet as a BCP 47 tag, written and read as `dc:language` in Xlsx and Ods; the Ods writer also gives it to the default cell style, where LibreOffice keeps a spreadsheet's language, as LibreOffice writes it (the Western, Asian or complex slot by script, and `style:rfc-language-tag` for a tag with more than a language and a country), and the Ods reader reads it from there when there is no `dc:language`. Html writes and reads it as the `lang` of the `html` element. [Issue #5040](https://github.com/PHPOffice/PhpSpreadsheet/issues/5040) [PR #5041](https://github.com/PHPOffice/PhpSpreadsheet/pull/5041) - Xlsx Writer and Reader: `Chart::setDescription()` is the alternative text of a chart, written as `descr` of the chart's frame and read back, and the frame takes the chart's name; LibreOffice exports it as `/Alt` of the chart in a tagged PDF. [Issue #5038](https://github.com/PHPOffice/PhpSpreadsheet/issues/5038) [PR #5039](https://github.com/PHPOffice/PhpSpreadsheet/pull/5039) - `BaseDrawing::setDecorative()` marks an image as decorative. Xlsx writes and reads it as Excel and LibreOffice do (`adec:decorative` in the image's `cNvPr`), Ods as LibreOffice does (`loext:decorative` in the frame's graphic style, which the writer now defines; the reader also takes ODF 1.4's `draw:decorative`); LibreOffice then leaves the image out of a tagged PDF as an artifact. Html writes and reads it as an empty `alt`. [Issue #5036](https://github.com/PHPOffice/PhpSpreadsheet/issues/5036) [PR #5037](https://github.com/PHPOffice/PhpSpreadsheet/pull/5037) -- Xlsx Writer and Reader: the description of a header/footer image is its alternative text, `alt` on the VML shape as Excel writes it. +- Xlsx Writer and Reader: the description of a header/footer image is its alternative text, `alt` on the VML shape as Excel writes it. [Issue #5056](https://github.com/PHPOffice/PhpSpreadsheet/issues/5056) [PR #5057](https://github.com/PHPOffice/PhpSpreadsheet/pull/5057) ### Removed