npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

emf-converter

v4.8.18

Published

Convert EMF/WMF metafile binaries to PNG data URLs or SVG (markup, data URL, React/JSX)

Readme

emf-converter

npm version CI license

A zero-dependency TypeScript library that converts EMF (Enhanced Metafile, including embedded EMF+ / GDI+ records) and WMF (Windows Metafile) files into PNG or SVG (markup, a base64 data URL, React elements, or a generated JSX/TSX component).

Windows metafiles are recorded GDI and GDI+ drawing calls, commonly embedded in Office documents and on the Windows clipboard. This library replays those calls the way Windows does: the PNG output is checked pixel for pixel against images painted by Windows itself (hundreds of ground-truth fixtures under src/__fixtures__/gdi, generated by scripts/gdi-fixtures), and the SVG output keeps vectors, text and gradients resolution-independent.

| Format | Description | Coordinate system | | -------- | ------------------------------ | ----------------------- | | WMF | Windows Metafile (16-bit) | Window/viewport mapping | | EMF | Enhanced Metafile (32-bit GDI) | Bounds-based scaling | | EMF+ | GDI+ extension embedded in EMF | World transform matrix |

Documentation and live demo · npm


What's new

  • SVG output: convertMetafileToSvg, convertMetafileToSvgDataUrl, convertMetafileToSvgTree + svgTreeToReact / svgTreeToJsx for JSX/TSX.
  • Windows-exact PNG by default: GDI shapes are drawn by a rasteriser fitted to Windows GDI (28.4 fixed-point geometry, GDI's fill rule, line algorithm, ellipse and Bezier construction, wide pens, dash styles), and EMF+ drawing follows the file's recorded GDI+ SmoothingMode with GDI+'s own rasteriser. Breaking: default PNG output is no longer Canvas-antialiased; pass gdiAntialias: true for the previous smooth edges.
  • Exact text with the fonts option: a built-in TrueType engine (hinting interpreter, dropout control, GDI font mapping and metrics, grayscale and ClearType) plus Windows raster .fon fonts. loadSystemFonts() reads the installed fonts in Node.js.
  • No canvas required: a built-in pure-JavaScript rasteriser renders SVG anywhere and PNG for drawings without text; @napi-rs/canvas is only needed for PNG output with text in plain Node.js.
  • Complete WMF playback: bitmaps, clipping, regions, mapping modes, palettes, flood fills, and embedded EMF comments, played as Windows' PlayMetaFile plays them.
  • Many correctness fixes found by the new fixtures (see the changelog).

Documentation and demo

The documentation site at https://christophervr.github.io/emf-converter/ includes a live demo: drop an .emf or .wmf file to see the PNG or SVG output, download it, or copy it as a TSX component.

The site is built with VitePress from the docs/ directory. Run it locally with bun run docs:dev.

Install

npm install emf-converter

No required dependencies:

  • Browser / Web Worker: OffscreenCanvas or HTMLCanvasElement is used automatically. Bundlers select the dedicated browser entry through conditional exports; it contains no native canvas or Node filesystem imports. Installing @napi-rs/canvas is only needed for the optional Node backend.

  • Node.js: SVG output, and PNG output for drawings without text, work out of the box through the built-in rasteriser. For PNG output of drawings with text, either pass fonts (see Exact text) or install the optional @napi-rs/canvas (prebuilt, no node-gyp):

    npm install @napi-rs/canvas

    Without either, PNG conversion of a drawing that contains text returns null rather than an image missing its text.

Quick start

import { convertMetafileToDataUrl } from 'emf-converter';

const buffer: ArrayBuffer = /* an .emf or .wmf file */;
const png = await convertMetafileToDataUrl(buffer);
// => "data:image/png;base64,iVBORw0KGgo..."  (the format is auto-detected)

// Limit the output size (aspect ratio preserved), or render at 2x.
const thumb = await convertMetafileToDataUrl(buffer, { maxWidth: 1024, maxHeight: 768 });
const hiDpi = await convertMetafileToDataUrl(buffer, { dpiScale: 2 });

// Smooth (Canvas-antialiased) edges instead of Windows' own rasterisation.
const smooth = await convertMetafileToDataUrl(buffer, { gdiAntialias: true });

Returns Promise<string | null>; null when the buffer is not a valid metafile (or, in plain Node.js, when it has text and neither fonts nor @napi-rs/canvas is available).

SVG output

import { convertMetafileToSvg, convertMetafileToSvgDataUrl } from 'emf-converter';

const markup = await convertMetafileToSvg(buffer);
// => '<svg xmlns="http://www.w3.org/2000/svg" width="..." height="..." viewBox="...">...</svg>'

const svgUrl = await convertMetafileToSvgDataUrl(buffer);
// => "data:image/svg+xml;base64,PHN2ZyB4bWxucz0i..."  (drop straight into <img src>)

Paths, text, gradients, clipping and pattern brushes stay vectors; bitmaps are embedded as <image> elements (PNG/JPEG/GIF/WebP bytes verbatim, never re-encoded). Raster operations that read the destination (all 256 ROP3 codes, bitwise ROP2, pattern brushes through ROP2) are evaluated exactly against a hidden raster mirror and embedded as image patches holding only the pixels they change, so the SVG is the same with or without a canvas backend.

Rendering in React (JSX / TSX)

convertMetafileToSvgTree returns a plain SvgNode tree. Turn it into live elements with any createElement-style factory (React, Preact, ...), so the SVG is part of your component tree and can be styled, sized and given props like any other element:

import { createElement, useEffect, useState, type ReactNode } from 'react';
import { convertMetafileToSvgTree, svgTreeToReact } from 'emf-converter';

export function Metafile({ buffer }: { buffer: ArrayBuffer }) {
	const [svg, setSvg] = useState<ReactNode>(null);
	useEffect(() => {
		let live = true;
		convertMetafileToSvgTree(buffer).then((tree) => {
			if (live && tree) {
				// Extra props land on the root <svg>: override size, add a class, aria, ...
				setSvg(svgTreeToReact(tree, createElement, { width: '100%', height: 'auto', role: 'img' }));
			}
		});
		return () => {
			live = false;
		};
	}, [buffer]);
	return svg;
}

Or generate a component at build time (the SVGR approach):

import { writeFileSync } from 'node:fs';
import { convertMetafileToSvgTree, svgTreeToJsx } from 'emf-converter';

const tree = await convertMetafileToSvgTree(buffer, { idPrefix: 'logo-' });
writeFileSync('Logo.tsx', svgTreeToJsx(tree!, { componentName: 'Logo' }));
// export function Logo(props: SVGProps<SVGSVGElement>) { return (<svg ... {...props}> ... </svg>); }

Attribute names are converted to React's spelling (stroke-width → strokeWidth, clip-path → clipPath, style strings → style objects). Strings that come from the metafile (font names, text) are always emitted as escaped JavaScript string literals in generated source, never spliced into JSX raw. When several converted SVGs are inlined in one page, give each its own idPrefix so their clip-path and gradient ids cannot collide (a unique prefix per conversion is the default).

Exact text

Text is only as exact as the fonts it is drawn with. Pass the font files the metafile uses as fonts (TrueType .ttf/.ttc and Windows raster .fon/.fnt), and text is drawn the way Windows GDI draws it:

import { convertMetafileToDataUrl, loadSystemFonts } from 'emf-converter';

const fonts = await loadSystemFonts(); // Node.js only; reuse the array across conversions
const png = await convertMetafileToDataUrl(buffer, { fonts });
  • Fonts are realised the way GDI's font mapper does it (face substitutes, pitch/family fallback, weight choice, cell vs em height, lfWidth stretching), with GDI's metrics, advances, underline and strike-out.
  • Glyphs are grid-fitted by the font's own TrueType instructions (including Windows' ClearType rules), scan-converted with dropout control, and placed on GDI's integer grid honouring Dx arrays, ETO_* flags and every TA_* alignment.
  • Non-antialiased, grayscale or ClearType rendering is chosen from the font's quality; fontSmoothing sets what DEFAULT_QUALITY means (Windows' default is ClearType).
  • Raster faces (MS Sans Serif, MS Serif, Courier, Small Fonts, System, Terminal, Fixedsys, Helv, Tms Rmn) are drawn from their bitmaps with GDI's size choice and stretching.
  • Rotated text uses GDI's rounded font matrix; EMF+ DrawString honours the text rendering hint, string-format tracking and margins, and texture/gradient brushes.

