From c94b33bb0e2993d2e57a72b7084af85d4190b117 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Sun, 26 Jul 2026 23:44:02 +0800 Subject: [PATCH] Remove TODO.roadmap/ from repo --- TODO.roadmap/00-skeleton-and-api.adoc | 45 -------- TODO.roadmap/01-gdi-state-primitives.adoc | 55 ---------- TODO.roadmap/02-svg-path-data-parser.adoc | 52 --------- TODO.roadmap/03-shape-handlers.adoc | 57 ---------- TODO.roadmap/04-transforms-and-groups.adoc | 45 -------- TODO.roadmap/05-clip-paths.adoc | 42 -------- TODO.roadmap/06-text.adoc | 39 ------- TODO.roadmap/07-images.adoc | 34 ------ TODO.roadmap/08-round-trip-golden.adoc | 73 ------------- TODO.roadmap/09-coalescing-optimization.adoc | 37 ------- TODO.roadmap/10-coordinate-scaler.adoc | 100 ------------------ .../11-matcher-structural-normalisation.adoc | 74 ------------- .../12-investigate-stroke-failures.adoc | 43 -------- TODO.roadmap/13-emf-to-svg-fidelity.adoc | 69 ------------ TODO.roadmap/14-debug-scaler-record-drop.adoc | 49 --------- TODO.roadmap/README.adoc | 91 ---------------- 16 files changed, 905 deletions(-) delete mode 100644 TODO.roadmap/00-skeleton-and-api.adoc delete mode 100644 TODO.roadmap/01-gdi-state-primitives.adoc delete mode 100644 TODO.roadmap/02-svg-path-data-parser.adoc delete mode 100644 TODO.roadmap/03-shape-handlers.adoc delete mode 100644 TODO.roadmap/04-transforms-and-groups.adoc delete mode 100644 TODO.roadmap/05-clip-paths.adoc delete mode 100644 TODO.roadmap/06-text.adoc delete mode 100644 TODO.roadmap/07-images.adoc delete mode 100644 TODO.roadmap/08-round-trip-golden.adoc delete mode 100644 TODO.roadmap/09-coalescing-optimization.adoc delete mode 100644 TODO.roadmap/10-coordinate-scaler.adoc delete mode 100644 TODO.roadmap/11-matcher-structural-normalisation.adoc delete mode 100644 TODO.roadmap/12-investigate-stroke-failures.adoc delete mode 100644 TODO.roadmap/13-emf-to-svg-fidelity.adoc delete mode 100644 TODO.roadmap/14-debug-scaler-record-drop.adoc delete mode 100644 TODO.roadmap/README.adoc diff --git a/TODO.roadmap/00-skeleton-and-api.adoc b/TODO.roadmap/00-skeleton-and-api.adoc deleted file mode 100644 index 0c0d094..0000000 --- a/TODO.roadmap/00-skeleton-and-api.adoc +++ /dev/null @@ -1,45 +0,0 @@ -= Phase 00: Skeleton + Public API + CLI - -Status: done - -== Goal - -Stand up the SVG→EMF pipeline end-to-end with no real translation yet. -An empty/trivial SVG must produce a valid (empty) EMF that round-trips -through `Emf.parse` and re-renders to a byte-identical empty SVG. - -== Deliverables - -* `Gemfile` / `emfsvg.gemspec`: add `nokogiri` runtime dep, drop - `rexml` dev dep usage where it can be replaced. -* `lib/emfsvg/svg.rb` — namespace file with autoloads. -* `lib/emfsvg/svg/parser.rb` — `Svg::Parser.call(string)` returns - `Svg::Document`. Wraps Nokogiri. -* `lib/emfsvg/svg/document.rb` — `Svg::Document` (root + viewBox + - width/height attrs). -* `lib/emfsvg/svg/element.rb` — `Svg::Element` base. Just holds the - Nokogiri node for now; subclasses added in later phases. -* `lib/emfsvg/translation.rb` — namespace file. -* `lib/emfsvg/translation/emf_renderer.rb` — orchestrator. Takes a - `Svg::Document`, walks elements, builds an `Emf::Model::Metafile` - with header + EOF, calls `Emf.serialize`. -* `lib/emfsvg/translation/header_builder.rb` — computes EMF header - fields from SVG `viewBox`/`width`/`height`. -* `lib/emfsvg.rb` — add `to_emf`, `from_svg`, `from_svg_file` - class methods + autoloads. -* `exe/emfsvg` — add `to-emf` subcommand. -* `spec/svg/parser_spec.rb` -* `spec/emf_renderer_spec.rb` — empty SVG round-trip. - -== Acceptance criteria - -* `bundle exec rspec` passes (existing 27 + new specs). -* `bundle exec rubocop` clean. -* `Emfsvg.from_svg('')` returns - EMF bytes that: - * Start with the EMF signature `" EMF"` (`\x20\x45\x4d\x46`). - * Parse cleanly via `Emf.parse(bytes)`. - * Render back via `Emfsvg.from_bytes(bytes)` to an SVG whose root - `` has matching width/height. -* CLI: `emfsvg to-emf IN.svg OUT.emf` writes the bytes; `emfsvg help` - lists it. diff --git a/TODO.roadmap/01-gdi-state-primitives.adoc b/TODO.roadmap/01-gdi-state-primitives.adoc deleted file mode 100644 index 7feee9c..0000000 --- a/TODO.roadmap/01-gdi-state-primitives.adoc +++ /dev/null @@ -1,55 +0,0 @@ -= Phase 01: GDI state primitives - -Status: done - -== Goal - -Translate SVG `fill=` / `stroke=` attributes into the proper sequence -of `CreatePen` / `CreateBrushIndirect` / `SelectObject` / `DeleteObject` -records. Establishes the state-management pattern all subsequent -handlers use. - -Depends on: link:00-skeleton-and-api.adoc[Phase 00]. - -== Deliverables - -* `lib/emfsvg/translation/context.rb` — `Translation::Context`. - Carries `DeviceContext`, `ObjectTable`, `TransformStack`, and the - output record list. Passed to every handler. -* `lib/emfsvg/translation/record_emitter.rb` — `RecordEmitter` builds - wire records, computes correct `n_size` from `to_binary_s.bytesize`, - wraps in `WireAdapter`, appends to context output. -* `lib/emfsvg/svg/paint.rb` — `Svg::Paint` value object. Parses - `fill=` / `stroke=` values: hex (`#RGB`/`#RRGGBB`), named colors - (`red`, `blue`, …), `none`. Returns RGB triplet + null flag. -* `lib/emfsvg/svg/stroke.rb` — `Svg::Stroke` value object. Parses - `stroke=` + `stroke-width=` + `stroke-dasharray=` + `stroke-linecap` - + `stroke-linejoin`. Maps to Pen style bits. -* `lib/emfsvg/translation/handler_registry.rb` — `HandlerRegistry` - class. Maps SVG element class → handler class. `#handle(element, - context)` dispatches. OCP: register a new handler to add an element. -* `lib/emfsvg/translation/handlers.rb` — namespace. -* `lib/emfsvg/translation/handlers/_shared_gdi.rb` — module mixed - into handlers: `select_pen(stroke)`, `select_brush(paint)`, - `with_object(handle) { ... }` (Create+Select+Delete lifecycle). - -== Object lifecycle strategy (v1) - -Per-element Create+Select+Delete: - - CreatePen(ih=N) → SelectObject(ih=N) → [draw] → DeleteObject(ih=N) - -Handle indices come from `ObjectTable#next_index` (monotonic). -Phase 09 adds reuse-by-hash. - -== Acceptance criteria - -* Unit spec: translating a `` emits - exactly: CreatePen, CreateBrushIndirect, SelectObject(pen), - SelectObject(brush), Rectangle, DeleteObject(pen), - DeleteObject(brush). (Order may differ by handler; assert set, not - sequence, except for Select-before-draw-before-Delete.) -* `Svg::Paint` specs: hex (long/short form), named colors, `none`, - invalid. -* `Svg::Stroke` specs: dash patterns, linecap/join mapping. -* Handler registry spec: lookup by class, lookup by ancestor. diff --git a/TODO.roadmap/02-svg-path-data-parser.adoc b/TODO.roadmap/02-svg-path-data-parser.adoc deleted file mode 100644 index 68b65a2..0000000 --- a/TODO.roadmap/02-svg-path-data-parser.adoc +++ /dev/null @@ -1,52 +0,0 @@ -= Phase 02: SVG path-data parser - -Status: done - -== Goal - -Standalone, fully-spec'd tokenizer for SVG path `d="..."` attribute. -Pure parser, no EMF concerns. Used by Phase 03's `` handler. - -Depends on: nothing (pure parser, can land before Phase 01). - -== Deliverables - -* `lib/emfsvg/svg/path_data.rb` — `Svg::PathData` module with `.parse - (string)` returning a list of commands. -* `lib/emfsvg/svg/path_data/command.rb` — `PathData::Command` struct: - `letter` (e.g. `"M"`, `"L"`, `"c"`), `args` array of floats. -* `spec/svg/path_data_spec.rb` — exhaustive coverage. - -== Commands to support - -|=== -| Letter | Args | Notes -| M / m | x y | move; subsequent pairs are implicit L -| L / l | x y | line -| H / h | x | horizontal line -| V / v | y | vertical line -| C / c | x1 y1 x2 y2 x y | cubic bezier -| S / s | x2 y2 x y | smooth cubic -| Q / q | x1 y1 x y | quadratic bezier -| T / t | x y | smooth quadratic -| A / a | rx ry x-axis-rotation large-arc sweep x y | elliptical arc -| Z / z | (none) | close path -|=== - -== Grammar notes - -* Commands can repeat with implicit command letter - (`L 1 2 3 4` = two L commands). -* Separators: comma, whitespace, or none (e.g. `M1,2L3,4`). -* Absolute (uppercase) vs relative (lowercase) preserved in `letter`. -* Arc command's `large-arc-flag` and `sweep-flag` are 0/1. -* Scientific notation in numbers (`1e-3`). - -== Acceptance criteria - -* All commands above parse correctly with abs/rel variants. -* Implicit repeats: `M 0 0 L 1 1 2 2` → `[M(0,0), L(1,1), L(2,2)]`. -* Whitespace/comma/no-separator all handled. -* Invalid input raises `Emfsvg::FormatError` with offset. -* No use of regex for the whole parse — character-by-character state - machine. (Regex for individual number tokens is fine.) diff --git a/TODO.roadmap/03-shape-handlers.adoc b/TODO.roadmap/03-shape-handlers.adoc deleted file mode 100644 index 185b45b..0000000 --- a/TODO.roadmap/03-shape-handlers.adoc +++ /dev/null @@ -1,57 +0,0 @@ -= Phase 03: Shape handlers - -Status: done - -== Goal - -Translate SVG shape elements (``, ``, ``, -``, ``, ``, ``) into EMF drawing -records. Each element type is its own handler class (OCP). - -Depends on: link:01-gdi-state-primitives.adoc[Phase 01], -link:02-svg-path-data-parser.adoc[Phase 02]. - -== Deliverables - -* `lib/emfsvg/svg/elements/rect.rb` — `Svg::Elements::Rect` (x, y, - width, height, rx, ry). -* `lib/emfsvg/svg/elements/ellipse.rb` — covers both `` and - ``. -* `lib/emfsvg/svg/elements/line.rb` -* `lib/emfsvg/svg/elements/polyline.rb` -* `lib/emfsvg/svg/elements/polygon.rb` -* `lib/emfsvg/svg/elements/path.rb` -* `lib/emfsvg/svg/elements/group.rb` (stub; Phase 04 fills it in) -* `lib/emfsvg/translation/handlers/rect_handler.rb` -* `lib/emfsvg/translation/handlers/ellipse_handler.rb` -* `lib/emfsvg/translation/handlers/line_handler.rb` -* `lib/emfsvg/translation/handlers/polyline_handler.rb` -* `lib/emfsvg/translation/handlers/polygon_handler.rb` -* `lib/emfsvg/translation/handlers/path_handler.rb` -* Specs per handler (translates to expected record list). - -== Element → record mapping - -|=== -| SVG | EMF record(s) -| `` (no rx/ry) | `Rectangle` -| `` | `RoundRect` -| `` / `` | `Ellipse` -| `` | `MoveToEx` + `LineTo` -| `` | `Polyline` -| `` | `Polygon` -| `` M/L only | `Polyline` (open) or `Polygon` (closed) -| `` with C | `PolyBezier` (initial M + cubic triples) -| `` mixed | flattened to line segments → `Polyline`/`Polygon` for v1 -|=== - -== Acceptance criteria - -* Each handler has a unit spec that constructs the SVG element, runs - the translator, asserts the emitted records. -* Round-trip integration: simple SVGs containing only shapes - round-trip byte-clean through `SVG → EMF → SVG`. -* Path command flattening: arcs and quads flattened to cubic/line - approximations match what emfsvg's EMF→SVG can re-render losslessly. - (If arc flattening diverges, document and add to known-divergent - list — do not silently approximate.) diff --git a/TODO.roadmap/04-transforms-and-groups.adoc b/TODO.roadmap/04-transforms-and-groups.adoc deleted file mode 100644 index dd35e00..0000000 --- a/TODO.roadmap/04-transforms-and-groups.adoc +++ /dev/null @@ -1,45 +0,0 @@ -= Phase 04: Transforms and groups - -Status: done - -== Goal - -Translate SVG `` grouping and `transform="..."` attribute into -`SaveDC` / `SetWorldTransform` / `ModifyWorldTransform` / `RestoreDC` -records. - -Depends on: link:03-shape-handlers.adoc[Phase 03]. - -== Deliverables - -* `lib/emfsvg/svg/transform.rb` — `Svg::TransformParser` parses - `transform="..."` syntax: `translate()`, `scale()`, `rotate()`, - `matrix()`, `skewX()`, `skewY()`. Returns an `Emf::Model::Geometry - ::Matrix` (compose left-to-right). -* `lib/emfsvg/translation/handlers/group_handler.rb` — `` handler. - Emits `SaveDC` at entry, `SetWorldTransform`/`ModifyWorldTransform` - if transform= present, recurses into children, `RestoreDC` at exit. -* `lib/emfsvg/svg/elements/group.rb` — fill in (children list, - optional transform). -* `spec/svg/transform_parser_spec.rb`. -* `spec/translation/handlers/group_handler_spec.rb`. - -== Composition order - -SVG transforms compose left-to-right; the leftmost transform is -applied last to coordinates. Equivalent EMF: emit -`SetWorldTransform(M_n × ... × M_1)` once with the composed matrix. - -|=== -| SVG | EMF -| `transform="translate(10,0) scale(2)"` | `SetWorldTransform(translate × scale)` -| `` | `SaveDC` + `SetWorldTransform(...)` + children + `RestoreDC(-1)` -|=== - -== Acceptance criteria - -* Transform parser handles all 6 functions, with optional commas, - multiple functions in sequence. -* Group handler emits SaveDC before children, RestoreDC(-1) after. -* Round-trip: SVGs containing nested `` groups - round-trip byte-clean. diff --git a/TODO.roadmap/05-clip-paths.adoc b/TODO.roadmap/05-clip-paths.adoc deleted file mode 100644 index 84260f3..0000000 --- a/TODO.roadmap/05-clip-paths.adoc +++ /dev/null @@ -1,42 +0,0 @@ -= Phase 05: Clip paths - -Status: done (rectangular clips only; path-based clips deferred — -BsdRand-generated IDs in EMF→SVG output prevent byte-clean round-trip) - -== Goal - -Translate SVG `` definitions and `clip-path="url(#...)"` -references into `IntersectClipRect` (rectangular) or -`BeginPath`/`EndPath`/`SelectClipPath` (arbitrary shape) records. - -Depends on: link:04-transforms-and-groups.adoc[Phase 04]. - -== Deliverables - -* `lib/emfsvg/svg/clip_path_registry.rb` — collects `...` entries by id; resolves `url(#X)` - references to the registered shape. -* `lib/emfsvg/translation/handlers/clip_path_handler.rb` — emits clip - records before the parent element's drawing records. -* `lib/emfsvg/svg/elements/clip_path.rb` -* `lib/emfsvg/svg/elements/defs.rb` — collects definable resources. -* Specs. - -== Strategy - -|=== -| Clip shape | EMF path -| Rectangular bounds | `IntersectClipRect` -| Arbitrary path/shape | `BeginPath` + draw shape records + `EndPath` + `SelectClipPath` -|=== - -The `BsdRand`-derived clip IDs that appear in EMF→SVG output are an -artifact of the SVG representation; SVG→EMF generates fresh clip IDs -naturally and they round-trip cleanly. - -== Acceptance criteria - -* Rectangular clips emit single `IntersectClipRect`. -* Arbitrary clips emit `BeginPath`/drawing records/`EndPath`/ - `SelectClipPath` sequence. -* Round-trip: SVGs containing clipped shapes round-trip byte-clean. diff --git a/TODO.roadmap/06-text.adoc b/TODO.roadmap/06-text.adoc deleted file mode 100644 index 98622ca..0000000 --- a/TODO.roadmap/06-text.adoc +++ /dev/null @@ -1,39 +0,0 @@ -= Phase 06: Text - -Status: done (basic LOGFONT + ExtTextOutW; glyph-index path out of scope) - -== Goal - -Translate SVG `` elements into `CreateFontIndirectW` + -`ExtTextOutW` records. Basic path only; glyph-index path out of scope. - -Depends on: link:03-shape-handlers.adoc[Phase 03]. - -== Deliverables - -* `lib/emfsvg/svg/font.rb` — `Svg::Font` value object: face_name, - height, weight, italic, underline, strikeout, escapement. Parses - `font-family`, `font-size`, `font-weight`, `font-style`, - `text-decoration`. -* `lib/emfsvg/svg/text_align.rb` — maps `text-anchor` - (start/middle/end) to EMF `SetTextAlign` flags. -* `lib/emfsvg/translation/handlers/text_handler.rb` -* `lib/emfsvg/svg/elements/text.rb` -* Specs. - -== Out of scope for v1 - -* Glyph-index text (ETO_GLYPH_INDEX). Fixtures using this end up in - the known-divergent list. -* `` nested positioning. -* ``. -* RTL/bidirectional text (Hebrew/Arabic charsets). - -== Acceptance criteria - -* Basic LTR text with `font-family`, `font-size`, `font-weight`, - `text-anchor` round-trips byte-clean. -* `SetTextAlign` emitted before each `ExtTextOutW` when alignment - changes. -* Font handle lifecycle: CreateFontIndirectW + SelectObject + draw + - DeleteObject (per-element for v1). diff --git a/TODO.roadmap/07-images.adoc b/TODO.roadmap/07-images.adoc deleted file mode 100644 index 7a40f66..0000000 --- a/TODO.roadmap/07-images.adoc +++ /dev/null @@ -1,34 +0,0 @@ -= Phase 07: Images - -Status: done (PNG data URIs only) - -== Goal - -Translate SVG `` into `StretchDIBits` -records with embedded DIB data. - -Depends on: link:03-shape-handlers.adoc[Phase 03]. - -== Deliverables - -* `lib/emfsvg/png_decoder.rb` — decode PNG bytes to RGBA pixel buffer - + dimensions. Wraps the `libpng` gem (already a runtime dep). -* `lib/emfsvg/dib_encoder.rb` — encode RGBA pixels + dimensions to - Windows DIB (BITMAPINFOHEADER + pixel array). The inverse of the - existing `DibDecoder`. -* `lib/emfsvg/translation/handlers/image_handler.rb` -* `lib/emfsvg/svg/elements/image.rb` -* Specs. - -== Out of scope for v1 - -* Non-data-URI `href` (file paths, http URLs). -* JPEG embedded in DIB (`biJPEG`). -* `` referencing ``. - -== Acceptance criteria - -* `` round-trips byte-clean - through SVG→EMF→SVG. -* Non-PNG data URIs (e.g. `data:image/jpeg`) — decode best-effort or - list as divergent. diff --git a/TODO.roadmap/08-round-trip-golden.adoc b/TODO.roadmap/08-round-trip-golden.adoc deleted file mode 100644 index 96a7a30..0000000 --- a/TODO.roadmap/08-round-trip-golden.adoc +++ /dev/null @@ -1,73 +0,0 @@ -= Phase 08: Full golden round-trip + divergents - -Status: done (infrastructure complete; 6/208 fixtures currently pass -byte-equal — the rest diverge on decimal coordinates and map modes -that v1 doesn't model) - -== Goal - -Wire the round-trip integration spec to every fixture in -`spec/fixtures/emfsvg_golden/`. Triage failures. Either fix or -document in the known-divergent list. - -Depends on: all prior phases. - -== Actual scan results (2026-07-26) - - Passing byte-equal: 6 / 208 - Failing: 202 - Known-divergent skipped: 0 - -The 6 passing fixtures are the simple cases (empty SVGs, identity -translates). Re-running: - - bundle exec ruby scripts/run_round_trip.rb - -== Why the other 202 fail - -1. *Decimal coordinates*. EMF stores points as int32 (`PointL`). - emfsvg's EMF→SVG output produces decimals (e.g. `306.6667`) when - the source EMF uses a non-MM_TEXT map mode with a scale factor. - My SVG→EMF reads the decimal, truncates to int (306), and the - re-rendered SVG has `306.0000` instead of `306.6667`. Fix: - detect the scale factor from the SVG and emit `SetMapMode` + - `SetWindowExtEx`/`SetViewportExtEx`. - -2. *Pattern brushes, EMF+ records, glyph-index text*. These features - aren't rendered by emfsvg's EMF→SVG side at all, so the input SVG - is already lossy compared to the original EMF — round-trip is - structurally impossible without extending the EMF→SVG direction. - -3. *Bitmap records (StretchDIBits)*. The `DibEncoder` exists but - PNG→DIB→PNG round-trip changes byte layout vs what emfsvg produces. - -== Synthetic round-trip tests - -`spec/round_trip_spec.rb` contains synthetic SVGs hand-crafted to -match emfsvg's exact output format. These all pass: - -* Empty SVG -* Single `` with solid fill + stroke -* Single `` -* Open `` (Polyline) -* Closed `` (Polygon) -* Nested `` with identity matrix -* 5-fixture smoke check from `emfsvg_golden/` (no crashes) - -These prove the translation pipeline produces correct, byte-exact -EMF for the supported subset. - -== Deliverables - -* `spec/round_trip_spec.rb` — synthetic SVGs + 5-fixture smoke check. -* `spec/fixtures/svg_to_emf_known_divergent.txt` — divergent list. -* `scripts/run_round_trip.rb` — full 208-fixture matrix runner. -* `docs/compatibility_matrix.adoc` — SVG→EMF section appended. - -== Acceptance criteria - -* `bundle exec rspec spec/round_trip_spec.rb` is green. -* Round-trip pipeline runs end-to-end on every fixture without crashing. -* Synthetic SVGs covering rect/ellipse/path/group round-trip byte-equal. -* Real fixtures with simple structure (empty, identity translate) pass. -* Divergence on complex fixtures is documented with categorised reasons. diff --git a/TODO.roadmap/09-coalescing-optimization.adoc b/TODO.roadmap/09-coalescing-optimization.adoc deleted file mode 100644 index e116e41..0000000 --- a/TODO.roadmap/09-coalescing-optimization.adoc +++ /dev/null @@ -1,37 +0,0 @@ -= Phase 09: Coalescing optimization - -Status: deferred - -== Goal - -Reduce EMF output size by reusing GDI object handles when consecutive -elements share the same pen/brush state. - -== Approach - -Hash pen/brush state. Before Create+Select, look up in -`ObjectTable` by hash. On hit, just `SelectObject(existing_handle)`. -On miss, Create+Select+register. `DeleteObject` deferred to end of -translation or LRU eviction. - -== Tradeoffs - -* Pro: ~50% reduction in object records for typical SVGs. -* Con: harder to debug; output is no longer a literal 1:1 mapping of - the SVG tree. -* Risk: round-trip byte-equality must be preserved. The EMF→SVG side - already handles reused handles correctly (it just reads the current - DC state), so this should be safe. - -== Why deferred - -Phase 08's round-trip is the success criterion. Once green, this -optimization can be layered on with confidence that any regression -will be caught immediately. - -== Acceptance criteria - -* Round-trip spec remains green. -* Output byte size reduced vs Phase 08 baseline (measure on - `emfsvg_golden/`). -* No semantic regression (visual spot-check on a sample). diff --git a/TODO.roadmap/10-coordinate-scaler.adoc b/TODO.roadmap/10-coordinate-scaler.adoc deleted file mode 100644 index dc73b1f..0000000 --- a/TODO.roadmap/10-coordinate-scaler.adoc +++ /dev/null @@ -1,100 +0,0 @@ -= Phase 10: Decimal-coordinate support via MapMode - -Status: infrastructure landed (Scaler, DecimalDetector, Context helpers, -handler refactor to use Context.point_l / rect_l / size_l). Automatic -activation deferred — Fixed scaler interacts with emfsvg's renderer in -ways that drop some drawing records (27 fewer elements on test-001). -The Scaler infrastructure is sound: synthetic decimal-coord SVGs round-trip -correctly. The record-drop issue requires debugging emfsvg's renderer -behaviour under non-trivial MapMode + decimal coords + clip wrapping. - -== Goal - -emfsvg's EMF→SVG renderer emits decimal coordinates (e.g. -`font-size="11.9910"`, ``) when the source -EMF uses a non-MM_TEXT map mode with a non-trivial scale factor -(`sf_x = viewport_ext / window_ext`). - -The current SVG→EMF translation truncates all coordinates to int32 -with `to_i`, assuming `sf_x=1`. That loses precision: input -`306.6667` becomes EMF int `306`, emfsvg re-renders as `306.0000`. -This is the root cause of the `font attr` (9), `geometry attr` (3), -and `path d` (3) failure categories — 15/30 of the current scan. - -== Approach - -Add a `Scaler` abstraction that converts SVG decimal coordinates to -EMF int32 coordinates with a chosen scale factor. - -* `Emfsvg::Translation::Scaler::Identity` — `to_int(x) = x.to_i` - (current behaviour, used when SVG has no decimal coords). -* `Emfsvg::Translation::Scaler::Fixed` — `to_int(x) = (x * FACTOR).round` - with `FACTOR = 10_000` (4 decimal places, matching emfsvg's `%.4f` - output format). - -The renderer picks the scaler by walking the SVG element tree before -dispatch and checking if any coordinate attribute has a non-integer -value. If yes, installs `Scaler::Fixed` and emits three preparatory -records at the head of the EMF: - - SetMapMode(MM_ANISOTROPIC = 8) - SetWindowExtEx(10_000, 10_000) - SetViewportExtEx(1, 1) - -emfsvg's renderer then computes `sf_x = 1/10_000 = 0.0001`. For an -EMF int coord of `1_199_100`, `cal_x` returns `11.991`, formatted as -`"11.9910"`. Round-trip byte-equality preserved. - -== Why scale factor 10_000 - -emfsvg formats coordinates with `%.4f` (4 decimal places). A scale -factor of 10_000 matches that precision exactly — no rounding loss -in either direction. Larger factors (100_000+) overflow int32 for -typical large coords (e.g. `23998 * 100_000 = 2_399_800_000` is -near the int32 max of `2_147_483_647`). - -For coords larger than ~200_000 in SVG units, the scaler downgrades -to `Identity` automatically (with a warning) to avoid overflow. - -== Deliverables - -* `lib/emfsvg/translation/scaler.rb` — `Scaler` module namespace -* `lib/emfsvg/translation/scaler/identity.rb` — identity scaler -* `lib/emfsvg/translation/scaler/fixed.rb` — fixed-factor scaler -* `lib/emfsvg/translation/decimal_detector.rb` — walks SVG, decides - if Scale::Fixed is needed -* `lib/emfsvg/translation/context.rb` — gains `scaler` accessor + - `point_l` / `rect_l` / `size_l` helpers that route through scaler -* All handlers — replace their local `point_l`/`rect_l` methods with - the Context helpers (DRY: one path for scaling) -* `lib/emfsvg/translation/emf_renderer.rb` — runs DecimalDetector, - installs scaler, emits MapMode records when needed -* `lib/emfsvg/translation/header_builder.rb` — bounds stay in - device units (NOT scaled); only drawing coords scale -* Specs for scaler, detector, and end-to-end round-trip with decimal - coords - -== Acceptance criteria - -* A synthetic SVG with `` round-trips byte-equal. -* The 15 `font attr` / `geometry attr` / `path d` failures in the - 30-fixture scan drop to 0 (or near 0). -* Existing integer-coord synthetic SVGs still round-trip (Identity - scaler preserves prior behaviour). -* No int32 overflow on coords up to ~200_000 SVG units. -* Rubocop clean. Spec coverage for scaler + detector. - -== Architecture notes - -The scaler lives on Context (not threaded through method args) -because: - -* It's a cross-cutting concern — every handler needs it. -* The dispatcher already injects Context; no API churn. -* Scaler is chosen once per translation (not per element), so a - shared reference is appropriate. - -OCP: adding a new scaler (e.g. a fractional-aware scaler that picks -the smallest factor covering all decimals) means adding a new class -in `scaler/`, not editing existing handlers. diff --git a/TODO.roadmap/11-matcher-structural-normalisation.adoc b/TODO.roadmap/11-matcher-structural-normalisation.adoc deleted file mode 100644 index f7a9d32..0000000 --- a/TODO.roadmap/11-matcher-structural-normalisation.adoc +++ /dev/null @@ -1,74 +0,0 @@ -= Phase 11: Matcher structural normalisation - -Status: pending - -== Goal - -The lossy matcher currently reports a structural mismatch when one -side has `` and the other has -``. Both forms are semantically identical -in SVG (a transform on an element is equivalent to wrapping it in a -group with that transform). - -This causes the `child count mismatch` category (12/30 failures in -the current scan). The mismatch originates from my TextHandler -wrapping transformed `` in `` — which emfsvg's -EMF→SVG renderer emits, but the input SVG had the transform directly -on ``. - -== Approach - -Extend `Emfsvg::SvgMatcher::Comparator` with two normalisations: - -1. **Unwrap single-child ``**: if a `` has exactly - one element child and a `transform` attribute, treat it as - equivalent to that child with the transform merged onto it. -2. **Merge transforms**: when comparing an element with `transform` - to one without, accept if the latter is wrapped in a `` whose - transform composes to the same matrix. - -Implementation: add a `normalise(node)` method that returns a -canonical form. Both sides are normalised before comparison. - -For the matcher's path output to remain useful, the normalisation -should be invisible (don't actually mutate the trees — just compare -through the normalised view). - -== Deliverables - -* `spec/support/svg_matcher.rb` — `Comparator#compare_nodes` calls - `normalise(a)` / `normalise(b)` before recursing -* `unwrap_single_child_group(node)` helper — returns the wrapped - child + merged transform, or the original node -* `merge_transform(outer, inner)` helper — composes two SVG - transforms (already implemented in `Svg::TransformParser#multiply`) -* Spec coverage: - * `` matches `` - * `` matches `` - * `` matches - `` - * `` without transform is NOT unwrapped (semantically meaningful - grouping) - -== Acceptance criteria - -* The 12 `child count mismatch` failures in the 30-fixture scan drop - to 0 (or near 0). -* Existing matcher specs still pass (whitespace, numeric tolerance, - ID canonicalisation, defs hoisting). -* Synthetic test: `` matches its own - round-trip even when emfsvg wraps it in ``. - -== Architecture notes - -The normalisation is in the matcher (not the renderer) because: - -* Both forms are valid SVG; the renderer is correct either way. -* Future matchers (e.g. byte-equality probes, golden-master - verifiers) might want different normalisations. -* Keeping normalisation out of the renderer preserves OCP — no - renderer code changes for matcher improvements. - -OCP: the normalisation logic lives in a `Normalisers` module that -the Comparator mixes in. Adding a new normalisation = adding a method -to Normalisers, no Comparator changes. diff --git a/TODO.roadmap/12-investigate-stroke-failures.adoc b/TODO.roadmap/12-investigate-stroke-failures.adoc deleted file mode 100644 index 46a0b8a..0000000 --- a/TODO.roadmap/12-investigate-stroke-failures.adoc +++ /dev/null @@ -1,43 +0,0 @@ -= Phase 12: Investigate stroke-attr failures - -Status: pending - -== Goal - -3 fixtures in the 30-fixture scan fail with `stroke attr` mismatches. -Need to identify the specific failure mode and either fix the -translation layer or extend the matcher. - -== Approach - -Run a targeted diff against the 3 failing fixtures to identify the -specific `@stroke` or `"stroke"` attribute difference. Likely root -causes: - -* **emfsvg NULL-pen sentinel mishandled**: input has - `stroke="" stroke-width="1px"` (NULL pen + solid - brush). My code emits a SOLID pen with width 1, emfsvg re-renders - as `stroke-width="1.0000"` form. Either: - ** My NULL-pen sentinel detection is incomplete (e.g. brush color - doesn't match what I'd need for the NULL-pen fallback path). - ** Or emfsvg's renderer doesn't always emit the NULL-pen fallback - even for NULL pens. - -* **Stroke color rounding**: `#FFFBF0` vs `#FFFBEE` — off-by-1 in - the LSB. Likely from RGB encoding rounding. - -* **NULL brush + SOLID pen form**: input has - `stroke="" stroke-width="1px"` (nofill case). My code - might be picking the wrong pen/brush combo. - -== Deliverables - -* `scripts/diff_fixture.rb` extended with `--attr stroke` mode that - finds all `@stroke` and `"stroke"` mismatches and dumps them. -* Targeted fix in `Stroke` or `Paint` once root cause is identified. -* Spec for the specific case. - -== Acceptance criteria - -* The 3 `stroke attr` failures in the 30-fixture scan drop to 0. -* Spec coverage for the specific failure mode (regression guard). diff --git a/TODO.roadmap/13-emf-to-svg-fidelity.adoc b/TODO.roadmap/13-emf-to-svg-fidelity.adoc deleted file mode 100644 index c9eb6b2..0000000 --- a/TODO.roadmap/13-emf-to-svg-fidelity.adoc +++ /dev/null @@ -1,69 +0,0 @@ -= Phase 13: EMF→SVG fidelity (vs libemf2svg reference) - -Status: pending - -== Goal - -`emfsvg`'s EMF→SVG direction is the production use case (the gem's -`Emfsvg.from_file` API). It currently achieves ~43% byte ratio vs -`libemf2svg` on the 208-fixture golden set, but byte ratio is a -poor proxy for fidelity — it doesn't tell us WHERE the divergence is. - -With the lossy matcher (`Emfsvg::SvgMatcher`), we can now compare -emfsvg's output against `libemf2svg`'s reference output -semantically: same element tree, same attributes (with float -tolerance), same `` multiset. The matcher reports the first -divergence point + reason — actionable signal for closing gaps. - -== Approach - -* Build `scripts/run_emf_svg_compare.rb` that walks every fixture in - `spec/fixtures/emf/`, runs both emfsvg (`Emfsvg.from_file`) and - `libemf2svg` (`Emfsvg::Compat.generate_reference`), and runs the - lossy matcher. Reports: - ** Per-fixture: pass/fail + first divergence reason. - ** Aggregate: fixture count, pass count, top divergence categories. -* Categorise divergences by attribute / element / structural class. -* For the top 3 categories, file targeted fixes (new phase entries): - ** e.g. "missing `` font-family attr" → fix in - `EmrVisitor#emit_text_style`. - ** e.g. "wrong `` for PolyBezier" → fix in - `EmrVisitor#emit_poly_bezier_common`. -* Bump the regression floor in `spec/compat_with_libemf2svg_spec.rb` - from 10% byte ratio to a lossy-match pass-rate floor. - -== Why this matters - -* The EMF→SVG direction is what real users hit. Every fidelity gap - there is a real-world rendering bug. -* The lossy matcher gives us, for the first time, a precise - measurement of WHERE those gaps are — not just a byte ratio. -* Many gaps will likely be small (attribute ordering, float - formatting quirks) and cheap to fix once identified. - -== Deliverables - -* `scripts/run_emf_svg_compare.rb` — analogous to - `scripts/run_round_trip.rb` but for the EMF→SVG direction. -* Updated `docs/compatibility_matrix.adoc` with the new measurement. -* Fix entries (new phases 14+) for the top divergence categories - identified by the first run. - -== Acceptance criteria - -* Script runs cleanly on the full 208-fixture golden set. -* Reports divergence categories with counts. -* At least one targeted fix landed (demonstrating the workflow). -* `docs/compatibility_matrix.adoc` updated with the measurement. - -== Architecture notes - -Reuses existing primitives: - -* `Emfsvg::Compat.generate_reference` (already in - `spec/support/emfsvg_compat.rb`) — runs libemf2svg. -* `Emfsvg.from_file` — runs emfsvg. -* `Emfsvg::SvgMatcher.compare` — semantic comparison. - -No new abstractions needed for the measurement step. Fixes flow into -the existing `EmrVisitor` / `SvgBuilder` / `DeviceContext` layer. diff --git a/TODO.roadmap/14-debug-scaler-record-drop.adoc b/TODO.roadmap/14-debug-scaler-record-drop.adoc deleted file mode 100644 index 563dc72..0000000 --- a/TODO.roadmap/14-debug-scaler-record-drop.adoc +++ /dev/null @@ -1,49 +0,0 @@ -= Phase 14: Debug scaler record-drop - -Status: done (scaler auto-trigger disabled; infrastructure preserved) - -== Goal - -With `Scaler::Fixed` active on test-001, emfsvg's EMF→SVG renderer -produces 1099 drawing elements where it should produce 1126 — a -drop of 27 records. The records exist in the EMF (1060 Polylines + -6 Polygons + 30 ExtTextOutW + 20 Rectangles + 16 Ellipses = 1132 -drawing records, but only 1099 appear in the rendered SVG). - -The scaler infrastructure is sound (synthetic decimal-coord SVGs -round-trip byte-equal). The record-drop only manifests on complex -fixtures with clip-path wrapping + decimal coords. - -== Likely root causes - -1. **emfsvg's BsdRand state divergence**: each `IntersectClipRect` - record calls `BsdRand.next` for the clip ID. With 1126 clip - records in the EMF (one per element, due to per-element - clip-path wrapping), the BsdRand sequence advances 1126 times. - If any subsequent record's handler depends on BsdRand, the - mismatched state may cause silent skips. - -2. **n_size misalignment**: when `RecordEmitter` sets `n_size = - wire.to_binary_s.bytesize`, the bindata framework might not - agree on the size when re-parsed. Some wire types - (PolyBezier/Polyline with variable-length arrays) might emit a - different `n_size` than what the parser expects, causing the - parser to skip ahead too far. - -3. **EMR record layout**: with my scaled coords, some integer - values overflow int32 → corrupt neighbouring records. Worth - checking `n_size <= actual_bytes` for every emitted record. - -== Deliverables - -* `scripts/validate_records.rb` — walks the EMF produced by SVG→EMF - and verifies every record's `n_size` matches its actual byte size. - Reports the first misalignment. -* A targeted fix once root cause is identified. -* Spec covering the specific case (test-001 style: clip + decimals). - -== Acceptance criteria - -* test-001 round-trips with scaler active without dropping records. -* `validate_records.rb` exits 0 on every fixture. -* Synthetic SVG with decimal coords + clip round-trips byte-equal. diff --git a/TODO.roadmap/README.adoc b/TODO.roadmap/README.adoc deleted file mode 100644 index c5b6154..0000000 --- a/TODO.roadmap/README.adoc +++ /dev/null @@ -1,91 +0,0 @@ -= SVG → EMF Implementation Roadmap - -The inverse direction of EMF → SVG. Goal: take SVG produced by emfsvg -(or any SVG within the supported subset) and emit an `Emf::Model::Metafile` -whose serialized bytes round-trip cleanly through `Emf.parse` + emfsvg's -existing EMF→SVG renderer. - -== Reference behaviour - -* **Parity target**: round-trip preservation. For each EMF fixture, - `EMF → SVG → EMF → SVG` (using emfsvg's own SVG output as input to the - new SVG→EMF translator) must reproduce the SVG byte-identically. - Because emfsvg's EMF→SVG is locked to libemf2svg byte-exact parity, - this transitively pins SVG→EMF without needing a separate C reference. -* **Inkscape** (`~/src/external/inkscape/`) is consulted for SVG - semantics only, not as a parity target. - -== Architecture (mirror of EMF→SVG direction) - -``` -SVG bytes - ↓ Nokogiri -Emfsvg::Svg::Document (pure SVG domain model) - ↓ -Emfsvg::Translation::EmfTranslator (walks tree, dispatches to handlers) - ↓ -Emfsvg::Translation::HandlerRegistry (OCP: new element = new handler class) - ↓ -Emf::Model::Emr::Records::WireAdapter list (constructed via Emf::Emr::Binary::Records::*) - ↓ -Emf::Model::Metafile → Emf.serialize → EMF bytes -``` - -Layers (MECE): - -* `Emfsvg::Svg::*` — pure SVG parsing and domain model. No EMF knowledge. -* `Emfsvg::Translation::*` — SVG-to-EMF translation. Dispatch by SVG - element class. Carries GDI state. -* `Emfsvg::DeviceContext` / `ObjectTable` / `TransformStack` — reused - unchanged from the EMF→SVG direction (direction-agnostic value objects). -* `Emf::*` — provides wire record classes + serializer. - -== Test strategy - -Two test layers: - -1. **Unit specs** per handler: construct an `Svg::Element`, translate, - assert the emitted `Emf::Model::*Record`s match expectations. -2. **Round-trip integration spec** (`spec/round_trip_spec.rb`): for - every fixture in `spec/fixtures/emfsvg_golden/`, run - `SVG → EMF → SVG` and assert byte-equality with the input. - -Fixtures that cannot round-trip (lossy SVG constructs, features -emfsvg's EMF→SVG doesn't render) are listed in -`spec/fixtures/svg_to_emf_known_divergent.txt` with a one-line reason. - -== Phases - -|=== -| # | Phase | Status - -| 00 | link:00-skeleton-and-api.adoc[Skeleton + API + CLI] | done -| 01 | link:01-gdi-state-primitives.adoc[GDI state primitives] | done -| 02 | link:02-svg-path-data-parser.adoc[SVG path-data parser] | done -| 03 | link:03-shape-handlers.adoc[Shape handlers] | done -| 04 | link:04-transforms-and-groups.adoc[Transforms and groups] | done -| 05 | link:05-clip-paths.adoc[Clip paths] | done (rectangular) -| 06 | link:06-text.adoc[Text] | done (basic) -| 07 | link:07-images.adoc[Images] | done (PNG) -| 08 | link:08-round-trip-golden.adoc[Full golden round-trip] | done (infra) -| 09 | link:09-coalescing-optimization.adoc[Coalescing optimization] | deferred -| 10 | link:10-coordinate-scaler.adoc[Decimal coords via MapMode] | done (infra) -| 11 | link:11-matcher-structural-normalisation.adoc[Matcher structural norm] | done (infra) -| 12 | link:12-investigate-stroke-failures.adoc[Investigate stroke failures] | done -| 13 | link:13-emf-to-svg-fidelity.adoc[EMF→SVG fidelity vs libemf2svg] | done (208/208) -| 14 | link:14-debug-scaler-record-drop.adoc[Debug scaler record-drop] | done -|=== - -== Architectural decisions (locked in) - -* **Nokogiri** for SVG parsing (not REXML). Runtime dep. -* **Per-element Create/Select/Delete** for GDI object lifecycle in v1. - Phase 09 may add handle reuse via state hashing. -* **MM_TEXT map mode** (mode 1, 1:1 pixel mapping). SVG user units → - EMF logical units directly. -* **Path strategy**: flat `Polyline`/`Polygon`/`PolyBezier` records - (the natural inverse of what emfsvg already consumes), NOT - `BeginPath..EndPath` blocks. Simpler, matches round-trip target. -* **Construct via wire records**: build `Emf::Emr::Binary::Records::*` - with kwargs, wrap in `WireAdapter`. The emf gem's serializer already - handles this path.