Skip to content

Byte-exact parity push: 112/186 (60.2%) emfsvg fixtures match libemf2svg - #11

Closed
ronaldtse wants to merge 30 commits into
mainfrom
feat/fix-cal-orgs
Closed

Byte-exact parity push: 112/186 (60.2%) emfsvg fixtures match libemf2svg#11
ronaldtse wants to merge 30 commits into
mainfrom
feat/fix-cal-orgs

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

Major byte-exact parity push for emfsvg vs libemf2svg: 112/186 fixtures (60.2%) now produce byte-identical SVG output, up from 0/186 (0%) before this branch.

The remaining 40% is dominated by PNG image-encoding differences (libpng vs our pure-Ruby encoder) and floating-point rounding (sub-LSB numeric divergence in coordinate transforms).

Key changes

  • SvgBuilder: rewrite to no-indentation, /> (no leading space), ASCII-8BIT output matching libemf2svg's exact byte format.
  • Renderer: explicit newline control via rawln / newline; reset BsdRand per render; pre-scan records to build {begin_path_idx => action} map for libemf2svg's two-pass pathStack analysis.
  • BsdRand: replicate BSD rand() (default seed 1) sequence so clip-path IDs match get_id() output.
  • TransformStack: RestoreDC returns the snapshot at the target level (consumed from stack) so clip_id restores correctly.
  • DeviceContext: clip_id attribute (saved/restored with DC); default arc_direction=0 (calloc default); Font.default uses nil face_name, height=0, weight=0; Brush default color BLACK (matches calloc).
  • EmrVisitor:
    • Replace tag(attrs) calls with raw byte-by-byte emission matching C's exact printf ordering.
    • emit_path_element / emit_shape_element mirror C's exact attribute ordering.
    • emit_stroke_draw / emit_fill_draw replicate C quirks: no_stroke NULL-pen pattern uses fill color; BS_SOLID→" " (no fill-rule), BS_NULL→"evenodd", BS_HATCHED→"nonzero" (brush-style overlap with poly-fill constants); stroke-linecap/join emit with leading+trailing space.
    • COSMETIC vs GEOMETRIC pen: COSMETIC uses width=1, GEOMETRIC uses pen.width.
    • Stock objects (WHITE_BRUSH, NULL_PEN, etc.) properly handled via SelectObject.
    • LOGFONT parsing: height, weight, italic, underline, strikeout, escapement (% 3600).
    • Text element: even empty text emitted; style ="..." with space before = quirk.
    • PolyBezier: double-M pattern for starting_point=1.
    • LineTo uses endPathDraw pattern; Polyline uses U_EMRPOLYLINE pattern.
    • SetPixelV: no-op (FLAG_IGNORED in libemf2svg).
    • Clip-path: <path d="M ... L ... L ... L ... L ... Z Z" /> polygon format.
    • ExtSelectClipRgn RGN_COPY resets clip.
    • BeginPath..EndPath: tracks @in_path, appends path data inline.
    • PolylineTo / PolyBezierTo don't emit starting M (matches C ispolygon=false).
    • Arc rewrite with int_el_rad and arcdir flags; radii via point_cal.
    • PolyPolygon: single <path> with sub-polygons separated by Z M.
    • emit_transform_group: <g ...>\n with scaleX/scaleY applied to eDx/eDy.
    • ExtCreatePen: correct EXTLOGPEN32 body offsets; COLORREF byte order R,G,B.
    • SetMiterLimit: convert float bits to uint32 (MS-EMF iMiterLimit is uint32).
    • scale_dim_y: preserve sign (no .abs) — matches C's scaleY.
    • Header translate fix_broken_y uses literal "0.00 00" (C source typo).
    • rgb_hex helper: always #RRGGBB (drop alpha/reserved byte).
    • Image element byte layout: width height x y [clip] xlink:href order with %.4f format.

Verification

$ ruby /tmp/byte_parity_check.rb
Exact byte match: 112/186 (60.2%)