Without fonts, text is drawn by the host's canvas font engine (supply fontFamilyMap to remap Windows face names). SVG output always keeps text as <text>; with fonts it carries GDI's exact per-glyph positions.

API

convertMetafileToDataUrl(buffer, options?)

| Parameter | Type | Description | | ----------- | ------------------------------ | --------------------------------------------------- | | buffer | ArrayBuffer | Raw EMF or WMF file bytes (format is auto-detected) | | options | EmfConvertOptions (optional) | See below | | Returns | Promise<string \| null> | PNG data URL, or null on failure |

EmfConvertOptions

| Field | Type | Default | Description | | -------------------- | ------------------------------------- | ----------------- | ----------- | | maxWidth | number | None | Maximum output width in pixels (aspect ratio preserved) | | maxHeight | number | None | Maximum output height in pixels | | dpiScale | number | 1 | Resolution multiplier; clamped to 4 | | maxCanvasDimension | number | 8192 | Hard cap on output width/height in pixels | | maxRecords | number | 200000/500000 | Records processed per stream before replay stops (EMF+ uses the higher default unless overridden) | | gdiAntialias | boolean | false (PNG) | true smooths every shape edge with Canvas antialiasing instead of reproducing Windows' own GDI/GDI+ rasterisation | | fonts | Array<ArrayBuffer \| ArrayBufferView> | None | TrueType (.ttf/.ttc) and raster (.fon/.fnt) font files for exact GDI text | | fontSmoothing | 'cleartype' \| 'gray' \| 'mono' | 'cleartype' | What DEFAULT_QUALITY / DRAFT_QUALITY / PROOF_QUALITY fonts render as (Windows' system setting) | | fontFamilyMap | Record<string, string> | None | Without fonts: maps Windows face names (case-insensitive) to locally available fonts, e.g. { calibri: 'Carlito' } |

