Skip to content

fix: read the pdfium outline on pypdfium2 4.x and beyond 15 levels - #4165

Open
junkreef wants to merge 2 commits into
docling-project:mainfrom
junkreef:fix/pdfium-outline-pypdfium2-4x
Open

fix: read the pdfium outline on pypdfium2 4.x and beyond 15 levels#4165
junkreef wants to merge 2 commits into
docling-project:mainfrom
junkreef:fix/pdfium-outline-pypdfium2-4x

Conversation

@junkreef

@junkreef junkreef commented Sep 4, 2026

Copy link
Copy Markdown

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 a PdfOutlineItem namedtuple, so the
call raised AttributeError and the outline was lost on a version this project
supports. 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-mode
handling stays in one place.

2. It truncated the outline at 15 levels. get_toc() walks recursively and defaults
to max_depth=15, dropping deeper entries with only a debug log, while the
docling-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.py gains seven tests: both bookmark shapes, entries without a
destination, 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.0 raised.

Issue resolved by this Pull Request:
Resolves #4160
Resolves #4161

Checklist:

  • Documentation has been updated, if necessary. (not necessary — internal helper, no
    user-facing behaviour or option changes)
  • Examples have been added, if necessary. (not necessary)
  • Tests have been added, if necessary.

junkreef and others added 2 commits September 4, 2026 05:34
`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>
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

DCO Check Passed

Thanks @junkreef, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@cau-git cau-git left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pdfium outline is silently truncated at 15 levels extract_outline_from_pdfium raises AttributeError on pypdfium2 4.x

2 participants