fix(extract): a bookmark that shared a line left no trace, so count it
GREEN on the two tests from the previous commit. `PdfOutline` gains
`collided`, and the identity it buys is the point rather than the counter:
NODES IN == len(marks) + unresolved + collided. Measured on R761 with the
shipped function, 2 763 == 2 762 + 0 + 1, where before the same document
reported 2 762 marks and 0 unresolved and said nothing about the third
number. The lost node was `SVV - Forside`, sharing line 0 with the tree's own
root node.
THE RULE WAS CHOSEN BY MEASUREMENT, NOT BY ARGUMENT. Keeping both nodes as
sibling marks was run through `find_candidates` on R761's own text and text:
the candidate list goes 2762 -> 2763, and the first candidate closes with an
EMPTY span (0, 0) because two candidates open at one offset. The orphan check
deletes an empty span in silence, so keeping both loses the same node one step
later and adds a front-matter title that is in no fasit. First-in-tree-order
wins, the loser is counted.
Known-negatives, all measured rather than assumed: `outlined-krav.pdf` and
`outline-broken-dest.pdf` unchanged in outcome and reporting `collided == 0`;
`~/okf-test/dokumenter` byte-identical against a frozen `e1f4faa` export
(`diff -r` empty, 52 md); the pinned K2 bundle's concept count and per-row
ranks unchanged (7 passed). The arm stays off by default and no boundary rule
moved.
pytest -q: 1556 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3f8f03cff5
commit
525d68ee90
1 changed files with 21 additions and 3 deletions
|
|
@ -679,10 +679,22 @@ class PdfOutline:
|
|||
and raising would refuse a file over a defect in one of its bookmarks.
|
||||
Dropping silently is the third option this library refuses everywhere else,
|
||||
so the count is part of the return value.
|
||||
|
||||
`collided` is the same principle applied to the OTHER way a node leaves
|
||||
without a boundary. Two bookmarks can resolve to one line -- measured on
|
||||
the 701-page process code, its tree's root node and `SVV - Forside` both
|
||||
land on line 0 -- and only the first can become a mark, because two
|
||||
candidates at one offset give the first an empty span that the orphan check
|
||||
then deletes without a word. That was measured on that document and is why
|
||||
keeping both was felled rather than argued. What the count buys is the
|
||||
identity: NODES IN == len(marks) + unresolved + collided, so a document
|
||||
that loses several nodes this way says so instead of returning a shorter
|
||||
list that looks complete.
|
||||
"""
|
||||
|
||||
marks: tuple[OutlineMark, ...]
|
||||
unresolved: int
|
||||
collided: int = 0
|
||||
|
||||
|
||||
def _outline_page_and_top(doc: object, dest: object, action: object) -> tuple[object, float | None]:
|
||||
|
|
@ -770,6 +782,7 @@ def pdf_outline(
|
|||
offset += len(page_lines[number]) + 1
|
||||
|
||||
unresolved = 0
|
||||
collided = 0
|
||||
placed: dict[int, OutlineMark] = {}
|
||||
with pdfplumber.open(io.BytesIO(data)) as pdf:
|
||||
try:
|
||||
|
|
@ -835,9 +848,14 @@ def pdf_outline(
|
|||
# FIRST in tree order wins a shared line. Two marks on one line
|
||||
# would give the second an empty span, and the orphan check
|
||||
# deletes an empty span silently -- the same trap the proposer
|
||||
# documents for a second-pass candidate list.
|
||||
placed.setdefault(at, OutlineMark(line=at, level=level, title=title))
|
||||
return PdfOutline(tuple(placed[at] for at in sorted(placed)), unresolved)
|
||||
# documents for a second-pass candidate list. The loser is
|
||||
# COUNTED rather than dropped: `setdefault` alone made a lost
|
||||
# node indistinguishable from a node that was never there.
|
||||
if at in placed:
|
||||
collided += 1
|
||||
continue
|
||||
placed[at] = OutlineMark(line=at, level=level, title=title)
|
||||
return PdfOutline(tuple(placed[at] for at in sorted(placed)), unresolved, collided)
|
||||
|
||||
|
||||
def _normalise_outline(value: str) -> str:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue