Whole-tree sync: cad-core/cad-ui split sources, nigig-build construction_frame migration, pdf port progress, mpesa/pay/uikit/doc updates, workspace members/profiles/lock, CI workflows and reviews. See individual file history for details.
6 KiB
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<GlyphPlacement> } |
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)
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—PdfColorSpacechannel counts (already incolorspace.rsbut re-assert via interpreter)path ops—m,l,c,v/y,h,re,hclipping, winding/even-oddtext—BT/ET,Tf,Td/Tm,T*,Tj/TJwith glyph placementsgraphics state—q/Q,cm,w/J/j/M/d,gswithca/CA/BMshading—shviaPaintShadingpendingXObject—Dofor image/form (form already expanded, image pending)marked content—BMC/BDC/EMCwithMCIDType 3—d0/d1metrics
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 --offlinestill 435 green (no regression)NIGIG_PDF_FEATURE_PARITY_PLAN.mdupdated:interpreter_test / streaming_interpreter_test | DONE | nigig interpreter.rs — aggregated PdfPath device + streaming- ADR
00xx-pdf-interpreter-aggregated-device.mdrecords whyPdfDevicenow 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_pathdefault impl calls split API, so existing devices (includingMakepadPdfDevice) 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.