@@ -585,6 +585,41 @@ protected function getObjectHeaderLen(array $objRefs): int
585585 return 5 + \strlen ($ objRefs [0 ]) + \strlen ($ objRefs [1 ]);
586586 }
587587
588+ /**
589+ * Merge missing xref offsets by scanning object headers directly in the PDF body.
590+ *
591+ * This is a recovery path for malformed xref streams where trailer references
592+ * (for example /Root) are present but corresponding xref entries are missing.
593+ */
594+ private function mergeMissingXrefOffsetsFromObjectHeaders (string $ pdfData , array $ xref ): array
595+ {
596+ if (!isset ($ xref ['xref ' ]) || !\is_array ($ xref ['xref ' ])) {
597+ $ xref ['xref ' ] = [];
598+ }
599+
600+ if (
601+ preg_match_all (
602+ '/(?:^|[\r\n])([0-9]+)[\x09\x0a\x0c\x0d\x20]+([0-9]+)[\x09\x0a\x0c\x0d\x20]+obj(?=[\x09\x0a\x0c\x0d\x20<])/i ' ,
603+ $ pdfData ,
604+ $ matches ,
605+ \PREG_OFFSET_CAPTURE
606+ ) > 0
607+ ) {
608+ foreach ($ matches [1 ] as $ idx => $ objMatch ) {
609+ $ objNum = $ objMatch [0 ];
610+ $ offset = $ objMatch [1 ];
611+ $ genNum = $ matches [2 ][$ idx ][0 ];
612+ $ objRef = $ objNum .'_ ' .$ genNum ;
613+
614+ if (!isset ($ xref ['xref ' ][$ objRef ])) {
615+ $ xref ['xref ' ][$ objRef ] = $ offset ;
616+ }
617+ }
618+ }
619+
620+ return $ xref ;
621+ }
622+
588623 /**
589624 * Get content of indirect object.
590625 *
@@ -1287,6 +1322,11 @@ public function parseData(string $data): array
12871322 $ xref = $ this ->getXrefData ($ pdfData );
12881323 }
12891324
1325+ $ rootObjectRef = $ xref ['trailer ' ]['root ' ] ?? null ;
1326+ if (\is_string ($ rootObjectRef ) && !isset ($ xref ['xref ' ][$ rootObjectRef ])) {
1327+ $ xref = $ this ->mergeMissingXrefOffsetsFromObjectHeaders ($ pdfData , $ xref );
1328+ }
1329+
12901330 // parse all document objects
12911331 $ objects = [];
12921332 foreach ($ xref ['xref ' ] as $ obj => $ offset ) {
0 commit comments