Skip to content

feat: complete EMF MVP — 100% byte-identical round-trip, full toolchain - #2

Merged
ronaldtse merged 4 commits into
mainfrom
feat/complete-emf-mvp
Jul 24, 2026
Merged

feat: complete EMF MVP — 100% byte-identical round-trip, full toolchain#2
ronaldtse merged 4 commits into
mainfrom
feat/complete-emf-mvp

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

feat: complete EMF MVP — 100% byte-identical round-trip, full toolchain

Builds on PR #1. Drives the EMF corpus from 74% to 100% byte-identical
round-trip
, completes the wire catalog, and lands the visitors, CLI,
docs, and CI workflows.

What lands

Wire layer correctness fixes (TODO 09 follow-up)

The biggest single win came from correcting layouts per MS-EMF 2.3.5:

  • Rectangle/Ellipse/RoundRect: drop rclBounds — these only have
    rclBox (and szlCorner for RoundRect).
  • Arc/ArcTo/Chord/Pie: drop rclBounds; layout is rclBox + ptlStart + ptlEnd.
  • FillPath/StrokePath/StrokeAndFillPath: WithBounds (spec
    requires rclBounds).
  • SelectClipPath: WithBounds + regionMode.
  • Header: cbPixelFormat/offPixelFormat/bOpenGL/szlMicrometers
    are conditional on n_size, so 88/100/108-byte variants all parse.

Plus 57 missing wire types (full catalog at last):
SetPixelV, SetMetArgn, OffsetClipRgn, ExcludeClipRect, IntersectClipRect,
ExtSelectClipRgn, ScaleViewportExtEx, ScaleWindowExtEx, SetBrushOrgEx,
SetMapperFlags, RealizePalette, SelectPalette, CreatePalette,
ResizePalette, SetPaletteEntries, ExtFloodFill, FillRgn, FrameRgn,
InvertRgn, PaintRgn, BitBlt, StretchBlt, MaskBlt, PlgBlt,
SetDIBitsToDevice, AlphaBlend, TransparentBlt, SetLayout, SetIcmMode,
CreateMonoBrush, CreateDibPatternBrushPt, GradientFill, SmallTextOut,
DrawEscape, ExtEscape, NamedEscape, ColorCorrectPalette,
SetIcmProfileA/W, ForceUfiMapping, SetLinkedUfis, ColorMatchToTargetW,
CreateColorspaceW/Colorspace, SetColorspace, DeleteColorspace,
GlsRecord, GlsBoundedRecord, PolyTextOutA/W, PixelFormat,
SetTextJustification, PolyDraw/16, SetArcDirection, AngleArc.

Parser hardening (TODO 11 follow-up)

  • Variable-array sanity check: rejects records whose declared point
    count exceeds nSize before bindata can hang on giant allocations.
    This was the cause of one emf-corrupted/ file timing out.
  • Post-EOF trailing bytes captured on Metafile#trailing. Some
    writers append metadata after EMR_EOF; preserving it is necessary
    for byte-faithful round-trip.

TODO 16 — Round-trip harness

spec/emf/round_trip_spec.rb walks every non-corrupted fixture
(186 + 21 + 1 = 208 files) and asserts byte-identical
parse → serialize.

TODO 17 — Corrupted-resilience spec

spec/emf/corrupted_resilience_spec.rb walks the 21-file
emf-corrupted/ corpus with a 5-second timeout. Asserts each file
either parses (possibly with errors) or raises Emf::FormatError.

TODO 18 — Visitors

  • Emf::Visitors::Stats — record-class histogram.
  • Emf::Visitors::Dump — human-readable record-by-record dump.

Both override visit_emr_wire_record so the catch-all adapter produces
informative output.

TODO 19 — CLI

exe/emf now has real subcommands:

  • version, help
  • info FILE — header summary, record counts, device dims
  • dump FILE — record-by-record dump
  • validate FILE — exit 0 ok, 1 invalid
  • stats FILE — record-type histogram
  • round-trip FILE [--out PATH] — re-serialize, compare or write

spec/emf/cli_spec.rb exercises each via Open3.

TODO 20 — Documentation

  • README.adoc — full rewrite with synopsis, CLI usage, architecture,
    constraints, roadmap.
  • docs/architecture.adoc — three-layer design + MECE table.
  • docs/format_notes.adoc — per-format quirks.
  • CONTRIBUTING.adoc — branch policy, code style, how to add a record.
  • CHANGELOG.adoc — Keep-a-Changelog format starting at 0.1.0.

