Skip to content

Change an annotation's kind in place - #103

Merged
bobsingor merged 1 commit into
embedpdf/mainfrom
embedpdf/feature/set-subtype
Nov 3, 2026
Merged

bobsingor merged 1 commit into
embedpdf/mainfrom
embedpdf/feature/set-subtype

Conversation

@bobsingor

Copy link
Copy Markdown
Contributor

Adds EPDFAnnot_SetSubtype(): change an annotation's kind in place by writing its /Subtype and nothing else. The annotation keeps its dictionary, object number, place in /Annots and every other entry, so its replies (/IRT), popup, group and author stay with it. /AP is left as it is; the caller draws the new kind with EPDFAnnot_GenerateAppearance().

Today the only way to turn a highlight into a squiggly line is delete and create, which loses all of that. ISO 32000-2 describes the four text markup subtypes with one table of entries (12.5.6.10), and Square and Circle with another (12.5.6.8), so within those a kind change is a new name and a new drawing.

  • A mechanic: which kinds may become which is the engine's decision, not this function's.
  • Refused: a null or invalid handle, a current or new subtype FPDFAnnot_IsSupportedSubtype() refuses, and a widget or a popup either way. A widget belongs to its form field and a popup to its parent.
  • Nothing to drop: FPDFAnnot_GetSubtype() reads the dictionary, and each render builds a new annotation list. A form-fill page view loaded before the change keeps the old subtype until the page loads again, as after FPDFPage_CreateAnnot(); the engine opens no form-fill environment.

Tests

  • SetSubtypeChangesTheKindInPlace: the refusals; the entries, index, color, contents and /AP kept; a redraw draws the new kind; a save round trip.
  • LayerSetSubtypeKeepsTheObjectAndAborts: an aborted transaction leaves the highlight; a committed one keeps the object number and survives a reopen from the saved delta.
  • The C API list.

Run natively on darwin-arm64: both pass, and so do all 170 *EPDFAnnot* embedder tests. check-c-api.py and check-fork-tags.py --strict pass.

The engine side, update() taking a sibling subtype, is planned on embedpdf/embed-pdf-viewer feature/annotation-kind-change (its docs are committed there). It needs this released and pinned.

EPDFAnnot_SetSubtype writes /Subtype and nothing else. The annotation
keeps its dictionary, object number, place in /Annots and every other
entry, and its /AP until the caller draws the new kind. It is a
mechanic: which kinds may become which is the caller's decision.
ISO 32000-2 describes the four text markup subtypes with one table of
entries, and Square and Circle with another.

A widget belongs to its form field and a popup to its parent, so
neither changes kind and nothing becomes one. A subtype the annotation
API doesn't create is refused too.

FPDFAnnot_GetSubtype() reads the subtype from the dictionary, and each
render builds a new annotation list, so there is nothing to drop. A
form-fill page view loaded before the change keeps the old subtype
until the page is loaded again, as after FPDFPage_CreateAnnot().

Tests: on a new document, the refusals, the entries and drawing kept,
a redraw that draws the new kind, and a save round trip; on a layer,
an aborted transaction leaves the highlight, and a committed one keeps
its object number and survives a reopen from the saved delta.
@bobsingor
bobsingor merged commit 66031e9 into embedpdf/main Nov 3, 2026
1 check passed
@bobsingor
bobsingor deleted the embedpdf/feature/set-subtype branch November 3, 2026 19:05
roanutil pushed a commit to roanutil/embed-pdf-viewer that referenced this pull request Nov 4, 2026
The patch resolver takes a subtype the annotation can become: a
highlight, underline, squiggly line or strikeout another of the four,
a square a circle and back. ISO 32000-2 describes each group with one
table of entries, and our declarations give them the same fields
(siblings.ts; a test holds them to it: same owners, read-backs, colors
and spaces, apart from a strikeout's intent). The patch is the new
kind's, checked against its schema. Any other subtype is refused,
naming the kinds it can become. A value the new kind has no field for
is refused, never dropped: the caller clears it first, in the same
change.

The prediction makes the annotation the new kind, a field only it has
reading null, and annotationPatchBetween states the subtype when the
kind changed.

Engine core only. The engines still keep /Subtype as it was until the
fork's EPDFAnnot_SetSubtype (embedpdf/runtime#103) is released and
pinned; the writer, undo and conformance follow with it.
roanutil pushed a commit to roanutil/embed-pdf-viewer that referenced this pull request Nov 4, 2026
Pin the runtime to f2dd40897, which has EPDFAnnot_SetSubtype
(embedpdf/runtime#103, #108); the bindings gain it and #104's
EPDFAnnot_SetNoColor.

An update whose resolved patch names a sibling kind calls
EPDFAnnot_SetSubtype before writing the fields, so they are written as
the new kind's and the appearance is drawn as one (appearanceImpactOf
already answers regenerate for a changed subtype). The undo fallback
keeps a changed subtype, so an undo after a later edit puts the old
kind back by value; the usual undo restores the dictionaries.

Conformance, on the local and the cloud engine: a highlight becomes a
squiggly line with its ref, nm, place in the order, reply and author;
a square becomes a circle with a new color and back; another family
and a replacement's intent are refused, and clearing the intent first
in the same change works; undo, redo, and undo after a restyle. The
prediction suite predicts four kind changes as the engine writes them.

The contract's update operation and the wire shape don't change, so
there is nothing for api:sync.
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