Skip to main content

M06 — Pages and case-file assembly

State: to do — Depends on: M03, M04

Goal​

Compose a case file from generated pages and third-party PDFs — pages, links, bookmarks, page labels, form fields, layers, attachments and logical structure — and say exactly what could not be carried.

This is the first business priority. The failure it exists to prevent is the ordinary one: a merge that keeps the pages and quietly loses everything the catalog held — the labels the viewer shows, the named destinations the links rely on, the field that two forms both call Nom and that now shares one value, the layer that was hidden and is now printed. Invariant 7 forbids the silent version of any of these; this milestone makes each one either carried or reported, structure by structure, and proves it on real files.

Scope​

In:

  • the page tree with its inherited attributes resolved (Resources, MediaBox, CropBox, Rotate), and a PdfPageCollection that inserts, removes, reorders, rotates and extracts, saved as a full rewrite or as an incremental update (M03);
  • cross-document deep copy, streams passed through encoded, with resource deduplication;
  • a merge that carries every catalog structure a merge can lose, each under a written policy: page labels (read, written, remapped), the name trees and /Dests with key renaming, /AcroForm with collision-safe renaming of fully qualified names, /OCProperties, /Extensions, output intents, viewer preferences, the open action, and — because ADR 17 already decided that structure trees are recombined — the logical structure tree;
  • an attachments and associated-files API at document, page and annotation level, streamed;
  • typed viewer preferences, page mode, page layout and open action;
  • signed and certified inputs handled through M04's coverage and change classification: a merge or an edit that would void a certification is refused, or reported when the caller insists;
  • a high-level assembly API with a plan that can be inspected before anything is written, and a report;
  • the command-line tool, AdCodicem.Pdf.Tool: info, validate, repair, merge, pages, and attachments under the track rule of the roadmap.

Out, explicitly:

  • split strategies, the full page-selection grammar, collation, images to pages, pruning of unreferenced resources, outline editing and JSON, GoToR and GoToE rewriting, and portfolio unpacking — M07; M06 has page ranges only (1-3,7,z), enough for its own verbs;
  • anything drawn on a page — stamps, headers and footers, Bates numbers, N-up, flattening /Rotate, fitting to A4 — M09;
  • decrypting an encrypted input — M16; until then an encrypted part is refused at analysis with the PdfEncryptedException the reader already raises;
  • filling, flattening or regenerating form fields, and any XFA handling beyond removing it from a merge — M16; appearance generation and annotation flattening — M11;
  • consolidating different subsets of one font across inputs, and linearized output — M23;
  • actively preserving PDF/A through a merge (bringing the output into conformance) — M20; M06 keeps a conformance claim only when it stays true, and otherwise removes it and says so;
  • signing the assembled volume — M26; any cryptographic check — M27;
  • the case-file model, exhibit stamps, the inventory and court presets — M18; a rendered table of contents page — M12 and M18.

Design​

Pages​

PdfPage one page of an open document, read-only: effective MediaBox, CropBox (intersected
with the MediaBox), BleedBox, TrimBox and ArtBox (defaulting to the CropBox), Rotate
(normalized to 0, 90, 180 or 270), UserUnit, Resources; its index and its object
PdfPageCollection the pages of a document: Count, indexer, and edits — Insert (a blank page of a given
size, or pages copied from another document), Add, Remove, Move, Rotate, Extract
PdfRectangle an immutable box, normalized (lower-left, upper-right)
  • Inheritance covers exactly the four keys ISO 32000 marks inheritable. Values are taken from the ancestors met while walking down from the root when the tree is indexed, never by following /Parent upwards: on a damaged file a /Parent can point anywhere. A page with no MediaBox anywhere on its path gets US Letter, the default readers apply, and the validator's finding (M02) is what reports it.
  • A kid that is null, or names an object the file lacks, is a page: blank, US Letter, with no resources — the page qpdf, poppler and PDFium show, and the count M02 settles and the manifest records (30, 13 and 65 for the three JHOVE files whose unsupported reason names this API). M02's finding reports each; extracting one yields a blank page, reported.
  • Edits are a plan, applied when the document is saved: nothing in the source changes until then. A full rewrite writes a fresh balanced page tree; an incremental update writes only the page-tree nodes and pages that changed. Rotation writes the page's own /Rotate, so that an inherited value is never changed for the page's siblings.
  • Removing a page asks every owner of a reference to it to rewrite or drop that reference: outline items, link destinations, named destinations, the open action, structure elements (/Pg, marked-content references, object references), widgets and their fields, article beads. Each drop is reported.
  • Signed and certified documents. Saving page edits on a document that carries signatures goes through M04's write guard, which classifies the pending change set as the next revision. A page-tree change is class Other, which no DocMDP level allows (P2 and P3 allow instantiating page templates, nothing more), so page edits on a certified document are refused with PdfSignatureInvalidationException unless the caller insists through M03's AllowInvalidatingSignatures; then they are written as an incremental update, which leaves the certified revision intact, and reported as voiding the certification. After an approval signature the edit is saved and reported with the class M04 gives it.

Deep copy and deduplication​

PdfObjectCopier (internal) copies an object's closure from a source document into the writer's
number space: one remap table per source, one deduplication index per output
  • What a copy never follows. /Parent, /P, the outline links (/Prev, /Next, /First, /Last), /Pg, and any reference to a page object. Each of those is rewritten by the structure that owns it, through the page map — source page to target page — whose target numbers are reserved ahead for every page the plan includes, so that a link to a later page needs no second pass. A reference to a page the plan excludes resolves to nothing, and its owner decides what that means.
  • How it walks. An explicit stack, never recursion; each object visited once; a reference to an object the source lacks copied as null (M02, T27); every read under the source's PdfReaderLimits.
  • Streams travel encoded. The raw bytes are copied from the source to the writer with an indirect /Length — never decoded, never recompressed, never held whole. Objects that lived in object streams are written by the M03 writer's own policy.
  • Deduplication is exact and deterministic. A stream's key is a SHA-256 (the BCL's, which Native AOT and trimming keep) of its encoded bytes and of its dictionary without /Length; a dictionary's or an array's key is computed over its canonical form, each reference replaced by its target's key, so that two font dictionaries pointing at equal font programs through different object numbers are equal. A cycle falls back to identity. Color profiles are keyed on their decoded bytes: the corpus carries one sRGB profile under four different encodings from PDFlib, Antenna House, 3-Heights, Acrobat and Distiller, sometimes under different condition identifiers.
  • What is shared. Objects reached from resources, appearance streams, file specifications and output intents. Never page objects, page content streams, or anything carrying /StructParent or /StructParents: M09 edits content streams in place and must not change two pages at once, and a structure key belongs to one part's parent tree.
  • Cost. The first occurrence of a stream is hashed while it is copied; a later candidate is hashed first and copied only if new. A stream is read at most twice and never held. The index holds a 32-byte key and a target number per shared object; the first occurrence wins.

Catalog structures: one written policy per key​

Merging B into a volume whose first part is A (the base, or whichever part the caller names):

Catalog entryPolicyReported as
/PagesRebuilt as a balanced tree, at most 32 kids per node, over the plan's pages—
Header and /VersionThe minimum version the output's features need, raised when a part requires it (ADR 40)the writer's write.version-raised (M03), carried into the report
/ExtensionsUnioned: per developer prefix, the highest level; the PDF 2.0 array form when one prefix has several—
/PageLabelsRebuilt from the page-label plan (below)—
/Dests and /Names /DestsOne name tree; the base keeps its keys, a later colliding key is renamed, every reference to it rewrittenassembly.destination-renamed
/Names /EmbeddedFilesMerged; colliding keys renamed, file names (/UF, /F) untouchedassembly.attachment-key-renamed
/Names /AP, /Templates, /Pages, /Renditions, /AlternatePresentationsMerged; colliding keys renamed and references rewrittenassembly.name-renamed
/Names /JavaScriptMerged, renamed on collision, never run (ADR 37)assembly.active-content-carried
/Names /IDS, /URLSUnioned (Web Capture)—
/OutlinesPer part: kept at the top level, nested under a new item titled by the caller, or dropped; items whose target is excluded removedassembly.outline-item-dropped
/AcroFormForms (below)assembly.field-renamed, assembly.xfa-removed
/OCPropertiesGroups and membership dictionaries unioned, distinct objects kept distinct even under one /Name; in /D, ON, OFF and Locked unioned, /Order grouped per part under the part's title, /RBGroups concatenated, /AS merged by event and category; /Configs concatenated—
/OutputIntentsDeduplicated by decoded profile; two different profiles for one subtype is a conflict, and the base's is keptassembly.output-intent-conflict
/StructTreeRoot, /MarkInfo, /LangStructure (below)assembly.untagged-part
/Metadata and the trailer's /InfoThe base's or the caller's; a conformance claim kept only while it stays true (below)assembly.conformance-claim-removed
/ViewerPreferences, /PageMode, /PageLayoutThe base's, overridden by the caller's typed values; unknown keys preserved—
/OpenActionThe base's; a destination remapped through the page map, an action carried as it is—
/AAThe base's; the others' document actions droppedassembly.document-action-dropped
/URIThe base's; a relative URI action from another part is made absolute against that part's own base firstassembly.uri-base-resolved
/ThreadsCarried when their beads' pages are included; beads on excluded pages cut outassembly.thread-shortened
/Perms/DocMDP: the certification policy (below); /UR3: removed, as no merge keeps usage rights validassembly.usage-rights-removed
/DSS, /LegalDropped: they belong to signatures the output does not carryassembly.signature-material-dropped
/CollectionA portfolio part contributes its cover pages only; M07 unpacks portfoliosassembly.portfolio-cover-only
/AFUnioned; more than one part with document-level associated files is reported, since a consumer such as Factur-X expects exactly oneassembly.associated-files-combined
/RequirementsUnioned—
/PieceInfo, /SpiderInfo, /NeedsRendering, /DPartRoot, and any other keyThe base's kept; each other part's dropped, by nameassembly.catalog-entry-dropped

The last row is the rule the table exists for: no key leaves a part without a report entry naming it.

Inside copied pages, owners rewrite what points outside a page: an annotation's /P, a markup's /Popup and the popup's /Parent, replies' /IRT, link /Dest and /A /D, a widget's /Parent into its field tree, /StructParent and /StructParents, article beads /B. Page /AA, /Thumb, /Metadata, /AF, /Group, /Tabs, /UserUnit, /VP and /PieceInfo are copied as they are. An annotation object shared by two pages, which the specification forbids and producers write, is copied once per page and reported.

Destinations and outlines​

  • An explicit destination is remapped through the page map. One whose page is excluded makes its link meaningless: the link annotation is removed, the outline item loses its destination, and both are reported.
  • Named destinations from the catalog's /Dests (name keys, PDF 1.1) and from the name tree (string keys) are merged into one name tree. A reference by name object is rewritten as a string when its key moves into the tree. The base keeps its keys; a later colliding key takes the smallest free suffix ~2, ~3… that collides with nothing. The rename map — part, old key, new key — is part of the report: M07 uses it to rewrite GoToR links and #nameddest= parameters.
  • Structure destinations (/SD, PDF 2.0) are remapped through the structure map.
  • Outlines are walked with a bound on items and depth and a visited set, since outlines with cycles exist in the corpus. /Count is recomputed, with its sign for the open state; /C, /F and /SE are kept. A part's outline is kept at the top level, nested under a new item carrying the part's title (the case-file default), or dropped.

Page labels​

