diff --git a/src/PdfParser/Type/PdfDictionary.php b/src/PdfParser/Type/PdfDictionary.php index 15fbc98..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[$key->value] = $value; + // ensure unique entries + $entries[PdfName::escape(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..36141a5 100644 --- a/tests/functional/FpdiTraitTest.php +++ b/tests/functional/FpdiTraitTest.php @@ -320,6 +320,22 @@ 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" + ], ]; } 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;