diff --git a/src/Smalot/PdfParser/PDFObject.php b/src/Smalot/PdfParser/PDFObject.php index 378ae15d..fb5d80f2 100644 --- a/src/Smalot/PdfParser/PDFObject.php +++ b/src/Smalot/PdfParser/PDFObject.php @@ -827,8 +827,14 @@ public function getTextArray(?Page $page = null): array // Restore previous selected font and graphics matrix case 'Q': - list($current_font, $current_font_size) = array_pop($clipped_font); - $current_position_cm = array_pop($clipped_position_cm); + // A malformed content stream can restore a graphics state it never saved, + // leaving nothing on these stacks to pop. + if ([] !== $clipped_font) { + list($current_font, $current_font_size) = array_pop($clipped_font); + } + if ([] !== $clipped_position_cm) { + $current_position_cm = array_pop($clipped_position_cm); + } break; // End marked content sequence diff --git a/tests/PHPUnit/Unit/PDFObjectTest.php b/tests/PHPUnit/Unit/PDFObjectTest.php index d9b27161..94e0759b 100644 --- a/tests/PHPUnit/Unit/PDFObjectTest.php +++ b/tests/PHPUnit/Unit/PDFObjectTest.php @@ -29,6 +29,22 @@ public function testGetTextOnPageWithoutContent(): void static::assertSame(' ', (new PDFObject($document, null, null))->getText(new Page($document))); } + /** + * A stream may concatenate save-state operators without whitespace ("qq"), + * which lexes as a single unknown token instead of two "q" operators. The + * matching "Q"s then restore a state that was never pushed, and the rest of + * the stream must still be read. + */ + public function testGetTextWithUnmatchedGraphicsStateRestore(): void + { + $document = new Document(); + $document->init(); + + $content = "qq\nBT /F1 12 Tf 10 10 Td (Hello) Tj ET\nQ\nQ\nBT 10 -20 Td (World) Tj ET"; + + static::assertSame("Hello\nWorld ", (new PDFObject($document, null, $content, new Config()))->getText()); + } + public function testTextArrayObjects(): void { $document = new Document();