Skip to content

About

A zero-dependency TypeScript library that converts EMF (Enhanced Metafile) and WMF (Windows Metafile) binary buffers into PNG data URLs by parsing their record streams and replaying drawing commands onto an HTML Canvas.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Repository files navigation

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, JPEG, WebP, SVG (markup, a base64 data URL, React elements, or a generated JSX/TSX component) or PDF. The input can be an ArrayBuffer, a typed array or Node.js Buffer, or a Blob, plain or gzip-compressed (.emz, .wmz).

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


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, JPEG 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 for PNG, JPEG and WebP. 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 and PDF output, and PNG/JPEG/WebP output for drawings without text, work out of the box through the built-in rasteriser. For PNG/JPEG/WebP 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/JPEG/WebP 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).

For the bytes themselves, use convertMetafile. It takes a format ('png' by default, 'jpeg', 'webp' or 'svg') and returns the encoded file, which is what you write to disk in Node.js:

import { readFile, writeFile } from 'node:fs/promises';
import { convertMetafile } from 'emf-converter';

const image = await convertMetafile(await readFile('drawing.emf'), { format: 'webp' });
if (image) {
	await writeFile('drawing.webp', image.data); // image.data is a Uint8Array
}

The data-URL functions are wrappers around convertMetafile, so both give the same image.

JPEG output

import { convertMetafileToJpegDataUrl } from 'emf-converter';

const jpeg = await convertMetafileToJpegDataUrl(buffer);
// => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."

// Quality 0-1 (default 0.92), the colour under transparent areas (default white),
// plus every PNG option (maxWidth, dpiScale, fonts, ...).
const preview = await convertMetafileToJpegDataUrl(buffer, { quality: 0.7, background: '#f4f4f4', maxWidth: 800 });

The same rendering as the PNG, encoded by the bundled baseline JPEG encoder, so the output is identical in browsers and Node.js. JPEG has no transparency: the metafile's transparent background is filled with background. Colour is kept at full resolution (4:4:4) so coloured lines and text stay crisp; chromaSubsampling: true halves it (4:2:0) for a smaller file.

WebP output

import { convertMetafileToWebpDataUrl } from 'emf-converter';

const webp = await convertMetafileToWebpDataUrl(buffer);
// => "data:image/webp;base64,UklGR..."

// Lossy WebP at quality 0-1 (default 0.8), plus every PNG option (maxWidth, dpiScale, fonts, ...).
const small = await convertMetafileToWebpDataUrl(buffer, { lossless: false, quality: 0.8, maxWidth: 800 });

The same rendering as the PNG, encoded by the bundled WebP encoder, so the output is identical in browsers and Node.js. The default is lossless: the pixels and the transparent background are kept exactly, and the file is usually smaller than the PNG. Lossy WebP (lossless: false) comes from a simple encoder. For line art and text it is usually much larger than lossless, so use it only for smooth, photo-like content. Images larger than 16383 pixels on a side return null.

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; arithmetic-coded and CMYK/YCCK JPEG are decoded by the bundled Windows-matching decoder and embedded as pixels, and a 12-bit JPEG draws nothing, as in Windows). 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 (up to 8 vertically, 5 horizontally). The size depends on the faces supplied: pass 8514sys.fon and 8514fix.fon along with vgasys.fon and vgafix.fon to get the System and Fixedsys sizes a 120 dpi or larger session draws.
  • Rotated text uses GDI's rounded font matrix. Text from a compatible-mode metafile replays the recorded Dx array and the slight horizontal stretch (about 1.0006) Windows playback applies, which re-grid-fits a few Segoe UI heights and changes the rotation matrix of rotated glyphs. 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.

PDF output

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

const fonts = await loadSystemFonts(); // Node.js only
const pdf = await convertMetafileToPdf(buffer, { fonts, title: 'Drawing' });
// => Uint8Array holding a PDF file, or null