The 186 EMF fixtures in emf/spec/fixtures/emf/ are compared against output from the libemf2svg binary at ../libemf2svg/emf2svg_ref.

Remaining gaps

  • 35 fixtures differ in PNG image encoding (libpng adaptive filtering vs our filter-0).
  • 14 fixtures differ in sub-LSP floating-point rounding (coordinate transforms).
  • 24 fixtures have other minor structural differences.
  • 1 fixture has transform group emission difference.

Closing these would require either a libpng binding or matching libpng's adaptive filter heuristics in pure Ruby.

ronaldtse added 30 commits July 24, 2026 17:37
Consumes the emf gem's domain model and emits SVG via a lightweight
in-house XML builder. No FFI, no GPL entanglement.

- Emfsvg::Renderer orchestrates the metafile walk, sets up the root
  <svg> with proper viewBox, and applies the libemf2svg fixBrokenYTransform
  heuristic for Sparx/Wine EMFs.
- Emfsvg::Visitors::EmrVisitor walks each Emf::Model::Emr::Records::WireAdapter
  and dispatches via HANDLERS hash to per-record visit_<type> methods.
- Emfsvg::DeviceContext snapshots GDI state (pen, brush, transform,
  text/bk colors, miter limit).
- Emfsvg::ObjectTable is the 1-indexed pen/brush/font registry.
- Emfsvg::TransformStack implements SaveDC/RestoreDC.
- Emfsvg::SvgBuilder is a light XML emitter (no external deps).

Handled record types (open/closed: adding a handler is one hash entry
plus one method):
- Primitives: Rectangle, Ellipse, RoundRect, LineTo, MoveToEx,
  Polygon/Polyline (+ 16-bit variants)
- Paths: BeginPath, EndPath, CloseFigure, FillPath, StrokePath,
  StrokeAndFillPath, AbortPath
- State: SetTextColor, SetBkColor, SetBkMode, SetMapMode, SetRop2,
  SetPolyFillMode, SetTextAlign, SetStretchBltMode
- Transforms: SetWorldTransform, ModifyWorldTransform
- Objects: CreatePen, CreateBrushIndirect, SelectObject, DeleteObject,
  SaveDc, RestoreDc
- Misc: SetMiterLimit, SetWindow/Viewport Org/Ext

Safety:
- Renderer caps output at 500 MB (MAX_OUTPUT_BYTES) and raises
  Emfsvg::RenderError before any blow-up.
- emf gem's parser caps input at 200 MB and validates variable-array
  counts, so the renderer inherits solid input bounds.

CLI:
- emfsvg convert INPUT.emf OUTPUT.svg with --width/--height/--namespace
  --verbose/--emf-plus options.
- emfsvg version, emfsvg help.

Specs: 27 examples covering the public API, SVG builder, state
snapshot, transform stack, object table, and CLI.
emfsvg is a clean-room rewrite handling a subset of EMF records. This
commit adds the infrastructure to quantify the gap against libemf2svg
(the C reference) and guard against regressions.

What this is NOT: a 'must produce identical SVG' test. libemf2svg emits
different whitespace, attribute ordering, doctype declarations, embedded
text/bitmap rendering, etc. Producing identical output would require
implementing all 246 record handlers.

What this IS:
- A well-formedness check on both outputs for every fixture.
- A structural-equivalence check that emfsvg only emits valid SVG
  drawing elements (no foreign tags).
- A regression floor: emfsvg produces >= 10% of libemf2svg's total byte
  output for the simple fixture glob. Catches catastrophic regressions
  without being noisy. Tighten as handlers are added.
- A developer script (scripts/run_compat_check.rb) that walks every
  fixture and prints a per-fixture + aggregate matrix showing byte
  ratio and element overlap.

Current state per the matrix (208 fixtures):
- Total libemf2svg output: 59 MB
- Total emfsvg output: 25 MB
- Byte ratio: 43.8%
- Avg element overlap: 28.8%

Files:
- lib/emfsvg/compat.rb: Compat module with generate_reference,
  generate_emfsvg, parse_svg, count_elements, compare_glob, summarize.
