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

  • EMF → SVG: 208/208 fixtures (100%) semantically match libemf2svg. Verified with the lossy SvgMatcher at 0.1px tolerance. 79/208 are byte-identical to libemf2svg’s output.

  • SVG → EMF: functional for shapes, transforms, clips, text, and images. Synthetic SVGs round-trip byte-equal through SVG → EMF → SVG. The lossy matcher and golden-master tooling are in place for iterative fidelity improvement.

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

Emf::Model::MetafileEmfsvg::RendererEmfsvg::Visitors::EmrVisitorEmfsvg::SvgBuilder

SVG → EMF

SVG bytes → Nokogiri → Emfsvg::Svg::DocumentEmfsvg::Translation::EmfRendererEmf::Model::MetafileEmf.serialize

Both directions share:

  • Emfsvg::DeviceContext / ObjectTable / TransformStack — GDI state

  • Emf::* — the emf gem’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 000.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
bundle exec rspec
bundle exec rubocop

Developer scripts

scripts/run_emf_svg_compare.rb [limit] [tolerance]

EMF→SVG fidelity vs libemf2svg (208 fixtures, 100% pass at 0.1px)

scripts/run_round_trip.rb [limit] [tolerance]

SVG→EMF→SVG round-trip scan

scripts/diff_fixture.rb <fixture.svg>

Element-by-element diff between input and round-trip output

scripts/diagnose_round_trip.rb [limit]

Per-fixture failure reasons for round-trip scan

scripts/regenerate_golden.rb

Regenerate libemf2svg + emfsvg golden baselines

scripts/run_compat_check.rb

Legacy compat matrix vs libemf2svg

Dependencies

  • emf (~> 0.1) — EMF binary parser, domain model, and serializer

  • nokogiri (~> 1.16) — 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.