M11 — Annotations and optional content
State: to do — Depends on: M09
Goal
Review, freeze and layer a received document: author every markup annotation a reviewer uses, with an appearance every viewer draws alike; remove annotations by subtype or author; flatten them selectively, as their flags and layers say; add links to existing pages; and read, create, remove and flatten optional content, down to a stamp that prints and does not show.
A case file is annotated before it is filed — a highlight on the clause that matters, a "REÇU" stamp on the exhibit, a note to a colleague — and frozen when it leaves the office, so that what the court sees is what the lawyer saw. Two failures are ordinary today: an annotation that looks different in each viewer, because the library wrote no appearance and each viewer invents one; and a flattening that prints what the screen hid, or drops what the printer showed, because it ignored the flags and the layers. This milestone makes the appearance ours and the flattening faithful, and it gives optional content — until now only carried by M06's merge — a model that the extraction (M15), the redaction (M19) and the rasterizer (M25) will all read.
Scope
In:
- the annotation model, read side: every subtype typed with its common, markup and subtype-specific
entries — flags, author, dates, unique name, color, opacity, popup, reply relation (
/IRT,/RT), review state, layer, appearance and appearance state — and a JSON listing of a document's annotations; - authoring highlight, underline, strike-out and squiggly from quads; text notes with popups, replies and
review states; free text, plain, as a callout and as a typewriter; stamps — the standard names, the caller's
own text ("RECEIVED", "PAID", "REÇU LE 26/09/2026"), images, and any of M09's
PdfStampmarks placed as a stamp annotation — the form a DocMDP P=3 certification permits where page content is forbidden, which M09 left here; square, circle, line, polygon, polyline, ink and caret; the file-attachment icon over an attachment M06 created; - appearance streams generated for every subtype we author, and on demand for annotations that lack one;
- removal by subtype, author, date or predicate, taking popups and replies along;
- selective flattening: the modes qpdf names (
all,print,screen), honoring/Fand/OC, by subtype or predicate, generating missing appearances first; - link annotations on existing pages:
GoToto an explicit or named destination,URI,GoToR, with/QuadPointsfor links that span lines; - in a tagged document, every annotation we add placed in the structure tree (
AnnotorLink, with itsOBJRand/Contents), and every one we remove or flatten taken out of it, so that a PDF/UA input stays so; - optional content: groups, membership dictionaries and visibility expressions, configurations and their usage-driven auto-state, read into a model with a visibility evaluator per event (View, Print, Export); groups created and content stamped into them; groups removed and layers flattened, rewriting the content streams that reference them; M06's merge given the typed model; print-only and screen-only stamps;
- validation rules on annotations and layers added to M02's structural profile; M03's version table gains the rows these features need;
- the tool's verbs:
annotations,flatten,layers.
Out, explicitly:
- widgets: field appearances, filling and form flattening — M16, which reuses this milestone's flattener; the flattener here leaves every widget alone;
/Redactannotations authored or applied — M19; read and listed here;- annotations exchanged as XFDF or FDF — M16, which brings XFDF;
- the text under a markup annotation, a comment summary quoting it, and quads from a text search — M15; M11 takes quads from the caller;
- screen, movie, sound, 3D and rich-media annotations authored — never (ADR 37); they are read, kept, and flattened to their appearance with the loss reported;
- signature appearances — M26; rendering annotations — M25, from the appearances written here;
- generating appearances as part of a PDF/A conversion — M21, which calls this milestone's generator;
- cloudy borders (
/BE /S /C) generated — drawn plain when we must draw one, and reported; ISO 19593 processing steps — not planned; - text that needs shaping or bidirectional reordering laid out properly by the core — HarfBuzz and UAX #9 live
beside the HTML engine (ADR 43): M08's
simple path draws it in logical order and reports it (
text.shaping-required), as it does for M09's stamps, and M12.6's appearances rendered from an HTML fragment are the way to set it.
Design
Where it lives
In the core, beside the page model M06 and M09 built:
Annotations/ PdfAnnotation and its typed views, builders, appearance generators, removal, flattening
OptionalContent/ the model, the visibility evaluator, group creation, removal and flattening
Content/ the marked-content filter (below), the seed of M19's editing pipeline
| Type | Responsibility |
|---|---|
PdfAnnotationCollection | A page's annotations, enumerated lazily from /Annots; add, remove, flatten |
PdfAnnotation and its views — PdfTextMarkupAnnotation, PdfTextAnnotation, PdfFreeTextAnnotation, PdfStampAnnotation, PdfShapeAnnotation, PdfLineAnnotation, PdfPolyAnnotation, PdfInkAnnotation, PdfCaretAnnotation, PdfLinkAnnotation, PdfFileAttachmentAnnotation, PdfPopupAnnotation, PdfWidgetAnnotation (read only), PdfUnknownAnnotation | Typed read of each subtype; an unknown subtype is kept and listed, never dropped |
PdfQuad | Four points, in the order viewers read (below) |
PdfMarkupOptions and the per-subtype option records | Immutable: author, contents, color, opacity, dates, unique name, border, layer, flags |
PdfAnnotationFilter | Subtypes, authors, a date range, or a predicate — for removal and flattening |
PdfFlattenOptions | Mode, subtypes, layer policy (Preserve or Apply), whether missing appearances are generated first |
PdfOptionalContent | The document's groups, memberships and configurations; create, remove, flatten |
PdfOptionalContentGroup, PdfOptionalContentMembership, PdfVisibilityExpression, PdfOptionalContentConfiguration, PdfLayerUsage | The typed model of ISO 32000-2 §8.11 |
PdfLayerVisibility | The evaluator: for a configuration and an event, the state of every group and whether a piece of content is visible |
PdfPrintOnlyStamp | A stamp that prints and does not show, or the reverse, in the form the table below chooses |
Every operation that changes a document takes M03's CancellationToken and IProgress<PdfProgress>, adding the
stage Flattening, and returns M06's PdfOperationReport, as M09's content operations do, with the codes
listed below.
Reading annotations
/Annotsis walked lazily, page by page; an entry that is not a dictionary, a dictionary that is a stream, a/Popupwhose/Parentis not its markup, an/IRTchain with a cycle — each is listed as it is and reported, never followed twice. Walks are iterative with visited sets, bounded by the index: M11 adds no reader limit.- Text entries (
/Contents,/T,/Subj) decode through #36's text-string reading;/RCrich text is XHTML, parsed withXmlReader, DTDs prohibited, its size bounded by the string's own, and read as text. - The listing — page, subtype, rectangle, flags, author, dates, contents, reply relation, state, layer,
whether an appearance exists — serializes to documented JSON; it is what the
annotationsverb prints and what M16's XFDF maps to.
Authoring
- Common entries.
/Pto the page,/Fwith Print set (every PDF/A part wants it),/Cand/CAfrom the options,/T,/Contents. Dates (/M,/CreationDate) and the unique name/NMcome from the caller or are left out; when a unique name is needed, it is derived deterministically from the page, the subtype and the annotation's content. Acrobat writes a GUID and the clock; invariant 6 forbids both. - Where it is written. Through M09's page edits: the annotation object and its appearance added, the page's
/Annotsextended, as an incremental update or a full rewrite. M04's write guard classifies the change (Annotationfor markup and links) before anything is written. - Quads. Four points each, written in the order Acrobat writes and every viewer reads — upper left, upper
right, lower left, lower right — though the specification's text describes a counter-clockwise order; read
in either.
/Rectis the union of the quads grown by the appearance's overhang. - Replies and states. A reply is a text annotation with
/IRTto its parent and/RT /R; a review state is one with/Stateand/StateModel(Review: Accepted, Rejected, Cancelled, Completed, None;Marked). A popup is created with every note,/Open false, its/Parentset.
Appearance streams
Every annotation we author carries /AP with a normal appearance: viewers regenerate missing appearances each
in their own way, PDF/A-2 and 3 require one on every annotation but popups and links, and PDF 2.0 requires it
more widely still — M02's version-aware shape rules report its absence there
(ADR 44). An appearance is a form
XObject written through M08's content-stream writer, numbers in "0.####", fonts embedded and subsetted by M08,
colors in the space the options give.
| Subtype | Built from | The appearance |
|---|---|---|
| Highlight | Quads, color, opacity | Each quad filled under an ExtGState with /BM /Multiply and /CA, so the text shows through |
| Underline, StrikeOut | Quads | A stroke along each quad's bottom edge, or through its middle, its width proportional to the quad's height |
| Squiggly | Quads | A zigzag along each quad's bottom edge, its period proportional to the height |
| Text | Position, icon name, color | Our own vector drawing of the named icon — Comment, Key, Note, Help, NewParagraph, Paragraph, Insert — with NoZoom and NoRotate set; a popup, which has no appearance |
| FreeText | Text, font, size, color, alignment, border, fill, callout line and its ending, /IT | The text laid out by M08 inside /Rect less /RD; /DA and /DS written; /RC written as a minimal XHTML body so that Acrobat can edit it |
| Stamp | A standard name, the caller's text with an optional date and author line, an image or form XObject, or an M09 PdfStamp | A framed text in an embedded font; the image scaled into /Rect (JPEG and PNG passed through by M07's containers); or the XObject M09 compiles the mark into |
| Square, Circle | /Rect, /BS width and dash, color, interior color, /RD | A rectangle, or an ellipse of four Bézier curves, inset by half the border width |
| Line | /L, both endings, leader lines and their extensions, caption | The line, its endings (Square, Circle, Diamond, open and closed arrows and their reversed forms, Butt, Slash), leaders, the caption in an M08 font |
| Polygon, PolyLine | /Vertices, endings for a polyline | A closed or an open path |
| Ink | /InkList | Each stroke as a polyline through the points given, round caps and joins; no smoothing that would move a point |
| Caret | /Rect, /Sy | A caret, or a paragraph mark drawn as a path |
| FileAttachment | Icon name | Our own drawing of PushPin (the default), Graph, Paperclip or Tag, the names viewers use — GraphPushPin and PaperclipTag, as ISO 32000-1's table prints them, read as the same icons; the file itself is M06's |
| Link | — | None by default (/Border [0 0 0]); a visible border on request |
| Watermark | A form XObject, optional /FixedPrint | The XObject — the annotation form of a print-only stamp |
- Missing appearances.
EnsureAppearances(filter)generates one for any annotation of the subtypes above that lacks it, from its own entries; an entry we cannot honor — a cloudy border, text that needs shaping — is drawn as closely as the table allows, or left without an appearance, and reported either way. - Text in free text, stamps and captions goes through M08's registry — the OFL sans face by default, as M09's
marks — and its simple path, with per-cluster fallback, CJK included. Text that needs shaping (Arabic, Hebrew,
the Indic scripts) or bidirectional reordering is drawn in logical order and reported by M08
(
text.shaping-required); a caller who needs it right supplies the appearance, as M12.6 will from HTML. - Conformance.
/Chas 0, 1, 3 or 4 components; under a PDF/A claim the appearance's color space must agree with the output intent, as for M09's marks and M10's codes, and every rule below is checked before a byte is written. A conflict follows M09'sPdfConformancePolicy:Refuse, the default, throwsPdfConformanceExceptionnaming the claim and the clause;RemoveClaimwrites the annotation, removes the claim from the XMP and reportsannotate.conformance-claim-removed.
Tagged documents
On a document with a structure tree, M09's rule — nothing added makes a tagged document untagged — holds for annotations too, in the terms PDF/UA-1 §7.18 sets:
- a markup annotation we add goes into an
Annotelement holding itsOBJR, with/Contentsas its description (the note's text, or a generated one: "Highlight", "Stamp: REÇU"); a link into aLinkelement with itsOBJRand/Contents;/StructParentset and the parent tree extended; the page's/Tabsset to/S; - the element goes under the structure element the caller names, or, when none is named, at the end of the
page's last element in the tree, reported as
annotate.tag-appended— the text under a highlight or a link is M15's to find, and wrapping its marked content inside theLinkelement waits for it; - popups are read differently by PDF/UA-1's readers — the PDF Association's tagging guidance leaves them untagged — so slice 6 settles their place against veraPDF's PDF/UA-1 profile and records the choice;
- a removed annotation takes its
OBJRwith it, and an element left empty is removed, bottom-up.
Removal
- By
PdfAnnotationFilter: subtypes, authors (/T, compared as decoded text), a date range on/Mor/CreationDate, or a predicate over the typed view. Widgets are excluded unless named, and then refused: removing a widget is removing a field's face, which is M16's. - A removed markup takes its popup and its replies (the transitive
/IRTclosure, iteratively); a reply whose parent stays is kept. A file-attachment annotation removed takes its embedded file, reported, since M06's API lists it at annotation level only. - Every other reference to a removed annotation — an
OBJR, a/Popup, an/IRTfrom outside the filter, an/AAor a JavaScript naming it — is dropped by its owner or reported, as M06 does for removed pages.
Flattening
- The placement algorithm is ISO 32000-2 §12.5.5's: the appearance's
/BBoxtransformed by its/Matrix, the smallest rectangle around the result mapped onto/Rect, and the page content extended withq … cm /Fx Do Qunder that mapping, through M09's overlay, its resource name chosen by M09's collision-free merge. The appearance stream is reused as it is: no copy, no re-encoding. - Modes, with qpdf's names and meaning:
All— every annotation that is neither Hidden nor, for an unknown subtype, Invisible;Print— those with the Print flag, as a printer would show them;Screen— those without NoView, as a screen would./ASselects the appearance state; an annotation without an appearance is given one first whenGenerateMissingAppearancesis on (the default for the subtypes above), and otherwise left in place and reported. - Layers. An annotation with
/OCis wrapped in/OC /name BDC … EMCnaming the same group or membership underPreserve(the default: the layer keeps deciding), or drawn or dropped by the default configuration's state for the mode's event underApply. - NoZoom and NoRotate. A viewer keeps such an icon unscaled and upright; flattening draws it at 100 % and,
on a page with
/Rotate, counter-rotates it about its upper-left corner, as the viewer would show it. - What flattening loses is reported: a note's text, which lived in
/Contentsand its popup; a screen annotation's rendition, flattened to its poster appearance (ADR 37); the reply thread. - Excluded: widgets (M16), links (kept working unless the filter names them, then removed — they have no appearance to draw), popups (removed with their parent).
- Imposition. M09's N-up drops the annotations it cannot carry onto a sheet (
geometry.annotations-dropped); it gains the option to flatten them first, inPrintmode, so that an imposed case file prints what the pages showed. - Tagged documents. A flattened appearance that carries text a reader should hear — free text, a stamp's
text — becomes marked content under the element that held the annotation, re-typed from
AnnottoSpan; every other flattened appearance is an/Artifact. - Signatures. Flattening changes content: an
Otherchange in M04's terms, treated as M09 treats a stamp — refused under a certification unless the caller allows it, and after approval signatures written as an incremental update and reported as lying outside every signature (below).
Optional content: the model
- Groups: name (decoded per #36 — UTF-16LE behind its mark and UTF-8 in PDF 2.0 tolerated),
/Intent, and the usage dictionary:View,Print,Export,Zoom,Language,User,PageElement,CreatorInfo. - Memberships:
/OCGs(one group or an array; nulls skipped) with/P(AllOn, AnyOn — the default —, AnyOff, AllOff), or/VE, which takes precedence. A membership naming no valid group has no effect. - Configurations:
/Dand each of/Configs—/BaseState(ON, OFF, and Unchanged in alternates),/ON,/OFF,/Intent,/Order(nested arrays with labels),/RBGroups,/Locked,/AS. - References:
/OC /name BDCin content streams (the name resolved through the resources'/Properties),/OCon form and image XObjects, on annotations — found through M07's resource-usage walk, which already reaches nested forms, Type 3 glyphs, patterns and annotation appearances.
The visibility evaluator
For a configuration and an event (View, Print or Export):
- the base state, then
/ON, then/OFF— a missing/Dread as a default configuration with every group on, as viewers read it; - then, if the event is given and
/ASlists it, each listed group takes the state its usage gives for that event's categories —ViewState,PrintState,ExportState;Zoomwhen the caller gives a zoom;LanguageandUseronly when the caller gives a context, otherwise left as they were; - a group whose
/Intentthe configuration does not include is visible, as the specification says; - a membership is evaluated by its policy or its expression; content under nested sections is visible only if every enclosing section is.
Visibility expressions and /Order are walked iteratively with visited sets — an expression may reach itself
through indirect arrays — so no bound on depth is needed and none is added. /RBGroups and /Locked change a
viewer's panel, not the initial state; a configuration that turns on two groups of one radio set is reported.
The evaluator is what M15's extraction and M19's sanitization consult for "hidden", and what M25's rasterizer
draws by. pdf.js applies a group's View and Print usage directly under its display and print intents, where the
specification applies usage only through /AS: where a document's usage and /AS disagree, the evaluator
follows the specification, and the acceptance records the difference rather than bending to the referee.
Creating layers and print-only stamps
CreateGroup(name, usage, intent)adds a group to/OCGs, to/D(on or off) and to/Order; M09'sPdfStamphas the slot for a group or a membership, which this milestone fills: the mark is written inside/OC … BDC … EMC.- Print-only and screen-only stamps. Two forms exist, and viewers honor them differently: the annotation
form — a Watermark annotation (PDF 1.6) with the Print and NoView flags, read-only and locked, its appearance
the XObject M09 compiles the
PdfStampinto,/FixedPrintwhen the caller wants the size fixed on paper — whose flags Acrobat, pdf.js and poppler all honor; and the layer form — the mark in a group whose usage says Print ON and View OFF, listed ON in/Dwith/ASentries for the View and Print events — which Acrobat and pdf.js honor, while a viewer that ignores/ASshows it on screen as well, the safe direction for a "COPY" mark.
| Input | PdfPrintOnlyStamp in its Auto form writes | Why |
|---|---|---|
| Untagged, no PDF/A claim | The annotation form | The widest support |
| Tagged, no PDF/UA or PDF/A claim | The layer form, the stamp as /Artifact /Pagination /Watermark | An annotation must be in the structure tree; a watermark is not content |
| A PDF/UA-1 claim, no PDF/A claim | The annotation form, tagged as an Annot element with OBJR and a /Contents giving the mark's text, the page /Tabs /S | PDF/UA-1 §7.10 (Matterhorn 20-003) forbids /AS in an optional-content configuration, which the layer form needs and M13 checks as a conflict; §7.18 has every annotation tagged, which this milestone already does |
| A PDF/A claim | Nothing under Refuse, M09's default: PdfConformanceException; under RemoveClaim, the form above for the document, the claim removed and reported (stamp.conformance-claim-removed) | Every part forbids the NoView flag; part 1 has no optional content; parts 2 and 3 forbid /AS. On part 4 the layer form is allowed only once veraPDF's PDF/A-4 profile has been seen to accept it on a corpus document |
Removing and flattening layers: the marked-content filter
Removing a group fixes it OFF for good; flattening fixes every group at the state the chosen configuration and event give. Both reduce to the same operation: partial evaluation of every membership, then a rewrite of every content stream, XObject and annotation that references a group.
- Memberships are simplified with the fixed groups substituted: a decided one becomes visible (its wrapper dropped) or hidden (its content removed); an undecided one is rewritten as a new membership over the groups that remain, the original left untouched while anything else shares it.
- Content streams are rewritten by a filter over M08's
PdfContentReader, written back through itsPdfContentBuilder. The reader takes a page's streams as one sequence, since a marked section may open in one stream and close in the next, and an inline image by its declared length where it has one; the filter followsBDC,BMCandEMCnesting,qandQ,BTandET,BXandEX. A page whose sequence M08 had to guess at — an inline image with no length — is rewritten only if the guess does not touch a removed section, and reported otherwise. The rewritten page's/Contentsis one new stream; the old streams are left to whatever else references them. - Hidden content still changes the graphics state: a viewer executes the state operators of an invisible
section and skips only its painting. Removing a section therefore keeps
cm,gs, colors,q/Q, clipping and text positioning; replaces each path-painting operator withn, so that a clip survives; dropssh, image and formDoand inline images; and replaces each text-showing operator with aTJof displacements only, computed from the font's widths, so that the text position moves exactly as it did. Deleting the bytes betweenBDCandEMCis not removal. - XObjects and annotations in a removed group: their
Dooperations removed, the annotations removed with their popups; in a group fixed ON, their/OCentry removed. - Structure. Marked content whose painting was removed loses its MCID's reference in the structure tree; elements left empty are removed bottom-up; the parent tree follows. Artifacts need nothing.
/OCPropertiesloses the fixed groups from every configuration,/Orderand/RBGroups, and disappears when no group remains. Groups nothing references are dropped on request, reported.
The filter is the first content-stream rewriter in the core. M09 leaves editing operators inside an existing stream to M19's pipeline — read, filter, rewrite —, and layer removal is the one such edit that cannot wait for it; so the pipeline is to grow from this filter rather than beside it, and the design keeps the operator table and the state tracking apart from the layer logic for that reason.
Merge (M06)
M06 merged /OCProperties as dictionaries, by a written policy. M11 checks the result with the typed model:
for each part, every group's state under the View and Print events after the merge equals its state before, and
a disagreement — two parts' configurations that cannot both hold — is reported with the groups it concerns.
Signatures, usage rights and conformance
- Annotations and links added or removed are M04's
Annotationclass: allowed under DocMDP P=3; under P=1 and P=2 refused withPdfSignatureInvalidationException, naming the signature and its permission, unless the caller setsAllowInvalidatingSignatures, when the update is written and each voided signature reported (write.signature-invalidated). - Flattening and every layer edit are
Other, as M09's stamps are, and follow M09's rule: refused under any certification on the same terms; after approval signatures, written as an incremental update — M03 refuses a full rewrite of a signed document — and reported as a change outside every signature (flatten.after-signature,optional-content.after-signature), with M09'sPdfSignedDocumentPolicy.Refusefor a caller who would rather not. It is the shape M04 reports assignature.unexplained-change, and the report says so. - A Reader-extended document keeps its usage rights through an annotation added in an incremental update only
if its
/UR3grants annotations; the grant is read from M04's transform parameters, and a save that exceeds it is reported (annotate.usage-rights-broken, as M09 reports its marks). Removing usage rights is M16's. - Under a PDF/A claim: the Print flag set and every other visibility flag clear on what we add; an appearance
on every annotation but popups and links; no
/CAbelow 1 and no blend mode on part 1, whose highlight therefore conflicts; no optional content and no embedded file on part 1, and on part 2 an attached file only if it is itself PDF/A; no/ASon parts 2 and 3. Each conflict goes to M09'sPdfConformancePolicy, so a claim is kept only while it stays true, as M06 decided for merges (ADR 17).
Output version (ADR 40)
M03's table of minimum versions gains, from ISO 32000-2's tables — checked against the Arlington model's
SinceVersion rather than written from memory (ADR 44):
| What M11 writes | Minimum |
|---|---|
Optional content: /OCProperties, /OC on content, XObjects or annotations | 1.5 |
A visibility expression (/VE) | 1.6 |
Constant opacity /CA on an annotation; a blend mode in an appearance | 1.4 |
| Squiggly | 1.4 |
Caret, Polygon, PolyLine; /IRT; a border effect | 1.5 |
Watermark annotations; /RT; /IT; /QuadPoints on a link; a line's leaders and caption | 1.6 |
No row needs 2.0: a blend mode goes in the appearance's graphics state rather than in the annotation's own
/BM, which only 2.0 has.
Validation rules
Added to M02's structural profile, documented in docs/website/docs/reference/validation-rules.md, and declared in the manifest's
findings of every document that earns them:
| Rule | Severity | Meaning |
|---|---|---|
annotation.reply-cycle | Warning | An /IRT chain returns to an annotation already in it |
annotation.popup-parent-mismatch | Warning | A popup whose /Parent does not name the markup that names it |
layer.group-undeclared | Warning | Content, an XObject or an annotation names a group absent from /OCGs |
layer.configuration-unknown-group | Warning | A configuration's /ON, /OFF, /Order, /RBGroups, /Locked or /AS names something that is not a declared group |
layer.group-unused | Information | A declared group nothing references — the HMCTS form's Watermark |
Report codes and the tool
The report is M06's PdfOperationReport, as for M09. Its codes: annotate.* (tag-appended, appearance-approximated,
attachment-removed, reference-dropped, usage-rights-broken, conformance-claim-removed), flatten.*
(no-appearance, contents-lost, active-content-removed, hidden-skipped, after-signature), and
optional-content.* (expression-simplified, structure-content-removed, unused-group-dropped,
merge-conflict, inline-image-guessed, after-signature), each documented with what to do about it — a claim
removed at M09's ConformanceLoss severity —, and M08's text.shaping-required and M09's stamp.* codes where
their operations run.
annotations FILE list [--json] | add SPEC.json | remove [--subtype S] [--author A] [--before DATE]
flatten FILE [--mode all|print|screen] [--subtype S…] [--layers preserve|apply] -o out.pdf
layers FILE list [--json] | remove NAME | flatten [--event view|print] | stamp TEXT --print-only
Slices
Each slice ends on a green commit, with its acceptance rows passing and the codes and rules it introduces documented.
- Reading annotations. Delivers the typed views, the lazy collection, the JSON listing, and the two
annotation.*rules. Addsexpect.annotations(count per subtype) to the manifest and its schema, written bybuild_corpus.pyfrom pikepdf. Proven by unit tests (every subtype, every flag,/Annotsthat is not an array, a popup that is its own parent, a reply cycle, a stream where a dictionary belongs) and by pdf.js'sgetAnnotations()and pikepdf, in containers, listing the same annotations as we do on every committed document. Leaves every write. - Text markup and the appearance frame. Delivers
PdfQuad, the appearance builder (form XObject, resources, the Multiply graphics state), highlight, underline, strike-out and squiggly, written through M09's page edits with M04's guard. Proven by unit tests (quads in both orders, rotated quads, a zero-area quad refused), by pdf.js and MuPDF rendering alike within the threshold, by qpdf's round trip, and by highlights over the words PyMuPDF finds on corpus pages. Leaves the other subtypes. - Notes, replies, states, caret, file-attachment icons. Proven by unit tests on reply closures and states, by pdf.js and PyMuPDF reporting the reply relation and the state on the annotated set, and by rendering. Leaves free text and stamps.
- Free text and stamps. Delivers FreeText in its three forms with
/DA,/DSand/RC, stamps by standard name, text and image, and M09's marks as stamp annotations. Proven by unit tests (fit, wrap, fallback, a string of RTL text drawn and reported), by rendering, and by pdftotext finding the stamp's text in its appearance, as it finds a widget's. Leaves the geometric subtypes. - Shapes, lines, polygons and ink. Delivers the remaining subtypes with borders, dashes, interior colors,
line endings, leaders and captions; cloudy borders drawn plain and reported. Proven by unit tests of each
geometry and by rendering on the annotated set's rotated and
UserUnitpages. Leaves links and tagging. - Links and tagging. Delivers links on existing pages, with quads, and the structure-tree placement of every annotation we add on a tagged document. Proven by pdf.js and pikepdf resolving every destination, and by veraPDF's PDF/UA-1 profile on the three PDF/UA-1 documents, annotated and linked. Leaves removal.
- Removal and missing appearances. Delivers
PdfAnnotationFilter, removal with its cascades and structure updates, andEnsureAppearances. Proven by unit tests (a reply chain crossing the filter, a file attachment, anOBJRin a deep tree), pikepdf listing what remains, veraPDF on tagged inputs, and rendering generated appearances against pdf.js's own drawing of the same annotations without them. Leaves flattening. - Flattening. Delivers the placement algorithm, the three modes, the layer policies, NoZoom and NoRotate,
tagged flattening, the loss reports and the signed-document rules. Proven by unit tests (FsCheck: for any
/Matrixand/BBox, the mapping sends the transformed box onto/Rect; the mode table over every flag combination), by rasterized comparison with the unflattened page, and byqpdf --flatten-annotationsin each mode, on the Reader X, PDFMaker and annotated-set pages. Leaves optional content, but for an annotation's own/OC, which it wraps as it finds it. - The optional-content model and evaluator. Delivers the typed model, the evaluator, the three
layer.*rules, andexpect.layers(names and default states) in the manifest. Proven by unit tests and FsCheck (the iterative evaluator equals a naive recursive one on any acyclic expression; memberships' policies on every assignment), and by pdf.js'sgetOptionalContentConfig()and its display and print renderings on every committed document with layers and the remote ones. Leaves every write to layers. - Creating layers and print-only stamps. Delivers
CreateGroup, stamps into a group,PdfPrintOnlyStampin both forms with the decision table, the version rows, the typed check of M06's merge. Proven by pdf.js's print and display intents, poppler's printing path (pdftocairo) and display path (pdftoppm), veraPDF on the PDF/UA and PDF/A inputs. Leaves removing and flattening layers. - Removing and flattening layers. Delivers the marked-content filter, partial evaluation, the structure
updates. Proven by unit tests (a section spanning two streams, an inline image containing
EMCin its data,BXsections, aTjinside a hidden section followed by visible text on the same line, a clip inside a hidden section, unbalancedqinside a section), by rendering against pdf.js with the group off, and by pdftotext on the result, on the OZEV invoice, the PDFMaker guide and the PDF Association's file. Leaves the facade and the budgets. - Facade, budgets and the tool. Delivers determinism across every operation,
AnnotationBenchmarks, the memory rows, and the three verbs. Proven by the full acceptance table andCorpusToolTests. Leaves M19 the filter to generalize.
Tests required
Unit — tests/AdCodicem.Pdf.Tests:
- Reading: every subtype and flag; each malformed shape named in slice 1;
/RCwith a DTD, an entity bomb, a megabyte of markup; text strings in every encoding #36 reads. - Authoring: every subtype's entries and appearance, snapshot by content bytes; quads in both orders;
/Rectfrom quads; dates and unique names never from a clock (a test runs under a frozen and a moving clock and compares bytes); colors of 1, 3 and 4 components; the color rule under each output intent. - Tagging:
AnnotandLinkelements,OBJR,/StructParent, parent tree,/Tabs; removal pruning empty elements; a tree whose parent tree has gaps. - Removal: each filter; reply closures with cycles and with parents outside the filter; every reference owner.
- Flattening: FsCheck on the placement mapping; the mode table against a truth table written from qpdf's
documentation, over all combinations of the ten flag bits; NoZoom and NoRotate on each
/Rotate; the two layer policies; certified and approval-signed documents. - Optional content: memberships under every policy; visibility expressions (FsCheck against a recursive
reference);
/ASfor each event and category; intents; alternates withUnchanged; expressions and/Orderwith cycles through indirect arrays; partial evaluation (FsCheck: for any assignment of the remaining groups, the simplified membership gives what the original gave with the fixed groups substituted). - The filter: each case of slice 11; FsCheck over generated content (random nesting of marked sections,
q/Q, text and paths): after removal, a reference interpreter in the test support sees the same graphics state and text position at every visible operator as before, and no painting from the removed group. - Hostile: a page with a million annotations (listing streams, memory bounded), a popup chain of a million,
an
/Ordera million deep, a content stream of nestedBDCto the decoding limit — each ends in a report or a typed exception inside time and allocation budgets.
Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container
(ADR 27):
- pdf.js, pinned
pdfjs-distunder Node —getAnnotations(),getOptionalContentConfig(), destinations, and page rendering under thedisplayandprintintents. - MuPDF (
mutool draw) — the rasterizer referee the roadmap asks pdf.js to agree with; poppler —pdftoppm(display),pdftocairo(its printing path),pdftotext. - qpdf —
--check, the round trip, and--flatten-annotationsin each mode. - pikepdf — annotation lists, structure-tree walks,
/OCProperties. - PyMuPDF — word quads for the text we highlight and link; annotation information as a second listing.
- veraPDF — PDF/A at the level claimed, PDF/UA-1, on every annotated, flattened and layered output of a claiming input.
- pyHanko — the modification level of an annotation added after a signature, and each signature intact.
Images are compared in the referee container, with Pillow and NumPy, so the test process decodes no image. The
threshold is M09's cross-engine threshold, recorded in status.md by M09: at 150 dpi, a pixel differs when a
channel differs by more than 24 of 255 and no pixel within one pixel of it in the other raster matches it; two
renderings agree when at most 1 % of the pixels of the region compared differ and no region of differing pixels
exceeds 144 pixels. The region compared here is an annotation's rectangle grown by two pixels, or the whole page
for flattening.
Acceptance conditions
"Every committed document" means every document under documents/ and vendor/ the reader opens, encrypted
ones excepted until M16, as in M06. "The annotated set" is the pages below, each carrying every subtype we author:
highlight, underline, strike-out and squiggly over the quads PyMuPDF finds for the manifest's textContains
strings; a note with two replies and a review state; free text plain and as a callout; a "REÇU" stamp and an
image stamp; square, circle, a line with arrows and a caption, polygon, polyline, ink, caret; a file-attachment
icon.
| Documents | Behavior | Verified by |
|---|---|---|
Every committed document — 1,124 widgets in 21 documents and 294 links in 26 (1,415 and 313 once M16 opens the encrypted ones), the note, highlight and two popups of vendor/opf-format-corpus/reader10-openoffice32-annotated-object-streams.pdf, the file attachment of pdfmaker10-word-file-attachment-annotation.pdf, the screen annotation of pdfmaker9-word-distiller-embedded-quicktime.pdf | Every annotation listed with its subtype, rectangle, flags, author, contents, popup, reply relation and layer as pdf.js and pikepdf list it; expect.annotations met | CorpusAnnotationTests.Every_annotation_is_listed_as_referees_list_it |
The annotated set on documents/invoice/chromium-invoice-fr.pdf, documents/contract/chromium-contract-fr.pdf, vendor/uk-ogl/word2019-ccs-contract-schedule.pdf, vendor/opf-format-corpus/pdfmaker7-powerpoint-va-cancer-database-course.pdf (/Rotate 90), vendor/us-federal/docusign-pdfkit-gsa-sf30-contract-modification.pdf (/Rotate 270), vendor/pdf-association/handwritten-compacted-syntax.pdf (UserUnit 0.88) | Each annotation renders alike in pdf.js and in MuPDF within the threshold; qpdf accepts the file, and after qpdf's rewrite every annotation reads back with the same entries | CorpusAnnotationTests.Every_annotation_we_create_displays_alike_in_pdfjs_and_mupdf, CorpusAnnotationTests.Every_annotation_we_create_survives_a_qpdf_round_trip |
| The same pages | Each highlight covers the words its quads name — at least 95 % of the pixels of PyMuPDF's word boxes changed in pdf.js's rendering, none outside the quads grown by a pixel | CorpusAnnotationTests.Highlights_cover_the_text_their_quads_name |
| The same pages | pdf.js reports each reply's parent and reply type and each state and state model we wrote; PyMuPDF agrees | CorpusAnnotationTests.Replies_and_review_states_read_as_viewers_read_them |
chromium-contract-fr.pdf ("Droit applicable" to page 2), vendor/pdf-association/indesign13-pdfua1-german-book-chapter.pdf (a phrase PyMuPDF finds across a line break, to a named destination), vendor/eu-publications/3heights-eu-consolidated-regulation-2026.pdf (a URI) | pdf.js resolves each link to the page, destination or URI we named, and reports the two-line link's quads as we wrote them; on the PDF/UA chapter, a Link element with its OBJR and /Contents, and veraPDF's PDF/UA-1 profile finds no failure the input did not have | CorpusAnnotationTests.Links_added_to_existing_pages_resolve_and_stay_tagged |
The annotated set on indesign13-pdfua1-german-book-chapter.pdf, vendor/pdf-association/indesign15-pdfua1-form.pdf and vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf | Every annotation in an Annot element with /Contents, every annotated page /Tabs /S; veraPDF's PDF/UA-1 profile finds no new failure | CorpusAnnotationTests.Annotating_a_pdfua_document_keeps_it_valid |
The annotated set on vendor/eu-publications/pdflib-oj-exchange-rates-greek.pdf and 3heights-eu-consolidated-regulation-2026.pdf (PDF/A-2a), vendor/verapdf/pdfa2b-content-pass.pdf (2b), vendor/zugferd/mustang-zugferd2-en16931-invoice.pdf (3u), vendor/docentric/fop-factur-x-visualization.pdf (3b), and on vendor/verapdf/pdfa1b-annotations-pass.pdf (1b) and vendor/eu-publications/antenna-house-oj-exchange-rates-2019.pdf (1a) | veraPDF upholds every claim on parts 2 and 3, the attached file being itself a PDF/A document on part 2; on part 1 the highlight, any opacity below 1 and the file attachment throw PdfConformanceException under Refuse, and under RemoveClaim are written with the claim removed and reported; never a claim veraPDF rejects | CorpusAnnotationTests.Annotating_a_pdfa_document_keeps_its_claim_or_reports_the_loss |
reader10-openoffice32-annotated-object-streams.pdf (both annotations by AnJackson), pdfmaker10-word-file-attachment-annotation.pdf, the annotated set | Removing highlights takes the highlight's popup and nothing else; removing AnJackson's leaves no annotation; removing file attachments reports the embedded file removed; removing a note takes its replies; pikepdf lists exactly what remains, qpdf accepts the file | CorpusAnnotationTests.Removing_by_subtype_or_author_takes_popups_and_replies_along |
| Third-party annotations without appearances — not in the corpus (below) — and the committed ones with their appearance removed by a recorded transformation | Appearances generated render within the threshold of pdf.js's own drawing of the same annotations, where pdf.js draws one | CorpusAnnotationTests.Missing_appearances_are_generated_as_viewers_draw_them |
reader10-openoffice32-annotated-object-streams.pdf, pdfmaker10-word-file-attachment-annotation.pdf, pdfmaker9-word-distiller-embedded-quicktime.pdf, the annotated set; remote, remote/opf-format-corpus/quartz-word-samhsa-prevention-pathways-fact-sheet.pdf once opened (an underline and its popup) | Flattened in All mode: the page renders as the annotated page did, in pdftoppm and in MuPDF, within the threshold, and as qpdf's --flatten-annotations=all output does; pikepdf finds no annotation but links and widgets; the note's text, the reply threads and the screen annotation's rendition are reported lost | CorpusFlatteningTests.Flattening_keeps_the_appearance_and_removes_the_annotations |
| The annotated set with every combination of the Print, NoView, Hidden and Invisible flags set by the test | Print and Screen modes keep exactly the annotations qpdf's same modes keep, and the flattened page renders as pdf.js's print and display intents rendered the annotated one | CorpusFlatteningTests.Flattening_honors_flags_as_qpdf_does |
The decrypted twin of vendor/us-federal/livecycle-uscis-ar11-xfa-form.pdf — not in the corpus (below): two links on its view-only layer; and the annotated set placed in a layer off by default | Under Preserve, the flattened annotations stay hidden in pdf.js's display rendering and the AR-11's links stay shown on screen and absent from print; under Apply, each is drawn or dropped as the default configuration says | CorpusFlatteningTests.Flattening_honors_the_layers_annotations_belong_to |
The committed documents with layers — vendor/uk-ogl/pdfmaker21-ozev-sample-invoice.pdf (Acrobat's Watermark, a foreground page element, with /AS for three events), vendor/uk-ogl/indesign-acrobat-hmcts-n208-form.pdf (a Watermark group nothing references), vendor/pdf-association/handwritten-utf16le-strings.pdf (two groups off by default, UTF-16LE names, XObjects with /OC), and the five PDFMaker 7 and 8 files (Background and HeaderFooter page elements, 176 marked sections in their page content, three of them with no /D); remote, remote/usgs/us-topo-washington-west-2023.pdf (31 layers), remote/pdfjs/arcmap-cff-fdselect-bug1146106.pdf, remote/us-states/powerbi-pdfium-acrobat-covid-dashboard-nine-updates.pdf, remote/pdf20examples/handwritten-pdf20-utf8-strings.pdf (UTF-8 names, off by default), remote/opf-format-corpus/pdfmaker6-word-eu-delegation-senate-statement.pdf, pdfmaker6-word-piarc-road-safety-audit.pdf, jhove-hul-117-acrobat101-student-design-report.pdf | Groups, names, default states, usage, /Order and /AS as pdf.js's getOptionalContentConfig() reports them; every marked section's and XObject's visibility under View and Print as pdf.js's two intents render it; the HMCTS group layer.group-unused; expect.layers met | CorpusOptionalContentTests.Every_layer_reads_as_pdfjs_reads_it |
pdfmaker21-ozev-sample-invoice.pdf, vendor/opf-format-corpus/pdfmaker707-word-va-esig-developer-guide.pdf (96 sections over 49 pages, through one shared membership dictionary), handwritten-utf16le-strings.pdf, indesign-acrobat-hmcts-n208-form.pdf | Removing OZEV's Watermark removes the 100-point "Sample" — pdftotext loses that word and keeps "Sample invoice" — and the page renders as pdf.js renders it with the group off; removing the guide's HeaderFooter removes every header and footer and keeps the body text and its structure; removing a group that was off changes no pixel; dropping the HMCTS group changes nothing else; qpdf accepts each file | CorpusOptionalContentTests.Removing_a_layer_removes_what_it_drew_and_nothing_else |
| The committed and remote documents of the layer-reading row above | Layers flattened for the View event: the page renders as pdf.js's default view within the threshold, pdftotext finds the input's visible text and not its hidden text, no /OCProperties remains, and the tagged ones keep a structure tree pikepdf walks without a dangling MCID | CorpusOptionalContentTests.Flattening_layers_keeps_what_the_viewer_showed |
chromium-contract-fr.pdf (untagged), indesign13-pdfua1-german-book-chapter.pdf and pdfmaker21-ozev-sample-invoice.pdf (tagged) | A "COPIE" stamp in Auto form appears under pdf.js's print intent and not under display; for the annotation form, also in poppler's printing path and not in pdftoppm; the chapter still passes PDF/UA-1 in veraPDF | CorpusOptionalContentTests.A_print_only_stamp_prints_and_does_not_show |
pdflib-oj-exchange-rates-greek.pdf (2a), mustang-zugferd2-en16931-invoice.pdf (3u), pdfa1b-annotations-pass.pdf (1b) | A print-only stamp throws PdfConformanceException under Refuse; under RemoveClaim it is written, the claim removed and reported, and veraPDF, asked for the part the file used to claim, finds exactly the clauses the report names | CorpusOptionalContentTests.A_print_only_stamp_on_pdfa_is_refused_unless_the_claim_may_go |
M06's layer merge set: indesign-acrobat-hmcts-n208-form.pdf and pdfmaker21-ozev-sample-invoice.pdf (two Watermark groups), handwritten-utf16le-strings.pdf, the decrypted twin of vendor/us-federal/livecycle-uscis-ar11-xfa-form.pdf, and the original once M16 opens it | Every part's groups keep their state under View and Print, as pdf.js renders each part's pages before and after | CorpusOptionalContentTests.Merged_layers_keep_every_parts_visibility |
vendor/us-federal/itext-govinfo-us-code-certified.pdf (DocMDP P=1), vendor/pyhanko/acrobat-reader-signed-twice.pdf and vendor/fr-licence-ouverte/fop-dictao-dila-signed-joafe-notice.pdf (approval signatures); remote, remote/eu-dss/pdfmaker11-nbu-sk-qualified-seal-docmdp-fieldmdp.pdf (P=2) | A note throws PdfSignatureInvalidationException on the certified files, and is written with the voided certification reported under AllowInvalidatingSignatures; on the approval-signed ones it is written in an update, and pyHanko finds every signature intact with an annotation-level modification; flattening is refused on the certified files on the same terms, and on the approval-signed ones written in an update, reported as flatten.after-signature, each signature still covering its revision in pyHanko | CorpusAnnotationTests.Annotating_a_signed_document_stays_within_its_permissions |
| A certification with DocMDP P=3 — not in the corpus (below) | A note, and one of M09's marks as a stamp annotation, are written with no option set, and pyHanko finds the certification intact | CorpusAnnotationTests.Annotations_are_allowed_where_the_certification_allows_them |
documents/stress/reportlab-journal-1000-pages.pdf; remote, remote/pdfjs/cairo-firefox-objstm-index-overflow-bug1978317.pdf (65,542 objects in one object stream, and a single page with many annotations) | A note and a highlight on every page, then flattening all: memory flat as pages grow, recorded in status.md; listing and removing the remote page's annotations holds memory bounded | CorpusAnnotationTests.Annotating_and_flattening_a_thousand_pages_holds_its_budget, AnnotationBenchmarks |
| Every write above | Two runs give identical bytes | CorpusAnnotationTests.Annotation_and_layer_edits_are_deterministic |
| The same operations through the tool | The AOT binary produces what the API produces | CorpusToolTests.Annotations_flatten_and_layers_match_the_api |
The remote rows close only on a green Remote corpus run, recorded in status.md with its date. The rows that
name the AR-11 form close on its decrypted twin; the original joins them when M16 opens it.
Corpus
What the corpus holds
- Markup annotations, few:
vendor/opf-format-corpus/reader10-openoffice32-annotated-object-streams.pdf— an OpenOffice file annotated in Adobe Reader X: a text note (/Name /Comment,/RCrich text, flags Print, NoZoom and NoRotate) and a highlight with/QuadPoints, both byAnJackson, each with an appearance and a popup without one, and a named appearance tree in/Names /AP;pdfmaker10-word-file-attachment-annotation.pdf— a file-attachment annotation with/RCand its icon's appearance, its file reachable only through it;pdfmaker9-word-distiller-embedded-quicktime.pdf— a screen annotation with a poster appearance. Remote:quartz-word-samhsa-prevention-pathways-fact-sheet.pdf, an underline and its popup in a damaged file;pdfmaker9-word-distiller-embedded-avi.pdf, another screen annotation. - Links: 313 committed in 31 documents (294 in 26 outside the encrypted ones), three files with links carrying
/QuadPoints(indesign-irs-pub1-chinese-traditional.pdf,indesign-acrobat-hmcts-n208-form.pdf,designer-distiller23-uscis-i9-javascript-form.pdf), tagged links withOBJRinindesign13-pdfua1-german-book-chapter.pdf(49 annotations,/Tabs /S), and remote in two more PDF/UA-1 files; two links on a layer in the AR-11 form. - Widgets: 1,415 in 25 documents (1,124 in 21 outside the encrypted ones) — never flattened or removed here, always listed.
- Layers: nine committed documents with
/OCProperties. Acrobat'sWatermarkgroup in the OZEV invoice (a 100-point "Sample" in an XObject with Acrobat's compound-type marker) and, referenced by nothing, in the HMCTS form; two groups off by default with UTF-16LE names, used by XObjects, in the PDF Association's hand-written file; PDFMaker'sBackgroundandHeaderFooterpage elements in five files, 176 marked sections between them, four of the five reaching their group through one shared membership dictionary; three of the five have no/D, which the specification requires, and two an empty/Order, which hides the group from a viewer's panel; the AR-11 form's print-only and view-only groups,/BaseState /OFFand/ASfor Print and View — the only committed layers whose state differs between screen and print, and AES-128 encrypted. Remote: 31 layers on the US Topo map, ArcMap's layers, the Power BI dashboard's, UTF-8 names off by default in PDF 2.0, two more PDFMaker 6 files and a JHOVE file. - Pages to annotate:
/Rotate 90and270,UserUnit0.88, crop boxes that differ from media boxes. - Claims: PDF/A-1a, 1b, 2a, 2b, 3b and 3u claims veraPDF upholds; three PDF/UA-1 claims it upholds.
- Signatures: a DocMDP P=1 certification and approval signatures committed; P=2 remote.
- Scale: the 1000-page journal; remote, a page with tens of thousands of annotations.
What it lacks
| Need | Why | Priority | Likely source |
|---|---|---|---|
| Documents annotated in real reviewing tools, with every markup subtype: underline, strike-out, squiggly, free text (plain, callout, typewriter), stamps (standard, custom, image), square, circle, line with endings, polygon, polyline, ink, caret, replies and review states — from Acrobat first, then Foxit, PDF-XChange, macOS Preview, pdf.js's editor, Okular | The committed corpus has one highlight, one note and one file attachment. Flattening and removal must meet what reviewers' tools write — each draws appearances, writes /RC and orders quads its own way — not only what we write | 1 | A contribution (W14 in docs/corpus-contributions.md); a public source: PDFium's hand-written .in fixtures under its BSD license (annots, ink_annot, line_annot, polygon_annot, links_highlights_annots, annotation_highlight_rollover_ap), which the corpus rules admit where the binary fixtures are not; generated here: LibreOffice's comment export, and pdf.js's editor driven in Chromium for free text, ink, highlight and stamps |
| Third-party annotations without appearance streams, of several subtypes | "Appearance streams generated for every subtype" must be proven on others' annotations, and M21 relies on the same generator | 1 | A public source: PDFium's hand-written annotation_markup_multiline_no_ap.in (BSD); generated here with pypdf's annotation classes, which write none; derived here, the committed annotations with their /AP removed by a recorded pikepdf transformation — each recorded in build_corpus.py |
The decrypted twin of livecycle-uscis-ar11-xfa-form.pdf | The only committed layers whose state differs between screen and print, carrying the only links on a layer, are AES-128 encrypted, which the reader opens in M16 | 1 | Derived here: qpdf --decrypt, a recorded transformation in build_corpus.py |
Acrobat's own print-only watermark ("show when printing" off on screen), and a Watermark annotation with /FixedPrint | Both our print-only forms should be checked against the reference implementation's own output; the committed Acrobat watermarks are shown and printed alike | 2 | A contribution (W14); failing that, generated here by a recorded pikepdf construction, marked as such |
Layers with visibility expressions, radio-button groups, locked groups, alternate configurations and a nested /Order with labels | None of these is committed; the PDFMaker memberships use /OCGs and /P only | 2 | A public source: GIS and CAD exports, of which the remote US Topo map is one — its /Order to be examined; generated here: Scribus's layered export, and a recorded pikepdf construction for /VE |
| A certification with DocMDP P=3 followed by an annotation, and one without | The allowed case of M04's permission table has no document, and the P=3 row above needs it; the same fixtures M04 lists as its own priority-1 gap, filled there | 1 | Generated here: pyHanko's certify and sign over a fictitious test PKI, as M04 plans |
A tagged document whose markup annotations are tagged (Annot elements with OBJR) | Removal and flattening in the structure tree are proven on our own tagging only; the committed tagged annotations are links | 2 | A contribution (W22: Acrobat's "Add tags" after commenting); the PDF/UA Reference Suite's remaining members, remote |
| Annotations with NoZoom and NoRotate on rotated pages, from Acrobat | Flattening's counter-rotation is proven on the unrotated Reader X page and our own rotated pages only | 3 | A contribution (W14), or derived by rotating the Reader X page with a recorded qpdf transformation |
| Free text and stamps in right-to-left and complex scripts, from Acrobat | Our reporting path should be checked against what arrives, and M12.6's HTML-rendered stamps will need a reference | 3 | A contribution (W09) |
Traps
- Quad points are in Acrobat's order, upper left, upper right, lower left, lower right, whatever the specification's prose says; every viewer reads that order, and writing the other one twists highlights.
- An appearance is what shows; the entries are hints. Without
/AP, each viewer draws its own guess, and they differ. We always write one. - Hidden content still changes the graphics state. A viewer skips painting in an invisible section but runs
its
cm, colors and text positioning; deleting the bytes betweenBDCandEMCmoves everything after it. - Inline images carry binary data in which
EMCandEImay appear; the filter skips them by size. - A marked section can span content streams, and
q/Qpairs can too; a page's streams are one sequence. - Usage divides viewers: Acrobat applies it through
/AS, pdf.js applies it under its intents whatever/ASsays, poppler and many others read only/D. A print-only group listed OFF in/Dnever prints there; listed ON, it shows on screen there. Hence two forms. - PDF/A forbids both print-only forms: the NoView flag in every part,
/ASin parts 2 and 3, optional content altogether in part 1 — and part 1 forbids the transparency a highlight is made of. PDF/UA-1 forbids the layer form:/ASin a configuration is Matterhorn 20-003's failure. - An empty
/Orderhides a layer from the panel, not from the page: two PDFMaker files'HeaderFootergroup is on, though no viewer lists it. - A required
/Dis often missing: three PDFMaker files have none. It is read as a default configuration with every group on, as viewers read it, and M02's shape rules report it. - A membership with no valid group has no effect, and a group missing from
/OCGsis read differently by different viewers: the first is visible, the second is reported. - Removing an annotation leaves references behind: popups, replies,
OBJR, actions naming it. Each has an owner that must drop it or report it. - A note's text is not in its appearance. Flattening a note leaves an icon and loses the text; say so.
- NoZoom and NoRotate describe a viewer's behavior, which flattening must freeze at one zoom and one rotation.
/NMand/Mfrom Acrobat are a GUID and the clock; ours come from the caller, or determinism is lost./RCis XML from the file: DTDs prohibited, size bounded, parsed as text only.- A FreeText's
/DAnames a font in the form's/DR; creating an/AcroFormonly to hold it gives a document with a form and no fields. The appearance carries its own font;/DAnames one that exists. - A link's
/Rectcovers everything between its quads in a viewer that ignores/QuadPoints. - A widget is a field's face. Flattening or removing one here would leave a field without a widget — M16's.
Documentation
docs/website/docs/guides/annotations.md— reading, authoring every subtype, replies and states, links on existing pages, removal, tagged documents, determinism.docs/website/docs/guides/flattening.md— the modes, flags and layers, what is lost and reported, signed documents.docs/website/docs/concepts/optional-content.md— groups, memberships, configurations, events and the evaluator; removal and flattening as fixing groups; why hidden content keeps its graphics state.docs/website/docs/guides/layers.md— creating layers, stamping into them, print-only and screen-only stamps and the table that chooses their form, removing and flattening layers.docs/website/docs/reference/annotation-and-layer-codes.md— everyannotate.*,flatten.*andoptional-content.*code.docs/website/docs/reference/validation-rules.md— theannotation.*andlayer.*rules added.docs/website/docs/reference/tool/—annotations,flatten,layers.docs/website/docs/introduction.mdanddocs/features/features.json— annotations and layers delivered.docs/architecture.md—Annotations/andOptionalContent/in the core; the marked-content filter M19 extends.docs/corpus.md—expect.annotationsandexpect.layers.
Exit criteria
- Every subtype in the appearance table is authored with its appearance; reading covers every subtype, unknown ones included; removal, flattening and links on existing pages work as designed.
- Optional content is read, evaluated per event, created, removed and flattened; print-only and screen-only stamps exist in both forms, chosen by the table; M06's merge is checked with the typed model.
- Annotations added to tagged documents are tagged, and those removed or flattened leave no dangling structure; every PDF/UA-1 input stays valid.
- The version rows are in M03's table and tested against the Arlington model's
SinceVersion. - The
annotation.*andlayer.*rules are in the structural profile, documented, and every manifest entry declares what they report on it. - The priority-1 gaps above are filled; each remaining gap is recorded in
docs/corpus-contributions.md. - The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on
a green
Remote corpusrun recorded instatus.md. - Unit tests cover each behavior, its degenerate cases and its hostile ones; the marked-content filter is fuzzed, seeded with the corpus's content streams, and a nightly campaign runs it.
- Integration tests confirm listings, renderings, flattening, layers, conformance and signatures through pdf.js, MuPDF, poppler, qpdf, pikepdf, PyMuPDF, veraPDF and pyHanko, each in a container.
-
AnnotationBenchmarksmeasures authoring, flattening and layer removal on the 1000-page journal withMemoryDiagnoser;status.mdrecords the budgets. - The tool's verbs ship in the dotnet tool and the AOT binaries, documented.
- The documentation site publishes the pages listed above.
- Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).