From 8c32c8334dd38b878b20dccd0f4440b7fab0b0ce Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 7 Sep 2026 12:22:56 +1000 Subject: [PATCH] Check a font is flagged before reading the keys only a used font has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _putpatterns() builds a resource dictionary for an SVG background by picking out the fonts an SVG has flagged, but it tests $font['used'] and $font['type'] before it tests $font['fo']. A core font that has drawn nothing yet has neither key, so any page whose only text lives inside a repeating SVG background raises "Undefined array key" on PHP 8 — and a page like that is exactly when the loop runs. Test the flag first. Nothing else changes: the output for such a document is byte for byte what it was. Co-Authored-By: Claude Opus 5 --- src/Writer/BackgroundWriter.php | 21 ++++-- tests/Mpdf/BackgroundPatternResourcesTest.php | 72 +++++++++++++++++++ tests/data/img/pattern-with-text.svg | 4 ++ 3 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 tests/Mpdf/BackgroundPatternResourcesTest.php create mode 100644 tests/data/img/pattern-with-text.svg diff --git a/src/Writer/BackgroundWriter.php b/src/Writer/BackgroundWriter.php index 6dbce84d3..160953c22 100644 --- a/src/Writer/BackgroundWriter.php +++ b/src/Writer/BackgroundWriter.php @@ -163,18 +163,27 @@ public function writePatterns() // _putpatterns $this->writer->write('/Font <<'); foreach ($this->mpdf->fonts as $font) { - if (!$font['used'] && $font['type'] === 'TTF') { + // The 'used' and 'type' keys are only reachable once a font is flagged, as a + // core font that has drawn nothing yet carries neither + if (!isset($font['fo']) || !$font['fo']) { continue; } - if (isset($font['fo']) && $font['fo']) { - if ($font['type'] === 'TTF' && ($font['sip'] || $font['smp'])) { + + if (isset($font['type']) && $font['type'] === 'TTF') { + if (!$font['used']) { + continue; + } + + if ($font['sip'] || $font['smp']) { foreach ($font['n'] as $k => $fid) { - $this->writer->write('/F' . $font['subsetfontids'][$k] . ' ' . $font['n'][$k] . ' 0 R'); + $this->writer->write('/F' . $font['subsetfontids'][$k] . ' ' . $fid . ' 0 R'); } - } else { - $this->writer->write('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + + continue; } } + + $this->writer->write('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); } $this->writer->write('>>'); } else { diff --git a/tests/Mpdf/BackgroundPatternResourcesTest.php b/tests/Mpdf/BackgroundPatternResourcesTest.php new file mode 100644 index 000000000..05e6ca84e --- /dev/null +++ b/tests/Mpdf/BackgroundPatternResourcesTest.php @@ -0,0 +1,72 @@ +render(); + + $this->assertStringContainsString( + '/PatternType 1', + $output, + 'Expected the repeating SVG background to be written as a tiling pattern' + ); + } + + /** + * The pattern's resource dictionary still has to name the font its Form XObject draws with. + */ + public function testPatternResourcesNameTheSvgFont() + { + $output = $this->render(); + + $this->assertSame(1, preg_match('#/PatternType 1.*?/Resources (\d+) 0 R#s', $output, $pattern)); + + $this->assertMatchesRegularExpression( + '#/Font\s*<<\s*/F\d+ [1-9]\d* 0 R#', + $this->object($output, $pattern[1]) + ); + } + + /** + * @param string $pdf + * @param string $number + * @return string + */ + private function object($pdf, $number) + { + preg_match('/(?:^|\s)' . $number . ' 0 obj\s*(.*?)endobj/s', $pdf, $matches); + + return isset($matches[1]) ? $matches[1] : ''; + } + + /** + * @return string + */ + private function render() + { + $mpdf = new Mpdf(['mode' => 'c']); + $mpdf->compress = false; + + $mpdf->WriteHTML( + '
' + ); + + $output = $mpdf->OutputBinaryData(); + $mpdf->cleanup(); + + return $output; + } + +} diff --git a/tests/data/img/pattern-with-text.svg b/tests/data/img/pattern-with-text.svg new file mode 100644 index 000000000..f2155ed65 --- /dev/null +++ b/tests/data/img/pattern-with-text.svg @@ -0,0 +1,4 @@ + + + Hi +