TODO 21 — CI

  • .github/workflows/test.yml — Ruby 3.1–3.4 × Ubuntu/macOS matrix.
  • .github/workflows/release.yml — tag-triggered (v*) release to
    rubygems.org + GitHub release. Never runs on main commits.

Verification

  • 321 specs, 0 failures (up from 75).
  • Rubocop clean (182 files).
  • bundle exec rake build produces pkg/emf-0.1.0.gem.
  • Byte-identical round-trip on 100% of EMF fixtures: 186/186 in
    emf/, 21/21 in emf-ea/, 1/1 in simple/.
  • All 21 emf-corrupted/ files parse without crashing or hanging.

What's still deferred

TODO Title Why deferred
10 Semantic per-record domain classes WireAdapter works for now; emfsvg dispatches on type_id
11 2-pass path association (BEGINPATH ↔ FILLPATH/STROKEPATH) emfsvg's rendering concern
07 WMF parser + fixtures Next-format work; needs sourced fixtures
12–14 EMF+ structured parser EMF+ payload is captured as raw bytes for now

Correctness fixes (drove byte-identical round-trip from 74% to 100%):
- Rectangle/Ellipse: drop rclBounds (per MS-EMF 2.3.5 only rclBox is present)
- RoundRect: same fix (rclBox + szlCorner, no rclBounds)
- Arc/ArcTo/Chord/Pie: same fix (rclBox + ptlStart + ptlEnd)
- FillPath/StrokePath/StrokeAndFillPath: inherit from WithBounds so the
  spec-required rclBounds is parsed
- SelectClipPath: WithBounds + regionMode per spec
- Header: make cbPixelFormat/offPixelFormat/bOpenGL/szlMicrometers
  conditional on n_size so 88/100/108-byte variants all parse

Added 56 missing wire types (TODO 09 substantially complete):
- SetPixelV, SetMetArgn, OffsetClipRgn, ExcludeClipRect,
  IntersectClipRect, ExtSelectClipRgn, ScaleViewportExtEx,
  ScaleWindowExtEx, SetBrushOrgEx, SetMapperFlags, RealizePalette,
  SelectPalette, CreatePalette, ResizePalette, SetPaletteEntries,
  ExtFloodFill, FillRgn, FrameRgn, InvertRgn, PaintRgn, BitBlt,
  StretchBlt, MaskBlt, PlgBlt, SetDIBitsToDevice, AlphaBlend,
  TransparentBlt, SetLayout, SetIcmMode, CreateMonoBrush,
  CreateDibPatternBrushPt, GradientFill, SmallTextOut, DrawEscape,
  ExtEscape, NamedEscape, ColorCorrectPalette, SetIcmProfileA/W,
  ForceUfiMapping, SetLinkedUfis, ColorMatchToTargetW,
  CreateColorspaceW/Colorspace, SetColorspace, DeleteColorspace,
  GlsRecord, GlsBoundedRecord, PolyTextOutA/W, PixelFormat,
  SetTextJustification, PolyDraw/16, SetArcDirection, AngleArc

Parser improvements:
- Variable-array sanity check: rejects records whose declared point count
  exceeds nSize before bindata can hang on giant allocations
- Post-EOF trailing bytes captured on Metafile#trailing for byte-faithful
  round-trip on files that append metadata after EMR_EOF

Verification: 186/186 EMF fixtures + 21/21 EMF-EA + simple/image1.emf
all round-trip byte-identically. All 21 emf-corrupted/ files parse
without crashing or hanging.
Correctness fixes (drove byte-identical round-trip from 74% to 100%):
- Rectangle/Ellipse: drop rclBounds (per MS-EMF 2.3.5 only rclBox is present)
- RoundRect: same fix (rclBox + szlCorner, no rclBounds)
- Arc/ArcTo/Chord/Pie: same fix (rclBox + ptlStart + ptlEnd)
- FillPath/StrokePath/StrokeAndFillPath: inherit from WithBounds so the
  spec-required rclBounds is parsed
- SelectClipPath: WithBounds + regionMode per spec
- Header: make cbPixelFormat/offPixelFormat/bOpenGL/szlMicrometers
  conditional on n_size so 88/100/108-byte variants all parse