The output is a one-page vector PDF 1.7. It uses the same replay as the SVG output, so paths, clipping, gradients, patterns, opacity, blend modes and images are kept as vectors or embedded (JPEG bytes are copied unchanged). The page is the drawing itself by default (1 pixel = 0.75 pt); pageSize: { width, height } in points fits it to a page instead.

Text is real text. With fonts, the matching TrueType face is embedded as a subset, so the text is selectable and searchable. Without fonts, text uses the standard PDF fonts (Helvetica, Times or Courier) and looks different from the original font. The output does not include PDF/A or tagged PDF. It has been checked with pdf.js.

Raw RGBA pixels

import { convertMetafileToImageData } from 'emf-converter';

const image = await convertMetafileToImageData(buffer);
if (image) {
	const { data, width, height } = image; // straight (non-premultiplied) RGBA, row-major, top-down
	// e.g. sharp(data, { raw: { width, height, channels: 4 } }).png().toFile('drawing.png');
}

This is the same rendering the PNG output encodes, without the encoding step, for code that needs the pixels themselves.

Inspecting a file

getMetafileInfo reads the header and records without rendering anything, so you can see what a file contains before converting it:

import { getMetafileInfo } from 'emf-converter';

const info = await getMetafileInfo(buffer);
// info.format: 'emf' | 'emf+' | 'emf+dual' | 'wmf'
// info.width, info.height: the PNG size at default options
// info.fonts: [{ name: 'Segoe UI', gdi: true, emfPlus: false }, ...]

The main use is finding out which font files to pass as fonts: info.fonts lists the face names the file declares, so you know which fonts to supply for exact text. The list is of declared fonts, and not every one is necessarily drawn with. Fonts inside nested WMFs are not collected. The width and height ignore maxWidth and dpiScale. See the API reference for every field.

Cancellation

Every conversion function accepts signal (an AbortSignal) and timeoutMs:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
	const png = await convertMetafileToDataUrl(buffer, { signal: controller.signal });
} finally {
	clearTimeout(timer);
}

// Or reject after a time limit, with a DOMException named TimeoutError.
const quick = await convertMetafileToDataUrl(buffer, { timeoutMs: 5000 });

When the signal aborts, the promise rejects with signal.reason (an AbortError when there is none). A null result still means the buffer is not a valid metafile. Conversion checks the signal at every record, so it stops within about one record's work. One very large record, such as a single giant blit, can overrun the limit. With a signal, conversion yields to the event loop about every 16 ms.

Command line

The package installs an emf-converter command, which npx emf-converter runs:

Usage: emf-converter <files...> [options]

Convert EMF, EMF+, WMF and gzip-compressed (.emz, .wmz) metafiles to PNG,
JPEG, WebP, SVG or PDF.

Inputs:
  <files...>            Files to convert. "-" reads one file from stdin.
                        Wildcards (* and ?) are expanded by the CLI itself.

