# Plan: 1:1 Port of dart-pdf `interpreter_test.dart` → `nigig-pdf` (Phase 2.5) **Date:** 2026-09-06 **Reference:** `dart-pdf/packages/pdf_graphics/test/interpreter_test.dart` (1885 LOC), `dart-pdf/packages/pdf_graphics/test/streaming_interpreter_test.dart` **Nigig baseline:** `crates/apps/pdf/pdf-graphics/src/content.rs` (interpreter), `crates/apps/pdf/pdf-graphics/src/recording.rs` (RecordingDevice), `crates/apps/pdf/pdf-graphics/src/device.rs` (PdfDevice) **Master plan:** `NIGIG_PDF_FEATURE_PARITY_PLAN.md` Phase 2 (`interpreter_test / streaming_interpreter_test | P2`) --- ## 1. Gap Audit | Aspect | dart `interpreter_test` device | nigig `RecordingDevice` | Gap | |---|---|---|---| | **Path API** | `fillPath(PdfPath path, PdfColor color, PdfFillRule rule, double alpha)` , `strokePath(PdfPath, PdfColor, PdfStroke, alpha)` , `clipPath(PdfPath, PdfFillRule)` — path is a single `PdfPath` object plus style | `move_to(x,y)`, `line_to`, `curve_to`, `close_path` + `fill_winding()` / `stroke()` — path is implicit sequence, style is separate state (`SetFillColor`, `SetStrokeWidth` etc.) | Dart device is aggregated (path+style in one call), nigig is split (state + path ops + paint). Port requires either adapting test to nigig's split API or adding aggregated `path()` method to `PdfDevice` (done for dense CAD via `path.rs` `PdfPath`) | | **Gradient/Mesh** | `fillPathGradient(PdfPath, PdfFillRule, PdfGradient, alpha)` , `fillMesh(PdfMesh, alpha)` — shadings and meshes as first-class calls | `PaintShading(String)` + `PendingShadings` — shadings are pending names, meshes not yet exposed | Mesh/gradient device calls not yet ported; currently via `Display` only | | **Text** | `showText(PdfTextRun)` with `PdfTextRun` carrying `PdfPath` glyph outlines, advances, etc. | `ShowTextWithMetrics { text, font_size, advance_x, glyphs: Vec }` | Close but `PdfTextRun` in dart has richer `PdfPath` per glyph for embedded fonts | | **State** | `save()`/`restore()` + `setFillColor` etc. but `fillPath` takes color as param, not state | State is `GraphicsState` stack, color set via `SetFillColor` then `FillWinding` | Dart's `fillPath(path, color, ...)` bundles color, nigig's splits it | **Verdict:** Interpreter *parsing* is ~90% 1:1 (all operators covered, plus Type 3, shading `sh`, streaming). Interpreter *device API* is ~60% 1:1 — the path+style aggregation is the missing bridge that `path.rs` Phase 7.5 just built. ## 2. Scope (Phase 2.5, ~1-2 sessions, no fork) ### 2.1 Add aggregated device methods to `PdfDevice` (back-compat, default no-op) ```rust fn fill_path(&mut self, path: PdfPath, color: [f64;4], rule: PdfFillRule, alpha: f64) { self.path(path); self.fill_winding() } // default impl via existing split API fn stroke_path(&mut self, path: PdfPath, color: [f64;4], stroke: PdfStroke, alpha: f64) { ... } fn clip_path(&mut self, path: PdfPath, rule: PdfFillRule) { ... } fn fill_gradient(&mut self, path: PdfPath, gradient: PdfGradient, rule: PdfFillRule, alpha: f64) {} fn fill_mesh(&mut self, mesh: PdfMesh, alpha: f64) {} ``` This lets the dart test's `RecordingDevice` be ported verbatim: it implements `PdfDevice` and records `fill_path` calls with `PdfPath` objects. ### 2.2 Port `interpreter_test.dart` (1885 LOC) → `crates/apps/pdf/pdf-graphics/tests/interpreter.rs` Split into groups as dart does: * `device families` — `PdfColorSpace` channel counts (already in `colorspace.rs` but re-assert via interpreter) * `path ops` — `m`, `l`, `c`, `v`/`y`, `h`, `re`, `h` clipping, winding/even-odd * `text` — `BT`/`ET`, `Tf`, `Td`/`Tm`, `T*`, `Tj`/`TJ` with glyph placements * `graphics state` — `q`/`Q`, `cm`, `w`/`J`/`j`/`M`/`d`, `gs` with `ca`/`CA`/`BM` * `shading` — `sh` via `PaintShading` pending * `XObject` — `Do` for image/form (form already expanded, image pending) * `marked content` — `BMC`/`BDC`/`EMC` with `MCID` * `Type 3` — `d0`/`d1` metrics Each group becomes a Rust `#[test]` that builds `PdfOp` vec via `parse_content_stream` + `RecordingDevice::from_ops` and asserts on `calls`, `fills`, `strokes`, `clips` as dart does, but using `PdfPath` + `PdfColor` + `PdfStroke`. ### 2.3 Port `streaming_interpreter_test.dart` → `tests/streaming_interpreter.rs` Verifies `ContentStreamIter` + `interpret_streaming` incremental path: same ops as above but fed via `streaming_interpreter` that yields `PdfOp` lazily, and that state survives across chunks. ## 3. Test Port Table (first tranche) | # | dart test | Rust test | Notes | |---|---|---|---| | T1 | `device families` (color space names) | `interpreter::tests::device_families` | Already in `colorspace.rs` — re-use | | T2 | `path ops` group | `path_ops_fill_and_stroke_with_path` | Use `PdfPathBuilder` to build expected path, `fill_path` to assert | | T3 | `text` group | `text_with_glyph_placements` | `ShowTextWithMetrics` glyphs already have offsets | | T4 | `graphics state save/restore` | `save_restore_preserves_path_and_color` | `q`/`Q` with `fill_path` inside | | T5 | `sh` shading | `sh_produces_pending_shading` | `PaintShading` pending | | T6 | `BMC/BDC/EMC` | `marked_content_carries_mcid` | `BeginMarkedContent` with `MCID` | Full 1885-line file is split across 2 sessions; T1-T6 are session 1 (~6 tests), remainder session 2. ## 4. Exit Criteria * 6+ new tests green: `cargo test -p nigig-pdf-graphics --test interpreter --offline` * `cargo test -p nigig-pdf-graphics --lib --offline` still 435 green (no regression) * `NIGIG_PDF_FEATURE_PARITY_PLAN.md` updated: `interpreter_test / streaming_interpreter_test | DONE | nigig interpreter.rs — aggregated PdfPath device + streaming` * ADR `00xx-pdf-interpreter-aggregated-device.md` records why `PdfDevice` now has both split (`move_to`) and aggregated (`fill_path`) APIs and that the aggregated one delegates to split by default. ## 5. Risks * **API duplication** — mitigation: aggregated `fill_path` default impl calls split API, so existing devices (including `MakepadPdfDevice`) need no change unless they want the aggregated path. * **Heavy test file** — mitigation: port in groups, not monolith; first tranche 6 tests is the proof.