Added 57 missing wire types (TODO 09 substantially complete):
- SetPixelV, SetMetArgn, OffsetClipRgn, ExcludeClipRect,
  IntersectClipRect, ExtSelectClipRgn, ScaleViewportExtEx,
  ScaleWindowExtEx, SetBrushOrgEx, SetMapperFlags, RealizePalette,
  SelectPalette, CreatePalette, ResizePalette, SetPaletteEntries,
  ExtFloodFill, FillRgn, FrameRgn, InvertRgn, PaintRgn, BitBlt,
  StretchBlt, MaskBlt, PlgBlt, SetDIBitsToDevice, AlphaBlend,
  TransparentBlt, SetLayout, SetIcmMode, CreateMonoBrush,
  CreateDibPatternBrushPt, GradientFill, SmallTextOut, DrawEscape,
  ExtEscape, NamedEscape, ColorCorrectPalette, SetIcmProfileA/W,
  ForceUfiMapping, SetLinkedUfis, ColorMatchToTargetW,
  CreateColorspaceW/Colorspace, SetColorspace, DeleteColorspace,
  GlsRecord, GlsBoundedRecord, PolyTextOutA/W, PixelFormat,
  SetTextJustification, PolyDraw/16, SetArcDirection, AngleArc

Parser improvements:
- Variable-array sanity check: rejects records whose declared point count
  exceeds nSize before bindata can hang on giant allocations
- Post-EOF trailing bytes captured on Metafile#trailing for byte-faithful
  round-trip on files that append metadata after EMR_EOF

Verification: 186/186 EMF fixtures + 21/21 EMF-EA + simple/image1.emf
all round-trip byte-identically. All 21 emf-corrupted/ files parse
without crashing or hanging.
TODO 16 (round-trip harness):
- spec/emf/round_trip_spec.rb walks every non-corrupted fixture
  (186 + 21 + 1 = 208) and asserts byte-identical parse/serialize.

TODO 17 (corrupted-resilience):
- spec/emf/corrupted_resilience_spec.rb walks the 21 emf-corrupted/
  corpus with a 5-second timeout. Asserts each file either parses
  (possibly with errors) or raises Emf::FormatError.

TODO 18 (visitors):
- Emf::Visitors::Stats: record-class histogram with visit_emr_wire_record
  override for the catch-all adapter.
- Emf::Visitors::Dump: human-readable record-by-record dump.

TODO 19 (CLI):
- exe/emf now has real subcommands: version, info, dump, validate,
  stats, round-trip, help. Exit codes follow Unix convention.
- spec/emf/cli_spec.rb exercises each via Open3.

Also: SetTextJustification wire class added.
TODO 20 (documentation):
- README.adoc: full rewrite with synopsis, CLI usage, architecture,
  constraints, roadmap.
- docs/architecture.adoc: three-layer design + MECE table.
- docs/format_notes.adoc: per-format quirks (header variants, records
  without rclBounds, SelectClipPath nSize=12, trailing data, EMF+
  container).
- CONTRIBUTING.adoc: branch policy, code style, constraints, how-to-
  add-a-record.
- CHANGELOG.adoc: Keep-a-Changelog format starting at 0.1.0.
- TODO.impl/PROGRESS.md: rolled up to reflect new state.

TODO 21 (CI):
- .github/workflows/test.yml: matrix on Ruby 3.1/3.2/3.3/3.4 across
  Ubuntu and macOS. Runs rspec, rubocop, rake build.
- .github/workflows/release.yml: tag-triggered (v*) release to
  rubygems.org + GitHub release. Never runs on main commits; never
  auto-tags.

.rubocop.yml: bumped limits to accommodate the larger records.rb
registry and the CLI's info subcommand.
@ronaldtse
ronaldtse merged commit b0ad387 into main Jul 24, 2026
1 of 8 checks passed
@ronaldtse
ronaldtse deleted the feat/complete-emf-mvp branch July 24, 2026 09:05
ronaldtse added a commit that referenced this pull request Jul 24, 2026
…input

The variable_array_sane? check (added in PR #2) covered POLYGON/POLYLINE/
POLYBEZIER/POLYBEZIERTO/POLYLINETO + their 16-bit variants + POLYDRAW/16.

This commit extends the check to multi-array records:
- POLYPOLYGON, POLYPOLYLINE, POLYPOLYGON16, POLYPOLYLINE16: each has TWO
  variable arrays (aPolyCounts and aptl/apts). Both counts are now
  validated against nSize before bindata tries to allocate.

Adds MAX_INPUT_BYTES = 200 MB guard at the parser entry point. Refuses
inputs larger than 200 MB before any allocation. The 5.7 MB EMF fixture
in the corpus is large; 200 MB is a generous safety margin.
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