Since get_outlines() returns references (Dest, A, or SE) rather than page numbers, you must implement a resolver to map these references to actual page numbers.
A resolver works by:
- Mapping all page objects in the document to their respective page numbers using
PDFPage.create_pages(document). - Recursively resolving
PDFObjRef types, DICTIONARY types (looking for the D key), LIST types (finding the first PDFObjRef), or NAMED_REF types (using document.get_dest(ref)). - Checking if the resolved object is a page by verifying if its
Type is LITERAL_PAGE.
from enum import Enum, auto
from pathlib import Path
from typing import Any, Optional
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage, LITERAL_PAGE
from pdfminer.pdfparser import PDFParser
from pdfminer.pdftypes import PDFObjRef
class PDFRefType(Enum):
PDF_OBJ_REF = auto()
DICTIONARY = auto()
LIST = auto()
NAMED_REF = auto()
UNK = auto()
class RefPageNumberResolver:
def __init__(self, document: PDFDocument):
self.document = document
# obj_id -> page_number
self.objid_to_pagenum: dict[int, int] = {
page.pageid: page_num
for page_num, page in enumerate(PDFPage.create_pages(document), 1)
}
@classmethod
def get_ref_type(cls, ref: Any) -> PDFRefType:
if isinstance(ref, PDFObjRef):
return PDFRefType.PDF_OBJ_REF
elif isinstance(ref, dict) and "D" in ref:
return PDFRefType.DICTIONARY
elif isinstance(ref, list) and any(isinstance(e, PDFObjRef) for e in ref):
return PDFRefType.LIST
elif isinstance(ref, bytes):
return PDFRefType.NAMED_REF
else:
return PDFRefType.UNK
@classmethod
def is_ref_page(cls, ref: Any) -> bool:
return isinstance(ref, dict) and "Type" in ref and ref["Type"] is LITERAL_PAGE
def resolve(self, ref: Any) -> Optional[int]:
ref_type = self.get_ref_type(ref)
if ref_type is PDFRefType.PDF_OBJ_REF and self.is_ref_page(ref.resolve()):
return self.objid_to_pagenum.get(ref.objid)
elif ref_type is PDFRefType.PDF_OBJ_REF:
return self.resolve(ref.resolve())
if ref_type is PDFRefType.DICTIONARY:
return self.resolve(ref["D"])
if ref_type is PDFRefType.LIST:
return self.resolve(next(filter(lambda e: isinstance(e, PDFObjRef), ref)))
if ref_type is PDFRefType.NAMED_REF:
return self.resolve(self.document.get_dest(ref))
return None