Options:
  -f, --format <fmt>    Output format: png, jpeg, webp, svg or pdf. Default:
                        from the --out extension, else png.
  -o, --out <path>      Output file for one input, or a directory (created if
                        missing) for several. Default: next to each input,
                        with the new extension. "-" writes to stdout.
                        Stdin input without --out also writes to stdout.
      --width <px>      Maximum output width in pixels.
      --height <px>     Maximum output height in pixels.
      --scale <n>       DPI scale factor (default 1; the library clamps to 4).
      --quality <0-1>   JPEG quality (default 0.92), or lossy WebP quality
                        (default 0.8).
      --lossy           Lossy WebP instead of the default lossless WebP.
      --background <c>  JPEG colour under transparent areas, #rgb or #rrggbb
                        (default #ffffff). JPEG only.
      --antialias       Smooth shape edges instead of reproducing Windows'
                        own rasterisation.
      --fonts <value>   "system" (default): the installed fonts, so text
                        matches Windows; "none": no font files; or a
                        directory of .ttf, .ttc, .fon or .fnt files.
  -q, --quiet           Print only errors.
  -h, --help            Show this help.
  -v, --version         Print the version.

Exit codes: 0 all files converted, 1 one or more files failed (the rest are
still converted), 2 usage error.

Examples:
  emf-converter drawing.emf
  emf-converter drawing.emf --width 1200 -o drawing.jpg
  emf-converter "charts/*.wmf" -f svg -o out/
  cat drawing.emz | emf-converter - -f png > drawing.png

The --fonts system default loads the installed fonts, so text matches Windows. Wildcards are expanded by the CLI itself (Node.js 22 or later), and a file named explicitly is converted whatever its extension.

API

MetafileInput

type MetafileInput = ArrayBuffer | ArrayBufferView | Blob;

Every conversion function accepts this. Typed arrays, DataViews and Node.js Buffers are read from their viewed bytes only, so pass the Buffer itself. Gzip-compressed .emz and .wmz files are inflated automatically (needs DecompressionStream: browsers and Node.js 18+; inflated size is capped at 256 MiB).

convertMetafile(input, options?)

Parameter Type Description
input MetafileInput Raw EMF or WMF file (format is auto-detected)
options ConvertOptions (optional) format ('png' default, 'jpeg', 'webp', 'svg') plus that format's options
Returns Promise<ConvertedImage | null> The encoded file, or null on failure

ConvertedImage

Field Type Description
format 'png' | 'jpeg' | 'webp' | 'svg' The output format
mimeType string image/png, image/jpeg, image/webp or image/svg+xml
data Uint8Array The file bytes
width number Pixel width, or the SVG's width
height number Pixel height, or the SVG's height

convertMetafileToDataUrl(buffer, options?)

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

EmfConvertOptions

ansiCodePage and oemCodePage set the known code pages of the playback device (for example, 936 from Windows GetACP() and 866 from GetOEMCP()). Windows decodes ANSI text records (EMF and WMF) in ANSI/DEFAULT fonts with the ANSI code page, WMF font face names too, and text in OEM fonts with the OEM code page. The defaults are 1252 and 437; explicit text charsets are unchanged. wmfAnsiCodePage overrides ansiCodePage for WMF input only. See the API reference for supported values.

Field Type Default Description
maxWidth number None Maximum output width in pixels (aspect ratio preserved)
ansiCodePage number 1252 Playback-device ANSI code page (GetACP()) for ANSI/DEFAULT text
oemCodePage number 437 Playback-device OEM code page (GetOEMCP()) for OEM text
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' }
signal AbortSignal None Cancels the conversion: the promise rejects with signal.reason when it aborts (see Cancellation)
timeoutMs number None Rejects with a DOMException named TimeoutError after this many milliseconds

convertMetafileToJpegDataUrl(buffer, options?)

Returns Promise<string | null>, a data:image/jpeg;base64,... URL (null in the same cases as the PNG function).

JpegConvertOptions (extends EmfConvertOptions)

Field Type Default Description
quality number 0.92 JPEG quality from 0 to 1, as for canvas.toDataURL('image/jpeg', quality)
background string '#ffffff' Colour under transparent pixels, #rgb or #rrggbb (other values fall back to white)
chromaSubsampling boolean false true stores colour at half resolution (4:2:0) for a smaller file

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

convertMetafileToWebpDataUrl(buffer, options?)

Returns Promise<string | null>, a data:image/webp;base64,... URL (null in the same cases as the PNG function, and for images larger than 16383 pixels on a side).

WebpConvertOptions (extends EmfConvertOptions)

Field Type Default Description
lossless boolean true true writes lossless WebP: exact pixels, transparency kept. false writes lossy WebP at quality (transparency is still kept).
quality number 0.8 Lossy quality from 0 to 1. Ignored when lossless is true.

convertMetafileToPdf(buffer, options?)

function convertMetafileToPdf(buffer: MetafileInput, options?: PdfConvertOptions): Promise<Uint8Array | null>;

Returns the bytes of a one-page vector PDF, or null for an invalid or empty metafile. svgTreeToPdf(tree, options?) makes the same PDF from an SvgNode tree.

PdfConvertOptions (extends SvgConvertOptions, plus PdfOptions)

Field Type Default Description
pageSize 'drawing' | { width: number; height: number } 'drawing' 'drawing' makes the page the drawing itself (1 pixel = 0.75 pt). { width, height } in points fits the drawing to a page of that size, centred.
fonts Array<ArrayBuffer | ArrayBufferView> None TrueType (.ttf, .ttc) files. Text in a matching font is embedded as a subset of that face. Other text uses the standard PDF fonts.
compress boolean true Deflate the content streams and image data.
title string None Document title in the PDF information dictionary.
decodeImage (bytes: Uint8Array, mime: string) => PdfDecodedImage | null (may be async) None Decodes an embedded image format that is neither JPEG nor handled by the bundled decoders (PNG, BMP, GIF, TIFF, WebP). Returns straight RGBA, or null to skip the image.

convertMetafileToImageData(buffer, options?)

function convertMetafileToImageData(buffer: MetafileInput, options?: EmfConvertOptions): Promise<RgbaImage | null>;

Returns { data: Uint8ClampedArray, width, height }: straight (non-premultiplied) RGBA, row-major and top-down, like ImageData. It is the rendering the PNG output encodes, without the encoding step.

getMetafileInfo(buffer, options?)

function getMetafileInfo(buffer: MetafileInput, options?: MetafileInfoOptions): Promise<MetafileInfo | null>;

Reads the header and records without rendering, and returns null for an invalid metafile.

MetafileInfoOptions

Option Type Default Description
maxRecords number As for the converters Record limit per stream.
ansiCodePage number 1252 ANSI code page for WMF face names, as for the converters.

MetafileInfo

Field Type Description
format 'emf' | 'emf+' | 'emf+dual' | 'wmf' 'emf+' is EMF+ only (the GDI records are a stub); 'emf+dual' has a GDI fallback.
bounds MetafileRect EMF: the header's device-pixel bounds (inclusive). WMF: the bounding box in logical units.
frame MetafileRect | null EMF: the picture frame in .01 mm. Placeable WMF: the bounding box in .01 mm.
device { width, height } | null EMF: the reference device size in pixels.
dpi { x, y } Reference device resolution (96 when unknown).
width / height number Pixel size convertMetafileToDataUrl produces at default options.
recordCount number Records read from the stream.
emfPlusRecordCount number Records read from the EMF+ streams inside EMR_COMMENT records.
recordTypes Record<string, number> Count by record name (EMR_*, META_*, EMFPLUS_*).
fonts MetafileFont[] Declared font faces: { name, gdi, emfPlus }. Not every one is necessarily drawn with.
hasText boolean true when any text-drawing record is present.
images { count, mimeTypes } Embedded images, including DIBs, EMF+ images and nested metafiles.
embeddedMetafiles number Nested metafiles among images.
description string | null The EMF header's application and document names.
truncated boolean true when the record limit was hit or the stream ends in a damaged record.

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. Records are replayed by the GDI, EMF+ or WMF handlers onto a Canvas (or the built-in pure-JavaScript rasteriser) for PNG, JPEG and WebP, or onto a recording context for SVG and PDF. GDI and GDI+ shapes, raster operations, brushes, clipping, text and WMF playback are all fitted to output painted by Windows itself.

See How it works for the pipeline, the Windows-parity details and the full list of supported records.

Limitations

Output is tested pixel for pixel against what Windows itself paints: 899 of 910 test pictures match exactly. The rest differ by a few pixels, all in ClearType and EMF+ text. Text matches Windows only when the metafile's fonts are passed in fonts.

See Limitations for the list, Parity notes for the measurements and Outstanding work for what is still open.

License

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

About

A zero-dependency TypeScript library that converts EMF (Enhanced Metafile) and WMF (Windows Metafile) binary buffers into PNG data URLs by parsing their record streams and replaying drawing commands onto an HTML Canvas.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages