@@ -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 ) {
0 commit comments