From f0689312687d23d531679206db6920668f6a05fd Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 7 Sep 2026 12:03:12 +1000 Subject: [PATCH] Support the UseAttachments page mode, and write /PageMode once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DisplayPreferences gains UseAttachments, which opens the reader's attachment pane on a document carrying embedded files. writeCatalog() had three independent places writing /PageMode — the outline pane implied by bookmarks, FullScreen from DisplayPreferences, and UseOC for the layer pane — so a document with bookmarks, layers and the pane open already emitted the key three times in one dictionary. A fourth mode would have made that worse, so the choice now happens in one place. The order preserves what readers resolve today, where the last write of a repeated key wins: UseOC, then FullScreen, then the new UseAttachments, and finally the outline pane, which is the only one the document never asked for explicitly. Mirrors mpdf/mpdf#2142. Co-Authored-By: Claude Opus 5 --- src/Mpdf.php | 2 +- src/Writer/MetadataWriter.php | 39 ++++++++++++--- tests/Mpdf/PageModeTest.php | 94 +++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 tests/Mpdf/PageModeTest.php diff --git a/src/Mpdf.php b/src/Mpdf.php index 523b8b7d6..50c34355c 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -9497,7 +9497,7 @@ function SetDash($black = false, $white = false) function SetDisplayPreferences($preferences) { - // String containing any or none of /HideMenubar/HideToolbar/HideWindowUI/DisplayDocTitle/CenterWindow/FitWindow + // String containing any or none of /HideMenubar/HideToolbar/HideWindowUI/DisplayDocTitle/CenterWindow/FitWindow/NoPrintScaling/FullScreen/UseAttachments $this->DisplayPreferences .= $preferences; } diff --git a/src/Writer/MetadataWriter.php b/src/Writer/MetadataWriter.php index be9423fe6..330318ce4 100644 --- a/src/Writer/MetadataWriter.php +++ b/src/Writer/MetadataWriter.php @@ -379,12 +379,11 @@ public function writeCatalog() //_putcatalog // Bookmarks if (count($this->mpdf->BMoutlines) > 0) { $this->writer->write('/Outlines ' . $this->mpdf->OutlineRoot . ' 0 R'); - $this->writer->write('/PageMode /UseOutlines'); } - // Fullscreen - if (is_int(strpos($this->mpdf->DisplayPreferences, 'FullScreen'))) { - $this->writer->write('/PageMode /FullScreen'); + $pageMode = $this->getPageMode(); + if ($pageMode !== null) { + $this->writer->write('/PageMode /' . $pageMode); } // Metadata @@ -463,10 +462,6 @@ public function writeCatalog() //_putcatalog $this->writer->write('>>'); } - if ($this->mpdf->open_layer_pane && ($this->mpdf->hasOC || count($this->mpdf->layers))) { - $this->writer->write('/PageMode /UseOC'); - } - if ($this->mpdf->hasOC || count($this->mpdf->layers)) { $p = $v = $h = $l = $loff = $lall = $as = ''; @@ -510,6 +505,34 @@ public function writeCatalog() //_putcatalog } } + /** + * A catalog carries a single /PageMode, so the modes the document asks for explicitly take + * precedence over the outline pane implied by simply having bookmarks. + * + * @return string|null + */ + private function getPageMode() + { + if ($this->mpdf->open_layer_pane && ($this->mpdf->hasOC || count($this->mpdf->layers))) { + return 'UseOC'; + } + + if (is_int(strpos($this->mpdf->DisplayPreferences, 'FullScreen'))) { + return 'FullScreen'; + } + + // UseAttachments is PDF 1.6 spec. + if (is_int(strpos($this->mpdf->DisplayPreferences, 'UseAttachments'))) { + return 'UseAttachments'; + } + + if (count($this->mpdf->BMoutlines) > 0) { + return 'UseOutlines'; + } + + return null; + } + /** * @since 5.7.2 */ diff --git a/tests/Mpdf/PageModeTest.php b/tests/Mpdf/PageModeTest.php new file mode 100644 index 000000000..80ba2dfd8 --- /dev/null +++ b/tests/Mpdf/PageModeTest.php @@ -0,0 +1,94 @@ +render(function (Mpdf $mpdf) { + $mpdf->WriteHTML('

Hello

'); + }); + + $this->assertStringNotContainsString('/PageMode', $output); + } + + public function testBookmarksOpenTheOutlinePane() + { + $output = $this->render(function (Mpdf $mpdf) { + $mpdf->WriteHTML('

Hello

'); + $mpdf->Bookmark('Chapter one'); + }); + + $this->assertSame(1, substr_count($output, '/PageMode')); + $this->assertStringContainsString('/PageMode /UseOutlines', $output); + } + + public function testUseAttachmentsDisplayPreference() + { + $output = $this->render(function (Mpdf $mpdf) { + $mpdf->SetDisplayPreferences('UseAttachments'); + $mpdf->WriteHTML('

Hello

'); + }); + + $this->assertSame(1, substr_count($output, '/PageMode')); + $this->assertStringContainsString('/PageMode /UseAttachments', $output); + } + + /** + * A dictionary cannot hold the same key twice, so an explicit request has to beat the + * outline pane that bookmarks would otherwise ask for. + */ + public function testExplicitPreferenceWinsOverBookmarks() + { + $output = $this->render(function (Mpdf $mpdf) { + $mpdf->SetDisplayPreferences('UseAttachments'); + $mpdf->WriteHTML('

Hello

'); + $mpdf->Bookmark('Chapter one'); + }); + + $this->assertSame(1, substr_count($output, '/PageMode')); + $this->assertStringContainsString('/PageMode /UseAttachments', $output); + } + + public function testFullScreenWinsOverUseAttachments() + { + $output = $this->render(function (Mpdf $mpdf) { + $mpdf->SetDisplayPreferences('FullScreenUseAttachments'); + $mpdf->WriteHTML('

Hello

'); + }); + + $this->assertSame(1, substr_count($output, '/PageMode')); + $this->assertStringContainsString('/PageMode /FullScreen', $output); + } + + public function testLayerPaneWinsOverEverythingElse() + { + $output = $this->render(function (Mpdf $mpdf) { + $mpdf->open_layer_pane = true; + $mpdf->SetDisplayPreferences('FullScreen'); + $mpdf->SetVisibility('screenonly'); + $mpdf->WriteHTML('

Hello

'); + $mpdf->SetVisibility('visible'); + $mpdf->Bookmark('Chapter one'); + }); + + $this->assertSame(1, substr_count($output, '/PageMode')); + $this->assertStringContainsString('/PageMode /UseOC', $output); + } + + private function render(callable $write) + { + $mpdf = new Mpdf(); + $mpdf->compress = false; + + $write($mpdf); + + $output = $mpdf->OutputBinaryData(); + $mpdf->cleanup(); + + return $output; + } + +}