Skip to content

Handle inline (non-reference) structure elements#1379

Open
gaoflow wants to merge 1 commit into
jsvine:developfrom
gaoflow:fix-inline-structure-element
Open

Handle inline (non-reference) structure elements#1379
gaoflow wants to merge 1 commit into
jsvine:developfrom
gaoflow:fix-inline-structure-element

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 30, 2026

Copy link
Copy Markdown

The bug

PDF.structure_tree / Page.structure_tree raise KeyError when a structure element is embedded inline — as a direct dictionary in another element's /K — rather than as an indirect reference:

File ".../pdfplumber/structure.py", line 407, in prune
    children = prune(children)
File ".../pdfplumber/structure.py", line 406, in prune
    element, children = s[repr(ref)]
KeyError: "{'Type': /'StructElem', 'S': /'P', 'Pg': <PDFObjRef:3>, 'K': 0}"

Per PDF 1.7 §14.7.2, the /K value "may be ... a structure element dictionary ... or an array of such". _make_element already has an explicit branch for an inline-dict child (# a single object.. ugh...), and _parse_struct_tree / _resolve_children already handle an inline dict at the root — but the per-child traversal does not, so a nested inline element crashes.

Root cause

In _parse_struct_tree's main loop, only PDFObjRef children are pushed onto the work queue (and thus registered in the s lookup). An inline structure-element dict is silently skipped, so it never gets an s entry — and prune() then hits s[repr(ref)]KeyError. _resolve_children follows the same PDFObjRef-only rule, so even past the crash an inline child would be dropped from the output.

The fix

Two symmetric one-line changes: in both the traversal loop and _resolve_children, treat a child that is a structure-element dictionary the same as an indirect reference (isinstance(child, (PDFObjRef, dict))). MCID dicts were already handled/continued earlier and are not in the lookup, so they're unaffected; prune() needs no change once inline elements are registered.

Tests

test_inline_structure_element builds a minimal tagged PDF in memory whose Document element holds an inline P element in its /K array, and asserts the parsed tree on both the document-level (pdf.structure_tree) and page-level (page.structure_tree) code paths. It fails on develop with the exact KeyError and passes with the fix; tests/test_structure.py + tests/test_mcids.py + tests/test_basics.py stay green (39 passed); black/isort/flake8 clean. CHANGELOG updated.

(None of the 65 shipped test PDFs exercise inline structure children, so this path was previously untested.)

A structure element may be embedded directly as a dictionary in another
element's /K, rather than via an indirect reference (PDF 1.7 section 14.7.2,
which _make_element and the struct-tree root already accommodate). The
per-child traversal only enqueued PDFObjRef children, so an inline child was
never registered, and structure_tree raised a KeyError in prune(). Enqueue
and resolve inline structure-element dicts too.
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.

1 participant