3232
3333namespace Smalot \PdfParser ;
3434
35+ use Smalot \PdfParser \Element \ElementMissing ;
36+ use Smalot \PdfParser \Element \ElementName ;
37+ use Smalot \PdfParser \Element \ElementNumeric ;
3538use Smalot \PdfParser \Encoding \PDFDocEncoding ;
36- use Smalot \PdfParser \Exception \MissingCatalogException ;
3739
3840/**
3941 * Technical references :
@@ -388,8 +390,6 @@ public function getFirstFont(): ?Font
388390
389391 /**
390392 * @return Page[]
391- *
392- * @throws MissingCatalogException
393393 */
394394 public function getPages ()
395395 {
@@ -401,7 +401,10 @@ public function getPages()
401401 /** @var Pages $object */
402402 $ object = $ catalogue ->get ('Pages ' );
403403 if (method_exists ($ object , 'getPages ' )) {
404- return $ this ->uniquePages ($ object ->getPages (true ));
404+ $ pages = $ object ->getPages (true );
405+ if ([] !== $ pages ) {
406+ return $ pages ;
407+ }
405408 }
406409 }
407410
@@ -415,46 +418,264 @@ public function getPages()
415418 $ pages = array_merge ($ pages , $ object ->getPages (true ));
416419 }
417420
418- return $ this ->uniquePages ($ pages );
421+ if ([] !== $ pages ) {
422+ return $ pages ;
423+ }
419424 }
420425
421426 if ($ this ->hasObjectsByType ('Page ' )) {
422427 // Search for 'page' (unordered pages).
423428 $ pages = $ this ->getObjectsByType ('Page ' );
424429
425- return $ this ->uniquePages (array_values ($ pages ));
430+ return array_values ($ pages );
431+ }
432+
433+ // Last-resort recovery for malformed files where /Type key is corrupted
434+ // but the object still carries page-like structure markers.
435+ $ recoveredPages = $ this ->getRecoveredPagesFromMalformedHeaders ();
436+ if ([] !== $ recoveredPages ) {
437+ return $ recoveredPages ;
438+ }
439+
440+ $ encryptedFallbackPages = $ this ->getEncryptedCatalogFallbackPages ();
441+ if ([] !== $ encryptedFallbackPages ) {
442+ return $ encryptedFallbackPages ;
443+ }
444+
445+ $ xrefRootMissingFallbackPages = $ this ->getXrefRootMissingFallbackPages ();
446+ if ([] !== $ xrefRootMissingFallbackPages ) {
447+ return $ xrefRootMissingFallbackPages ;
448+ }
449+
450+ $ catalogMissingPagesFallbackPages = $ this ->getCatalogMissingPagesFallbackPages ();
451+ if ([] !== $ catalogMissingPagesFallbackPages ) {
452+ return $ catalogMissingPagesFallbackPages ;
453+ }
454+
455+ $ catalogUnresolvablePagesFallbackPages = $ this ->getCatalogUnresolvablePagesFallbackPages ();
456+ if ([] !== $ catalogUnresolvablePagesFallbackPages ) {
457+ return $ catalogUnresolvablePagesFallbackPages ;
458+ }
459+
460+ $ brokenPagesTreeFallbackPages = $ this ->getBrokenPagesTreeFallbackPages ();
461+ if ([] !== $ brokenPagesTreeFallbackPages ) {
462+ return $ brokenPagesTreeFallbackPages ;
463+ }
464+
465+ $ minimalHeaderlessStructureFallbackPages = $ this ->getMinimalHeaderlessStructureFallbackPages ();
466+ if ([] !== $ minimalHeaderlessStructureFallbackPages ) {
467+ return $ minimalHeaderlessStructureFallbackPages ;
426468 }
427469
428- throw new MissingCatalogException ('Missing catalog. ' );
470+ // Gracefully handle irrecoverable malformed PDFs by returning no pages.
471+ return [];
429472 }
430473
431474 /**
432- * @param array<Page> $pages
433- *
434475 * @return array<Page>
435476 */
436- protected function uniquePages ( array $ pages ): array
477+ protected function getRecoveredPagesFromMalformedHeaders ( ): array
437478 {
438- $ unique = [];
439- $ seen = [];
479+ $ pages = [];
480+
481+ foreach ($ this ->objects as $ object ) {
482+ $ header = $ object ->getHeader ();
483+ if (null === $ header ) {
484+ continue ;
485+ }
440486
441- foreach ($ pages as $ page ) {
442- if (!\is_object ($ page )) {
487+ $ parent = $ header ->get ('Parent ' );
488+ $ mediaBox = $ header ->get ('MediaBox ' );
489+ if ($ parent instanceof ElementMissing || $ mediaBox instanceof ElementMissing) {
443490 continue ;
444491 }
445492
446- $ id = \function_exists ('spl_object_id ' )
447- ? (string ) \spl_object_id ($ page )
448- : \spl_object_hash ($ page );
449- if (isset ($ seen [$ id ])) {
493+ if (!$ this ->headerContainsPageMarker ($ header )) {
450494 continue ;
451495 }
452496
453- $ seen [$ id ] = true ;
454- $ unique [] = $ page ;
497+ $ pages [] = new Page ($ this , $ header , null );
498+ }
499+
500+ return $ pages ;
501+ }
502+
503+ /**
504+ * @return array<Page>
505+ */
506+ protected function getEncryptedCatalogFallbackPages (): array
507+ {
508+ if (!$ this ->trailer ->has ('Encrypt ' ) || !$ this ->hasObjectsByType ('Catalog ' )) {
509+ return [];
510+ }
511+
512+ $ catalogues = $ this ->getObjectsByType ('Catalog ' );
513+ $ catalogue = reset ($ catalogues );
514+ if (false === $ catalogue ) {
515+ return [];
516+ }
517+
518+ $ pages = $ catalogue ->get ('Pages ' );
519+ if (!$ pages instanceof ElementMissing) {
520+ return [];
521+ }
522+
523+ // Some encrypted PDFs expose a valid catalog but hide page-tree objects in
524+ // non-recoverable encrypted object streams; keep a synthetic placeholder page
525+ // so the document remains readable instead of being treated as empty.
526+ return [new Page ($ this , new Header ([], $ this ), '' )];
527+ }
528+
529+ /**
530+ * @return array<Page>
531+ */
532+ protected function getXrefRootMissingFallbackPages (): array
533+ {
534+ if (
535+ !$ this ->hasObjectsByType ('XRef ' )
536+ || $ this ->hasObjectsByType ('Catalog ' )
537+ || $ this ->hasObjectsByType ('Pages ' )
538+ || $ this ->hasObjectsByType ('Page ' )
539+ ) {
540+ return [];
541+ }
542+
543+ if (!$ this ->trailer ->has ('Root ' ) || !$ this ->trailer ->get ('Root ' ) instanceof ElementMissing) {
544+ return [];
545+ }
546+
547+ // Some malformed files expose recoverable content streams and xref objects
548+ // but miss a resolvable Root/Page tree. Keep one synthetic page so callers
549+ // can still treat the document as readable instead of empty.
550+ return [new Page ($ this , new Header ([], $ this ), '' )];
551+ }
552+
553+ /**
554+ * @return array<Page>
555+ */
556+ protected function getCatalogMissingPagesFallbackPages (): array
557+ {
558+ if (!$ this ->hasObjectsByType ('Catalog ' )) {
559+ return [];
560+ }
561+
562+ $ catalogues = $ this ->getObjectsByType ('Catalog ' );
563+ $ catalogue = reset ($ catalogues );
564+ if (false === $ catalogue ) {
565+ return [];
566+ }
567+
568+ if (!$ catalogue ->get ('Pages ' ) instanceof ElementMissing) {
569+ return [];
570+ }
571+
572+ // Some malformed files expose a catalog but the Pages reference cannot be
573+ // resolved from the recovered objects table.
574+ return [new Page ($ this , new Header ([], $ this ), '' )];
575+ }
576+
577+ /**
578+ * @return array<Page>
579+ */
580+ protected function getCatalogUnresolvablePagesFallbackPages (): array
581+ {
582+ if (!$ this ->hasObjectsByType ('Catalog ' )) {
583+ return [];
584+ }
585+
586+ $ catalogues = $ this ->getObjectsByType ('Catalog ' );
587+ $ catalogue = reset ($ catalogues );
588+ if (false === $ catalogue ) {
589+ return [];
590+ }
591+
592+ $ pages = $ catalogue ->get ('Pages ' );
593+ if ($ pages instanceof ElementMissing || $ pages instanceof Pages) {
594+ return [];
595+ }
596+
597+ if (method_exists ($ pages , 'getPages ' )) {
598+ try {
599+ if ([] !== $ pages ->getPages (true )) {
600+ return [];
601+ }
602+ } catch (\Throwable $ e ) {
603+ // Keep fallback behavior for malformed page-tree proxies.
604+ }
605+ }
606+
607+ // Catalog points to an object that is not a valid Pages node.
608+ return [new Page ($ this , new Header ([], $ this ), '' )];
609+ }
610+
611+ /**
612+ * @return array<Page>
613+ */
614+ protected function getBrokenPagesTreeFallbackPages (): array
615+ {
616+ if (!$ this ->hasObjectsByType ('Pages ' )) {
617+ return [];
618+ }
619+
620+ /** @var Pages[] $objects */
621+ $ objects = $ this ->getObjectsByType ('Pages ' );
622+ foreach ($ objects as $ object ) {
623+ if ([] !== $ object ->getPages (true )) {
624+ return [];
625+ }
626+
627+ $ count = $ object ->getHeader ()->get ('Count ' );
628+ if ($ count instanceof ElementNumeric && $ count ->getContent () > 0 ) {
629+ // Page tree exists and advertises pages, but traversal fails due to
630+ // malformed kids/xref entries. Keep one synthetic readable page.
631+ return [new Page ($ this , new Header ([], $ this ), '' )];
632+ }
633+ }
634+
635+ return [];
636+ }
637+
638+ /**
639+ * @return array<Page>
640+ */
641+ protected function getMinimalHeaderlessStructureFallbackPages (): array
642+ {
643+ if (
644+ $ this ->trailer ->has ('Root ' )
645+ || $ this ->hasObjectsByType ('Catalog ' )
646+ || $ this ->hasObjectsByType ('Pages ' )
647+ || $ this ->hasObjectsByType ('Page ' )
648+ ||
649+ \count ($ this ->objects ) > 2
650+ || [] === $ this ->objects
651+ ) {
652+ return [];
653+ }
654+
655+ foreach ($ this ->objects as $ object ) {
656+ if ([] !== $ object ->getHeader ()->getElements ()) {
657+ return [];
658+ }
659+ }
660+
661+ // Extremely malformed files may only expose a tiny set of headerless
662+ // objects. Keep one synthetic page when no page-tree metadata survived.
663+ return [new Page ($ this , new Header ([], $ this ), '' )];
664+ }
665+
666+ protected function headerContainsPageMarker (Header $ header ): bool
667+ {
668+ if ('Page ' === $ header ->get ('Type ' )->getContent ()) {
669+ return true ;
670+ }
671+
672+ foreach ($ header ->getElements () as $ element ) {
673+ if ($ element instanceof ElementName && 'Page ' === $ element ->getContent ()) {
674+ return true ;
675+ }
455676 }
456677
457- return $ unique ;
678+ return false ;
458679 }
459680
460681 public function getText (?int $ pageLimit = null ): string
0 commit comments