Skip to content

Synthesize /Square annotation appearances - #6

Closed
sebgoubier wants to merge 1 commit into
soadzoor:mainfrom
sebgoubier:fix/square-annotation-appearance-synthesis
Closed

sebgoubier wants to merge 1 commit into
soadzoor:mainfrom
sebgoubier:fix/square-annotation-appearance-synthesis

Conversation

@sebgoubier

Copy link
Copy Markdown
Contributor

What this fixes

The /Square item of #2: Visible /Square annotation 0 has no usable normal appearance; native synthesis is not implemented.

The annotation that triggers it, probed from a real drawing, is:

/Subtype /Square  /Rect [938 2102 925 2158]  /Border [0 0 0]  /F 64
no /AP, no /C, no /IC, no /CA

A zero-width border with no interior colour. It paints nothing — and it takes the whole page down with it. That is what makes this worth fixing rather than tolerating: the correct appearance for this annotation is the empty one.

nativeAppearanceSynthesis.ts already had the mechanism and the precedent. /Link returns null for a zero-width border, documented there as "explicitly non-painting… not an unsupported visible annotation". /Square now follows the same rule, so the registry's policy — hidden annotations return null, visible unsupported ones fail — is untouched.

What the synthesis does

ISO 32000-1 12.5.6.8, matching what existing processors draw:

  • path = /Rect inset by half the border width (the border straddles the path)
  • /C strokes it, falling back to black when absent, exactly as /Link already does
  • /IC fills it; unlike /C it has no default, so an absent /IC simply leaves the square unfilled
  • operator B, f or S depending on which of the two colours paints
  • /CA 0 returns null; a translucent square raises the same typed "not yet synthesizable" error that a translucent Link border does
  • neither a painting border nor an interior colour → null

One deliberate call worth flagging: an empty /C is transparent (as the existing /Link tests already assert), but the declared border width still positions the path. Colour decides what is painted, width decides where the path runs — so a transparent border with an interior colour fills the inset rectangle, not the full /Rect. The test states this explicitly.

Three helpers became subtype-neutral so /Square could reuse them rather than duplicate their validation: readAnnotationColor(key, missing, label), readAnnotationBorderStyle(annotation, label, signal) and readAnnotationGeometry(annotation, label). /Link keeps its exact diagnostics through the label argument.

Tests

testSquareAppearanceSynthesis covers six squares in one fixture page: the non-painting one from the real file, a /C-less black border, an interior colour with a zero-width border, border plus interior, a transparent /C with an interior, and /CA 0. Each asserts the emitted content stream, so the inset arithmetic and the choice of B/f/S are pinned, not just the absence of a throw.

All six fail on main with the production error.

Checks

  • npm run test:file -- scripts/test-native-appearance-synthesis.mjs — passes, /Link and /Widget cases included
  • npm testtsc --noEmit clean, 36 / 37 fast files pass
  • npm run test:integration — 41 / 41
  • npm run test:unit — 66 / 68
  • the real drawing now converts through node PDFtoHEP.js

The two failing files (test-text-lod-core.mjs, test-room-segment-extractor.mjs) also fail on main without this change; they are Windows-only path failures (/C:/dev/... resolving to C:\C:\dev\...), unrelated here.

Scope

/Square only. /Circle is its twin in 12.5.6.8 and would reuse all of this plus the ellipsePath helper already in the file — happy to add it here or in a follow-up, whichever you prefer. Independent of #4 and #5.

Refs #2

A visible /Square without /AP failed the whole page, even when the
correct appearance for it is the empty one. The annotation found in a
real drawing declares /Border [0 0 0] with no /C, /IC or /CA: a
zero-width border and no interior colour, which paints nothing at all.

The synthesizer already had both the mechanism and the precedent. A
zero-width /Link border returns null, documented there as explicitly
non-painting rather than an unsupported visible annotation. /Square now
follows the same rule, so the registry policy stated on
resolveAnnotationAppearance is untouched: hidden annotations return
null, visible unsupported ones still fail.

The synthesis follows ISO 32000-1 12.5.6.8. The path is /Rect inset by
half the border width, since the border straddles it. /C strokes it and
falls back to black exactly as /Link does; /IC fills it and has no
default, so an absent /IC simply leaves the square unfilled. The
operator is B, f or S depending on which colour paints. /CA 0 returns
null and a translucent square raises the same typed error a translucent
Link border does.

One deliberate reading: an empty /C is transparent, as the existing
/Link tests already assert, but the declared border width still places
the path. Colour decides what is painted, width decides where the path
runs, so a transparent border over an interior colour fills the inset
rectangle rather than the whole /Rect. The test states this.

Three helpers became subtype-neutral rather than duplicated:
readAnnotationColor, readAnnotationBorderStyle and
readAnnotationGeometry. /Link keeps its exact diagnostics through the
label argument.

Refs soadzoor#2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@soadzoor

Copy link
Copy Markdown
Owner

I cherrypicked your changes, but extended them with support for Underline border (/BS << /S /U … >>), and Cloudy border (/BE << /S /C … >>) features, as GPT suggested. Note that I don't have specific PDFs to test these features out - if you do, please have a look in the latest release I plan to deploy today (1.30). Thank you!

@soadzoor soadzoor closed this Sep 17, 2026
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.

2 participants