- spec/compat_with_libemf2svg_spec.rb: 426 specs across all fixtures.
- scripts/run_compat_check.rb: developer-facing matrix printer.
- docs/compatibility_matrix.adoc: documents the gap and the path
  forward.
- emfsvg.gemspec: add rexml dependency for SVG parsing.
Generates libemf2svg's output for all 208 EMF fixtures and commits it
as spec/fixtures/golden/<name>.svg — the reference target emfsvg aims
to converge with. Also commits emfsvg's current output as
spec/fixtures/emfsvg_golden/<name>.svg — the regression baseline that
locks emfsvg's own behavior byte-for-byte.

Why both:
- spec/fixtures/golden/         — what libemf2svg produces (reference)
- spec/fixtures/emfsvg_golden/  — what emfsvg produces today (baseline)

The golden_spec walks every fixture and asserts emfsvg's output byte-
matches its own baseline. Any drift fails. To update after an
intentional change: scripts/regenerate_golden.rb, review the diff,
commit.

Files:
- spec/fixtures/golden/*.svg (208 files, ~57 MB) — libemf2svg reference
- spec/fixtures/emfsvg_golden/*.svg (208 files, ~25 MB) — emfsvg baseline
- spec/fixtures/README.md — documents layout, regeneration, spec usage
- spec/golden_spec.rb — 208 byte-exact regression specs
- scripts/regenerate_golden.rb — canonical regenerator for both sets

The current emfsvg golden set reflects the partial implementation
(~43% byte ratio vs libemf2svg). As handlers are added, regenerate
the emfsvg golden and tighten the structural-equivalence checks in
spec/compat_with_libemf2svg_spec.rb.
Documentation (spec/fixtures/README.adoc):
- Replaces README.md with detailed .adoc documenting how the golden
  set was obtained: libemf2svg build, emf2svg_ref.c wrapper source,
  regenerate_golden.rb usage, reproducibility, license notes.
- Records that the wrapper was GPLv2 by inheritance from libemf2svg
  but the SVG output itself is not a derivative work per FSF guidance.

New handlers (closes some of the gap with libemf2svg):
- SetPixelV: emits a 1x1 rect with the pixel color (~24K records
  across the corpus).
- ExtTextOutA/W: emits <text> with UTF-16LE/CP1252 decode, font
  family from LOGFONT_PANOSE, text color, bold/italic/underline.
  (~31K records across the corpus.)
- CreateFontIndirectW: parses LOGFONT face name and stores Font in
  the object table for SelectObject.
- PolyBezier / PolyBezier16: emits <path> with cubic Bezier C
  segments (groups of 3 control points).
- IntersectClipRect / ExcludeClipRect: stubs (no visual output yet;
  clip region tracking is a follow-up).

emfsvg::DeviceContext gains @font and a Font value object.

Compat matrix shift:
- Before: 43.8% byte ratio, 28.8% element overlap
- After:  47.4% byte ratio, 35.5% element overlap

emf wire changes (companion repo emf):
- ExtTextOutW/ExtTextOutA wire classes now declare the full
  MS-EMF 2.3.8 layout (iGraphicsMode through offDx) instead of
  stubbing the trailing fields as 'rest :body'. The stub still
  preserved bytes for round-trip; this change makes the fields
  accessible to emfsvg for text rendering.

emfsvg_golden regenerated to reflect the new output. 208 fixtures
all pass byte-identical against the new baseline.

661 specs, 0 failures. Rubocop clean.
Closes TODOs 01, 03, 04, 05 from TODO.finalize/. Profile shows the
Raw record count dropped from 3539 to 38 — the Ellipse/Rectangle/
RoundRect wire classes had been incorrectly inheriting WithBounds
(extra 16 bytes per record), causing bindata to fail and fall through
to Raw. Fixed in companion emf PR.

TODO.finalize/ documents the full roadmap to libemf2svg parity
(16 TODOs).

New handlers (TODO 01):
- SetBrushOrgEx, SetMetArgn, OffsetClipRgn, SetIcmMode, SetLayout,
  ScaleViewportExtEx, ScaleWindowExtEx, SetMapperFlags,
  SetArcDirection, RealizePalette, SelectPalette, CreatePalette,
  ResizePalette, SetPaletteEntries — all state-only, no SVG output.
- DeviceContext extended with brush_origin, icm_mode, layout,
  mapper_flags, arc_direction accessors.

New drawing handlers (TODO 03, 04):
- PolyPolygon, PolyPolygon16, PolyPolyline, PolyPolyline16 —
  multi-subpath shapes via emit_poly_multi helper.
- PolyDraw, PolyDraw16 — mixed-segment paths via emit_poly_draw
  helper that interprets the abTypes array (PT_MOVETO/LINETO/
  BEZIERTO/CLOSEFIGURE).
- AngleArc — parametric arc with start/sweep angles.

Arc family (TODO 05):
- Arc, ArcTo, Chord, Pie all share emit_arc helper. Computes
  start/end angles from ptl_start/ptl_end rays through rclBox
  centre; honours SetArcDirection; emits SVG A command with
  correct large-arc and sweep flags. Chord closes with Z, Pie
  closes via centre.

No-op handlers:
- DrawEscape, ExtEscape, NamedEscape, SmallTextOut, SetLinkedUfis,
  ForceUfiMapping, ColorCorrectPalette, SetIcmProfileA/W,
  ColorMatchToTargetW, CreateColorspace/W, SetColorspace,
  DeleteColorspace, GlsRecord, GlsBoundedRecord, PixelFormat,
  PolyTextOutA/W, SetTextJustification — registered as no-ops so
  they no longer log as unhandled.

Compat matrix shift:
- Before: 47.4% byte ratio, 35.5% element overlap
- After:  48.2% byte ratio, 37.3% element overlap

emfsvg_golden regenerated.

661 specs, 0 failures. Rubocop clean.
New files:
- lib/emfsvg/dib_decoder.rb: pure-Ruby DIB decoder supporting:
  * BI_RGB (uncompressed) at 1/4/8/24/32-bit
  * BI_BITFIELDS at 16/32-bit with custom masks
  * BI_JPEG / BI_PNG (pass-through)
  * Top-down (negative height) and bottom-up variants
  * Returns an RGBA pixel buffer
- lib/emfsvg/png_encoder.rb: minimal pure-Ruby PNG encoder
  (colour type 6 RGBA, no interlacing, zlib-compressed IDAT).
  Output suitable for data:image/png;base64,... URIs.

New handlers (TODO 07):
- StretchDIBits: decodes DIB via offBmiSrc/offBitsSrc, re-encodes
  as PNG, emits <image> element.
- AlphaBlend, TransparentBlt, BitBlt, StretchBlt: decode via the
  trailing body bytes (best effort for non-StretchDIBits layouts).

Wire changes (companion emf repo, in this PR):
- StretchDIBits wire class now declares the full MS-EMF 2.3.1.7
  layout (offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc, iUsageSrc,
  dwRop, cxDest, cyDest).
- PolyTextOutA/PolyTextOutW: class name fix to match autoload.
- ColorMatchToTargetW: class name fix to match autoload.

Compat matrix: still 48.2% byte ratio (bitmap fixtures are a
minority of the corpus), but the capability is now in place for
fixtures that do use bitmaps. Visual: images appear where they
should.

661 specs, 0 failures. Rubocop clean.
Major improvements:

Clip region tracking (TODO 06):
- New Emfsvg::ClipRegion value object (rect or path bounds)
- DeviceContext gains clip_region accessor (saved/restored via
  TransformStack on SaveDC/RestoreDC)
- IntersectClipRect, ExcludeClipRect, ExtSelectClipRgn,
  SelectClipPath all now compute and emit <clipPath> definitions
- fill_attrs and stroke_attrs merge clip-path attribute so all
  subsequent drawing primitives are clipped

GradientFill (TODO 10):
- visit_gradient_fill parses Trivertex structs from body bytes,
  emits <rect> per vertex pair with the start vertex colour

DibDecoder fixes (TODO 07):
- Fixed recursive safe_getbyte (was calling itself, causing stack
  overflow)
- Fixed nil-from-getbyte causing TypeError/NoMethodError on 5
  fixtures
- All 186 fixtures now render without error (was 181/186)

Also fixed:
- visit_set_metargn handler name mismatch (HANDLERS entry was
  visit_set_met_argn but method was visit_set_metargn)
- Removed respond_to? from visit_emr_wire_record dispatch
  (use rescue for method_not_found instead)
- class << self for DibDecoder and PngEncoder (was module_function
  in a class, which doesn't work in Ruby 3.4)
- Base64 encoding via pack('m0') instead of base64 gem (which was
  removed from default gems in Ruby 3.4)

Compat matrix: 48.2% -> 58.8% byte ratio (significant jump).
Element overlap: 37.3% -> 36.5% (slight dip from clip defs).
All 186 fixtures render. Rubocop clean. 27 unit specs pass.
ExtCreatePen (TODO 02):
- visit_ext_create_pen now parses the body bytes to extract
  pen_style (line style bits 0-3), width, and COLORREF from
  the LOGBRUSH32 embedded in the struct. Previously stubbed
  to a default Pen.

Text alignment (TODO 09):
- emit_ext_text now maps GDI text alignment flags to SVG:
  * TA_LEFT/RIGHT/CENTER -> text-anchor: start/end/middle
  * TA_TOP/BOTTOM/BASELINE -> dominant-baseline: text-before-edge/
    text-after-edge/alphabetic
- Font size uses abs(height) to handle negative (matched) heights.
- text-decoration combines underline + line-through properly.

Region ops (TODO 08):
- visit_fill_rgn: extracts region bounds from RGNDATAHEADER,
  fills with the brush color.
- visit_frame_rgn: draws a bordered rect around the region.
- visit_invert_rgn: emits an invert-filter rect (MVP).
- visit_paint_rgn: fills with current brush.
- New parse_rgn_body_bounds helper for extracting rclBounds
  from the ih_brush + cbRgnData + RGNDATA layout.

Compat matrix: 58.8% -> 59.5% byte ratio.
All 186 fixtures render without error.
Rubocop clean. No constraint violations.
…tural)

This is the single biggest architectural fix for visual correctness.
Without viewport scaling, all coordinates in MM_ANISOTROPIC fixtures
(586 occurrences across the corpus, nearly half) were in logical
units instead of device units, making the rendered SVG visually
wrong.

Implementation:
- DeviceContext gains window_ext, viewport_ext, window_org,
  viewport_org (saved/restored via TransformStack).
- SetWindowOrgEx, SetWindowExtEx, SetViewportOrgEx, SetViewportExtEx
  handlers now store the values in the DC.
- emit_viewport_scale: when SetViewportExtEx arrives (the last of
  the three extent records), compute sx=viewport_ext/window_ext
  and emit a <g transform='translate(vx,vy) scale(sx,sy)
  translate(-wx,-wy)'> group wrapping all subsequent content.
- The group is closed before opening a new one, and at the end
  via flush_open_groups.
- MM_TEXT (mode 1) skips the group entirely (1:1 mapping).

Compat: byte ratio 59.5% (unchanged — viewport scaling adds <g>
wrappers but doesn't change content volume). Element overlap
36.5% -> 36.7%. The visual correctness improvement is significant
but not measurable by byte ratio alone.

All 186 fixtures render. Rubocop clean.
…ODO 13)

fill_attrs now detects BS_HATCHED brush style and emits <defs><pattern>
with the corresponding hatch pattern (HS_HORIZONTAL, HS_VERTICAL,
HS_FDIAGONAL, HS_BDIAGONAL, HS_CROSS, HS_DIAGCROSS). Each uses an 8x8
tile with foreground-color lines on background-color fill (when
BkMode is OPAQUE) or transparent background (when TRANSPARENT).

All 186 fixtures render. Rubocop clean.
…fecycle

Studied libemf2svg C source systematically (emf2svg_utils.c point_cal,
stroke_draw, fill_draw, text_style_draw; emf2svg.c transform_draw;
emf2svg_rec_bitmap.c dib_img_writer; emf2svg_rec_clipping.c
clip_rgn_mix). Applied findings:

Stroke (matches libemf2svg stroke_draw):
- Pen style bits now fully decoded: bits 0-7 = line style,
  bits 8-11 = line cap (round/square/butt), bits 12-15 = line
  join (round/bevel/miter with miterlimit).
- Dash lengths scaled: dash_len = width*5, dot_len = width.
- NULL pen: falls back to 1px fill-color border (not stroke=none)
  when fill is not null. Matches libemf2svg no_stroke().
- Format: %.4f to match libemf2svg output format.

Text (matches libemf2svg text_style_draw + text_draw):
- Fixed EMRTEXT wire layout: ptlReference is POINTL (int32 x 2),
  not WmfRect16 (int16 x 4). Was parsing wrong fields entirely.
  This was the biggest single fix — text positions were wrong.
- Vertical offset: font_height * 0.9 for TA_TOP alignment.
- CDATA wrapping of text content.
- style='white-space:pre;' attribute.
- Font rotation from escapement (tenths of degree).
- font-weight as numeric value, not just 'bold'.
- text-decoration uses comma separator.

Transform groups (matches libemf2svg transform_draw):
- Close previous <g transform> before opening new one. Previously
  groups accumulated without closing, causing incorrect nesting.
- matrix() format uses %.4f and raw dx/dy (not maybe_flip_y).

Compat: 59.5% -> 81.1% byte ratio (+21.6pp). All 186 render.
Rubocop clean.
…6.9%)

Studied libemf2svg C source systematically — read emf2svg_utils.c
(point_cal, stroke_draw, fill_draw, text_style_draw), emf2svg_rec_control.c
(EMRHEADER_draw, EMREOF_draw), emf2svg.c (top-level driver), and
emf2svg_rec_drawing.c (U_EMRRECTANGLE_draw). Applied exact findings:

Renderer (matches libemf2svg SVG root structure):
- XML declaration format matches exactly.
- <svg> tag: version=1.1, xmlns, xmlns:xlink, width=%.4f, height=%.4f.
  No viewBox attribute. Matches C printf format strings.
- <g transform='translate(-RefX*scaling, -RefY*scaling)'> wrapper.
  Computed from rclBounds.left/top. Matches C's RefX/RefY computation.
- scaling = imgWidth / abs(bounds.right - bounds.left).
  Defaults to 1.0 when no size override.
- fixBrokenYTransform: width/height get +1, translate is (0,0).
- pxPerMm = szlDevice.cx / szlMillimeters.cx.
- EOF closes two </g> (transform + header) then </svg>.

EmrVisitor (per-element point_cal matching C's coordinate pipeline):
- cal_x(x) = ((x - windowOrgX) * sf_x + viewPortOrgX) * scaling
- cal_y(y) = ((y - windowOrgY) * sf_y + viewPortOrgY) * scaling
- sf_x/sf_y: exact MapMode switch matching C's point_cal.
  MM_TEXT=1:1. LOMETRIC: pxPerMm*0.1. HIMETRIC: pxPerMm*0.01.
  LOENGLISH: pxPerMm*0.01*25.4. HIENGLISH: pxPerMm*0.001*25.4.
  TWIPS: pxPerMm/1440*25.4. ISOTROPIC: uniform scale.
  ANISOTROPIC: independent sx/sy from viewport/window extents.
  Metric modes negate Y (*-1) to flip axis.
- maybe_flip_y now delegates to cal_y (per-element scaling).
- Removed viewport group approach (emit_viewport_scale).
  Per-element cal_x/cal_y handles scaling correctly for ALL records.
- fmt(n) helper: format('%.4f', n) to match C's printf format.

Stroke (matches C's stroke_draw exactly):
- NULL pen: falls back to 1px fill-color border (not stroke=none).
- Pen style bits: 0-7=line style, 8-11=cap (round/square/butt),
  12-15=join (round/bevel/miter+miterlimit).
- Dash lengths: width*5 for dash, width for dot. Format %.4f.
- Stroke width: min(width,1)px for cosmetic, scaled for geometric.

Text (matches C's text_style_draw + text_draw):
- ptlReference (POINTL, not WmfRect16) for text position.
- CDATA wrapping. style='white-space:pre;'.
- Vertical offset: font_height*0.9 for TA_TOP.
- Rotation from font_escapement (tenths of degree).
- font-weight as numeric value. text-decoration with comma separator.

Compat matrix: 81.1% -> 96.9% byte ratio (+15.8pp).
The remaining 3.1% is PNG encoder differences and minor format gaps.

All 186 fixtures render. Rubocop clean. 27 unit specs pass.
Every coordinate in every drawing handler now goes through the
point_cal pipeline (cal_x for x, cal_y for y) matching libemf2svg's
exact coordinate transformation.

Handlers updated:
- Rectangle: lt_x/lt_y/rb_x/rb_y via cal_x/cal_y, dims from diffs
- Ellipse: cx/cy/rx/ry computed from scaled corner coords
- RoundRect: same as Rectangle + scaled corner radius
- LineTo: x1/y1/x2/y2 all scaled via cal_x/cal_y
- SetPixelV: x/y scaled
- emit_path_polygon: all points go through cal_x/cal_y/fmt
- visit_poly_bezier/16: all control points scaled
- emit_poly_draw: all M/L/C coords scaled
- AngleArc: center/radius via cal_x/cal_y
- emit_arc: rclBox/ptlStart/ptlEnd via cal_x/cal_y
- emit_ext_text: ptlReference via cal_x/cal_y

All coordinates formatted as %.4f (matching libemf2svg printf format).

Compat: 92.5% byte ratio (ratio decreased from 96.9% because
%.4f formatting of ALL coordinates produces longer attribute values
that are CLOSER to libemf2svg's format but differ from the previous
integer format. The visual correctness is significantly higher).

All 186 fixtures render. 27 unit specs pass. Rubocop clean.
Major rewrite to match libemf2svg's exact byte output:

SvgBuilder:
- No indentation, no leading space before />
- ASCII-8BIT encoding to match libemf2svg output
- Add rawln, newline methods for explicit newline control

Renderer:
- Use rawln for XML declaration, <svg>, <g> (no auto-newline)
- Reset BsdRand per render

BsdRand: BSD rand() sequence (default seed 1) to match libemf2svg's
get_id() → rand() for clip-path IDs.

TransformStack: RestoreDC now returns the snapshot at the target level
(consumed from stack) so clip_id is properly restored.

DeviceContext:
- Add clip_id attribute (saved/restored with DC)
- Font.default uses nil face_name, height=0, weight=0 (matches C calloc)

EmrVisitor:
- Replace tag+attrs with raw byte-by-byte emission matching libemf2svg
- emit_path_element / emit_shape_element mirror C's exact ordering
- emit_stroke_draw / emit_fill_draw replicate C's quirks including:
  * no_stroke pattern (NULL pen uses fill color)
  * BS_SOLID→space, BS_NULL→"evenodd", BS_HATCHED→"nonzero" fill-rule
  * stroke-linecap/join emit with leading+trailing space
- Stock objects (WHITE_BRUSH, NULL_PEN, etc.) properly handled
- LOGFONT parsing: height, weight, italic, underline, strikeout, escapement
- escapement % 3600 (matches C's lfEscapement % 3600 normalization)
- Text element: even empty text emitted, style ="..." with space before =
- PolyBezier: double-M pattern (M cur then M point[0]) for starting_point=1
- LineTo: endPathDraw pattern (leading space before fill="none")
- Polyline: U_EMRPOLYLINE pattern (no leading space)
- SetPixelV: FLAG_IGNORED (no output)
- Clip-path: <path d="M ... L ... L ... L ... L ... Z Z" /> polygon format
- ExtSelectClipRgn RGN_COPY resets clip
…eatePen fix

- emit_poly_multi: all sub-polygons in one <path d=...> with Z M between
  sub-polygons, matching U_EMRPOLYPOLYGON_draw.
- emit_transform_group: emit `<g ...>\n` (trailing newline), and apply
  scaleX/scaleY to eDx/eDy (sign-preserving, no .abs).
- visit_angle_arc: rewrite to match arc_circle_draw startPathDraw pattern
  with double-M (M cur then M start).
- visit_ext_create_pen: fix body offsets — elpPenStyle at offset 16
  (not 20), COLORREF at offset 28 (not 36).
- emit_header_translate_group: use '0.00 00' for fix_broken_y (matches
  libemf2svg C source typo).
…ii via point_cal)

- emit_arc: full rewrite using libemf2svg's int_el_rad for boundary
  intersection, large_arc/sweep flags from arcdir (positive=1/1, else 0/0),
  and point_cal-scaled radii (matching C's point_draw on radii).
- emit_path_element_closed for endFormDraw pattern (chord/pie).
- int_el_rad helper replicating libemf2svg's ellipse intersection math.
- visit_set_arc_direction: maps AD_CLOCKWISE(2)→1, AD_COUNTERCLOCKWISE(1)→-1
  per C source.
- DeviceContext default arc_direction=0 (matches calloc'd C state).
…Draw

Updates @current_x/@current_y after PIE arc so the next record's
startPathDraw emits the correct cur position.

64/186 fixtures now byte-match libemf2svg exactly.
When inside a path block:
- visit_begin_path emits `<path d="` and sets @in_path=true
- visit_move_to_ex emits `M X,Y ` to the open d-string
- visit_line_to emits `L X,Y ` to the open d-string
- visit_polygon/ polyline/ poly_bezier append directly
- visit_close_figure emits `Z `
- visit_end_path closes with `" stroke fill />` (no leading space
  before fill — U_EMRENDPATH_draw differs from endPathDraw here)

Standalone LineTo/PolyBezier still uses endPathDraw pattern
(leading_fill_space: true).
- Pre-scan records to build {begin_path_idx => action} map matching
  libemf2svg's two-pass pathStack analysis. visit_end_path then knows
  whether to emit fill/stroke/stroke_fill attrs.
- rgb_hex helper: always emit #RRGGBB (drop alpha/reserved byte).
- Image emit: width height x y order, %.4f format, scale_dim_x/y.
- scale_dim_y: preserve sign (no .abs) — matches libemf2svg's scaleY.
- visit_ext_create_pen: correct EXTLOGPEN32 offsets in body.
…stroke)

Massive parity boost: 101/186 fixtures (54.3%) now byte-match libemf2svg.

C's stroke_draw switch on (style & 0x000F0000):
  U_PS_COSMETIC  (0x00000000): width_stroke(states, out, 1)  -- hardcoded 1
  U_PS_GEOMETRIC (0x00010000): basic_stroke -> width_stroke(...pen.width)

Default pen (style=0) is COSMETIC, so width=1 not pen.width.
Also flush_open_groups emits </g> per line.

111/186 fixtures (59.7%) now byte-match libemf2svg.
…nt32)

112/186 fixtures (60.2%) now byte-match libemf2svg.
After arc_draw in C, cur_x/cur_y is updated to the last emitted point
(end point for arc, center for pie, end for chord).
…ath blocks

- emit_transform_group returns early if @in_path (transforms deferred to
  BeginPath/EndPath per C's two-pass analysis).
- emit_poly_multi inside @in_path appends to open d-string instead of
  emitting a new <path> element (which produced malformed nested <path).
- visit_begin_path guards against double-emission.

All 416 well-formed-SVG compat specs now pass.
All 214 specs pass (208 golden + 6 svg_builder).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant