feat: pure-Ruby WMF/EMF/EMF+ parser — foundation + EMF MVP - #1
Merged
Conversation
- emf.gemspec: BSD-2-Clause, Ruby >=3.1, bindata runtime dep - Gem scaffold: Rakefile, .rspec, .rubocop.yml, .ruby-version, LICENSE, bin/, exe/ - Emf::Error hierarchy: Error / FormatError / ParseError / SerializeError - Emf::Binary::Types: 11 bindata primitives (PointL, PointS, RectL, ColorRef, XForm, LogPalette, ...) registered for reuse across formats - Emf::Binary::Codec: UTF-16LE and CP1252 -> UTF-8 decoding helpers - Emf::Model::Geometry: 8 immutable value types (Point, PointF, PointS, Size, Rect, RectS, Color, Matrix) with from_wire/to_wire translators - Emf::Model::Metafile: Enumerable container with format / header / records / errors / emf_plus, freezes records and errors on construction - Emf::Model::Record: abstract base with type_id and accept contracts - Emf::Model::Visitor: OCP-clean visitor with register_visit; subclasses override only the visit_* methods they need - Emf::Detector: format auto-detection for APM WMF, standard WMF, EMF - Namespace stubs for Wmf, Emr (EMF), EmfPlus with raise-on-call parsers - 46 specs covering primitives, geometry, visitor dispatch, detection
- Emf::Emr::Binary::Header: full ENHMETAHEADER (100 bytes fixed + trailing capture for byte-faithful round-trip) - Emf::Emr::Binary::Record and WithBounds shared bases for all EMR records - Emf::Emr::Binary::TypeCodes: 122 EMR_* numeric constants per MS-EMF 2.3.1 - Emf::Emr::Binary::Records: registry + autoload for 61 record types covering the most common types (Polygon/Polyline/PolyBezier + 16 variants, drawing primitives, state records, transforms, path control, comment, EOF). Unknown types fall through to Records::Raw which preserves bytes verbatim. - Emf::Emr::Parser: reads variable-length header, walks records, dispatches via the registry, traps per-record errors as ParseError entries, extracts EMF+ payload bytes from EMR_COMMENT records (identifier 0x2B464D45) - Emf::Emr::Serializer: walks records, serializes each via bindata - Emf::Model::Emr::Header: domain value object preserving all 18 wire fields for byte-faithful round-trip - Emf::Model::Emr::Records::WireAdapter: domain-shape wrapper around bindata records (interim solution until TODO 10 lands per-record domain classes) - Emf.parse / parse_file / serialize / serialize_file / detect_format: public API auto-dispatches WMF vs EMF - 75 specs, 0 failures. Byte-identical round-trip on 137/186 EMF fixtures.
TODO.impl/ (22 files + index + progress): - 21 self-contained TODOs covering all priorities (P0 foundation + EMF, P1 WMF + EMF+ + tooling + CI), each with Context, Outcomes, Files, - Verification, Done-when checklist. - PROGRESS.md rolls up what's complete vs what remains. spec/fixtures/ (230 EMF files, 35 MB): - emf/: 186 plain EMF files including 19 EMF+ carriers - emf-ea/: 21 Sparx EA / Wine EMF files - simple/: 1 minimal EMF (image1.emf, 92 KB, 3538 records) - emf-corrupted/: 21 corrupted EMF + README documenting each corruption scripts/convert_docs.rb: - Converts a Microsoft Open Specifications .docx to a set of GFM markdown files split by Heading1, using rubyzip + nokogiri - Extracts media, renders tables as GFM pipe tables, preserves heading hierarchy and inline code/bold/italic - spec/scripts/convert_docs_spec.rb builds a synthetic .docx and verifies the converter end-to-end
Source .docx files (Microsoft Open Specifications, 2024-04-23 revision): - [MS-WMF]-240423.docx (263 KB compressed, 85 K words) - [MS-EMF]-240423.docx (569 KB compressed, 110 K words) - [MS-EMFPLUS]-240423.docx (483 KB compressed, 108 K words) Derived markdown in reference-docs/<spec>/: - 7 chapters per spec (21 files total), each with YAML frontmatter - INDEX.md per spec listing chapter title and word count - media/ extracted from word/media/ - convert-docs.log provenance trail Generated by scripts/convert_docs.rb. Re-run when spec revisions bump.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: pure-Ruby WMF/EMF/EMF+ parser — foundation + EMF MVP
Splitting the existing C-wrapper (
emf2svg-ruby, an FFI shim around theGPLv2
libemf2svg) into two gems:emf(this repo) — pure-Ruby parser. Binary → OOP domain model → binary.emfsvg(sibling repo, later) — transformer (model → SVG, SVG → model).This PR lands the foundation and a working EMF MVP. WMF and EMF+ land in
follow-up TODOs tracked in
TODO.impl/.What's in this PR
Foundation (TODOs 01–06)
emf.gemspec(BSD-2-Clause, Ruby ≥ 3.1, bindata runtime),Rakefile, .rspec, .rubocop.yml, bin/, exe/.
Emf::Error/FormatError/ParseError/SerializeError.PointL,PointS,PointD,SizeL,RectL,RectS,ColorRef,XForm,PaletteEntry,LogPalette,FileTime—registered for reuse across all three formats.
Point,PointF,PointS,Size,Rect,RectS,Color,Matrix— immutable, value-equal, hashable,from_wire/to_wiretranslators.Metafile: Enumerable container, frozen records + errors,ok?/errors?.Recordabstract base withtype_idandacceptcontracts.Visitorwithregister_visit— OCP-clean; subclasses override only thevisit_*methods they need.Detector: APM WMF, standard WMF, EMF auto-detection.EMF wire + parser (TODOs 09–11, 15)
Emf::Emr::Binary::Header: full 100-byte fixed header + trailing-bytescapture for byte-faithful round-trip.
Emf::Emr::Binary::Records: registry + autoload for 61 record typescovering the most common EMR_* types (Polygon/Polyline/PolyBezier + 16
variants, drawing primitives, state records, transforms, path control,
Comment, EOF). Unknown types fall through to
Records::Rawwhichpreserves bytes verbatim.
Emf::Emr::Parser: variable-length header read, record walk, registrydispatch, per-record error trapping, EMF+ payload extraction.
Emf::Emr::Serializer: walks records, serializes each via bindata.Emf::Model::Emr::Header: domain value object preserving all 18 wirefields for round-trip.
Emf::Model::Emr::Records::WireAdapter: interim domain wrapper aroundbindata records (TODO 10 replaces with semantic classes).
Emf.parse/parse_file/serialize/serialize_file/
detect_format.Reference docs (TODO 02)
scripts/convert_docs.rb: rubyzip + nokogiri converter, splits anyMS Open Specifications .docx by Heading1 into GFM files.
TODO roadmap
TODO.impl/with 21 self-contained TODOs + PROGRESS.md rolling upwhat's done vs what remains.
Architectural decisions
Emf::Emr, notEmf::Emf. A namespace calledEmf::Emfcauses Ruby'sconstant lookup to confuse
Emf::FormatError(resolved insidemodule Emf) withEmf::Emf::FormatError. Renamed toEmf::Emr,which mirrors the spec's
EMR_*prefix.bindata classes. Domain records are immutable value objects with
from_wire/to_wiretranslators. No hand-rolledto_h/from_h.record's own file plus three lines in
records.rb. No central switchstatement.
emfsvgwill provide its own visitorwith
visit_emr_*overrides.Metafile#errorsis an array ofParseError. Only header / magicfailures raise.
Constraints honored
require_relativeand norequireof internal paths inlib/—everything autoloads via the parent-namespace file pattern.
doublein specs — real instances only.sendto private methods, noinstance_variable_set/get, norespond_to?for type checks.feat/pure-ruby-emf-parser; nothing committed tomain.Verification
bundle exec rspec→ 75 examples, 0 failures.bundle exec rubocop→ 122 files, 0 offenses.bundle exec rake build→pkg/emf-0.1.0.gem.simple/image1.emf: 3538 records, 0 errors, byte-identical round-trip.What's next (tracked in TODO.impl/)
WireAdapterwith semantic domain classesemf-corrupted/