SVG functions

| Function | Returns | | --- | --- | | convertMetafileToSvg(buffer, options?) | Promise<string \| null>, standalone SVG markup | | convertMetafileToSvgDataUrl(buffer, options?) | Promise<string \| null>, a data:image/svg+xml;base64,... URL | | convertMetafileToSvgTree(buffer, options?) | Promise<SvgNode \| null>, the tree the helpers below consume | | svgTreeToString(tree) / svgTreeToDataUrl(tree) | Markup / base64 data URL for an existing tree | | svgTreeToReact(tree, createElement, rootProps?) | Live elements via React.createElement (or any compatible factory) | | svgTreeToJsx(tree, { componentName?, typescript?, spreadProps? }) | JSX/TSX component source code |

SvgConvertOptions (extends EmfConvertOptions)

| Field | Type | Default | Description | | --- | --- | --- | --- | | gdiAntialias | boolean | true (SVG) | false embeds Windows' aliased GDI shape pixels as image patches instead of smooth vector edges | | exactRasterOps | boolean | true | false skips the raster mirror and expresses destination-reading raster ops with SVG mix-blend-mode equivalents | | imageResampling | 'renderer' \| 'exact' | 'renderer' | 'exact' bakes EMF+ DrawImage at device resolution with GDI+'s resampling kernel instead of letting the SVG renderer scale the original image | | includeSize | boolean | true | Emit width/height on the root <svg> (viewBox is always emitted); false gives a fluid SVG | | idPrefix | string | emf1-, emf2-, ... | Prefix for generated element ids; keep it unique per inlined SVG |

loadSystemFonts(options?)

Node.js only (returns [] elsewhere; the package stays browser-safe). Reads the installed .ttf, .ttc, .fon and .fnt files from the platform font folders (Windows, Linux, macOS, and per-user folders) for the fonts option. Options: dirs (scan these instead), filter(path, name), maxDepth.

How it works