PdfPageLabels the ranges of a document; the label of page i
PdfPageLabelRange StartIndex, Style, Prefix, FirstNumber
PdfPageLabelStyle Decimal, UpperRoman, LowerRoman, UpperLetters, LowerLetters, None
PdfPageLabelPlan Keep (each part's own labels), Continuous (one decimal run across the volume),
PerPart (a prefix per part, numbering restarting at 1), or explicit ranges
  • Reading walks the number tree with bounds on depth, kids and entries; keys past the last page are ignored, a duplicated key keeps the last value. When the tree has no entry for page 0 — the USGS methods report's starts at index 7 — the pages before the first entry have an empty label, which is what an entry with no style gives; the specification requires the entry, and the writer adds it when it rebuilds a tree.
  • Remapping: labels are materialized per page (style, prefix, number) as pages move, then compressed into the fewest ranges that reproduce them. A sequence no range can express — descending numbers after a reorder — costs one range per page. The output tree is balanced.
  • Letters run A to Z, then AA to ZZ, then AAA: the letter is repeated, not counted in base 26. Roman numerals past 3,999 repeat M, as pdf.js does.
  • Prefixes are text strings, decoded and re-encoded for the output version; #36 must be fixed first, since PDFDocEncoding, UTF-8 in PDF 2.0 and UTF-16LE seen in the wild all occur in label prefixes.

Forms​

PdfFieldCollisionPolicy Rename (default), Nest, Join, Refuse
  • A field's fully qualified name is built from the decoded /T of it and its ancestors, walked with a bound and a visited set (a field tree with a cycle exists in the remote corpus). Two names collide when they are equal, or when one is the other plus a .-separated tail (a terminal a in one part, a.b in another).
  • Rename gives the colliding top-level field of the later part the smallest free suffix _2, _3…; Nest puts each part's fields under a new non-terminal field named by the caller, so every name in part k becomes piece-k.…; Join makes equal names share one value, the legacy behavior, only when asked, and refuses two fields of different types; Refuse stops the plan. Every rename is in the report, old name and new.
  • A field tree is copied with its widgets; kids whose widgets sit on excluded pages are pruned, and a field left with none is dropped and reported.
  • /DR is unioned. A resource name that means different objects in two parts — nearly every form in the corpus defines its own /Helv — is renamed in the later part, and the /DA strings of that part's fields rewritten: /DA is a content-stream fragment, tokenized, and only the operand of Tf changes.
  • /NeedAppearances is true if any part says so — viewers then regenerate every field, the base's too, so it is reported. /SigFlags is recomputed from what the output carries rather than or'd. /CO is concatenated in plan order.
  • /XFA is removed: one template cannot describe the merged fields, and two cannot coexist. The AcroForm of a hybrid form is kept. A dynamic-XFA placeholder page is merged as it is (ADR 37). Both are reported.
  • Values are carried untouched; no appearance is regenerated.

Signed, certified and portfolio inputs​

A signature covers bytes, and a merged file is new bytes: no signature survives into a merged output, and none is claimed to.

PdfSignedPartPolicy CarryUnsigned (default), AttachOriginal, Refuse
PdfCertifiedPartPolicy Refuse (default), Insist
  • Approval signatures: CarryUnsigned keeps the field and its widget's appearance and removes the signature value, reporting assembly.signature-not-carried with the field's name. AttachOriginal does the same and adds the part's original file as a page-level associated file with AFRelationship /Source on the part's first page, so the evidence stays verifiable and travels with the part when M07 splits the volume.
  • Certification (/Perms /DocMDP): the plan lists the part as refused, and writing a plan with a refusal throws a typed exception. Insist treats it as an approval signature and reports assembly.certification-voided. Refusal by default is decided (the maintainer, 2026-09-27), as is M07's default for a portfolio's certified member.
  • /DSS and /Legal are dropped and reported. Signature fields keep taking part in name collisions.
  • Portfolios: a part with a /Collection contributes its cover pages, and its members are reported, pointing at M07's unpacking.
  • Nothing here needs cryptography: M04 says what each signature covers and its PdfSignatureKind — approval, certification, document time stamp, usage rights (ADR 41).

Logical structure (ADR 17)​

  • The output's structure root holds a Document element; each tagged part's top-level elements go under a Part element carrying the part's title, or directly under Document when the caller asks.
  • Parent tree: each part's keys are offset by the running next key; pages' /StructParents and annotations' and XObjects' /StructParent rewritten; /ParentTreeNextKey recomputed.
  • Role map and class map: unioned; a custom type mapped differently by two parts is renamed in the later one (Heading1 becomes Heading1_2) and its elements' /S rewritten; class names alike, with /C.
  • ID tree: colliding element identifiers renamed. Namespaces (PDF 2.0): unioned.
  • Language: when a part's catalog /Lang differs from the output's, it is set on that part's Part element, so no text loses its language.
  • Excluded pages: marked-content and object references to them removed; elements left empty pruned, with a bound on the walk.
  • Untagged parts: in a volume whose other parts are tagged, an untagged part's content is outside the structure; /MarkInfo /Marked stays true for the rest, and the part is reported. A PDF/UA claim does not survive it.
  • PdfStructurePolicy.Drop removes the structure, clears /Marked, removes the claims, and says so.

Conformance claims​

A PDF/A or PDF/UA claim in the output's XMP is written only when every part makes the same claim, the output intents did not conflict, and — for PDF/UA and levels a — every part was tagged. Otherwise it is removed and assembly.conformance-claim-removed names the claim and the part that broke it. M06 never repairs conformance (M20), and never leaves a claim it has reason to doubt.

Attachments and associated files​

PdfAttachments the files a document carries, at every level: document (EmbeddedFiles
name tree and catalog /AF), page (/AF and FileAttachment annotations),
annotation
PdfAttachment read side: key, file name (/UF over /F, decoded), description, MIME type,
size, dates, checksum, relationship, level and owner; OpenRead() streams
the decoded bytes under the document's reader limits
PdfAttachmentSpec write side: file name, a Stream read forward once, MIME type, description,
relationship, dates supplied by the caller — never the clock
PdfAssociatedFileRelationship Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema,
Unspecified
  • Writing streams the content: compressed on the fly with an indirect /Length; /Params (size and checksum) is an indirect object reserved ahead and written after the stream, when both are known.
  • /CheckSum is an MD5 computed by a managed implementation in the core, not the BCL's: browser WebAssembly has no MD5 (M23 runs the core there), and output must not depend on the platform (invariant 6).
  • The MIME type is written as a name with / escaped as #2F. Adding a key that exists is refused or renamed, as the caller chooses. Name-tree keys stay sorted by bytes.
  • /AF and AFRelationship are PDF 2.0 keys that ISO 19005-3 brought into PDF 1.7 for PDF/A-3; they are written in 1.7 output without raising the version, so M03's version table gains no row for them — the choice M14 relies on for its PDF/A-3 invoices.
  • Related files (/RF, Mac resource forks) are read as absent and reported.

Viewer preferences and open action​

PdfViewerPreferences immutable: HideToolbar, HideMenubar, HideWindowUI, FitWindow, CenterWindow,
DisplayDocTitle, NonFullScreenPageMode, Direction, PrintScaling, Duplex,
PickTrayByPdfSize, PrintPageRange, NumCopies, Enforce; unknown keys preserved
PdfPageMode UseNone, UseOutlines, UseThumbs, FullScreen, UseOC, UseAttachments
PdfPageLayout SinglePage, OneColumn, TwoColumnLeft, TwoColumnRight, TwoPageLeft, TwoPageRight
PdfOpenAction a destination (remapped on merge) or an action carried as it is

The assembly API defaults for a case file are UseOutlines and DisplayDocTitle, since a case file opens on its bookmarks and PDF/UA-1 requires the title shown.

The assembly API​

PdfAssembly the facade: Add(source, part options) in order; Analyze() -> PdfAssemblyPlan;
WriteAsync(plan, output, cancellation, progress) -> PdfOperationReport
PdfAssemblySource a file path, a stream factory, or an open PdfDocument
PdfAssemblyPartOptions immutable: page ranges, title (its bookmark and its Part element), outline
placement (Keep, Nest, Drop), labels for this part
PdfAssemblyOptions immutable: field collision, signed-part, certified-part and structure policies;
page-label plan; deduplication; output version (ADR 40); document identifier,
dates and metadata supplied by the caller; viewer preferences; the base part
PdfAssemblyPlan what analysis found, per part: page count, collisions and their resolution,
refusals, the version the output will need — inspectable before a byte is written
PdfOperationReport entries (code, document or part, page or object, message); counts by code;
serializes to stable JSON — the one report every operation on existing
documents returns: M06's assembly and page edits, M07's, M09's, M11's, M16's
  • Analysis opens each part in turn, keeps what the plan needs — page count, name-tree keys, fully qualified field names, the outline, the labels, the catalog's keys, the signatures and their kind (M04), the minimum version — and closes it.
  • Writing opens each part again, in plan order, and copies it forward: one part open at a time. The page tree, outline, name trees, fields, structure root and catalog are written last, from numbers reserved ahead. Memory is the largest page's object closure, plus the plan's metadata, plus the deduplication index.
  • Determinism: plan order, the source's key order, deterministic suffixes, first-occurrence deduplication and reserved numbering give the same bytes for the same inputs and options.
  • Cancellation and progress follow M03's convention — IProgress<PdfProgress> then a CancellationToken, checked without allocating — with one stage for analysis (Analyzing) and Merging, the stage M03 named for writing, both counted in pages.
  • The writer stays internal. M03 left PdfWriter's public shape to M06 or M08; M06 needs none, since the assembly API is the public way to write a document made of parts.
  • Report codes are assembly. plus a kebab-case name, public and stable from the first stable release, disjoint from reader diagnostic codes and validation rule identifiers. A reader diagnostic raised while a part is read is carried into the report under that part.
  • Editing one document in place and merging share the copier and every policy: PdfPageCollection.Insert with pages from another document is a two-part assembly.

The command-line tool​

AdCodicem.Pdf.Tool a dotnet tool, and a Native AOT binary per platform; command adpdf
info FILE version, pages, encryption, producer, labels, outline summary,
fields, attachments, revisions and signature coverage (M04), claims
validate FILE [--profile] the M02 report
repair IN -o OUT [--mode] M05
merge -o OUT IN[:RANGES]... the assembly API; --plan PLAN.json for every option; --report
pages IN -o OUT select|remove|rotate|move|insert-blank RANGES
attachments FILE list|add|extract|remove
  • Every verb has --json, documented and stable. Exit codes: 0 success, 1 findings at error severity, 2 usage, 3 unreadable input, 4 refused by policy, 5 undecided — nothing failed, and a verdict waits for a person, for missing material or for a check that could not run (M20's validate, M27's verify) —, 6 done at a lower level than asked, as the caller allowed (M21's pdfa convert). One table for every verb; a later verb uses it and never gives a code a second meaning. No network, no telemetry, no clock in outputs.
  • Native AOT and trimming warnings are build errors. The argument parser is System.CommandLine 2 or a small parser of our own, whichever gives the smaller AOT binary with no warning; the slice records the measurement. The package identifier follows ADR 24; the command is adpdf, recorded among the ADR index's decisions too small for a record.
  • repair needs M05, which the execution order places before M06; if M05 has not closed, the verb lands with M05.

Slices​

Each slice ends on a green commit, with its acceptance rows passing and the report codes it introduces documented.

  1. Pages and inheritance. Delivers PdfPage, the effective boxes, inherited attributes resolved from the indexed path, PdfPageCollection read side. Proven by unit tests on hand-built trees (inheritance at every level, a /Parent that lies, a tree deeper than the bound, a page with no MediaBox) and by PageTreeRefereeTests: pikepdf, which pushes inherited attributes down, in a container, agreeing page by page on every committed document. Leaves every write for later.
  2. Page edits in place. Delivers rotate, remove, move and insert-blank on one document, saved as a full rewrite and as an incremental update; references to a removed page dropped by their owners and reported; the report type and its first codes; M04's write guard on certified documents. Proven by unit tests per owner of a page reference, by qpdf's --json page list after each edit on the corpus's multi-page documents, and by vendor/us-federal/itext-govinfo-us-code-certified.pdf refusing a rotation. Leaves copying between documents.
  3. Deep copy, extraction and deduplication. Delivers PdfObjectCopier, the page map, extraction of pages into a new document, a pages-only merge, and structural deduplication. Every catalog structure not yet carried is dropped with a report entry, so the slice is honest before it is complete. Proven by unit tests (cycles, self-references, references to missing objects, a stream longer than a window, a hostile depth), by extracting every page of every committed document and joining them back (qpdf --check, pdftotext page by page, both in containers), and by the size measurement on the Chromium report and its two qpdf rewrites. Leaves links, names, labels, forms, layers, attachments and structure.
  4. Destinations, links and outlines. Delivers the page map applied to explicit destinations, the merged name tree with renaming and the rename map, outline placement, removal of what points at excluded pages, and the base's open action remapped. Proven by unit tests (name-object and string keys, a collision chain, a cyclic outline, an outline item whose destination is a named destination that does not exist), and by pdf.js (getOutline, getDestination, getPageIndex) and pikepdf in containers resolving every link and bookmark of the merged corpus sets to the expected page. Leaves GoToR to M07.
  5. Page labels. Delivers PdfPageLabels, the plans, remapping on edit and merge. Adds expect.pageLabels to the manifest and its schema — "the label of each page, in page order, as qpdf's --json reports it (M06)" — for every document with a /PageLabels tree, written by build_corpus.py from qpdf's --json. Proven by unit tests (letters past Z, roman past 3,999, a tree without key 0, a reorder that forces one range per page, hostile trees) and by pdf.js getPageLabels and qpdf agreeing on every label of the merged sets. Leaves label-range splitting to M07.
  6. Interactive forms. Delivers the AcroForm merge: fully qualified names on decoded text, the four policies, /DR renaming with /DA rewriting, pruning, /SigFlags, /CO, /NeedAppearances, XFA removal. Fills expect.formFields for the committed forms, which the schema already says are asserted from M06. Proven by unit tests (prefix collisions, a type conflict under Join, a field tree with a cycle, /DA with unusual spacing) and by pikepdf listing every field, its name, type and value after the two Cerfa forms are merged. Leaves filling and appearances to M11 and M16.
  7. Layers, output intents, extensions and the rest of the catalog. Delivers the /OCProperties merge, output-intent deduplication and conflicts, /Extensions union, typed viewer preferences, page mode and layout, /AA, /URI, /Threads, /Perms /UR3, the conformance-claim rule, and the policy for every remaining key — the catalog table complete. Proven by unit tests per key, pdf.js's getOptionalContentConfig for layer visibility, qpdf's --json for extensions, and veraPDF in a container for every PDF/A claim the output keeps. Leaves associated files.
  8. Attachments and associated files. Delivers PdfAttachments read and write at every level, streamed, with the managed MD5, and the merge of embedded-file trees and /AF arrays. Relies on expect.attachments, which the schema already says is asserted from M06. Proven by unit tests (a file spec with both /F and /UF, a name tree whose /AF points at another file specification, a huge declared /Size, a duplicate key) and by poppler's pdfdetach and pikepdf in containers listing and extracting the same files, byte for byte. The attachments verb waits for slice 12.
  9. Logical structure. Delivers the structure merge: Document and Part, parent-tree renumbering, role and class map collisions, ID tree, /Lang per part, pruning, untagged parts, PdfStructurePolicy.Drop. Proven by unit tests (colliding role maps, an XObject with /StructParents reached from two parts, an element whose content is all on excluded pages) and by veraPDF's PDF/UA-1 profile on the merged tagged set, plus a pikepdf walk that finds every marked-content identifier reachable exactly once.
  10. Signed, certified and portfolio parts; the version. Delivers the signed-part and certified-part policies with AttachOriginal, /DSS and /Legal removal, portfolio covers, and M03's minimum version (ADR 40) applied across parts, its raise carried into the report. Proven by unit tests over M04's signature model, and by pyHanko in a container: the original attached to a part validates as intact once extracted by pdfdetach, and no signature in a merged output is reported valid.
  11. The assembly facade. Delivers PdfAssembly, PdfAssemblyPlan, PdfOperationReport and its JSON, cancellation and progress, the determinism guarantee, and AssemblyBenchmarks. Proven by the full acceptance table, by a test that saves every merge twice, and by the memory test over ten, fifty and one hundred documents. Leaves nothing in the core.
  12. The command-line tool. Delivers AdCodicem.Pdf.Tool with its six verbs, --json, exit codes, the dotnet tool package and the Native AOT binaries built in CI. Proven by unit tests of each verb's handler in-process (arguments, exit codes, a JSON snapshot per verb) and by CorpusToolTests: the AOT binary, run over every committed document, agrees with the manifest and with the dotnet tool. Leaves split, collate, images, outline and unpack to M07.

Tests required​

Unit — tests/AdCodicem.Pdf.Tests:

  • Inheritance: each inheritable key at each level; a value overridden below; a /Parent pointing elsewhere; a tree whose depth passes the bound; a page with no MediaBox on its path.
  • Every page-reference owner: removing the page a link, an outline item, a named destination, the open action, a structure element, a widget and an article bead point at, each rewritten or dropped and reported.
  • The copier: cycles, a self-referencing dictionary, a reference to a missing object, 10,000 nested arrays, a stream with an indirect /Length that lies, the same font reached through two object numbers (deduplicated), two fonts that differ by one byte (not), an object with /StructParents (never shared).
  • Every row of the catalog table: carried, renamed or dropped exactly as written, with its report entry.
  • Collisions: named destinations (name object and string), embedded-file keys, field names (equal, prefix, type conflict), /DR names with /DA rewriting, role map and class map entries, ID tree keys; each suffix chosen so the result collides with nothing, and the same suffix every run.
  • Page labels: every style, letters past Z, roman past 3,999, a prefix in each text encoding, a tree without key 0, remapping after every edit; FsCheck: for any sequence of edits, each page's label after compression equals its materialized label.
  • Attachments: add, list, read, replace and remove at each level; streaming verified by a source that refuses to be read twice; the managed MD5 against RFC 1321's test vectors; FsCheck: add then read returns the bytes written.
  • Signed, certified, portfolio and encrypted parts: each policy's outcome and report entry; an encrypted part refused at analysis.
  • Determinism: FsCheck over random plans drawn from corpus documents — two writes, identical bytes.
  • Hostile: page-label trees, outlines, field trees, name trees and structure trees with cycles, excessive depth and absurd counts end in a report or a typed exception, inside time and allocation budgets.
  • The tool: each verb's arguments, exit codes and JSON, in-process.

Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container (ADR 27):

  • qpdf — --check on every output; --json for pages, labels, outlines, form fields, attachments and extensions.
  • pikepdf — pushed-down page attributes; link and outline destination resolution; the field list with names, types and values; optional content; a structure-tree walk.
  • pdf.js, pinned pdfjs-dist under Node — page labels, outline, named and explicit destinations, optional-content visibility: what a viewer shows.
  • poppler — pdftotext page by page, and pdfdetach for attachments.
  • veraPDF — every PDF/A or PDF/UA claim an output keeps; PDF/UA-1 on merged tagged documents.
  • pyHanko — the original attached under AttachOriginal still validates as intact; no merged output claims a valid signature.

Acceptance conditions​

"Every committed document" means every document under documents/ and vendor/ the reader opens, encrypted ones excepted until M16.

DocumentsBehaviorVerified by
Every committed documentEvery page's effective MediaBox, CropBox, Rotate and resources equal what pikepdf reports once it has pushed inherited attributes downCorpusPageTests.Every_page_resolves_what_it_inherits, PageTreeRefereeTests
vendor/opf-format-corpus/pdfmaker9-word-distiller-pdfa1b-test-document.pdf (its MediaBox only on the page tree's root), vendor/opf-format-corpus/distiller3-dea-cfr-damaged.pdf (19 pages two levels down, their MediaBox on intermediate nodes), the LibreOffice invoice and reports (a root /Resources every page overrides)A page extracted alone, moved to another place or rotated keeps the box and the resources it inherited, as its own entries, and a rotation changes only its own /Rotate; qpdf agrees. Dropping the resources it never uses is M07'sCorpusAssemblyTests.Extracted_pages_keep_what_they_inherited
Every committed documentExtracting every page and joining them back gives the same page count, each page's text as pdftotext extracts it from the source, and a file qpdf --check acceptsCorpusAssemblyTests.Extracting_and_rejoining_every_page_changes_nothing_visible
documents/contract/chromium-contract-fr.pdf, then as appendices vendor/uk-ogl/word2019-ccs-contract-schedule.pdf and vendor/pdf-association/indesign13-pdfua1-german-book-chapter.pdf (26 internal links, named destinations, a seven-item outline), with documents/report/chromium-report-fr.pdf as the generated pagesqpdf accepts the volume; pdf.js and pikepdf resolve every link and bookmark to the page its source named, shifted by its part's offsetCorpusAssemblyTests.Merged_contract_keeps_every_link_and_bookmark_on_its_page
documents/report/chromium-report-fr.pdf, documents/report/qpdf-objstm-report.pdf, documents/archival/qpdf-linearized-report.pdf — the same eight names in each catalog's /DestsSixteen names renamed and listed in the report; each copy's eight links land in its own copyCorpusAssemblyTests.Colliding_named_destinations_are_renamed_and_links_follow
vendor/opf-format-corpus/word9-distiller405-usgs-nwql-volatile-organics-methods.pdf (86 pages, 14 top-level items), pdfmaker707-word-va-esig-developer-guide.pdf (80 internal links, 5 items), pdfmaker7-powerpoint-va-cancer-database-course.pdf (52 items, every page rotated 90°), pdfmaker707-word-law-library-iraq-legal-history.pdf (a destination name tree), vendor/eu-publications/3heights-eu-consolidated-regulation-2026.pdf (a destination name tree, 55 URI links)In one volume, every item and internal link resolves in pdf.js to its expected page, URI links are untouched, rotations keptCorpusAssemblyTests.Outlines_and_links_resolve_after_merge
vendor/opf-format-corpus/reader10-openoffice32-annotated-object-streams.pdf (a note and a highlight with their popups, a named appearance tree), pdfmaker9-word-distiller-embedded-quicktime.pdf (a screen annotation and a rendition action)Popups stay attached to their markup, the appearance tree is merged, rich media is carried and reported (ADR 37)CorpusAssemblyTests.Annotations_keep_their_popups_and_active_content_is_carried
vendor/fr-licence-ouverte/libreoffice-cerfa-13983-form.pdf (20 fields) and vendor/fr-licence-ouverte/pdfmaker-acrobat-cerfa-12156-form.pdf (406 fields, a calculation order, document JavaScript) — Code postal, Commune, Nom and Prénom in both426 distinct fully qualified names in pikepdf, the four renamed and reported, none joined; the two /Helv fonts shared when equal and kept apart when not; the JavaScript carried and reportedCorpusAssemblyTests.Colliding_form_fields_are_renamed_not_joined
Two fillings of one form with different values — not in the corpus (below)Merged, both values survive, each under its own nameCorpusAssemblyTests.Merging_two_fillings_of_one_form_keeps_both_values
word9-distiller405-usgs-nwql-volatile-organics-methods.pdf (labels from index 7 only: lower roman, then decimal from 78), vendor/docentric/fop-factur-x-visualization.pdf (a redundant second range), vendor/pdf-association/handwritten-utf16le-strings.pdf (a UTF-16LE prefix), vendor/opf-format-corpus/distiller952-pscript5-kb-pdf-risk-inventory.pdf (56 pages)Under Keep, every page shows its source label in pdf.js and qpdf; under Continuous and PerPart, the labels the caller asked for, in bothCorpusAssemblyTests.Page_labels_follow_their_pages, CorpusAssemblyTests.Continuous_pagination_shows_the_labels_asked_for
vendor/uk-ogl/indesign-acrobat-hmcts-n208-form.pdf and vendor/uk-ogl/pdfmaker21-ozev-sample-invoice.pdf (two different layers both named Watermark), vendor/pdf-association/handwritten-utf16le-strings.pdf (two groups off by default, UTF-16LE names), pdfmaker707-word-law-library-iraq-legal-history.pdf (a group on but absent from its empty /Order), and the decrypted twin of vendor/us-federal/livecycle-uscis-ar11-xfa-form.pdf — not in the corpus (below): print-only and view-only groups with usage and /AS; the original once M16 opens itEach layer distinct, with its default visibility and its usage, listed in /Order under its part, as pdf.js reportsCorpusAssemblyTests.Layers_keep_their_identity_and_visibility
vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf and vendor/eu-publications/pdflib-oj-exchange-rates-greek.pdf (PDF/A-2a, one sRGB profile, same bytes); pdflib-oj-exchange-rates-greek.pdf and vendor/eu-publications/3heights-eu-consolidated-regulation-2026.pdf (the same profile, differently encoded, under another identifier); vendor/verapdf/pdfa1b-annotations-pass.pdf and vendor/verapdf/pdfa2b-content-pass.pdf (Adobe RGB and Apple RGB)One output intent where the decoded profiles are equal; a conflict reported otherwise; never a claim veraPDF rejects — kept and upheld, or removed and reportedCorpusAssemblyTests.Conformance_claims_are_kept_only_while_they_stay_true
indesign13-pdfua1-german-book-chapter.pdf (ADBE extension level 3 under a 1.4 header) and vendor/us-federal/livecycle-irs-1040-2022-xfa-ur3.pdf (ADBE level 8, usage rights, XFA)The output declares 1.7 with ADBE level 8; usage rights and XFA removed, each reportedCorpusAssemblyTests.Extensions_are_unioned_and_what_cannot_survive_is_reported
vendor/opf-format-corpus/pdfmaker9-word-distiller-embedded-quattro-pro-spreadsheet.pdf (a document-level file), pdfmaker10-word-file-attachment-annotation.pdf (an annotation's file only), vendor/bfo/bfo-pdfa2b-embedded-pdf.pdf (a PDF inside), documents/invoice/qpdf-invoice-with-facturx-xml.pdf, and the Factur-X invoices of vendor/zugferd/ and vendor/docentric/ (/AF with AFRelationship; in pypdf2-facturx-python-false-pdfa3b.pdf, a name tree and an /AF naming different file specifications)Every attachment listed with its level, name, MIME type and relationship as pikepdf lists it, its bytes equal to pdfdetach's; expect.attachments metCorpusAttachmentTests.Every_attachment_is_listed_and_read_as_referees_see_it
The same documents, merged in one volumeEvery attachment survives with its relationship; two factur-x.xml files keep their names under renamed keys, and the combination is reportedCorpusAttachmentTests.Attachments_survive_a_merge
pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf, indesign13-pdfua1-german-book-chapter.pdf, vendor/pdf-association/indesign15-pdfua1-form.pdf — three PDF/UA-1 claims veraPDF upholds, in en, de-DE and en-GBveraPDF's PDF/UA-1 profile finds no failure on the volume that no input already had; each part's language kept on its Part element; every marked-content identifier reachable onceCorpusAssemblyTests.Merged_tagged_documents_stay_tagged
vendor/us-federal/itext-govinfo-us-code-certified.pdf (DocMDP P=1)Rotating a page in place, or merging it, is refused; insisted, it proceeds and is reported, and under AttachOriginal the original taken out by pdfdetach validates as intact in pyHankoCorpusAssemblyTests.Certified_documents_are_refused_unless_the_caller_insists
vendor/pyhanko/acrobat-reader-signed-twice.pdf, vendor/lu-legilux/antenna-house-legilux-memorial-pades-lta.pdf, vendor/us-federal/docusign-pdfkit-gsa-sf30-contract-modification.pdf, with libreoffice-cerfa-13983-form.pdf (its empty Signature1 and Signature2)Signature fields carried unsigned and renamed where they collide; /DSS dropped; each reported; pyHanko finds no valid signature in the output, and no report claims oneCorpusAssemblyTests.Signed_parts_are_carried_unsigned_and_reported
The Chromium report and its two qpdf rewrites; documents/invoice/chromium-invoice-fr.pdf with qpdf-invoice-with-facturx-xml.pdf; documents/stress/reportlab-journal-1000-pages.pdf with itselfThe output is no larger than the sum of the inputs minus the deduplicated bytes, within a stated overhead for the rebuilt trees; each font program written onceCorpusAssemblyTests.Merged_size_accounts_for_every_deduplicated_resource
Ten, fifty, then one hundred committed documentsPeak memory follows the largest page and the plan's metadata, not the number or size of the documents; budget recorded in status.mdCorpusAssemblyTests.Assembling_one_hundred_documents_holds_its_memory_budget, AssemblyBenchmarks
Every merge aboveTwo writes give identical bytesCorpusAssemblyTests.Assembly_is_deterministic
Remote: remote/opf-format-corpus/jhove-hul-29-wpdf-book-chapter-scan.pdf (a null kid), jhove-hul-35-atypon-pdfplus-journal-article.pdf and jhove-hul-35-pdfium-urban-planning-report.pdf (kids naming objects the file lacks)PdfPageCollection holds the manifest's 30, 13 and 65 pages, each such kid a blank US Letter page as qpdf shows it; extracting one yields a blank page, reportedCorpusPageTests.Missing_kids_are_blank_pages_as_qpdf_counts_them
Remote: remote/opf-format-corpus/acrobat9-portfolio-signed-3d.pdfIts cover page merged, its five members reported as not unpackedCorpusAssemblyTests.A_portfolio_part_contributes_its_cover
vendor/us-federal/illustrator-irs-pub1-english.pdf (an incremental save that removed text); remote, remote/us-states/powerbi-pdfium-acrobat-covid-dashboard-nine-updates.pdf (ten revisions)Merged as the latest revision shows it: the removed text does not come back in pdftotext, and no object only an earlier revision reaches is in the outputCorpusAssemblyTests.Only_the_latest_revision_is_carried
Remote: remote/govinfo/us-code-2023-title42.pdf (9,302 pages, signed)Twenty pages extracted with memory flat; its signature reported as not carriedCorpusAssemblyTests.Extracting_from_the_largest_document_holds_its_budget
Remote: remote/pdfcpu/quartz-avow-certified-docmdp-md5-sigref.pdf, remote/eu-dss/pdfmaker11-nbu-sk-qualified-seal-docmdp-fieldmdp.pdf, remote/pdfium-tests/foxit-phantompdf-certification-signature-visible.pdfRefused as certified by default, and each reported when insistedCorpusAssemblyTests.Certified_documents_are_refused_unless_the_caller_insists
Remote: remote/eu-dss/dss-signed-widget-self-parent-startxref-past-eof.pdf (a widget that is its own parent)Merged without a hang; the field tree's cycle reportedCorpusAssemblyTests.Hostile_field_trees_end_in_a_report
Every committed document, through the toolinfo --json gives the manifest's page count and encryption; merge of the contract set yields what the API yields; the AOT binary and the dotnet tool agree byte for byteCorpusToolTests.Info_matches_the_manifest, CorpusToolTests.Merge_matches_the_api

The remote rows close only on a green Remote corpus run, recorded in status.md with its date.

Corpus​

What the corpus holds​

  • Page trees: every committed document the reader opens, to compare with pikepdf page by page; inherited MediaBox in two (one from the root, one from intermediate nodes), a root /Resources overridden by every page in the LibreOffice output; /Rotate 90 or 270 in pdfmaker7-powerpoint-va-cancer-database-course.pdf, distiller7-pscript5-census-housing-units-2005.pdf, distiller952-pscript5-kb-pdf-risk-inventory.pdf (one page of 56) and docusign-pdfkit-gsa-sf30-contract-modification.pdf; /CropBox differing from the MediaBox in dozens; UserUnit in handwritten-compacted-syntax.pdf.
  • Destinations and outlines: the catalog /Dests of the Chromium report and its qpdf derivatives (eight identical names — a ready-made collision); destination name trees in seven vendored files; eight documents with two or more top-level bookmarks, among them out-of-order items and two items on one page; handwritten-cyclic-toc.pdf, whose outline and links form a cycle.
  • Page labels: 32 committed documents with a /PageLabels tree, four of them encrypted until M16, mostly decimal; lower roman in the USGS report and with the UTF-16LE prefix; remote, handwritten-pdf20-utf8-strings.pdf with a UTF-8 prefix and outline titles, and long labeled documents (pdfmaker81-word-va-kernel-systems-guide.pdf, 464 pages).
  • Forms: thirty AcroForm dictionaries, four with XFA; real name collisions between the two Cerfa forms, and between the signature fields of five signed files and the Cerfa's two empty ones; /Helv in nearly every /DR; /CO in two; /NeedAppearances in none; a cyclic field tree in the remote corpus.
  • Layers: nine documents with /OCProperties, two different layers named Watermark, two groups off by default in the PDF Association's hand-written file, print-only and view-only usage in the AR-11 form (AES-128, until M16 or its decrypted twin), and three PDFMaker files whose /OCProperties lack the /D ISO 32000 requires, which M02's shape rules report.
  • Output intents and claims: 30 committed PDF/A claims, 28 with veraPDF's verdict, one sRGB profile under four encodings and two condition identifiers, Adobe RGB, Apple RGB, CGATS; three PDF/UA-1 claims veraPDF upholds.
  • Extensions: ADBE levels 3 and 8, one under a 1.4 header.
  • Attachments: document-level, annotation-level only, a nested PDF, six Factur-X and ZUGFeRD invoices with /AF, one whose name tree and /AF disagree; remote, an attachment added by an incremental update and one with AFRelationship /Source.
  • Signatures: a DocMDP P=1 certification committed, four more remote that open without a password; approval signatures, PAdES B-LTA, DocuSign, /DSS; usage rights in five files.
  • Active content: document JavaScript, a screen annotation with a rendition, a JavaScript open action.
  • Scale: the 1000-page journal committed; US Code Title 42 and the ten-revision dashboard remote; the Acrobat 9 portfolio remote.

What it lacks​

NeedWhyPriorityLikely source
Two fillings of one AcroForm with different, fictitious valuesThe roadmap's "merging two forms that both define a field of the same name keeps both values" needs values; no committed form is filled in two ways1Generated here: reportlab-subscription-form.pdf filled twice with pypdf, a derived variant recorded in build_corpus.py
The decrypted twin of vendor/us-federal/livecycle-uscis-ar11-xfa-form.pdf (M11's gap as well)The only committed groups whose state differs between screen and print are AES-128 encrypted, which the reader opens in M16; the layer row above needs them now1Derived here: qpdf --decrypt, a recorded transformation in build_corpus.py
A contract whose appendices are third-party PDFs, with bookmarks and cross-references between body and appendicesThe corpus's contract has no link, no bookmark and no appendix; the acceptance above substitutes a set of separate files2Generated here: LibreOffice from an ODT with an annex, headings exported as bookmarks and cross-reference fields as links; a Word-made annex as a contribution
A page tree that inherits /Rotate, /CropBox and /Resources from intermediate nodesOnly MediaBox is inherited in the committed corpus, from two files, one damaged2A public source first (older Distiller and Ghostscript output); failing that, derived with pikepdf by a recorded restructuring
Page labels in upper roman and letters, with real prefixes (A-1, Annexe 2-)Committed labels are decimal or lower roman; letters and prefixes are what case files use2Generated here: qpdf --set-page-labels on a committed document
Two tagged documents whose role maps map one custom type differently, and colliding class namesThe role-map collision rule has no real case; the corpus's role maps do not conflict3A contribution, or two producers' tagged output with their own style names (Word and InDesign)
A document with a /URI base and relative URI linksThe rule that resolves a part's relative links against its own base has no real case3Generated here: LibreOffice with relative link export, the base added by a recorded transformation
An intact document with article threadsThreads are carried only by a damaged remote file3A public source: magazine and newsletter PDFs from Distiller-era publishing in GovDocs1
A PDF/UA-1 document from another producer than InDesign and PDFlibThe merged-structure acceptance rests on two producers3A public source (the PDF/UA Reference Suite's remaining members are remote), or a contribution

Traps​

  • A merge is where libraries lose data quietly. The test of this milestone is not that the output opens, but that every structure named in the catalog table is either present or in the report. A test that checks only page count and text passes on the worst merge.
  • /Parent is not a way up. Following it drags a whole source page tree into the copy, and on damaged files it loops or points at the wrong node. Inheritance comes from the indexed path; copies never follow it.
  • Names in /Dests are name objects; in the name tree they are strings. Moving the first into the second changes how every link refers to them.
  • The same profile, compressed differently, is still the same profile; different profiles under the same condition identifier are not. Compare decoded bytes.
  • Sharing a content stream between two pages is legal and a trap for M09, which appends to content in place. Deduplication stops at resources.
  • An XObject with /StructParents cannot be shared between parts: its key belongs to one parent tree.
  • Field names are text: Prénom in PDFDocEncoding and in UTF-16BE is one name. #36 comes first.
  • /Helv is not one font: every producer writes its own under that name, and viewers regenerate appearances from /DR by name.
  • /NeedAppearances true in one part changes how every other part's fields look.
  • A signed input cannot stay signed. Anything that suggests otherwise — keeping /V, keeping /DSS, keeping SigFlags AppendOnly — misleads the next reader into validating a signature that covers nothing.
  • A page-label tree without key 0 is common; readers disagree less than they seem to, since a range with no style gives an empty label, which is what pdf.js shows.
  • Two Factur-X invoices in one volume make neither a Factur-X invoice. The core does not know Factur-X; it reports combined document-level associated files, and M14 decides.
  • Resource names inside /DA strings are the one place a resource name hides in a string rather than a name object.

Documentation​

  • docs/website/docs/tutorials/first-case-file.md — assembling two received PDFs into one, with a bookmark for each and their page labels kept, step by step; its code a sample under samples/, held to the tutorial by a test (ADR 47). Assembling case files is the first business priority, and the path a newcomer should learn by doing.
  • docs/website/docs/concepts/pages.md — the page tree, inheritance, effective boxes, edits as a plan.
  • docs/website/docs/concepts/merge-fidelity.md — what a merge carries, the catalog table, conformance claims, signed inputs; what M07 and M09 add.
  • docs/website/docs/guides/assembling-a-case-file.md — the assembly API end to end: parts, titles, labels, collisions, the plan, the report.
  • docs/website/docs/guides/attachments.md — attachments and associated files at every level.
  • docs/website/docs/reference/assembly-report-codes.md — every assembly.* code, its meaning and what to do about it.
  • docs/website/docs/guides/installing-the-tool.md — installing the tool, as a dotnet tool or a Native AOT binary.
  • docs/website/docs/reference/tool/index.md — each verb and its options, --json, exit codes.
  • docs/website/docs/introduction.md — assembly and the tool added to what the library does.
  • docs/architecture.md — the copier, the page map and deduplication in Documents/; the tool package.
  • docs/releasing.md — the tool's package and its AOT binaries.
  • docs/corpus.md — expect.pageLabels, and formFields and attachments now asserted from M06.

Exit criteria​

  • PdfPage and PdfPageCollection exist, with inheritance resolved from the indexed path and edits saved as full rewrite and incremental update.
  • The copier passes streams through encoded, deduplicates exactly, and never shares page content or structure-keyed objects.
  • Every row of the catalog table is implemented, with its report entry, and no catalog key leaves a part unreported.
  • Page labels, forms, layers, output intents, extensions, attachments and structure merge as designed.
  • Signed, certified, portfolio and encrypted parts follow their policies; certification is never voided silently.
  • #36, which M03 fixes before its slice 8, is closed before slice 5 — fixed here first should M03 not have done it.
  • The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on a green Remote corpus run recorded in status.md.
  • Unit tests cover each behavior, its degenerate cases and its hostile ones; FsCheck properties hold for labels, attachments and determinism.
  • Integration tests confirm the outputs through qpdf, pikepdf, pdf.js, poppler, veraPDF and pyHanko, each in a container.
  • AssemblyBenchmarks measures merging and extracting with MemoryDiagnoser, and the memory budget of the one-hundred-document assembly is enforced in CI and recorded in status.md.
  • The tool ships as a dotnet tool and as Native AOT binaries with no trimming or AOT warning, and its JSON outputs are documented.
  • The documentation site publishes the pages listed above; status.md records the measurements.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).