emfsvg: pure-Ruby EMF ↔ SVG transformer
emfsvg converts between Microsoft Enhanced Metafile (EMF) and SVG
in both directions. It is a clean-room Ruby replacement for the FFI
wrapper around the GPLv2 libemf2svg C library.
Status
Both conversion directions are implemented. Compatibility is enforced by strict byte comparisons against the pinned corpus and committed SVG artifacts; the lossy matcher is a separate diagnostic tool, not the parity acceptance gate.
Current integration uses a temporary emf Git pin. The image1 round trip now
matches exactly, and a deliberately pinned C build reproduces all 208 committed
C goldens byte for byte. The original historical builder remains unknown. Full
Ruby corpus parity, corrupted-input resource behavior, native backend, and a
containing released emf package remain unqualified. See
the compatibility matrix for contracts and
current evidence, and fixture setup before running
corpus-dependent checks.
Synopsis
require "emfsvg"
# EMF → SVG
svg = Emfsvg.from_file("image.emf")
svg = Emfsvg.from_bytes(emf_bytes, width: 400, height: 300)
# SVG → EMF
emf = Emfsvg.from_svg_file("drawing.svg")
emf = Emfsvg.from_svg(svg_string)
Command-line tool
emfsvg convert INPUT.emf OUTPUT.svg [options] emfsvg to-emf INPUT.svg OUTPUT.emf emfsvg version emfsvg help
Options (convert):
-w, --width PIXELS Override viewport width
-h, --height PIXELS Override viewport height
-n, --namespace NAME XML namespace prefix (e.g. svg)
-v, --verbose Print record-by-record dispatch to STDERR
--emf-plus Render EMF+ records instead of EMF fallback
Architecture
EMF → SVG |
|
SVG → EMF |
SVG bytes → leptris → |
Both directions share:
-
Emfsvg::DeviceContext/ObjectTable/TransformStack— GDI state -
Emf::*— theemfgem’s wire record classes + serializer -
Emfsvg::SvgMatcher(spec/support) — lossy semantic SVG comparison
SVG → EMF handler registry (OCP)
Adding a new SVG element type means writing a handler class and registering it — no existing handler code changes:
# lib/emfsvg/translation/handlers/my_element_handler.rb
class MyElementHandler
include SharedGdi
def self.call(element, context)
# emit EMF records via context.emit(...)
end
end
# Register in EmfRenderer.register_default_handlers! DEFAULT_REGISTRY.register(Svg::Elements::MyElement, Handlers::MyElementHandler)
Lossy SVG matcher
Emfsvg::SvgMatcher compares two SVG documents semantically:
-
Float tolerance (configurable, default 1.0px)
-
Auto-generated ID canonicalisation (
clip-N,img-N) -
libemf2svg typo normalisation (
0.00 00→0.0000) -
<defs>hoisting + multiset comparison -
<g transform><X/></g>≡<X transform/>flattening
Used by:
-
spec/round_trip_spec.rb— SVG→EMF→SVG round-trip verification -
spec/fidelity_regression_spec.rb— EMF→SVG fidelity vs libemf2svg -
scripts/run_round_trip.rb— full golden-master round-trip scan -
scripts/run_emf_svg_compare.rb— full fidelity scan vs libemf2svg
Installation
gem install emfsvg
Or in your Gemfile:
gem "emfsvg", "~> 0.1" gem "emf", "~> 0.1"
Requires Ruby >= 3.1.
Development
git clone https://github.com/claricle/emfsvg cd emfsvg bundle install
The checkout’s Gemfile temporarily pins emf to
5c2c019138bab6f3c67f7602e8b410ba5a67c429 for model integration. This is
separate from the external fixture corpus revision and does not establish
compatibility with a released emf package.
Follow the explicit corpus setup to materialize
EMF_FIXTURE_ROOT at the manifest’s exact revision. Missing roots, changed
inventories, or changed fixture bytes fail setup; there is no sibling-checkout
or developer-machine fallback.
bundle exec rspec spec/parity_manifest_spec.rb spec/parity_corpus_spec.rb bundle exec rspec spec/golden_spec.rb spec/round_trip_golden_spec.rb spec/compat_with_libemf2svg_spec.rb --dry-run bundle exec rubocop
Dry-run verifies collection, not parity. The strict comparison groups currently have known failures; the complete suite is not a passing qualification gate. Ordinary compatibility specs use committed artifacts without invoking C.
Live C tools require both EMF2SVG_REF_PATH (an executable wrapper) and
EMF2SVG_REF_LIB_DIR (its library directory), resolved only by the spec support
Emfsvg::Compat helper. Neither has a machine-specific default. The live fidelity
spec is opt-in with EMFSVG_LIVE_REFERENCE=1; see the fixture setup for commands
and the verified current reference build recipe.
Developer scripts
|
Lossy EMF→SVG diagnostic against the explicitly configured live C reference |
|
Lossy SVG→EMF→SVG scan of the verified artifact inventory |
|
Element-by-element diff between input and round-trip output |
|
Per-fixture failure reasons for round-trip scan |
|
Stage both baseline sets and report drift without changing committed artifacts |
|
Promote both sets and manifest digests after verified C provenance checks |
|
Diagnostic matrix against the explicitly configured live C reference |
Regeneration requires the verified corpus and explicit C setup. --write also
requires a verified reference status, exact source revision, build recipe, and
matching executable checksum in the manifest. Review artifact and manifest
diffs before committing. See the regeneration
procedure for promotion safeguards and limits.
Resource calibration qualification
The resource-calibration command is gated by production qualification. The
repository carries a reviewed ledger for 21 corrupted inputs in
spec/fixtures/reviewed_corrupt_outcomes.json. Those independently reviewed
expectations define the outcome kind, exact ordered warnings, and exception
details where applicable; an SVG digest is an optional additional constraint.
A reviewed expectation is not a runtime observation. A ledger entry counted as
contracted means its review requirements are complete, while matched means
an actual observation also passed the contract. The qualification gate runs the
public conversion for the pinned 9 valid controls and all 21 corrupted inputs
through Ruby, native, and auto-selected-native backends before resource
calibration can proceed.
Qualification requires explicit artifact paths through EMF_RELEASE_PACKAGE
(the exact released emf .gem whose SHA-256 and version match the manifest),
EMF_NATIVE_LIBRARY (the native artifact matching its manifest SHA-256), and
EMF_NATIVE_PROVENANCE (JSON whose artifact_sha256, source_revision, and
abi exactly match the native-library provenance in the manifest). The gate
also checks the installed runtime against the release archive, native selection,
input identities, and evidence for drift. Missing, mismatched, or incomplete
metadata and artifacts fail closed before samples run.
After the release and native artifacts and the manifest are qualified, run the verification mode with explicit host safety caps:
EMF_RELEASE_PACKAGE=/absolute/path/emf-version.gem \
EMF_NATIVE_LIBRARY=/absolute/path/libemf.so \
EMF_NATIVE_PROVENANCE=/absolute/path/native-provenance.json \
bundle exec ruby scripts/calibrate_resource_limits.rb check \
--manifest spec/fixtures/parity_manifest.yml \
--fixture-root "$EMF_FIXTURE_ROOT" \
--outer-rss-cap-mib "$EMFSVG_CALIBRATION_RSS_CAP_MIB" \
--outer-as-cap-mib "$EMFSVG_CALIBRATION_AS_CAP_MIB" \
--timeout-seconds "$EMFSVG_CALIBRATION_TIMEOUT_SECONDS" \
--baseline spec/fixtures/resource_limits/emfsvg-v1.ymlThis checkout is not qualified: its manifest marks the runtime unqualified,
identifies development revision 99a8c21256cbe902e243cc0bcac65b282ec52bf4
without a released package digest, marks the native artifact unavailable, and
has not pinned the diagnostics and resources contract revisions. The command
therefore refuses with the current evidence. A reviewed ledger, passing
synthetic tests, or a current-source run does not establish released-package,
native-artifact, whole-corpus parity, or platform qualification. See
the compatibility matrix for detailed
contracts and remaining evidence.
Dependencies
-
emf(~> 0.1) — EMF binary parser, domain model, and serializer -
leptris(>= 1.9.365.1) — SVG parser -
fontisan— cmap parsing for glyph-index text -
libpng(~> 1.6) — PNG encode/decode for image round-trip -
base64,bigdecimal— stdlib
License
BSD-2-Clause. See LICENSE.txt.