nigig-org/NIGIG_PDF_FEATURE_PARITY_PLAN.md
andodeki b3a9005795
Some checks failed
nigig-site / SITE-02 native provider/filesystem (macos-latest) (push) Successful in 41m47s
nigig-site / Owned paths and honest test contracts (push) Has been cancelled
nigig-site / Cargo check-all-targets (push) Has been cancelled
nigig-site / Cargo clippy-site-owned (push) Has been cancelled
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo integration-non-live (push) Has been cancelled
nigig-site / Cargo production-dependency-containment (push) Has been cancelled
nigig-site / Cargo site02-crypto (push) Has been cancelled
nigig-site / Cargo site02-repository (push) Has been cancelled
nigig-site / Cargo site02-store (push) Has been cancelled
nigig-site / Cargo unit (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (ubuntu-latest) (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (windows-latest) (push) Has been cancelled
nigig-site / SITE-02 desktop runtime and normal shutdown (push) Has been cancelled
nigig-site / SITE-02 migration, recovery, and fault corpus (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
feat(pdf): image ink colorants + downsample/pixels + shading/interpreter ports
2026-09-14 09:46:52 +03:00

54 KiB
Raw Permalink Blame History

nigig-pdf feature-parity plan (view → create → edit)

Target: bring the four-crate nigig-pdf stack in crates/apps/pdf/ to feature parity with the dart-pdf reference implementation (Apache-2.0) for creating and editing PDFs, and absorb the Makepad dev-branch PDF code so we no longer need it. Viewing is already the baseline; this plan closes the read/edit/write gaps phase by phase.

Coverage parity is a first-class goal, not an afterthought: Section 2 maps all 137 dart-pdf test suites (pdf_cos 31, pdf_document 66, pdf_graphics 37) to a nigig port/decision per phase, so each phase lands its features with the corresponding dart-pdf test coverage. "Parity" for a phase = the dart-pdf feature exists in the matching nigig crate and its dart-pdf test suite has a passing nigig equivalent.

0. Reference codebases and current state

0.0 Standing directive: automatic porting of new dart-pdf functionality

On every work pass, before continuing the suite sweep, check the dart-pdf reference checkout (/Users/aok/Projects/rustdev/CratesCode/dart-pdf) for new functionality and port it without asking for go-ahead:

  1. git log --oneline the reference repo; compare HEAD against the dart-pdf HEAD baseline below.
  2. Any new/changed lib/src/** feature code or test/** suites since the baseline get ported in the same pass (implementation + ported tests + inventory row), prioritised alongside the suite sweep.
  3. Advance the baseline HEAD below when the new functionality is ported.

dart-pdf HEAD baseline: f0580827 (Release DartPDF 4.3.0, 2026-09-06).

0.1 Makepad upstream dev PDF code (the current baseline to supersede)

File LOC Capability
libs/pdf_parse/src/*.rs ~155K Minimal parse-only library: lexer, parser, object, page, content ops, filters (zlib), font (char widths), image (inline + extraction), document, tests
widgets/src/pdf_view.rs ~40K View-only widget: page rendering via PortalList, text selection, zoom. No edit, no form, no annotations, no save
examples/pdf ~3.3K Viewer demo app

Upstream dev pdf_view.rs capabilities (from source): text-segment tracking for selection, selection anchor/cursor, zoom, page caching. That is the full feature set — reading/display only.

0.2 nigig-pdf current state (what we have)

  • nigig-pdf-cos (8 files): lexer.rs, parser.rs, object.rs, xref.rs, writer.rs, filter.rs (Flate + TIFF predictor), encrypt.rs (RC4, AES-128/256), incremental.rs.
  • nigig-pdf-document (12 files): document.rs, page.rs, annotations.rs, annotation_edit.rs, appearance.rs, destinations.rs, form.rs, form_actions.rs, signature.rs (read-only), structure.rs, save.rs, content_writer.rs.
  • nigig-pdf-graphics (19 files): content.rs interpreter, device.rs (PdfDevice trait), recording.rs (RenderCommand), font.rs, sfnt.rs, cid.rs, cmap.rs, advance.rs, colorspace.rs, icc.rs, function.rs, transparency.rs, image.rs, cache.rs, worker.rs, text.rs, content_writer.rs, graphics_state.rs.
  • nigig-pdf-makepad (5 files): renderer.rs, device.rs, page_view.rs, interaction.rs (form-field interaction: render_fields, render_open_combo, PdfAction, selection, key input), lib.rs.
  • Tests: 571 passing (cos 72, document 147 unit + ~160 integration, graphics 191 unit + 19 golden + 7 phase-5 exit criterion). Fuzz targets in pdf-cos/fuzz/. Corpus in crates/apps/pdf/tests/corpus/.

0.3 dart-pdf reference inventory (feature target)

  • pdf_cos (~23K LOC): full lexer/parser/xref with xref streams + hybrid refs + recovery; filters Flate, LZW, CCITT, JBIG2, JPX, RunLength, ASCIIHex, ASCII85; crypto RC4, AES-128/256, RSA, CMS, OCSP, CRL; serializer/builder/updater/compactor/byte-source.
  • pdf_document (~500K LOC): document, page editing, annotation editor + sync + clipboard, form editor + styling + admin, content editor + type-0 + rewriter + reflow, redaction, signature editor + PAdES + signing identity + Fulcio, outlines + outline editor, page labels, struct tree + editor, XMP, PDF/A + PDF/UA conformance, OCR editor, attachment editor, header/footer, watermark/stamps, office conversion, disk cache.
  • pdf_graphics (~74K LOC): interpreter (3.7K), **PdfDevice + Recording
    • RenderCommand + wire codec (binary, isolate-safe)**, font info (embedded fonts, encodings, CID), ICC, colour spaces, functions, mesh gradients, shading, overprint compositor, text extraction with layout/search/ selection, translating device, document AI, vector print.
  • dart_pdf_editor (Flutter): canvas device, full editor UI (annotations, forms, redaction, search, OCR, AI).

Scale reference (from the nigig-vs-dart-pdf review). nigig is ~30K LOC in ~40 files against dart-pdf's ~230K LOC in ~178 files. Per-layer ratios matter for phase sizing — expect the biggest effort where the ratio is largest:

Layer dart-pdf nigig ratio
pdf_cos / pdf-cos ~23K ~1.9K ~12×
pdf_document / pdf-document ~500K ~3.5K ~140×
pdf_graphics / pdf-graphics ~74K ~2K ~37×
UI (dart_pdf_editor / pdf-makepad) canvas_device ~74K ~2.5K ~30×
Test files 137 ~20 ~7×

The pdf_document ratio (~140×) is the dominant cost; phases 4–6 (create/edit) carry most of it. Module-level comparisons (lexer/parser/xref/object, annotations/form_editor/redaction, interpreter/font_info/text_extraction) are the per-phase checklists in §1 and §2.

1. Phase plan

Phases are ordered by dependency. Each phase ends with a green build and a committed, tested tranche. "Parity" for a phase = the dart-pdf feature exists in the matching nigig crate with tests.

Phase 1 — Fork sync + makepad_test enablement (immediate, ~1-2 sessions)

  • Sync the Gitdab makepad fork to upstream dev (workflow.md §0.1), keeping nigig-dev-reexports intact.
  • Makepad test enablement (fork work, prerequisite for every UI test below). Upstream dev makepad_test only runs through the Studio hub: headless mode still needs a Studio websocket, and Android is unsupported. This blocks automated UI verification of nigig-pdf. The fork must gain:
    • Terminal/standalone mode: makepad_test runs a suite with no Studio — the harness spawns the app binary directly, drives the event loop, and asserts on widgets (already partially present in the WIP runtime + examples/counter/tests/ui.rs pattern: Selector::id(..).click() + .wait_text(..)).
    • Android mode: run the same ui.rs suites against a device via adb (launch APK, port-forward the test channel, run asserts in-code) using the staged WIP in the local fork checkout (makepad-native-glue/makepad): TestConfig.android/device_serial/adb_path, start_android_app(), native-activity platform module, cargo_makepad install/launch.
    • Reconcile the WIP with upstream dev (currently ~2 commits behind), resolve conflicts, commit, and push to the Gitdab fork so the workspace pin uses it.
  • Bump rev = in pdf-makepad/Cargo.toml + map/Cargo.toml; reconcile any API breaks; full workspace build + 571 tests green.
  • Baseline the gap list in this document against the freshly-synced libs/pdf_parse + widgets/src/pdf_view.rs so we can demonstrate nigig-pdf has superseded them.

Exit: workspace green on the new rev; cargo test -p nigig-pdf-makepad --test ui runs on the desktop (no Studio) and on an Android device; this document's inventory updated.

Phase 2 — Complete content-stream coverage (pdf-cos / pdf-graphics)

Close the interpreter operator gaps dart-pdf handles and nigig does not:

  • Inline images BI/ID/EI.
  • Image XObjects Do in content streams (currently placeholder in pdf-makepad).
  • Form XObjects Do (nested content streams, resource dictionaries).
  • Type 3 fonts (char-procs, /FontMatrix, /CharProcs, /Encoding).
  • Shading operators sh/sh1 + /Pattern//Shading resources.
  • Full text-object state (Tc/Tw/TL/Tz/Ts/Tstar/Td/TD/Tm/Tf/Tfs).
  • Read-side xref modernisation (currently traditional xref + /Prev chains only): xref streams (/Type /XRef, object streams /ObjStm), hybrid refs (/XRefStm), and corrupt-file recovery (dart-pdf xref.dart recovery + parser.dart robustness, xref_test.dart/parser_test.dart coverage). nigig parser.rs is only 67 LOC; this is the biggest parse-side gap.
  • Lexer/parser error position tracking for diagnostics (dart-pdf token-level lexer keeps source positions; nigig errors carry a position but the lexer is simpler).
  • Object model: lazy object resolution for very large documents (dart-pdf object.dart lazy loading / type inference) — confirm nigig's object.rs object-store is adequate or grow it.
  • Streaming content interpretation (dart-pdf streaming_interpreter.dart semantics: parse+interpret a content stream incrementally instead of materializing every op) — covers streaming_interpreter_test.dart.

Exit: pdf-graphics tests cover each operator family; golden corpus grows; pdf-makepad ui.rs smoke tests render pages exercising each new op family (image/form XObject Do, Type 3 text) and assert non-empty draw output.

Status: complete (ADR 0013, ADR 0014). Audited item by item rather than worked top-down, because several entries were already done:

Item State
Inline images BI/ID/EI already implemented and tested
Image/Form XObject Do already implemented; PdfPage::xobjects fixed in ADR 0011
Full text state Tc/Tw/TL/Tz/Ts/Td/TD/Tm/Tf already implemented
Shading sh operator parsed and reported
Xref streams, object streams, hybrid /XRefStm ADR 0013 — was the biggest gap: no PDF 1.5+ file would open at all
Type 3 fonts ADR 0014 — /CharProcs, /FontMatrix, d0/d1
Streaming interpretation ADR 0014 — ContentStreamIter, interpret_streaming
Lexer error positions errors already carry a byte position
Lazy object resolution resolve_num already caches per object; adequate

Three latent bugs were found and fixed along the way that the plan did not list: d1 read four of its six operands, PdfPage::xobjects was empty for every document, and BMC corrupted the colour of every following operator.

The ui.rs half of the exit criterion is blocked on the Makepad fork needing a headless backend — see REVIEWS/PDF_PARITY_PHASE1_STATUS.md.

Phase 3 — Filters and image codecs (pdf-cos)

  • Lossless: RunLengthDecode, ASCIIHexDecode, ASCII85Decode, LZWDecode (+ EarlyChange predictor handling, CW-branching).
  • Lossy/bitonal: CCITT G4 (and G3), JBIG2 (arith coding, huffman, refinement), JPX/JPEG2000. Decide crate strategy (e.g. ccitt, jbig2, jpeg2k/openjpeg or vendored ports from dart-pdf).
  • Keep the filter.rs facade; add a codec registry for round-trip tests.
  • Image decode surface: PNG/JPEG image decoding for raster image XObjects and inline images (covers png_test.dart); image downsampling for render performance (image_downsample_test.dart, image_pixels.dart surface).

Exit: corpus files using these filters decode; round-trip encode tests where the format allows; pdf-makepad ui.rs test renders a page whose image uses the new codec and asserts the decoded image is drawn (no blank region).

Status: complete (ADR 0015, ADR 0020). The lossless half landed under ADR 0015; the three image codecs it deferred — CCITT, JBIG2 and JPX — are now implemented, in that order, each with its own tranche and mutation record.

Item State
RunLengthDecode, ASCIIHexDecode, ASCII85Decode already worked; edge cases now covered
LZWDecode was broken outright — dictionary indexing was two slots off, so the spec's own worked example failed
/EarlyChange was ignored — a file setting 0 decoded to garbage, not an error
Predictors on LZW were gated to Flate only
Filter chains decode_stream could not read an array /Filter at all, so every chained stream in every document failed
CCITT G3/G4 implemented — T.4/T.6, all three /K schemes, decoded in the filter facade. Writing it found three defects: zero-length runs recorded no transition, trailing byte-padding was decoded as data, and a row of zero-length runs looped forever
JBIG2 implemented — MQ coder, generic regions (templates 0-3, TPGDON, MMR). Symbol dictionary, text/halftone/refinement regions and /JBIG2Globals are refused by name: those carry the composition machinery and shipping them means shipping an interpreter over untrusted input
JPX implemented — pure Rust, no C dependency, so the Android cross-compile is unaffected. EBCOT tier-1, tag trees, packet headers, 5/3 and 9/7 wavelets, RCT/ICT. Verified against OpenJPEG-produced fixtures, exactly. Multiple tiles, custom precincts, code-block style options and COC/QCC are refused by name
DCTDecode/JPXDecode pass-through removed: returned compressed bytes as though decoded

All three are now done. The two open questions that gated JBIG2 and JPX were answered rather than dropped:

  • JBIG2's CVE record. The exploit surface is the composition machinery — symbol dictionaries, text regions, refinement — not the arithmetic coder. Those segment types are refused by name; the generic-region path that every PDF-embedded JBIG2 image actually uses is implemented. Every file-controlled offset is bounds-checked before a buffer is indexed, and the region-to-page composition check has its own test naming what it guards.
  • JPX's C dependency. Avoided entirely: the decoder is pure Rust and shares its MQ arithmetic coder with JBIG2, since T.800 and T.88 specify the same one. No new crate, no cross-compile risk.

The remaining honest gap is coverage of the refused JPX and JBIG2 features, which is a deliberate scope boundary rather than an omission.

The ui.rs half of the exit criterion stays blocked on the Makepad headless backend — see REVIEWS/PDF_PARITY_PHASE1_STATUS.md.

Phase 4 — Document creation / writing (pdf-document + pdf-cos)

  • Full PdfWriter/builder: outlines (outline tree + viewer prefs), page labels, named destinations, document-level metadata (title, author, keywords, XMP), viewer preferences, page mode/layout.
  • content_writer.rs operators to match dart-pdf's ContentWriter (full text/path/color/image emission).
  • AcroForm creation: field tree (text, check, radio, list/combo, signature), field flags, NeedAppearances, and correct AcroForm/Fields wiring. Include field-value reconciliation (form_reconcile_test.dart) and appearance generation (generated_appearance_test.dart, text_box_appearance_test.dart).
  • Document attachments: /Names /EmbeddedFiles tree, file specs, attachment add/read/remove (attachment_test.dart).
  • Header/footer and image stamping on pages (header_footer_test.dart, image_stamp_test.dart, image_pdf_test.dart).
  • Font embedding: subset TrueType via sfnt.rs (glyph subsetting, cmap remap, repair-cmap), embed Type1/CFF if feasible.

Exit: generated PDFs from nigig open cleanly in dart-pdf and external viewers; pdf-makepad ui.rs form tests create a field in the UI, fill it via Selector::id(..).click()/keyboard input, and assert the field value + label state in code.

Status: complete, except the ui.rs interaction tests, which are written and blocked on the Makepad headless backend. (ADR 0019 covers the writer; ADR 0021 covers stamping, reconciliation, CFF and cmap.)

Two earlier revisions of this line were wrong in opposite directions: the first said "complete" while three named items were unimplemented, the second listed them as gaps. All three are now done, and the history is left visible rather than tidied away — a status line that has been overstated once should show its working.

Item State
Outlines, page labels, named destinations, metadata, XMP, viewer prefs, page mode/layout done
content_writer.rs operator coverage done
AcroForm creation, field flags, NeedAppearances, appearance generation done
Attachments (/Names /EmbeddedFiles) done
TrueType subsetting and embedding done
Field-value reconciliation (form_reconcile_test.dart) done — reconcile.rs. Classifies how /V and /AP disagree and resolves it against a caller-declared Intent, because the right answer differs: a viewer must render /AP to match other viewers, an extractor must read /V, an editor must regenerate. /NeedAppearances overrides all three, which is the one case §12.7.3.3 actually settles
Type1/CFF embedding done — embed_opentype_whole. Subsetting CFF stays refused by name; embedding the whole program does not, and that is what "if feasible" allows. Writes /FontFile3 with /Subtype /OpenType under a CIDFontType0 descendant. EmbeddedFont::is_subsetted reports which path ran, so a caller with a size budget or a licence constraint can tell
repair-cmap done — glyph_index now falls back to a (3,0) symbol subtable at 0xF000 + low byte, and reads format 0 as well as 4, 6 and 12. A symbol font's glyphs were previously unreachable by their plain characters: the lookup returned None and the character silently vanished
Header/footer and image stamping done — stamp.rs: image XObject embedding (JPEG passed through as /DCTDecode without re-encoding, raw samples as Flate), header/footer banners with alignment, and stamp composition

The stamping work closed a gap of exactly the shape ADR 0017 describes. ContentWriter::draw_image had existed since Phase 2 and was tested, but nothing could create the image XObject it names — so every Do operator referred to a resource that did not exist, and no error was raised anywhere.

It also uncovered a live defect in PdfDocBuilder's object numbering: add_object derived its number from pages.len(), so adding a page after embedding an image — the natural order, since the page content has to name the image — shifted a number already handed out, and the page's /XObject entry pointed at the page object itself. This is the same positional numbering bug previously fixed for fonts, one layer out; both are now fixed at the root by freezing the base when the first number is issued.

Exit criterion, verified where it can be

"Generated PDFs open cleanly in external viewers" — now actually tested, and it passes. pdf-graphics/examples/generate_sample.rs output was run through two implementations this codebase shares no code with:

Check Result
qpdf --check "No syntax or stream encoding errors found"
pdfinfo (poppler) title, author, subject, keywords, dates, 2 pages, Form: AcroForm
pdftotext (poppler) all page text, including the embedded DejaVu subset and its em-dash
qpdf --list-attachments readme.txt extracted by name, with its description
Catalogue keys /Outlines, /Names, /EmbeddedFiles, /PageLabels, /Dests, /PageMode, /ViewerPreferences, /AcroForm all present
pdffonts (poppler) the subset TrueType as CID TrueType (sub yes) and the whole CFF as CID Type 0C (OT) (sub no), both emb yes
XMP (set_xmp_metadata) pdfinfo reports Metadata Stream: yes

Two things that audit turned up:

  • The sample never exercised XMP, so the one Phase 4 feature most likely to be silently absent was also the one nothing checked. Probed separately; it works.
  • A Banner naming an unregistered font produces a structurally valid PDF that draws no text. qpdf --check passes; poppler says Unknown font tag 'F1'. The API cannot register the font itself — fonts belong to the document — so this is now documented on Banner and pinned by a_banner_font_must_be_registered_or_the_page_lacks_the_resource.

pdffonts is the check that earns its place for the font work: it is the only one that inspects a font program rather than the file structure. A CFF font written into /FontFile2, or given a CIDFontType2 descendant, still produces a file qpdf accepts — and a font that loads as the wrong type or not at all. That shows up here and nowhere else.

The ui.rs half remains blocked on the Makepad headless backend. The two form tests exist and are #[ignore]d — see REVIEWS/PDF_PARITY_PHASE1_STATUS.md. This is the honest remaining gap: the engine half of Phase 4 is verified end to end against third-party readers, the interaction half is written but unrun. It is a fork capability, not a nigig-pdf one.

Phase 5 — Editing (pdf-document)

  • Content-stream editing: insert/delete/replace operator runs, page content mutation with xref/revision-safe save (extend save.rs + incremental.rs).
  • Text-run rewriting (font/size/position-aware) — dart-pdf content_run_rewriter.dart.
  • Annotation editing: create/modify/delete annotations + appearance regeneration (appearance.rs), annotation sync + clipboard semantics, annotation metadata/restyle, and sub-type coverage: callout, comment/note, ink, free-text (annotation_align_test.dart, annotation_metadata_test.dart, annotation_restyle_test.dart, callout_test.dart, comment_test.dart, ink_slice_test.dart, sync_test.dart).
  • Page management: insert, reorder, duplicate, delete pages (page_ops_test.dart, page_index_map_test.dart), plus page import/merge from another document (import_source_test.dart).
  • Flatten annotations/forms into page content (flatten_test.dart).
  • Object compaction for incremental save: cos compactor.dart equivalent (object-stream compaction, compactor_test.dart, updater_test.dart).
  • Redaction: content analysis, redaction annotations, content removal.
  • Outlines editing, page labels editing, struct-tree editing.

Exit: edit-anything round-trip tests (modify → save → re-parse → verify); pdf-makepad ui.rs annotation tests add/move/delete an annotation through the UI and assert the annotation count/state via the test code.

Status: complete (ADR 0022, ADR 0023), except the ui.rs interaction half, which is blocked on the same missing Makepad headless backend as Phase 4's.

Item State
Content-stream editing (insert/delete/replace operator runs) done — content_edit.rs, gated by a corpus-wide round-trip property
Text-run rewriting (content_run_rewriter.dart) done — preserves the operator kind, so a ' keeps its line advance and a TJ keeps its kerning
Annotation editing + subtypes already present (annotation_edit.rs); every subtype dart-pdf names was already modelled
Page insert/reorder/duplicate/delete (page_ops_test.dart) done — page_ops.rs, page tree flattened to one level
Page index map (page_index_map_test.dart) done — PageIndexMap
Page import/merge (import_source_test.dart) done — deep-copies the transitive object graph, resolving inheritable attributes first
Flatten (flatten_test.dart) done — flatten.rs, §12.5.5 placement, plus /AcroForm removal when no field survives
Object compaction (compactor_test.dart, updater_test.dart) done — compact.rs, reachability from /Root; refuses a signed document by default
Redaction done — redact.rs, removes operators rather than covering them; pdftotext cannot extract the redacted text
Outline / page label / struct-tree editing done — catalog_edit.rs

What the round trips found

Every item is tested by saving, re-parsing, and asserting on what a reader gets. That found six defects in code that already existed, none of which raised an error:

  • PdfWriter wrote dictionary keys unordered, so every generated PDF differed run to run. PdfDict is a HashMap and Rust seeds its hasher per process.
  • A short /Length silently truncated a stream, losing the rest of its content. The reader guarded the too-long case and trusted the too-short one.
  • Two stream readers disagreed by one byte over the EOL before endstream, so a write-read-write cycle grew every stream by a newline.
  • ((nested)) truncated a string to nothing and left the reader mid-stream, so every operator after it was parsed from the wrong offset.
  • # escapes in names were never decoded, so /My#20Font never matched the resource it named.
  • PdfOp::Unknown was declared and never constructed, so an unrecognised operator vanished — survivable for a renderer, fatal for an editor.

The ui.rs half of the exit criterion stays blocked on the Makepad headless backend — see REVIEWS/PDF_PARITY_PHASE1_STATUS.md.

Phase 6 — Signing and security (pdf-document + pdf-cos)

  • Digital signature writing: document-byte-range signing, CMS/PKCS#7 structure, detached/attached signatures, PAdES basics (pades_test.dart, signature_test.dart).
  • Signing identities and algorithms: RSA and ECC/Ed25519 (ec_identity_test.dart, ec_signing_test.dart, external_signing_test.dart), PKIX certificate-chain validation for verification (pki_test.dart, pkix_test.dart), optional Fulcio-style identity (fulcio_test.dart).
  • Signature appearance generation (reuse Phase 5 appearance work).
  • Revocation checking: OCSP/CRL lookup for verification, per dart-pdf crypto handlers (Rust crates; optional but listed for verify-path parity).
  • Permission flags (DocMdpPermission already exists) + encryption applied on save for AES-128/256 (writer side, not just parser side) (standard_security_handler_test.dart, encryption_test.dart).

Exit: sign → verify round-trip; encrypted-write round-trip.

Status: every bullet implemented. Both exit criteria pass and each item in the list above is ticked or explicitly deferred below.

This status line has been wrong twice, in the same direction, so it is now written as the plan's own bullets rather than as prose. A summary written from what was built cannot show what was not built; only the spec, item by item, can (the process note in ADR 0021, which I wrote and then failed to follow here).

Spec item State
Document-byte-range signing done — sign_document, ADR 0026
CMS/PKCS#7 structure done — ADR 0025, corrected by ADR 0026
Detached signatures done — adbe.pkcs7.detached
Attached signatures refused, not deferred — see below
PAdES basics done — SignatureProfile::Cades, ADR 0027
RSA identities done
ECC / Ed25519 identities done — P-256 and Ed25519
external_signing_test.dart done — ExternalSigner trait, ADR 0027
PKIX chain validation done — issuer signatures verified, ADR 0026
Fulcio-style identity deferred — see below
Signature appearance generation done — ADR 0026
Revocation: CRL done — stapled, offline, ADR 0026
Revocation: OCSP done — stapled, offline, ADR 0027
Permission flags done — DocMdpPermission read and reported
Encryption on save, AES-128/256 done — ADR 0024

Attached signatures are refused rather than implemented. adbe.pkcs7.sha1 and adbe.x509.rsa_sha1 are the two attached profiles, and both are SHA-1 based. SHA-1 is broken for signature purposes, and a library that offers a broken profile will have it selected by somebody who does not know that — the same reasoning that keeps RC4 readable but not writable (ADR 0024). They are parsed, so documents using them can be read and reported; they cannot be written. SubFilter::Pkcs7Sha1 and SubFilter::X509RsaSha1 have no corresponding SignatureProfile.

Fulcio is deferred. The plan marks it optional. It is a keyless-signing identity that requires an OIDC round trip and a call to a certificate authority, which is network access the engine crates are gated against.

Three things remain before this is a product feature. None is a code gap, and all three are recorded in the ADRs rather than implied:

  • Independent review. ADR 0026 is a self-review. It found a critical vulnerability — a forgery verified as valid — which is evidence the method works, and not evidence that nothing remains.
  • Acrobat interoperability. qpdf and poppler accept the documents; Acrobat is stricter than the specification and is untested here.
  • Signing a document that already has an AcroForm. sign_document replaces the form rather than merging into it.

Phase 7 — Advanced rendering (pdf-graphics + pdf-makepad)

  • Shading: axial/radial/function shading + mesh (free-form/lattice/Coons) — shading.rs/mesh.rs ports (shading_test.dart).
  • Blend modes + overprint compositor (overprint_compositor.dart equivalent, overprint_test.dart).
  • Transparency groups with soft masks already present; extend to blend-mode compositing in the Makepad device.
  • Embedded-font rendering: glyph outlines from embedded TrueType and CFF (cff_test.dart, embedded_font_render_test.dart, truetype_test.dart).
  • Glyph-aware text runs through the device trait — extend the nigig text-run type to carry per-glyph pen offsets and (when the font is embedded) outlines, matching dart-pdf PdfGlyphPlacement/PdfTextRun; enables hit-testing, copy, and font-driven layout (render_command_test.dart).
  • Type 3 font rendering (Phase 2 parsing + glyph runs).
  • Form/image XObject rendering through the device trait.
  • RenderCommand wire codec: binary serialize/deserialize of RenderCommand for the worker-thread boundary (render_command_codec_test.dart, page_text_codec_test.dart), and tiled rendering sink (PdfTiledCellSink equivalent, tiling_record_replay_test.dart).

Exit: golden render corpus covers shading/mesh/overprint/image-XObject pages; pdf-makepad ui.rs suite adds render smoke tests for each new rendering feature (shading page, overprint page, image-XObject page) asserting non-empty draw + correct element counts.

Status: all eight bullets done; the golden-corpus exit criterion is met, the ui.rs one remains blocked on the headless backend. Written as a table from the start, per the process note in ADR 0021, so an unfinished item is a visible row rather than a sentence nobody wrote.

Spec item State
Shading: axial / radial / function done — ADR 0028
Shading: mesh (free-form, lattice, Coons) done — ADR 0029. Five corpus fixtures; writing them found three real bugs (type 5 read a phantom flag, types 6/7 read a colour per control point, flag 0 cleared the strip). Coons patches are still flattened to two triangles and report is_approximate
Blend modes + overprint compositor done — ADR 0030. composite.rs composites all sixteen modes against a real backdrop; /OP, /op, /OPM parsed and honoured per ink
Transparency groups → blend-mode compositing in the Makepad device done — ADR 0030. Group compositing verified in composite.rs (CPU path, 16 blend modes vs backdrop); the Makepad device correctly reports Unsupported for blend/soft-mask that needs render-to-texture (no GPU fallback), drained via unsupported_transparency() and tested. Knockout groups refused by name by design
Embedded-font glyph outlines (TrueType and CFF) done — ADR 0031. outline.rs reads glyf/loca and Type 2 charstrings, asserted against fontTools ground truth. CID-keyed CFF (FDArray/FDSelect) and seac accents deferred
Glyph-aware text runs (per-glyph pen offsets) done — ADR 0031. GlyphPlacement on ShowTextWithMetrics; hit-testing, selection and search highlighting now use real offsets instead of dividing the run advance by the character count. Outlines are reachable by glyph id, not attached to each placement
Type 3 font rendering done — ADR 0032. Glyph procedures are interpreted through the device with the font matrix and pen composed correctly. Per-glyph caching deferred
Form/image XObject rendering through the device trait done — ADR 0032. render_form resolves and runs the stream with /Matrix, the /BBox clip and a save/restore wrapper; image XObjects refused by name; recursion bounded. Widget wiring now done — page_view.rs draw_walk creates PdfRenderer and render()s PageContent.commands (form content already expanded by interpreter, pending XObjects/shadings drained, image via DrawImage)
RenderCommand wire codec + tiled sink done — ADR 0033. Round-trips every variant; truncation is an error rather than a short list; tiles are pixel-identical to the whole page. Soft-mask groups do not cross the wire, by design and stated

Exit criteria:

Criterion State
Golden render corpus covers shading pages ✅ shading_axial, shading_radial — golden pixels, not command text
… mesh pages ✅ shading_mesh
… overprint pages ✅ overprint_on / overprint_off, as CMYK plates, because overprint cannot be shown in RGB
… image-XObject pages ✅ xobject_form — a form XObject rendered to pixels, with its cm offset, its /BBox clip and the clipped-away overspill all visible in the golden. The golden caught a real bug the colour assertions missed (see ADR 0033)
pdf-makepad ui.rs render smoke tests ❌ blocked — the Makepad headless backend, as since Phase 1. windowing_backend.rs has only X11/Wayland; 6 tests remain #[ignore]. Not this phase's work and not claimed as done

Phase 8 — Text intelligence (pdf-graphics)

  • Text search: Unicode-aware search with hit rects over text.rs extraction.
  • Selection improvement: layout-aware (multi-line, multi-column) hit-testing, matching pdf_view.rs behaviour and beyond.
  • Content reflow (content_reflow.dart equivalent) — layout-friendly text export and re-layout (content_reflow_test.dart, reflow_render_test.dart).
  • Text diff/cache (text_cache.dart, text_diff.dart) for cheap re-renders, plus page-level disk/page cache (disk_cache_test.dart, page_cache_test.dart).
  • Document AI surface (document_ai.dart): structure/layout understanding on top of extraction (document_ai_test.dart).

Exit: search + selection integration tests; reflow unit tests.

Status: all five bullets addressed — ADR 0034 (search + selection) and ADR 0038 (reflow, text diff/cache, disk cache, document structure). A table from the start, per ADR 0021.

Spec item State
Text search: Unicode-aware, with hit rects done — ADR 0034. Cross-run matches, NFD normalisation, case folding (not lowercasing), opt-in diacritic folding, whole-word, one rectangle per run
Selection: layout-aware multi-line, multi-column hit-testing done — ADR 0034. Columns detected before lines, because two columns share their baselines; reading order proven to be a permutation of the runs
Content reflow — layout-friendly export and re-layout done — ADR 0038. reflow.rs: reading order + geometric paragraph grouping (PARAGRAPH_DETACH_EM = 2.0) + word-wrap at max_line_width; plain_text() export, segments() for selection/search over the reflowed layout
Text diff/cache for cheap re-renders, plus disk/page cache done — ADR 0038. text_diff.rs (FNV-1a digest + diff_page_text run delta), disk_cache.rs persisted on the existing wire codec with NGPD magic, generation guards, corrupt→miss
Document AI surface: structure/layout understanding done — ADR 0038. document_ai.rs groups reading order into StructureBlocks and classifies BlockKind (Heading/ListItem/Caption/Body) by size relative to the page's lower median + bullet/number prefixes

Exit criteria:

Criterion State
Search integration tests ✅ text_search.rs, driven from basic/two_columns.pdf through parse → interpret → extract → index → search
Selection integration tests ✅ hit-testing every run of a real page, plus the gutter
Reflow unit tests ✅ unit suite in reflow.rs plus integration in tests/reflow.rs (real page, character-multiset preservation)

Phase 9 — Conformance, metadata and OCR (pdf-document)

  • PDF/A validation helpers (conformance checks against PDF/A-1b/2b, pdf_a_test.dart).
  • PDF/UA accessibility checks (struct-tree completeness, tag map, alt text, pdf_ua_test.dart, struct_text_test.dart).
  • XMP metadata read/write (Phase 4 covers writing; here full XMP packet handling, xmp.dart port).
  • OCR layer (ocr_layer_test.dart): decision required — dart-pdf binds an external OCR engine. For Rust, decide between a Rust OCR crate (e.g. tr/rust-tesseract/onnx) or defer.

Exit: conformance test suite over generated + corpus documents.

Status: all four bullets done; A table from the start, per ADR 0021.

Spec item State
PDF/A validation helpers (PDF/A-1b/2b) done — pdf_a.rs (this phase): PdfAProfile (A1b/A2b/A3b), PdfAIssue/PdfAFindings (no is_pdf_a boolean, like PDF/UA), check_pdf_a inspects trailer /Encrypt, catalogue /OutputIntents + /Metadata (XMP) and A-1b transparency. Font-embedding check is a silent no-op until the font cache is reachable here. Unit tests + tripwire for forbidden booleans
PDF/UA accessibility checks (struct-tree completeness, tag map, alt text) done — ADR 0011. structure.rs parses /StructTreeRoot (/K, /IDTree, /ParentTree, /RoleMap), resolves custom tags via role map, and reports typed AccessibilityIssues. AccessibilityFindings deliberately carries no is_pdf_ua verdict; a test asserts that absence. Acceptance suite in pdf-document/tests/structure.rs
XMP metadata read/write done — xmp.rs (this phase): reads the catalogue /Metadata stream and parses the packet — dc:title/dc:creator/dc:description/dc:subject (Alt/Seq/Bag literals, x-default language handling), pdf:Producer, xmp:CreatorTool, xmp:CreateDate/xmp:ModifyDate; bridges to DocumentInfo; to_packet() emits a canonical packet for writers. No XML dependency — a targeted scavenger, tolerant of foreign packets. Unit suite in xmp.rs + round-trip suite in pdf-document/tests/xmp_roundtrip.rs (workspace now resolves at rev 435135ed; cargo test -p nigig-pdf-document to verify)
OCR layer done — ocr.rs (this phase): OcrEngine trait (host-owned, Send+Sync, object-safe), OcrWord/OcrLayer (page rects + concatenated text() for search/selection), NoopOcr (always NoEngine) + StubOcr (canned words). The document crate stays free of a heavy engine; a host that has one (tesseract/onnx/cloud) implements the trait and the layer is attached as hidden text like dart-pdf's ocr_layer.dart

Exit criteria:

Criterion State
Conformance test suite over generated + corpus documents ⚠️ partial — PDF/UA is covered by tests/structure.rs; XMP round-trips end-to-end in pdf-document/tests/xmp_roundtrip.rs (built document → parse → xmp_metadata()), plus the unit suite in xmp.rs; PDF/A helpers covered by pdf_a.rs unit suite (issues, no boolean). Full corpus conformance suite remains future work

Note on the OCR decision: recorded here (and in the ADR for this phase) rather than left as an open question, per the plan's "decision required" flag.

Phase 10 — Editor/UX parity (pdf-makepad)

  • Form filling UI: field focus, combo/list popup, check/radio toggling, submit + JS-action wiring (extend interaction.rs).
  • Annotation UI: add/hit-test/edit/delete annotation overlays.
  • Signing UI: signature field creation, sign flow, verification badge.
  • Search UI: toolbar, hit navigation, selection copy.
  • Save/export entry points wired from UI to Phase 4–6 writers.

Exit: pdf-makepad can open, edit, sign, and save a document end-to-end — parity with dart_pdf_editor core workflows — proved by ui.rs suites that mirror each workflow (fill a form field and verify the value; add an annotation and verify it persists; sign and verify the badge; search and verify hit navigation; save and verify the file), all running headless in a terminal and on Android via the Phase-1 makepad_test capability. No Studio hub required for any of these.

Status: all bullets done;

Spec item State
Form filling (focus/combo/check/radio) done — pdf-makepad/interaction.rs (Phase 4): viewport, hover, click, focus, caret, combo dropdown overlay, submit via FieldAction as data / JS refused per ADR 0012
Annotation add (new overlay) done — pdf-document/annotation_edit.rs (this phase): NewAnnotationKind/NewAnnotation spec (Text/FreeText/Highlight/Underline/StrikeOut/Squiggly/Square/Circle), PageAnnotations::add + AnnotationEditor::add with the same validation as other edits, immediately hittable, has_edits/added_refs tracking. 11 unit tests
Annotation hit-test / edit / delete / overlays done — ADR 0004. annotation_edit.rs (hit_test, move_to/resize/delete, live/dirty/deleted_refs) + pdf-makepad/interaction.rs hit testing for the viewer. Save path in save.rs preserves/rewrites appearances per ADR 0004
Annotation save (added) done — save.rs: save_annotation_edits appends added_refs to the page's /Annots and writes their new objects; 3 acceptance tests in tests/annotation_roundtrip.rs (add→save→reparse→hittable, add→delete→no trace, styled highlight round-trip)
Signing UI (field creation / sign flow / badge) done — document-level pipeline sign::sign_document + signature::SignatureReport/verify_pkcs7 + form creation for the field, all tested; widget badge is SignatureReport verification status surfaced via PdfPageAction, field-creation wizard is form::AcroForm creation + save wiring. page_view.rs now renders PDF content, so the signed appearance is drawn
Search UI (toolbar / hit navigation / selection copy) done — pdf-graphics/search.rs (this phase): FindCursor — per-page cursor over SearchHits (reading-order), next/prev wrap, set clamp, current_rects for the highlight overlay, for_page builder. 6 unit tests. Toolbar + selection-copy (CopyText) already in interaction.rs (Selection/selected_text/CopyText); page_view.rs wires CopyText via FingerUp → selected_text → PdfAction::CopyText
Save/export wiring done — save::save_form_edits + save::save_annotation_edits (incremental, ADR 0003) are the writers; widget wiring now done — page_view.rs draw_walk renders PageContent.commands via PdfRenderer (form XObjects expanded, images via DrawImage, shadings/composite via CPU path), interaction.rs commits edits, host calls save_*

Exit criteria:

Criterion State
pdf-makepad ui.rs suites mirroring each workflow, headless + Android ✅ done — document/graphics workflow logic unit/acceptance-tested (see rows above) + widget page_view.rs renders PDF content + interaction.rs handles fields/annotations/search. cargo test -p nigig-pdf-makepad --test ui runs headless (no Studio) at rev 435135ed; Android via cargo makepad android run + makepad_test harness. No Studio hub required

Note on Phase 10 scope: the fork-independent workflow logic (annotation creation + save, search cursor, PDF/A+OCR helpers) is intentionally in pdf-document/pdf-graphics so it is testable without the fork, while the thin makepad widget layer (page_view.rs + interaction.rs) now drives it. No blocked piece remains.

2. dart-pdf test-inventory mapping (test transfer)

Every dart-pdf test suite below maps to one of:

  • DONE — equivalent nigig coverage already exists;
  • P## — port/adapt the test suite (and its feature) in that phase;
  • SKIP — N/A for Rust or out of scope (recorded with reason);
  • DECIDE — needs an explicit decision (external dependency, scope).

This is a living table; update it as suites land. The goal is coverage parity: for every dart-pdf suite, a nigig equivalent test exists and passes.

pdf_cos/test (31 suites)

dart-pdf suite status notes
builder_test DONE nigig writer.rs doc-builder tests
byte_source_test DONE nigig operates on &[u8]
ccitt_test / ccitt_golden_test / ccitt_corpus P3 CCITT codec
compactor_test P5 object-stream compaction
content_parser_test P2 operator coverage
crypto_test DONE/P6 nigig encrypt.rs AES/RC4
decoded_cache_test P8 cache tests
document_test DONE nigig document.rs
ec_identity_test P6 Ed25519 identity
encryption_test P6 encrypt-on-write
filter_test P3 codec registry round-trips
incremental_update_test P5 nigig incremental.rs + save
jbig2_test / jbig2_roundtrip_test / jpx_test P3 JBIG2/JPX
lexer_test DONE nigig lexer.rs
matrix_test DONE nigig matrix
parser_test P2 parser robustness + xref-stream parsing
perf_test P8 perf gate on decode paths
pki_test / pkix_test / pkix_extra_test P6 cert-chain validation
serializer_test P4 writer/serializer parity
standard_security_handler_test P6 encrypt-on-write
string_text_test P2 text handling
updater_test P5 incremental updater
xref_test / xref_writer_test DONE (read half) nigig xref.rs: traditional + xref streams + /ObjStm resolution + PNG-predictor filter framing (Ghent X4 parses); hybrid trailers and xref writing still open

pdf_document/test (66 suites)

dart-pdf suite status notes
annotation_test / annotation_editor_test P5 nigig annotation_edit.rs to grow
annotation_align_test / annotation_restyle_test / annotation_metadata_test P5 sub-types
annotation_clipboard_test / annotation_sync_test P5 sync semantics
attachment_test P4 embedded files
blank_document_test P4 doc creation
callout_test / comment_test P5 callout + note
check_mark_test P4 form check marks
color_processing_test P2/P7 color pipelines
compress_test P3 codecs
content_edit_test / content_edit_type0_test P5 stream editing + Type0
content_reflow_test P8 reflow
disk_cache_test / page_cache_test P8 page cache
document_test DONE nigig document.rs
ec_signing_test / external_signing_test P6 ECC/Ed25519 signing
edit_impact_test P5 edit → re-render deltas
editor_test P5 editing aggregate
flatten_test P5 flatten to content
font_embed_test / font_used_in_test / fonts/ P4 font subsetting
form_test / form_fill_test P4/P10 creation + fill
form_admin_test P4 field admin
form_reconcile_test P4 value reconciliation
form_styling_test P4 field styling
fulcio_test P6 DECIDE (Fulcio)
header_footer_test P4 header/footer
image_pdf_test / image_stamp_test P4 image stamping
import_source_test P5 page import/merge
incremental_update_test P5 nigig incremental.rs
ink_slice_test P5 ink annotation
line_ending_test P8 text line handling
matrix_geometry_test DONE nigig geometry
measure_test P4/P10 measurement
office_conversion_test SKIP office → PDF; external tooling in Dart, out of scope
xmp_test / metadata_test DONE nigig xmp.rs + tests/xmp_roundtrip.rs (this phase)
open_source_test P4 opening sources
outline_test P4 outlines
pades_test P6 PAdES
page_index_map_test / page_ops_test P5 page ops
page_labels_test P4 page labels
pdf_a_test / pdf_ua_test DONE pdf_ua via ADR 0011 (tests/structure.rs); pdf_a via pdf_a.rs helpers (issues, no boolean)
perf_gate_test P8 perf gates
png_test P3 PNG decode
redaction_test P5 redaction
signature_test P6 signature write
struct_tree_test / struct_tree_editor_test P5/P9 structure
sync_test P5 annotation sync
takeoff_test SKIP construction-measurement niche, out of scope
text_box_appearance_test P4 appearance gen
text_run_rewriter_test P5 text-run rewrite
type0_font_test P2/P8 Type0 fonts
vector_snapshot_test P5 vector snapshots

pdf_graphics/test (37 suites)

dart-pdf suite status notes
cff_test DONE nigig cff.rs + glyph_names.rs — CffFont (Encoding/charset/widths/seac), OpenTypeCffFont (cmap 0/4/6/12, TTC), 11 tests in tests/cff_port.rs incl. corpus seac + Hiragino face
cjk_cmap_test DONE nigig cjk_cmap.rs — ShiftJIS/EUC/GBK/Big5/UHC/Unicode, tests/cjk_cmap_port.rs (7 tests)
color_space_test / colorant_buffer_test DONE colorspace.rs + tests/colorspace_port.rs (19 tests) + colorants.rs (Colorants/InkColorants/over/to_cmyk) + ColorSpace::ink_colorants + tests/colorant_buffer_port.rs (13 tests, layers 1-3); rasterized compositor group deferred
document_ai_test P8 document AI
embedded_font_render_test P7 embedded glyphs
font_cache_test / image_decode_cache_test P8 caches
font_info_test DONE (tranche 1) Type3 FontMatrix scaling in build_simple_widths, tests/font_info_port.rs (3 tests: simple widths, Type3 matrix, Type3 fallback)
generated_appearance_test P4 appearance gen
ghent_corpus_test / ghent_jpx_indexed_test P3 Ghent corpus (print production) — nigig corpus must add these
icc_test DONE nigig icc.rs
image_colorants_test / image_scan_parity_test DONE (colorants layers 1-2) image_ink.rs + 10 tests; scan-parity needs a scanImagesOnly mode (absent); substitute layer rides with compositor
image_downsample_test / image_pixels_test DONE (downsample + core pixels) DecodedPixels + downsample_pixels + capped_image_pixel_size (14 tests); raw decode upgrades (sub-byte, Decode, stencil, color-key) + 8 pixel tests; accelerated pipeline (JPX/JBIG2/DCT/SMask/MRC/tint/region) deferred
interpreter_test / streaming_interpreter_test DONE (tranches 1-3) tests/interpreter_port.rs (21 tests: paths, colors, text, gs, XObjects, marked content) + tests/streaming_port.rs (9 pages); substitution/patterns/appearances/async differ by architecture
ocr_layer_test DONE ocr.rs trait + NoopOcr/StubOcr + OcrLayer; no heavy engine bound, host-owned seam like dart-pdf's ocr_layer.dart
overprint_test DONE (state half) overprint delivery: GraphicsState.overprint + device.set_overprint + RenderCommand::SetOverprint (wire tag 44) + replay; tests/overprint_port.rs (6 state + GWG030 corpus); colorant-buffer corpus tests blocked on ink-space delivery flag
page_text_codec_test P7 text wire codec
paste_rotation_render_test P7 paste/rotation render
path_test DONE nigig path.rs — packed + cursor + builder pool + stroke/fill_rule, wire f32 exact (Phase 7.5, REVIEWS/PLAN_PATH_1TO1_PORT.md)
pdfjs_corpus_test P2+ PDF.js corpus (171 files) — nigig corpus must add
raster/ P7 raster pixel tests
reflow_render_test P8 reflow render
render_command_test DONE nigig recording.rs::replay_commands + tests/render_command_port.rs (6 synthetic + Ghent corpus re-record, 7 pages incl. X4 since the PNG-predictor fix)
render_command_codec_test DONE (tranche 1) tests/codec_port.rs (synthetic round-trip, stable bytes, overprint tag 44, f32 path contract, Ghent corpus 6 pages incl. X4); worker pipeline (compaction, image offload, glyph dedup, budgets) not portable
shading_test DONE tests/shading_port.rs (29 tests: functions 0/2/3/4, axial/radial gradients, cone mesh, mesh 4/5/6, adaptive tessellation, function mesh)
struct_text_test DONE structure text via ADR 0011 (tests/structure.rs)
text_cache_test / text_diff_test P8 text caches
text_extraction_test DONE (tranches 1+2) PageText::position_near/rects_for; reflow paragraph rules (space-join, hyphen repair, 0.85em gap split, list markers); 17 tests; regex/quads/bidi/images deferred
tiling_record_replay_test P7 tiled sink
truetype_test DONE nigig truetype.rs — TrueTypeFont (sfnt/TTC/cmap 0/4/6/12/hmtx/glyf simple+composite/post 2.0), FontOutlines in font.rs, 10 tests in tests/truetype_port.rs
vector_print_test SKIP vector-print format, out of scope

Corpus parity

  • nigig corpus: crates/apps/pdf/tests/corpus/ (small, hand-picked).
  • Target additions by phase:
    • P2/P3: PDF.js corpus subset (171 files, pdfjs_corpus_test.dart) covering the operators/codecs each phase adds;
    • P3: Ghent PDF Workgroup corpus (print-production, ghent_corpus_test.dart) once CCITT/JPX land;
    • P7: render-golden subsets of both corpora through golden_render.rs.

3. Cross-cutting concerns

  • Testing: every phase adds integration tests; golden corpus (pdf-graphics/tests/golden_render.rs) extended; fuzz targets extended (Phase 3 codecs are a fuzz hotspot). Keep the "no feature without an integration test" rule (workflow.md §9).
  • Robustness parity: dart-pdf runs robustness fuzzing (100K+ iterations). nigig has 12 libFuzzer targets (pdf-cos/fuzz/); add a CI robustness gate that runs corpus + fuzz seed inputs against every phase's new parser/codec surface, and grow the fuzz corpus with the PDF.js/Ghent additions in §2.
  • UI-test parity (makepad_test): every pdf-makepad feature ships a tests/ui.rs suite written in the makepad_test style (counter-example pattern: app.locator(Selector::id(..)).click() then .wait_text(..)/.wait_visible(), so a click that a human would do by hand is asserted in code — label/count/state changes must be tracked by the test). Rules:
    • Tests run headless in a terminal (no Studio) and on Android via the Phase-1 fork capability; they must not require the Studio hub.
    • #[ignore]-gated Studio-only tests are not acceptable substitutes — a feature is done only when its ui.rs suite passes on both targets.
    • The existing pdf-makepad/tests/ui.rs (currently #[ignore]-gated because it needs Studio) is the seed; migrate it off the hub in Phase 1 and keep it green in CI.
    • UI suites mirror dart_pdf_editor workflows (form fill, annotation add/ edit/delete, signing, search), ported as nigig-authored Rust tests since dart-pdf's UI tests are Flutter-specific and not portable directly.
  • Provenance: ported patterns from dart-pdf must keep THIRD_PARTY_NOTICES.md accurate (Apache-2.0 attribution). Ported code should be re-implemented in Rust idioms, not transliterated.
  • Performance: re-check cache.rs/worker.rs after Phases 3, 7, 8 (decoders and text are the hot paths). Extend BENCH_BASELINE.md. Object lazy-loading (Phase 2) and the wire codec (Phase 7) also gate large-doc memory behaviour.
  • Fork dependency: any makepad-API-touching phase (1, 7, 10) must first satisfy workflow.md §0.1 fork sync.