fix: read the pdfium outline on pypdfium2 4.x and beyond 15 levels - #4165
fix: read the pdfium outline on pypdfium2 4.x and beyond 15 levels#4165junkreef wants to merge 2 commits into
Conversation
`extract_outline_from_pdfium` called `bookmark.get_title()` and
`bookmark.get_dest()`, which exist on pypdfium2 5. On 4.30
`PdfDocument.get_toc()` yields a `PdfOutlineItem` namedtuple instead, so the
call raised
AttributeError: 'PdfOutlineItem' object has no attribute 'get_title'
and any document outline was lost on a version this project explicitly supports
(`pypdfium2>=4.30.0,!=4.30.1,<6.0.0`). `PyPdfiumDocumentBackend.get_document_outline()`
raises there today.
The 4.30 namedtuple already carries what the 5 API reaches through the
destination object -- `page_index`, `view_mode` and `view_pos` -- so read either
shape and keep the view-mode handling in one place. Verified on a 2252-page
manual under 4.30.0: 1531 bookmarks, all with a resolved page and vertical
position.
Signed-off-by: Junpei Kishi <junkreef@longarch.net>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PdfDocument.get_toc()` walks the outline recursively and defaults to `max_depth=15`, dropping every entry below that with only a debug log. The docling-parse walker next to it is iterative and unbounded, so the two extractors disagreed on any document with a deeper table of contents, and `PyPdfiumDocumentBackend` silently lost those entries. Pass an explicit bound instead. It stays finite because the traversal is recursive and a malformed document should not exhaust the interpreter stack, but 128 is far past anything a real table of contents reaches. Signed-off-by: Junpei Kishi <junkreef@longarch.net> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
✅ DCO Check Passed Thanks @junkreef, all your commits are properly signed off. 🎉 |
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
cau-git
left a comment
There was a problem hiding this comment.
@junkreef Thanks for taking on maintenance work. Seeing this proposal I am considering if we should raise the minimum pypdfium2 version to 5.x instead. It is almost a year since 5.x was released.
In case pypdfium2 4.x support is really needed, the code should do explicit version checks.
| # lazy import (see module docstring) | ||
| from pypdfium2._helpers.misc import PdfiumError | ||
|
|
||
| if hasattr(bm, "get_title"): |
There was a problem hiding this comment.
Instead of probing the available API with introspection, please do a clean version check upfront (module level) and add conditionals on the version here.
| # no genuine table of contents reaches while staying far inside the recursion | ||
| # limit. (`extract_outline_from_docling_parse` walks iteratively and needs no | ||
| # such bound.) | ||
| _PDFIUM_OUTLINE_MAX_DEPTH = 128 |
There was a problem hiding this comment.
Do you have a legitimate PDF that uses more than 15 outline levels? I wonder which material would hit that limit, as pypdfium made the choice of 15 levels probably for a reason.
Two independent defects in
extract_outline_from_pdfium(), one commit each.1. It only spoke the pypdfium2 5 bookmark API.
bm.get_title()/bm.get_dest()do not exist on 4.30, where
get_toc()yields aPdfOutlineItemnamedtuple, so thecall raised
AttributeErrorand the outline was lost on a version this projectsupports. The namedtuple already carries what the 5 API reaches through the destination
object, so a small
_outline_entry()helper reads either shape and the view-modehandling stays in one place.
2. It truncated the outline at 15 levels.
get_toc()walks recursively and defaultsto
max_depth=15, dropping deeper entries with only a debug log, while thedocling-parse extractor next to it is iterative and unbounded. The bound is now passed
explicitly. It stays finite — the traversal is recursive, so a malformed document should
not exhaust the interpreter stack — but 128 is far past anything a real table of
contents reaches.
Verification
tests/test_pdf_outline.pygains seven tests: both bookmark shapes, entries without adestination, a view mode that carries no vertical position, blank titles, the page-height
cache, and a 20-level outline. Six of them fail on
v2.124.0.The deep-outline test builds its PDF from bytes rather than with a PDF library: nothing
that can author an outline is a dependency of this project, and the test should run
everywhere rather than skip.
Checked on a 2252-page manual under pypdfium2 4.30.0: 1531 bookmarks, all with a
resolved page and vertical position, where
v2.124.0raised.Issue resolved by this Pull Request:
Resolves #4160
Resolves #4161
Checklist:
user-facing behaviour or option changes)