Skip to content

openpdf-renderer: render annotation appearance streams (/AP /N) - #1606

Open
andreasrosdalw wants to merge 3 commits into
LibrePDF:masterfrom
andreasrosdalw:feature/renderer-annotation-appearances
Open

openpdf-renderer: render annotation appearance streams (/AP /N)#1606
andreasrosdalw wants to merge 3 commits into
LibrePDF:masterfrom
andreasrosdalw:feature/renderer-annotation-appearances

Conversation

@andreasrosdalw

@andreasrosdalw andreasrosdalw commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • OpenPdfCorePageRenderer only ever walked the page content stream, so annotations — stamps, highlights, square/circle markup, free-text callouts, filled-in form field appearances — never rendered at all, regardless of what the PDF producer baked into their /AP appearance streams. Any PDF using form fields or markup annotations rendered as if it had none.
  • After the page content stream finishes, each /Annots entry's normal appearance (/AP /N — directly a stream, or a sub-dictionary of states selected by /AS, e.g. a checkbox's /Yes vs /Off) is now rendered at its /Rect, per the PDF §12.5.5 placement algorithm: the appearance's /BBox is mapped through its own /Matrix, reduced to an axis-aligned bounding box, then fitted to /Rect by independent X/Y scale + translate (no rotation/shear beyond what /Matrix itself already encodes).
  • Reuses the existing renderForm() for the actual /Matrix//BBox-clip/content-stream handling, since an appearance stream is structurally a Form XObject — added a small shared formMatrixOf() helper so both call sites parse /Matrix the same way.
  • Skips Hidden/NoView-flagged annotations (/F bits 2/6) and Popup annotations, and skips annotations with no usable appearance stream. Deliberately does not synthesize a default appearance (e.g. a Link's usually-invisible border, or a Square/Circle's /IC//C//BS-derived look) when the PDF doesn't provide one — that's a separate, larger feature.
  • Also resets the page transform/clip to their initial (page-to-device) state before rendering annotations, since /Rect coordinates are defined in page space and a content stream with unbalanced q/Q (or a top-level cm) could otherwise leave stale transform/clip state behind from content-stream processing.
  • Updated openpdf-renderer/README.md (XObject/annotation coverage prose + the supported-features table) and the class-level Javadoc.

OpenPdfCorePageRenderer only ever walked the page content stream, so
annotations -- stamps, highlights, square/circle markup, free text
callouts, filled-in form field appearances -- never rendered at all,
regardless of what the PDF producer baked into their /AP appearance
streams. That's a real, common gap: any PDF with form fields or markup
annotations rendered as if they were blank.

After the page content stream finishes, render each /Annots entry's normal
appearance (/AP /N -- directly a stream, or a sub-dictionary of states
selected by /AS, e.g. a checkbox's /Yes vs /Off) at its /Rect, per the PDF
§12.5.5 placement algorithm: the appearance's /BBox is mapped through its
own /Matrix, reduced to an axis-aligned bounding box, then fitted to /Rect
by independent X/Y scale + translate. Reuses the existing renderForm() for
the actual Matrix/BBox-clip/content-stream handling, since an appearance
stream is structurally a Form XObject. Skips Hidden/NoView-flagged and
Popup annotations, and annotations with no usable appearance stream --
deliberately does not synthesize a default appearance (e.g. a Link's
usually-invisible border) when the PDF doesn't provide one.

Also resets the page transform/clip to their initial (page-to-device)
state before rendering annotations, since annotation /Rect coordinates are
defined in page space and a content stream with unbalanced q/Q (or a
top-level cm) could otherwise leave stale state behind.
@codacy-production

codacy-production Bot commented Jul 28, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 9 complexity · 0 duplication

Metric Results
Complexity 9
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

- Move the ANNOTATION_FLAG_HIDDEN / ANNOTATION_FLAG_NOVIEW constants next
  to the other constant fields at the top of the class instead of
  declaring them inline next to their first use.
- Test file: use the already-imported PdfWriter/Rectangle instead of
  fully-qualified names, and extract the shared annotation/appearance
  setup between the two new tests into buildAnnotatedPdf(boolean) to
  remove the duplication between them.
andreasrosdalw added a commit to andreasrosdalw/OpenPDF that referenced this pull request Jul 28, 2026
Consolidated into LibrePDF#1607 along with the README updates for LibrePDF#1604, LibrePDF#1605,
and LibrePDF#1606, so these four independent PRs (all based on master) don't each
carry their own overlapping README.md diff and risk merge conflicts with
each other depending on merge order.
andreasrosdalw added a commit to andreasrosdalw/OpenPDF that referenced this pull request Jul 28, 2026
Consolidated into LibrePDF#1607 along with the README updates for LibrePDF#1603, LibrePDF#1605,
and LibrePDF#1606, so these four independent PRs (all based on master) don't each
carry their own overlapping README.md diff and risk merge conflicts with
each other depending on merge order.
andreasrosdalw added a commit to andreasrosdalw/OpenPDF that referenced this pull request Jul 28, 2026
Consolidated into LibrePDF#1607 along with the README updates for LibrePDF#1603, LibrePDF#1604,
and LibrePDF#1606, so these four independent PRs (all based on master) don't each
carry their own overlapping README.md diff and risk merge conflicts with
each other depending on merge order.
Consolidated into LibrePDF#1607 along with the README updates for LibrePDF#1603, LibrePDF#1604,
and LibrePDF#1605, so these four independent PRs (all based on master) don't each
carry their own overlapping README.md diff and risk merge conflicts with
each other depending on merge order.
@sonarqubecloud

Copy link
Copy Markdown

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