diff --git a/samples/bugs/EncodingAsIndirectPDFObject.pdf b/samples/bugs/EncodingAsIndirectPDFObject.pdf new file mode 100644 index 00000000..93cf7357 Binary files /dev/null and b/samples/bugs/EncodingAsIndirectPDFObject.pdf differ diff --git a/src/Smalot/PdfParser/Font.php b/src/Smalot/PdfParser/Font.php index 8e1fbce1..1e870603 100644 --- a/src/Smalot/PdfParser/Font.php +++ b/src/Smalot/PdfParser/Font.php @@ -94,7 +94,19 @@ public function getDetails(bool $deep = true): array $details['Name'] = $this->getName(); $details['Type'] = $this->getType(); - $details['Encoding'] = ($this->has('Encoding') ? (string) $this->get('Encoding') : 'Ansi'); + $encoding = $this->has('Encoding') ? $this->get('Encoding') : null; + if ($encoding instanceof PDFObject) { + // Encoding is an indirect reference to an encoding dictionary (PDF spec Table 5.11). + // Encoding extends PDFObject, so this branch handles both cases. + // Extract BaseEncoding name; absent means the font's built-in encoding is the base. + $baseEncoding = $encoding->getHeader()->get('BaseEncoding'); + $baseEncodingStr = ($baseEncoding instanceof Element) ? (string) $baseEncoding : ''; + $details['Encoding'] = $baseEncodingStr !== '' ? $baseEncodingStr : 'Ansi'; + } elseif ($encoding instanceof Element) { + $details['Encoding'] = (string) $encoding; + } else { + $details['Encoding'] = 'Ansi'; + } $details += parent::getDetails($deep); diff --git a/tests/PHPUnit/Integration/FontTest.php b/tests/PHPUnit/Integration/FontTest.php index b103fdfc..e741e598 100644 --- a/tests/PHPUnit/Integration/FontTest.php +++ b/tests/PHPUnit/Integration/FontTest.php @@ -593,4 +593,35 @@ public function testDecodeContentIssue549(): void // check result $this->assertEquals('foobar-', $font->decodeContent("foobar-\x8D")); } + + /** + * Font::getDetails() must not throw when a font's Encoding entry is an + * indirect object reference that resolves to a plain PDFObject instead of + * an Element โ€” i.e. an encoding dictionary that lacks /Type /Encoding. + * + * This is a valid PDF structure per PDF spec Table 5.11: the dictionary + * may carry only a /Differences array and omit /Type and /BaseEncoding. + * Without the fix, PHP throws: + * "Object of class PDFObject could not be converted to string" + * + * @see https://github.com/smalot/pdfparser/issues/822 + */ + public function testGetDetailsWithEncodingAsIndirectPDFObject(): void + { + $filename = $this->rootDir.'/samples/bugs/EncodingAsIndirectPDFObject.pdf'; + $parser = $this->getParserInstance(); + $document = $parser->parseFile($filename); + + foreach ($document->getPages() as $page) { + foreach ($page->getFonts() as $font) { + // Must not throw "PDFObject could not be converted to string" + $details = $font->getDetails(); + $this->assertIsString($details['Encoding']); + $this->assertNotEmpty($details['Encoding']); + } + } + + // Text extraction must still work correctly + $this->assertSame('Hello', trim($document->getText())); + } } diff --git a/tests/PHPUnit/Unit/FontTest.php b/tests/PHPUnit/Unit/FontTest.php index f60818ff..57912d0e 100644 --- a/tests/PHPUnit/Unit/FontTest.php +++ b/tests/PHPUnit/Unit/FontTest.php @@ -34,11 +34,102 @@ use PHPUnitTests\TestCase; use Smalot\PdfParser\Config; use Smalot\PdfParser\Document; +use Smalot\PdfParser\Element; +use Smalot\PdfParser\Encoding; use Smalot\PdfParser\Font; +use Smalot\PdfParser\Header; use Smalot\PdfParser\PDFObject; class FontTest extends TestCase { + /** + * Font::getDetails() must not throw when Encoding is an indirect reference + * that resolves to a PDFObject instead of an Element. + * + * Such PDFs store the Encoding as an object reference (e.g. "12 0 R") whose + * resolved target is a plain PDFObject without /Type /Encoding โ€” a valid + * structure per PDF spec Table 5.11 (encoding dictionary with /Differences). + * + * @see https://github.com/smalot/pdfparser/issues/822 + */ + public function testGetDetailsEncodingAsPDFObjectWithBaseEncoding(): void + { + $document = new Document(); + $encodingObj = new PDFObject( + $document, + new Header(['BaseEncoding' => new Element('WinAnsiEncoding')]) + ); + $font = new Font($document, new Header(['Encoding' => $encodingObj])); + + $details = $font->getDetails(false); + + self::assertSame('WinAnsiEncoding', $details['Encoding']); + } + + /** + * When Encoding is a PDFObject without a BaseEncoding entry the font uses + * its built-in encoding as base (PDF spec ยง5.5.5). getDetails() must return + * 'Ansi' as fallback, consistent with Encoding::getDetails()['BaseEncoding']. + */ + public function testGetDetailsEncodingAsPDFObjectWithoutBaseEncoding(): void + { + $document = new Document(); + $encodingObj = new PDFObject($document, new Header([])); + $font = new Font($document, new Header(['Encoding' => $encodingObj])); + + $details = $font->getDetails(false); + + self::assertSame('Ansi', $details['Encoding']); + } + + /** + * When Encoding is an Encoding instance (PDFObject subclass, /Type /Encoding + * present) the BaseEncoding name must be returned. + */ + public function testGetDetailsEncodingAsEncodingInstance(): void + { + $document = new Document(); + $encodingObj = new Encoding( + $document, + new Header(['BaseEncoding' => new Element('MacRomanEncoding')]) + ); + $font = new Font($document, new Header(['Encoding' => $encodingObj])); + + $details = $font->getDetails(false); + + self::assertSame('MacRomanEncoding', $details['Encoding']); + } + + /** + * When Encoding is a direct name element (e.g. /WinAnsiEncoding) the name + * is returned as-is โ€” the original pre-fix behaviour must be preserved. + */ + public function testGetDetailsEncodingAsDirectElement(): void + { + $document = new Document(); + $font = new Font( + $document, + new Header(['Encoding' => new Element('WinAnsiEncoding')]) + ); + + $details = $font->getDetails(false); + + self::assertSame('WinAnsiEncoding', $details['Encoding']); + } + + /** + * When no Encoding entry is present getDetails() must return 'Ansi'. + */ + public function testGetDetailsEncodingMissingDefaultsToAnsi(): void + { + $document = new Document(); + $font = new Font($document, new Header([])); + + $details = $font->getDetails(false); + + self::assertSame('Ansi', $details['Encoding']); + } + /** * decodeText must decode \b. *