Skip to content

Commit e786ad9

Browse files
committed
fix: recover missing root object when xref stream is incomplete
(cherry picked from commit 3022f5d)
1 parent 74dca68 commit e786ad9

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

862 Bytes
Binary file not shown.

src/Smalot/PdfParser/RawData/RawDataParser.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,41 @@ protected function getObjectHeaderLen(array $objRefs): int
523523
return 5 + \strlen($objRefs[0]) + \strlen($objRefs[1]);
524524
}
525525

526+
/**
527+
* Merge missing xref offsets by scanning object headers directly in the PDF body.
528+
*
529+
* This is a recovery path for malformed xref streams where trailer references
530+
* (for example /Root) are present but corresponding xref entries are missing.
531+
*/
532+
private function mergeMissingXrefOffsetsFromObjectHeaders(string $pdfData, array $xref): array
533+
{
534+
if (!isset($xref['xref']) || !\is_array($xref['xref'])) {
535+
$xref['xref'] = [];
536+
}
537+
538+
if (
539+
preg_match_all(
540+
'/(?:^|[\r\n])([0-9]+)[\x09\x0a\x0c\x0d\x20]+([0-9]+)[\x09\x0a\x0c\x0d\x20]+obj(?=[\x09\x0a\x0c\x0d\x20<])/i',
541+
$pdfData,
542+
$matches,
543+
\PREG_OFFSET_CAPTURE
544+
) > 0
545+
) {
546+
foreach ($matches[1] as $idx => $objMatch) {
547+
$objNum = $objMatch[0];
548+
$offset = $objMatch[1];
549+
$genNum = $matches[2][$idx][0];
550+
$objRef = $objNum.'_'.$genNum;
551+
552+
if (!isset($xref['xref'][$objRef])) {
553+
$xref['xref'][$objRef] = $offset;
554+
}
555+
}
556+
}
557+
558+
return $xref;
559+
}
560+
526561
/**
527562
* Get content of indirect object.
528563
*
@@ -984,6 +1019,11 @@ public function parseData(string $data): array
9841019
$xref = $this->getXrefData($pdfData);
9851020
}
9861021

1022+
$rootObjectRef = $xref['trailer']['root'] ?? null;
1023+
if (\is_string($rootObjectRef) && !isset($xref['xref'][$rootObjectRef])) {
1024+
$xref = $this->mergeMissingXrefOffsetsFromObjectHeaders($pdfData, $xref);
1025+
}
1026+
9871027
// parse all document objects
9881028
$objects = [];
9891029
foreach ($xref['xref'] as $obj => $offset) {

tests/PHPUnit/Integration/DocumentIssueFocusTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ public function testPDFDocEncodingDecode(): void
112112
self::assertStringContainsString($testSubject, $details['Subject']);
113113
}
114114

115-
public function testParseFileWhenStartxrefPointsNearXrefKeyword(): void
115+
/**
116+
* Ensures malformed xref streams with missing /Root xref entries still recover pages.
117+
*
118+
* @see https://github.com/mozilla/pdf.js/blob/master/test/pdfs/issue18986.pdf
119+
*/
120+
public function testMalformedXrefStreamMissingRootEntryStillParsesPage(): void
116121
{
117-
$document = (new Parser())->parseFile($this->rootDir.'/samples/bugs/PullRequest794.pdf');
122+
$document = (new Parser())->parseFile($this->rootDir.'/samples/bugs/PullRequest812-pdf.js.pdf');
118123

119124
self::assertCount(1, $document->getPages());
120125
}

0 commit comments

Comments
 (0)