From a9e3f1691e171abc4847fdaac6ed49e8250a40e9 Mon Sep 17 00:00:00 2001 From: Jan Slabon Date: Tue, 6 Oct 2026 15:22:02 +0200 Subject: [PATCH 1/4] Added handling of escaped dictionary keys --- src/FpdfTrait.php | 3 ++- src/FpdiTrait.php | 2 +- src/PdfParser/Type/PdfDictionary.php | 2 +- src/PdfParser/Type/PdfName.php | 24 +++++++++++++++++- tests/functional/FpdiTraitTest.php | 8 ++++++ .../PdfParser/Type/PdfDictionaryTest.php | 25 ++++++++++++++++++- 6 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/FpdfTrait.php b/src/FpdfTrait.php index f768c8f..5a101b6 100644 --- a/src/FpdfTrait.php +++ b/src/FpdfTrait.php @@ -13,6 +13,7 @@ use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; use setasign\Fpdi\PdfParser\PdfParserException; use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfName; use setasign\Fpdi\PdfParser\Type\PdfNull; /** @@ -144,7 +145,7 @@ protected function _putlinks($n) $values = $pl['importedLink']['pdfObject']->value; foreach ($values as $name => $entry) { - $this->_put('/' . $name . ' ', false); + $this->_put('/' . PdfName::escape($name) . ' ', false); $this->writePdfType($entry); } diff --git a/src/FpdiTrait.php b/src/FpdiTrait.php index cb72a73..ec32ec1 100644 --- a/src/FpdiTrait.php +++ b/src/FpdiTrait.php @@ -618,7 +618,7 @@ protected function writePdfType(PdfType $value) } elseif ($value instanceof PdfDictionary) { $this->_put('<<', false); foreach ($value->value as $name => $entry) { - $this->_put('/' . $name . ' ', false); + $this->_put('/' . PdfName::escape($name) . ' ', false); $this->writePdfType($entry); } $this->_put('>>'); diff --git a/src/PdfParser/Type/PdfDictionary.php b/src/PdfParser/Type/PdfDictionary.php index 15fbc98..6238e2c 100644 --- a/src/PdfParser/Type/PdfDictionary.php +++ b/src/PdfParser/Type/PdfDictionary.php @@ -74,7 +74,7 @@ public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, P break; } - $entries[$key->value] = $value; + $entries[PdfName::unescape($key->value)] = $value; } $v = new self(); diff --git a/src/PdfParser/Type/PdfName.php b/src/PdfParser/Type/PdfName.php index 8e3a557..4da35e8 100644 --- a/src/PdfParser/Type/PdfName.php +++ b/src/PdfParser/Type/PdfName.php @@ -49,11 +49,33 @@ public static function unescape($value) return $value; } - return \preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) { + return \preg_replace_callback('/#([a-fA-F\d]{2})/', static function ($matches) { return \chr(\hexdec($matches[1])); }, $value); } + /** + * Escapes a name string. + * + * @param string $value + * @return string + */ + public static function escape(string $value): string + { + $pattern = '/[\x00-\x21\x7E-\xFF()<>\[\]{}\/%#]/'; + if (!\preg_match($pattern, $value)) { + return $value; + } + + return \preg_replace_callback( + $pattern, + static function ($matches) { + return '#' . \substr('00' . \dechex(\ord($matches[0])), -2); + }, + $value + ); + } + /** * Helper method to create an instance. * diff --git a/tests/functional/FpdiTraitTest.php b/tests/functional/FpdiTraitTest.php index ccbfbde..8156761 100644 --- a/tests/functional/FpdiTraitTest.php +++ b/tests/functional/FpdiTraitTest.php @@ -320,6 +320,14 @@ public function writePdfTypeProvider() "AnyToken\n" . "endobj\n" ], + [ + PdfDictionary::create(['A B' => PdfName::create(PdfName::escape('A B'))]), + "<>\n" + ], + [ + PdfDictionary::create(['#41' => PdfName::create('#41')]), + "<>\n" + ], ]; } diff --git a/tests/functional/PdfParser/Type/PdfDictionaryTest.php b/tests/functional/PdfParser/Type/PdfDictionaryTest.php index a6a966a..804b5bf 100644 --- a/tests/functional/PdfParser/Type/PdfDictionaryTest.php +++ b/tests/functional/PdfParser/Type/PdfDictionaryTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use setasign\Fpdi\PdfParser\PdfParser; use setasign\Fpdi\PdfParser\StreamReader; +use setasign\Fpdi\PdfParser\Type\PdfArray; use setasign\Fpdi\PdfParser\Type\PdfDictionary; use setasign\Fpdi\PdfParser\Type\PdfHexString; use setasign\Fpdi\PdfParser\Type\PdfName; @@ -113,7 +114,29 @@ public function parseProvider() 'A' => PdfName::create('B') ]) ], - // @todo + [ + '/A 123/#41 321>>', + PdfDictionary::create([ + 'A' => PdfNumeric::create(321) + ]) + ], + [ + '/#41 123/A 321>>', + PdfDictionary::create([ + 'A' => PdfNumeric::create(321) + ]) + ], + [ + '/Rect [1 2 3 4]/Re#63t[5 6 7 8]>>', + PdfDictionary::create([ + 'Rect' => PdfArray::create([ + PdfNumeric::create(5), + PdfNumeric::create(6), + PdfNumeric::create(7), + PdfNumeric::create(8), + ]) + ]) + ] ]; return $data; From 51ecbd8098eeba0d29dc40b06482ee7042678f1d Mon Sep 17 00:00:00 2001 From: Jan Slabon Date: Fri, 9 Oct 2026 11:58:15 +0200 Subject: [PATCH 2/4] Reduced complexity --- src/FpdfTrait.php | 2 +- src/FpdiTrait.php | 2 +- src/PdfParser/Type/PdfDictionary.php | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/FpdfTrait.php b/src/FpdfTrait.php index 5a101b6..695eaa2 100644 --- a/src/FpdfTrait.php +++ b/src/FpdfTrait.php @@ -145,7 +145,7 @@ protected function _putlinks($n) $values = $pl['importedLink']['pdfObject']->value; foreach ($values as $name => $entry) { - $this->_put('/' . PdfName::escape($name) . ' ', false); + $this->_put('/' . $name . ' ', false); $this->writePdfType($entry); } diff --git a/src/FpdiTrait.php b/src/FpdiTrait.php index ec32ec1..cb72a73 100644 --- a/src/FpdiTrait.php +++ b/src/FpdiTrait.php @@ -618,7 +618,7 @@ protected function writePdfType(PdfType $value) } elseif ($value instanceof PdfDictionary) { $this->_put('<<', false); foreach ($value->value as $name => $entry) { - $this->_put('/' . PdfName::escape($name) . ' ', false); + $this->_put('/' . $name . ' ', false); $this->writePdfType($entry); } $this->_put('>>'); diff --git a/src/PdfParser/Type/PdfDictionary.php b/src/PdfParser/Type/PdfDictionary.php index 6238e2c..62393f0 100644 --- a/src/PdfParser/Type/PdfDictionary.php +++ b/src/PdfParser/Type/PdfDictionary.php @@ -74,7 +74,8 @@ public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, P break; } - $entries[PdfName::unescape($key->value)] = $value; + // ensure unique entries + $entries[PdfName::escape(PdfName::unescape($key->value))] = $value; } $v = new self(); From b1bad26b157374c4f5a1daf46617c01a692df72f Mon Sep 17 00:00:00 2001 From: Jan Slabon Date: Fri, 9 Oct 2026 11:59:15 +0200 Subject: [PATCH 3/4] Removed useless use-statement --- src/FpdfTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FpdfTrait.php b/src/FpdfTrait.php index 695eaa2..f768c8f 100644 --- a/src/FpdfTrait.php +++ b/src/FpdfTrait.php @@ -13,7 +13,6 @@ use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; use setasign\Fpdi\PdfParser\PdfParserException; use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; -use setasign\Fpdi\PdfParser\Type\PdfName; use setasign\Fpdi\PdfParser\Type\PdfNull; /** From 4a051fab6f2cc3097167f2261e86c4b35d16f230 Mon Sep 17 00:00:00 2001 From: Jan Slabon Date: Fri, 9 Oct 2026 12:06:02 +0200 Subject: [PATCH 4/4] Update FpdiTraitTest.php --- tests/functional/FpdiTraitTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/functional/FpdiTraitTest.php b/tests/functional/FpdiTraitTest.php index 8156761..36141a5 100644 --- a/tests/functional/FpdiTraitTest.php +++ b/tests/functional/FpdiTraitTest.php @@ -320,13 +320,21 @@ public function writePdfTypeProvider() "AnyToken\n" . "endobj\n" ], + [ + PdfDictionary::create(['A B' => PdfName::create('A B')]), + "<>\n" // invalid structure but correct behavior + ], [ PdfDictionary::create(['A B' => PdfName::create(PdfName::escape('A B'))]), + "<>\n" // invalid structure but correct behavior + ], + [ + PdfDictionary::create([PdfName::escape('A B') => PdfName::create(PdfName::escape('A B'))]), "<>\n" ], [ PdfDictionary::create(['#41' => PdfName::create('#41')]), - "<>\n" + "<>\n" ], ]; }