Skip to content

Editor: imported pages bring their structure elements (#183) - #226

Merged
timzifer merged 3 commits into
mainfrom
claude/issue-183
Oct 14, 2026
Merged

timzifer merged 3 commits into
mainfrom
claude/issue-183

Conversation

@timzifer

Copy link
Copy Markdown
Owner

Fixes #183. Stacked on #225 (merge that first; this PR then shows only its own commit).

API

type ImportOptions struct {
	// …
	Structure bool // the structure elements whose content is on imported pages
}

What is brought (internal/pdfedit/structure.go)

  • The elements of the source's /StructTreeRoot with content on an imported page (marked content, MCRs, OBJRs), with the elements above them; an element left without content is dropped.
  • Placement. Where the result has a tree, the elements follow its own in a /Part under its single /Document element, or under its root when it has none: the source's own /Document becomes the Part, or a Part is made around its top elements. The Part gets the source catalogue's /Lang where it differs from the result's. A result without a tree (NewEditor, or an untagged edited document) takes the first tree brought as its own; further sources become Parts under its Document.
  • Keys. /StructParents of pages, /StructParent of their annotations and /StructParents//StructParent of the form XObjects they draw are renumbered from the result's /ParentTreeNextKey; the parent tree is written anew as one flat number tree (the result's entries plus the new ones). A page imported twice is tagged once, its first copy (the second copy and its annotations have no keys). A form XObject that only pages which did not come draw as tagged content loses its keys rather than keep a dangling one.
  • References. /Pg, MCR /Stm, OBJR /Obj (the first copy of the annotation) and /Ref are mapped to the copies; references to elements that are not kept become null or are left out.
  • Maps. /RoleMap merged; where the result maps a type otherwise, the copied elements are written with what the source means by it (Heading → H1 under a result that maps Heading → P). /ClassMap merged, a class defined otherwise renamed Cls_2 (in /C too). /IDTree rebuilt with the kept elements, an identifier the result has renamed id_2. /Namespaces joined.
  • /MarkInfo gets /Marked true, the result's other entries kept.
  • Pages of the edited document imported into itself bring nothing more, as for outline and destinations.

Deviation from the issue's text: the parent tree is written as one flat /Nums array instead of being extended in place; number trees cannot take more entries without their /Limits being rebuilt, as for the destinations tree in #135.

Tests

  • TestImportStructure: into a tagged document (role, class and ID conflicts, Lang of the part), into an untagged one, into a new one, from two documents into a new one; save and update. Every case checks the tree as Document.Structure reads it and the parent tree against the elements (parentTreeErrors: every page, annotation and form key leads to the elements that name its content, every entry is used, keys below /ParentTreeNextKey).
  • TestImportStructureTwice, TestImportStructureOptimised (object streams, linearised, pruned, joined, compressed), TestImportNoStructure (unchanged default).
  • Corpus API run (cmd/corpus run -api) gets the step import structure: the pages looked at, imported with their structure into the document itself (opened a second time), must keep as much marked content in the tree as the originals. pdf.js corpus, 3 pages per file: no difference; only the refusals every other save step has (signed, no assembly permission).
  • go test ./..., -tags cera_nogyre green locally; lint clean but the CRLF noise.

Docs: docs/features.md updated (editor list, "not done" list, tagged PDF row).

The /BBox of an annotation's appearance stream is mapped onto its /Rect,
and the border of a field lies on the edge of that box. With the box off
the pixels, the exact clip and the border's edge, both antialiased,
lightened the edge pixels twice: a field Editor.SetFields wrote was drawn
with other edges than the same value shown from a FormState, which is not
clipped. The clip of an annotation's appearance is now rounded outward to
whole device pixels when it stays on the axes, as PDFium, MuPDF, Poppler
and Ghostscript clip rectangles. The accuracy report against PDFium is
unchanged.

overlayFile puts its fields off the pixels, which the test held them to.

Fixes #222.
The first fix of #222 rounded the clip of every annotation's appearance
outward to whole pixels. That made the render of an appearance stream
differ from the page it is flattened to, whose content clips exactly: the
corpus API run reported 41 files of pdf.js whose flattened first page
differed, by up to 4.9 % of its pixels.

The other way round: the appearance cera generates for a widget is
clipped to the widget's box exactly, as the stream Editor.SetFields
writes for it and the form Editor.Flatten draws are. The render with a
FormState, the saved file and the flattened page are drawn alike at any
scale; the border of a field off the pixels is lighter on its outer edge
in all three, as the appearance streams of documents are.

The corpus API report and the accuracy report against PDFium are the same
as on main.
ImportOptions.Structure brings the elements of the source's structure
tree whose content is on imported pages, with the elements above them;
an element left without content is dropped. Where the result has a tree,
the elements follow its own in a Part under its Document element (or its
root): the source's Document becomes the Part, or one is made around its
elements, with the source's /Lang where it differs. A result without a
tree takes the first one brought as its own.

The keys of the parent tree are numbered anew after the result's: pages
(/StructParents), their annotations (/StructParent) and the form XObjects
they draw. A page imported twice is tagged once, its first copy; a form
only pages that did not come draw as tagged content loses its keys. Marked
content and object references are mapped to the copies. Role maps are
merged, a type the result maps otherwise written as what the source means
by it; a class the result defines otherwise is renamed Cls_2, an
identifier the result has id_2; namespaces are joined. /MarkInfo says
/Marked.

The corpus API run gets a step: the pages looked at, imported with their
structure into the document itself, keep their marked content in the
tree. Over the pdf.js corpus it finds no difference.

Fixes #183.
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.

ImportPages: bring the structure tree along

1 participant