A three-phase pipeline: parse → replay → export. The header parser reads the drawing bounds (a placeable WMF is sized from its header's units per inch), the output surface is created (clamped to maxCanvasDimension), and the records are replayed in order by the GDI, EMF+ or WMF handlers. PNG output draws onto a Canvas (OffscreenCanvas, HTMLCanvasElement, @napi-rs/canvas, or the built-in pure-JavaScript rasteriser); SVG output draws onto SvgContext, a recorder implementing the part of the Canvas 2D API the replay uses, mirrored onto a hidden raster wherever a raster operation must read the destination.

Everything below is verified against output painted by Windows itself; src/gdi-parity.fixture.test.ts holds the per-fixture bounds.

  • GDI shapes (gdi-raster.ts, gdi-raster-widen.ts): 28.4 fixed-point geometry; GDI's fill rule (ALTERNATE/WINDING); one-pixel lines by GDI's diamond rule with its tie-breaks; GDI's own Bezier flattener, ellipse, rounded-rectangle, arc (GDI's trigonometry table and SetArcDirection), chord and pie construction; cosmetic dash styles (dash 18/6, dot 3/3, ...) and geometric dashes; wide pens widened from GDI's own pen polygons with every cap and join and the miter limit; rotated and skewed world transforms. Pixel-exact on the shape fixtures. A path bracket's GDI geometry has the points, point types, figure starts and direction that Windows' GetPath reports (checked against the Windows data in Wine's gdi32 path tests).
  • Raster operations: all 256 ROP3 codes for BitBlt/StretchBlt/StretchDIBits/PatBlt, exact per bit against the destination, brush and source, with GDI's stretch modes, mirrored rectangles and rotated destinations (each device pixel mapped back to one source texel). Every SetROP2 mode, including the bitwise AND/OR/XOR family, for shapes, paths and pattern-brush fills.
  • Brushes: hatch, monochrome and DIB pattern brushes anchored to the brush origin, with the background mode; GDI+ solid, hatch, texture (bilinear, WrapMode-aware, as GDI+ samples them), linear gradients (GDI+'s own interpolation table: preset colours, blend shapes, gamma correction, every WrapMode) and path gradients (true boundary-shaped falloff, every WrapMode).
  • Clipping: every GDI and GDI+ region combine mode exact for every clip (vector where possible, otherwise scan-converted to pixel regions, which is how GDI stores them), path clips with their fill mode, and region offsets.
  • EMF+: fills, pens and clips follow the recorded SmoothingMode with GDI+'s own fill rasteriser (8 x 4-sample antialiasing, blend arithmetic) and pen widener (joins, caps, dash caps, compound lines, inset alignment); DrawImage with every InterpolationMode/PixelOffsetMode kernel, ImageAttributes wrap modes, drawn in record order under the live clip; embedded metafiles replayed as vectors; continuation records reassembled; compressed textures and images decoded before replay.
  • Text: see Exact text.
  • WMF: PS_INSIDEFRAME half-pixel width sweeps and mirrored line fixtures match Windows exactly; scaled shapes retain a small residual. Metric map modes default to a 96 dpi reference device; wmfReferenceDpi accepts a scalar or { x, y } to reproduce another device.

Supported records

  • EMF: Record playback includes logical palettes (PALETTEINDEX, DIBPALETTEINDEX, PALETTERGB, DIB_PAL_COLORS), EMR_ALPHABLEND (Windows' exact integer blend), EMR_TRANSPARENTBLT, EMR_MASKBLT, EMR_PLGBLT, EMR_SETDIBITSTODEVICE, EMR_GRADIENTFILL (rectangles and triangles), EMR_FILLRGN / EMR_FRAMERGN / EMR_INVERTRGN / EMR_PAINTRGN, EMR_EXTFLOODFILL, EMR_ANGLEARC, EMR_POLYDRAW(16), EMR_FLATTENPATH / EMR_WIDENPATH / EMR_ABORTPATH, alongside shapes, paths, EMR_EXTTEXTOUTA/W, EMR_POLYTEXTOUTA/W, EMR_SMALLTEXTOUT, text justification, blits, clipping and transforms. Colour space, ICM, OpenGL, escape and font-driver records are consumed without effect, as on a Windows display.
  • EMF+: every record in MS-EMFPLUS (Beziers, cardinal curves, regions, containers, save/restore, compositing mode, rendering origin, text contrast, StrokeFillPath, the terminal-server SetTSGraphics / SetTSClip, MultiFormat* played as GDI+ plays them), with 32-bit, compressed 16-bit and relative point data, and every object type (solid, hatch, texture and gradient brushes; pens with caps, joins, dash styles, dash caps, compound lines and custom line caps; paths, regions, bitmap and metafile images, fonts, string formats, image attributes). Several encodings follow what GDI+ actually does where it differs from MS-EMFPLUS (relative points, StrokeFillPath, SetTSGraphics, SetTSClip).
  • WMF: META_ANIMATEPALETTE, META_ARC, META_BITBLT, META_CHORD, META_CREATEBITMAP, META_CREATEBITMAPINDIRECT, META_CREATEBRUSH, META_CREATEBRUSHINDIRECT, META_CREATEFONTINDIRECT, META_CREATEPALETTE, META_CREATEPATTERNBRUSH, META_CREATEPENINDIRECT, META_CREATEREGION, META_DELETEOBJECT, META_DIBBITBLT, META_DIBCREATEPATTERNBRUSH, META_DIBSTRETCHBLT, META_ELLIPSE, META_EOF, META_ESCAPE, META_EXCLUDECLIPRECT, META_EXTFLOODFILL, META_EXTTEXTOUT, META_FILLREGION, META_FLOODFILL, META_FRAMEREGION, META_INTERSECTCLIPRECT, META_INVERTREGION, META_LINETO, META_MOVETO, META_OFFSETCLIPRGN, META_OFFSETVIEWPORTORG, META_OFFSETWINDOWORG, META_PAINTREGION, META_PATBLT, META_PIE, META_POLYGON, META_POLYLINE, META_POLYPOLYGON, META_REALIZEPALETTE, META_RECTANGLE, META_RESIZEPALETTE, META_RESTOREDC, META_ROUNDRECT, META_SAVEDC, META_SCALEVIEWPORTEXT, META_SCALEWINDOWEXT, META_SELECTCLIPREGION, META_SELECTOBJECT, META_SELECTPALETTE, META_SETBKCOLOR, META_SETBKMODE, META_SETDIBTODEV, META_SETLAYOUT, META_SETMAPMODE, META_SETMAPPERFLAGS, META_SETPALENTRIES, META_SETPIXEL, META_SETPOLYFILLMODE, META_SETRELABS, META_SETROP2, META_SETSTRETCHBLTMODE, META_SETTEXTALIGN, META_SETTEXTCHAREXTRA, META_SETTEXTCOLOR, META_SETTEXTJUSTIFICATION, META_SETVIEWPORTEXT, META_SETVIEWPORTORG, META_SETWINDOWEXT, META_SETWINDOWORG, META_STRETCHBLT, META_STRETCHDIB, META_TEXTOUT. Where Windows no longer plays a record the way MS-WMF describes it (Win16 device bitmaps in META_BITBLT / META_STRETCHBLT, META_CREATEPATTERNBRUSH, banded META_SETDIBTODEV), the converter follows Windows.

Limitations

Everything is measured against output painted by Windows itself; src/gdi-parity.fixture.test.ts holds the exact per-fixture bounds. Open work items are tracked in docs/outstanding-work.md.

  • Colour adjustment and image effects: all eleven EMF+ image effects are supported. Complete native tables cover every legal ColorCurve intensity. Deferred draws retain their effects. JPEG, GIF and TIFF have bundled decoders for environments without Canvas. Blur, sharpen, HSL and tint match the native algorithm captures; red-eye and GDI colour adjustment retain measured differences. Passing playback tolerances does not always mean pixel-exact output; see the detailed limitations and parity priorities.
  • Pen transforms: EMF+ pen transforms (uniform, nonuniform and skewed) match GDI+ exactly on the 17 pen-transform fixtures, including dashes, which GDI+ lays out along the path in world space at the untransformed pen width. Native fixtures verify dashes under both uniform and non-uniform scales. GDI+ rejects singular pen transforms; the converter ignores such malformed transform data. Square, round, diamond and arrow anchor caps use their native shapes; the new centered-cap fixtures match every pixel with and without antialiasing.
  • Text: ANSI, multi-string and small-text records render with residuals under 0.1% on their record fixtures. The broader font-engine fixtures still have larger ClearType, EMF+ antialiasing and TextContrast differences. ANSI decoding uses the host's TextDecoder for common Windows code pages; Johab and OEM CP437 use bundled Windows mappings. Vertical ETO_PDY advances work with and without the fonts engine. Without fonts, SVG text measurements use deterministic estimates, so precise justification requires supplying the matching fonts.
  • Wide pens and paths: the 2,136 native wide-path fills and widened ellipse outlines, including their inner triangles, match exactly. Square-capped curves, line-to-curve joins, fractional transformed inside-frame geometry and nonuniform GDI pen nibs remain open. EMF+ 1-pixel antialiased lines can differ by one sample at their ends; additional closed-figure Inset and compound-pen captures are still needed.
  • GM_COMPATIBLE recordings: EMF files do not record the graphics mode, and Windows plays RoundRect, Arc, Chord, Pie and null-pen Ellipse records back differently from how a GM_COMPATIBLE application drew them on screen; the converter follows Windows' playback.
  • EMF+ sampling: rotated HighQualityBicubic DrawImage retains 0.013% of pixels beyond eight channel levels, with more widespread small sampling differences in interpolation and path gradients. Linear Blend tables and the nested-metafile scale/clip fixtures now match exactly.
  • WMF: inside-frame half-pixel width sweeps and mirrored LAYOUT_RTL line fixtures match exactly; scaled shapes retain seven pixels in the current fixture. Metric map modes default to a 96 dpi reference device; supply wmfReferenceDpi when the original resolution is known.

License

Apache-2.0, free for commercial and closed-source use, with an explicit patent grant.