emf-converter 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
  [![CI](https://github.com/ChristopherVR/emf-converter/actions/workflows/ci.yml/badge.svg)](https://github.com/ChristopherVR/emf-converter/actions/workflows/ci.yml)
5
5
  [![license](https://img.shields.io/npm/l/emf-converter.svg)](LICENSE)
6
6
 
7
- A zero-dependency TypeScript library that converts **EMF** (Enhanced Metafile) and **WMF** (Windows Metafile) binary buffers into **PNG data URLs** or **SVG** (markup, a base64 data URL, React elements, or a generated JSX/TSX component) by parsing their record streams and replaying the drawing commands.
7
+ 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).
8
8
 
9
- Windows Metafiles store a sequence of GDI drawing commands and are commonly embedded inside Office documents (Word, PowerPoint) and Windows clipboard data. This converter reads the raw binary, interprets each record, and replays the drawing operations either onto a Canvas to produce a rasterised PNG, or onto an SVG recorder that keeps vectors, text, and gradients resolution-independent. It handles three formats:
9
+ 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.
10
10
 
11
11
  | Format | Description | Coordinate system |
12
12
  | -------- | ------------------------------ | ----------------------- |
@@ -18,24 +18,18 @@ Windows Metafiles store a sequence of GDI drawing commands and are commonly embe
18
18
 
19
19
  ---
20
20
 
21
- ## Breaking change: `convertEmfToDataUrl` / `convertWmfToDataUrl` removed
21
+ ## What's new in 4.0
22
22
 
23
- Versions before 3.0.0 exported two functions, `convertEmfToDataUrl(buffer, options?)` and `convertWmfToDataUrl(buffer, options?)`. They have been replaced by a single auto-detecting function:
24
-
25
- ```diff
26
- -import { convertEmfToDataUrl, convertWmfToDataUrl } from 'emf-converter';
27
- -const emfPng = await convertEmfToDataUrl(emfBuffer);
28
- -const wmfPng = await convertWmfToDataUrl(wmfBuffer);
29
- +import { convertMetafileToDataUrl } from 'emf-converter';
30
- +const emfPng = await convertMetafileToDataUrl(emfBuffer);
31
- +const wmfPng = await convertMetafileToDataUrl(wmfBuffer);
32
- ```
33
-
34
- `convertMetafileToDataUrl` detects the format from the buffer itself, so the same call works for either. See "Quick start" below.
23
+ - **SVG output**: `convertMetafileToSvg`, `convertMetafileToSvgDataUrl`, `convertMetafileToSvgTree` + `svgTreeToReact` / `svgTreeToJsx` for JSX/TSX.
24
+ - **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.
25
+ - **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.
26
+ - **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.
27
+ - **Complete WMF playback**: bitmaps, clipping, regions, mapping modes, palettes, flood fills, and embedded EMF comments, played as Windows' `PlayMetaFile` plays them.
28
+ - Many correctness fixes found by the new fixtures (see the changelog).
35
29
 
36
30
  ## Demo
37
31
 
38
- Try it right in your browser: drop in an `.emf` or `.wmf` file and see the rendered PNG, conversion time, and output size:
32
+ Drop an `.emf` or `.wmf` file into the browser demo to see the PNG or SVG output, download it, or copy it as a TSX component:
39
33
 
40
34
  **https://christophervr.github.io/emf-converter/**
41
35
 
@@ -45,34 +39,35 @@ Try it right in your browser: drop in an `.emf` or `.wmf` file and see the rende
45
39
  npm install emf-converter
46
40
  ```
47
41
 
48
- No required dependencies. PNG output needs a Canvas API at runtime; SVG output needs none (it works anywhere JavaScript runs, and uses a canvas only when one is available, for exact destination-reading raster ops and text measurement):
42
+ No required dependencies:
49
43
 
50
- - **Browser / Web Worker**: nothing else to install, `OffscreenCanvas` or `HTMLCanvasElement` is used automatically.
51
- - **Node.js** (no DOM, no Worker): install the optional [`@napi-rs/canvas`](https://www.npmjs.com/package/@napi-rs/canvas) package as well:
44
+ - **Browser / Web Worker**: `OffscreenCanvas` or `HTMLCanvasElement` is used automatically.
45
+ - **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](#exact-text)) or install the optional [`@napi-rs/canvas`](https://www.npmjs.com/package/@napi-rs/canvas) (prebuilt, no `node-gyp`):
52
46
 
53
47
  ```bash
54
48
  npm install @napi-rs/canvas
55
49
  ```
56
50
 
57
- It's a prebuilt, Skia-based native module (no `node-gyp` required). When it's not installed, PNG conversion in plain Node.js returns `null` instead of throwing; SVG conversion still works.
51
+ Without either, PNG conversion of a drawing that contains text returns `null` rather than an image missing its text.
58
52
 
59
53
  ## Quick start
60
54
 
61
55
  ```typescript
62
56
  import { convertMetafileToDataUrl } from 'emf-converter';
63
57
 
64
- const emfBuffer: ArrayBuffer = /* loaded from file or network */;
65
- const pngDataUrl = await convertMetafileToDataUrl(emfBuffer);
66
- // => "data:image/png;base64,iVBORw0KGgo..."
58
+ const buffer: ArrayBuffer = /* an .emf or .wmf file */;
59
+ const png = await convertMetafileToDataUrl(buffer);
60
+ // => "data:image/png;base64,iVBORw0KGgo..." (the format is auto-detected)
67
61
 
68
- // Works the same for WMF, the format is auto-detected from the bytes.
69
- const wmfPng = await convertMetafileToDataUrl(wmfBuffer);
62
+ // Limit the output size (aspect ratio preserved), or render at 2x.
63
+ const thumb = await convertMetafileToDataUrl(buffer, { maxWidth: 1024, maxHeight: 768 });
64
+ const hiDpi = await convertMetafileToDataUrl(buffer, { dpiScale: 2 });
70
65
 
71
- // Optional: limit output dimensions (aspect ratio preserved)
72
- const scaled = await convertMetafileToDataUrl(emfBuffer, { maxWidth: 1024, maxHeight: 768 });
66
+ // Smooth (Canvas-antialiased) edges instead of Windows' own rasterisation.
67
+ const smooth = await convertMetafileToDataUrl(buffer, { gdiAntialias: true });
73
68
  ```
74
69
 
75
- Returns `Promise<string | null>`, `null` if the buffer is invalid or no Canvas API is available.
70
+ 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).
76
71
 
77
72
  ### SVG output
78
73
 
@@ -86,11 +81,11 @@ const svgUrl = await convertMetafileToSvgDataUrl(buffer);
86
81
  // => "data:image/svg+xml;base64,PHN2ZyB4bWxucz0i..." (drop straight into <img src>)
87
82
  ```
88
83
 
89
- SVG output needs **no canvas at all**, so it also works in plain Node.js without `@napi-rs/canvas`. Paths, text, gradients, clipping, and pattern brushes stay vectors; bitmaps are embedded as PNG/JPEG `<image>` elements (browser-native formats are embedded as-is, never re-encoded).
84
+ 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.
90
85
 
91
86
  ### Rendering in React (JSX / TSX)
92
87
 
93
- `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:
88
+ `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:
94
89
 
95
90
  ```tsx
96
91
  import { createElement, useEffect, useState, type ReactNode } from 'react';
@@ -114,7 +109,7 @@ export function Metafile({ buffer }: { buffer: ArrayBuffer }) {
114
109
  }
115
110
  ```
116
111
 
117
- Or generate a component at build time (the SVGR approach) and commit or import the `.tsx` file:
112
+ Or generate a component at build time (the SVGR approach):
118
113
 
119
114
  ```typescript
120
115
  import { writeFileSync } from 'node:fs';
@@ -125,36 +120,50 @@ writeFileSync('Logo.tsx', svgTreeToJsx(tree!, { componentName: 'Logo' }));
125
120
  // export function Logo(props: SVGProps<SVGSVGElement>) { return (<svg ... {...props}> ... </svg>); }
126
121
  ```
127
122
 
128
- 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 the same page, give each its own `idPrefix` so their clip-path/gradient ids cannot collide (a unique prefix per conversion is the default).
123
+ 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).
124
+
125
+ ### Exact text
126
+
127
+ 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:
128
+
129
+ ```typescript
130
+ import { convertMetafileToDataUrl, loadSystemFonts } from 'emf-converter';
131
+
132
+ const fonts = await loadSystemFonts(); // Node.js only; reuse the array across conversions
133
+ const png = await convertMetafileToDataUrl(buffer, { fonts });
134
+ ```
135
+
136
+ - 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.
137
+ - 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.
138
+ - Non-antialiased, grayscale or ClearType rendering is chosen from the font's quality; `fontSmoothing` sets what `DEFAULT_QUALITY` means (Windows' default is ClearType).
139
+ - 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.
140
+ - Rotated text uses GDI's rounded font matrix; EMF+ `DrawString` honours the text rendering hint, string-format tracking and margins, and texture/gradient brushes.
141
+
142
+ 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.
129
143
 
130
144
  ## API
131
145
 
132
146
  ### `convertMetafileToDataUrl(buffer, options?)`
133
147
 
134
- | Parameter | Type | Description |
135
- | ----------- | ----------------------------- | ---------------------------------------------------- |
136
- | `buffer` | `ArrayBuffer` | Raw EMF or WMF file bytes (format is auto-detected) |
137
- | `options` | `EmfConvertOptions` (optional)| Output size, DPI scale, record limits, font mapping |
138
- | **Returns** | `Promise<string \| null>` | PNG data URL or `null` on failure |
148
+ | Parameter | Type | Description |
149
+ | ----------- | ------------------------------ | --------------------------------------------------- |
150
+ | `buffer` | `ArrayBuffer` | Raw EMF or WMF file bytes (format is auto-detected) |
151
+ | `options` | `EmfConvertOptions` (optional) | See below |
152
+ | **Returns** | `Promise<string \| null>` | PNG data URL, or `null` on failure |
139
153
 
140
154
  #### `EmfConvertOptions`
141
155
 
142
- | Field | Type | Default | Description |
143
- | -------------------- | -------------------------- | -------------- | --------------------------------------------------------------------------- |
144
- | `maxWidth` | `number` | None | Maximum output width in pixels (aspect ratio preserved) |
145
- | `maxHeight` | `number` | None | Maximum output height in pixels |
146
- | `dpiScale` | `number` | `1` | Resolution multiplier for sharper output; clamped to `4` |
147
- | `maxCanvasDimension` | `number` | `8192` | Hard cap on canvas width/height in pixels |
148
- | `maxRecords` | `number` | `200000`/`500000` | Cap on records processed per stream before replay stops (EMF+ uses the higher default unless overridden) |
149
- | `fontFamilyMap` | `Record<string, string>` | None | Maps Windows face names (case-insensitive) to fonts available locally, e.g. `{ calibri: 'Carlito' }` |
150
- | `gdiAntialias` | `boolean` | `true` | `false` rasterises GDI vector shapes without antialiasing on GDI's pixel grid, matching Windows output pixel for pixel (slower on shape-heavy files) |
151
-
152
- ```ts
153
- const png = await convertMetafileToDataUrl(buffer, {
154
- dpiScale: 2,
155
- fontFamilyMap: { calibri: 'Carlito', 'ms shell dlg': 'Tahoma' },
156
- });
157
- ```
156
+ | Field | Type | Default | Description |
157
+ | -------------------- | ------------------------------------- | ----------------- | ----------- |
158
+ | `maxWidth` | `number` | None | Maximum output width in pixels (aspect ratio preserved) |
159
+ | `maxHeight` | `number` | None | Maximum output height in pixels |
160
+ | `dpiScale` | `number` | `1` | Resolution multiplier; clamped to `4` |
161
+ | `maxCanvasDimension` | `number` | `8192` | Hard cap on output width/height in pixels |
162
+ | `maxRecords` | `number` | `200000`/`500000` | Records processed per stream before replay stops (EMF+ uses the higher default unless overridden) |
163
+ | `gdiAntialias` | `boolean` | `false` (PNG) | `true` smooths every shape edge with Canvas antialiasing instead of reproducing Windows' own GDI/GDI+ rasterisation |
164
+ | `fonts` | `Array<ArrayBuffer \| ArrayBufferView>` | None | TrueType (`.ttf`/`.ttc`) and raster (`.fon`/`.fnt`) font files for exact GDI text |
165
+ | `fontSmoothing` | `'cleartype' \| 'gray' \| 'mono'` | `'cleartype'` | What `DEFAULT_QUALITY` / `DRAFT_QUALITY` / `PROOF_QUALITY` fonts render as (Windows' system setting) |
166
+ | `fontFamilyMap` | `Record<string, string>` | None | Without `fonts`: maps Windows face names (case-insensitive) to locally available fonts, e.g. `{ calibri: 'Carlito' }` |
158
167
 
159
168
  ### SVG functions
160
169
 
@@ -171,41 +180,51 @@ const png = await convertMetafileToDataUrl(buffer, {
171
180
 
172
181
  | Field | Type | Default | Description |
173
182
  | --- | --- | --- | --- |
174
- | `exactRasterOps` | `boolean` | `true` | Evaluate destination-reading raster ops (exact ROP3 blits, exact bitwise ROP2) against a hidden raster mirror when a canvas backend exists, embedding only the pixels they change. `false` (or no canvas) uses SVG `mix-blend-mode` equivalents instead |
175
- | `includeSize` | `boolean` | `true` | Emit `width`/`height` on the root `<svg>` (the `viewBox` is always emitted). `false` gives a fluid SVG that fills its container |
183
+ | `gdiAntialias` | `boolean` | `true` (SVG) | `false` embeds Windows' aliased GDI shape pixels as image patches instead of smooth vector edges |
184
+ | `exactRasterOps` | `boolean` | `true` | `false` skips the raster mirror and expresses destination-reading raster ops with SVG `mix-blend-mode` equivalents |
185
+ | `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 |
186
+ | `includeSize` | `boolean` | `true` | Emit `width`/`height` on the root `<svg>` (`viewBox` is always emitted); `false` gives a fluid SVG |
176
187
  | `idPrefix` | `string` | `emf1-`, `emf2-`, ... | Prefix for generated element ids; keep it unique per inlined SVG |
177
188
 
178
- `maxWidth`/`maxHeight`/`dpiScale`/`maxCanvasDimension` define the SVG's coordinate space (`viewBox`) and the resolution of any embedded raster content.
189
+ ### `loadSystemFonts(options?)`
190
+
191
+ 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`.
179
192
 
180
193
  ## How it works
181
194
 
182
- A three-phase pipeline: **parse → replay → export**. The header parser extracts the drawing bounds, a Canvas is created and clamped to 8192×8192 (configurable via `maxCanvasDimension`), then records are scanned sequentially and dispatched to GDI, EMF+, or WMF handlers that drive the Canvas 2D context. Embedded bitmaps (DIB and GDI+ pixel formats) and recursively embedded metafiles are resolved asynchronously after the synchronous replay completes.
195
+ 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.
196
+
197
+ Everything below is verified against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the per-fixture bounds.
183
198
 
184
- For SVG output the same handlers drive `SvgContext` (`svg-context.ts`), a recorder implementing the part of the Canvas 2D API the replay uses: paths are accumulated in device space exactly as Canvas does (arcs and ellipses as cubic Beziers, which affine maps carry exactly), fills become device-space `<path>`s, strokes are mapped back into their stroke-time user space so line widths and dashes scale like Canvas's, successive clips become a chain of `<clipPath>`s, gradients and pattern brushes become paint servers, text becomes `<text>`, and raster content becomes PNG `<image>`s encoded by a small dependency-free PNG encoder (`png-encoder.ts`). With no canvas implementation at all, bitmap scratch work runs on a pure-JavaScript raster (`software-raster.ts`), so SVG conversion works in plain Node.js without `@napi-rs/canvas`.
199
+ - **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.
200
+ - **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.
201
+ - **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).
202
+ - **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.
203
+ - **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.
204
+ - **Text**: see [Exact text](#exact-text).
205
+ - **WMF**: played as `PlayMetaFile` plays it (GM_COMPATIBLE whole-pixel rules, mapping modes, bitmaps, pattern brushes, clipping and regions, palettes, flood fills, text spacing and justification, right-to-left layout); an EMF embedded in `MFCOMMENT` escapes is played instead, as Windows does.
185
206
 
186
- It supports 300+ EMF GDI record types, the EMF+ (GDI+) record set, and legacy WMF records, including state, transforms, objects, shapes, poly/path operations, text, bitmaps, gradients, raster operations, and clipping:
207
+ ### Supported records
187
208
 
188
- - **Clip regions with exact boolean combine modes**: `Intersect`, `Union`, `Xor`, `Exclude`, `Complement`, and `Replace` are exact for every clip, for `EMR_INTERSECTCLIPRECT` / `EMR_EXCLUDECLIPRECT` / `EMR_EXTSELECTCLIPRGN` (all `RGN_*` modes), path-bracket clips (`EMR_SELECTCLIPPATH`, combined with any region mode), the EMF+ `SetClipRect` / `SetClipPath` / `SetClipRegion` records (all `CombineMode` values, including nested region-node trees), and clip translation via `EMR_OFFSETCLIPRGN` / EMF+ `OffsetClip`. Simple cases keep their vector form: subtraction and symmetric difference are expressed through even-odd fill-rule clipping, which Canvas 2D cannot do with plain `clip()` stacking. Everything else (a clip that is already an intersection of several shapes, self-intersecting or curved operands) is evaluated exactly by scan-converting the operands at device pixel centres with their own fill rules (`emf-clip-scanline.ts`) and storing the result as disjoint pixel-aligned rectangles, which is how GDI itself stores regions.
189
- - **Gradient brushes**: GDI+ linear gradients render as Canvas linear gradients with their full colour-stop list (preset blend colours and blend factors are expanded into stops, and the optional brush transform rotates or shears the gradient axis). `WrapMode` tiling (`Tile` / `TileFlipX` / `TileFlipY` / `TileFlipXY`) is supported for a linear gradient at any angle, by unrolling one period into a single hard-stopped gradient spanning the drawing surface. Path gradients render the true boundary-shaped falloff (per-vertex surround colours, blend curves, preset colours, focus scales, curved boundaries flattened at GDI+'s own 0.25 flatness), computed per device pixel for all five `WrapMode` values, with each device point folded analytically into the base tile (`emf-plus-exact-fill.ts`). Verified against real GDI+ fixtures under `src/__fixtures__/gdi/`: path gradients match within 0.15% of pixels in every wrap mode.
190
- - **Raster operations (ROP3)**: `EMR_BITBLT`, `EMR_STRETCHBLT`, and `EMR_STRETCHDIBITS` (and `PatBlt`-style brush-only fills) evaluate all 256 ROP3 codes exactly, per pixel and per bit, against the real destination, brush, and (when present) source pixels, matching Windows GDI bit-for-bit rather than approximating with Canvas composite modes.
191
- - **Raster operations (ROP2)**: every `SetROP2` mode is mapped. `R2_BLACK`, `R2_WHITE`, `R2_NOP`, `R2_COPYPEN`, `R2_NOTCOPYPEN`, and `R2_NOT` are emulated exactly via colour inversion and `difference` compositing. The bitwise AND/OR/XOR family (`R2_MASKPEN`, `R2_MERGEPEN`, `R2_XORPEN`, `R2_NOTXORPEN`, `R2_MASKPENNOT`, `R2_MERGEPENNOT`, `R2_MASKNOTPEN`, `R2_MERGENOTPEN`, `R2_NOTMASKPEN`, `R2_NOTMERGEPEN`) is evaluated exactly too, bit-for-bit against the true destination pixel, for a filled and/or stroked Rectangle, RoundRect, Ellipse, Arc/Chord/Pie, Polygon, Polyline, or PolyPolygon, whether drawn immediately or built up across an `EMR_BEGINPATH`/`EMR_ENDPATH` bracket: the shape's coverage is computed on/off per pixel (GDI does not antialias) within the shape's bounding box, combined with the exact pen/brush colour by the same truth-table evaluator the ROP3 blit path uses, and composited through the active clip region. A pattern (hatch, monochrome, or DIB) brush fill combines exactly with every `SetROP2` mode, bitwise ones included.
192
- - **GDI world transforms**: `EMR_SETWORLDTRANSFORM` / `EMR_MODIFYWORLDTRANSFORM`'s full affine, including rotation and skew, is applied to Rectangle, RoundRect, Ellipse, Arc/Chord/Pie, Polygon, Polyline, PolyPolygon, path (`BeginPath`/`MoveTo`/`LineTo`/`Poly*To`/`EndPath`) drawing, bitmap blits (`EMR_BITBLT`/`EMR_STRETCHBLT`/`EMR_STRETCHDIBITS`, including the exact ROP3 path), and raster text (`EMR_EXTTEXTOUTW`). A rotated/skewed Ellipse (and Arc/Chord/Pie) is rendered from the mapped shape's exact semi-axes and rotation angle (the eigendecomposition of the linear part). RoundRect corners are exact elliptical arcs honouring unequal corner width and height, built as Beziers in logical space and mapped through the transform, so they rotate and skew exactly. Text under a skewed transform is sheared as well as rotated, matching GDI. Bitmap blits under a rotated or skewed transform are evaluated exactly (ROP3 truth table, brush pattern, source stretch-mode sampling) on a local raster whose axes are the exact transformed basis vectors, sampled at least once per device pixel, and then placed; real GDI does rotate a `BitBlt`/`TextOut` destination (`src/__fixtures__/gdi/rotate-bitblt-25deg`, `rotate-text-25deg`). This full-affine handling is also required for GDI+-exported EMF files even without rotation, since they record coordinates at 16× sub-pixel precision with a compensating world-transform scale.
193
- - **GDI pattern-brush fills**: a hatch, monochrome (`EMR_CREATEMONOBRUSH`), or DIB (`EMR_CREATEDIBPATTERNBRUSHPT`) pattern brush selected for a Rectangle/RoundRect/Ellipse/Arc/Polygon/Polyline/PolyPolygon fill (or a bracketed path's `EMR_FILLPATH`/`EMR_STROKEANDFILLPATH`) paints the real tiled pattern, anchored to the brush origin (`EMR_SETBRUSHORGEX`), instead of a flat placeholder colour. This is filled per pixel (via the shape's already-built path and the same `sampleTile` sampler the exact ROP3 blit evaluator uses) rather than through a Canvas `CanvasPattern`: every canvas backend this package targets was measured to filter a `CanvasPattern` regardless of `imageSmoothingEnabled` (which only ever affects `drawImage`), smearing a hard-edged pattern tile's texels across several device pixels even at a 1:1 pattern transform. `EMR_CREATEDIBPATTERNBRUSHPT`/`EMR_CREATEMONOBRUSH` pattern brushes used as the *source* of a `BitBlt`/`StretchBlt`/`PatBlt` ROP3 blit were already exact before this (see Raster operations (ROP3) above); this closes the same brushes used for a vector shape fill.
194
- - **GDI pixel grid**: thin (odd-width) pens paint the pixels their coordinates name, as GDI does, rather than two half-intensity rows straddling a pixel boundary. The `gdiAntialias: false` option goes further and rasterises every GDI vector fill and stroke without antialiasing on GDI's own pixel grid, matching Windows output to within 0.25% of pixels on the real-GDI shape fixtures, at a 3-4x cost on shape-heavy files.
195
- - **EMF+ TextureFill brushes**: a TextureFill brush (brush type 2) paints its real image, tiled per `WrapMode` and placed by the brush and world transforms. Shape fills (FillRects, FillEllipse, FillPie, FillPolygon, FillPath) are computed one device pixel at a time and match a real GDI+ fixture pixel-exactly. Compressed (PNG/JPEG) embedded images, the common real-world case, are decoded by an async pre-decode pass (`emf-plus-texture-predecode.ts`) before the synchronous replay starts. EMF+ objects split across `EMFPLUS_OBJECT` continuation records, including ones spread over several `EMR_COMMENT` records, are reassembled by one routine shared by the replay and the pre-decode pass (`emf-plus-continuation.ts`), so a large split texture brush decodes like any other.
196
- - **EMF+ Image objects and DrawImage**: `BitmapDataType` ([MS-EMFPLUS] 2.1.1.2: `Pixel = 0`, `Compressed = 1`) is read correctly for a standalone Image object. `DrawImage` / `DrawImagePoints` honour the source rectangle and the full destination parallelogram (so rotation and shear are kept), and resample the way GDI+ does for `InterpolationMode` Default / LowQuality / Bilinear / NearestNeighbor under `PixelOffsetMode` None or Half (`emf-plus-image-resample.ts`), matching a real GDI+ `DrawImage` fixture (`src/__fixtures__/gdi/image-draw-png`) within 8 levels per channel on every pixel.
209
+ - **EMF**: 115 of the 119 record types defined in MS-EMF, including 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_EXTTEXTOUTW`, blits, clipping and transforms. Colour space, ICM, OpenGL, escape and font-driver records are consumed without effect, as on a Windows display.
210
+ - **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`).
211
+ - **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.
197
212
 
198
213
  ## Limitations
199
214
 
200
- - **Curved and diagonal GDI edges are antialiased by default**: GDI rasterises shape edges without antialiasing, while Canvas (and SVG renderers) smooth them, so by default about 1-3% of pixels differ on the rotated/curved real-GDI fixtures (`src/__fixtures__/gdi/rotate-*`, `skew-rect`, `pattern-fill-ellipse-color`, `rop2-bitwise-path-bracket`); axis-aligned shapes are exact or nearly so. With `gdiAntialias: false` the residual drops to occasional one-pixel stepping differences on steep lines and curves (at most 0.25%). See `src/gdi-parity.fixture.test.ts` for the per-fixture tolerances this is regression-tested against.
201
- - **A rotated bitmap blit resamples once at placement**: the ROP3 combine itself is exact on the local raster, but painting that raster into a rotated position is a resample that cannot stay nearest-neighbour-exact at every output pixel; measured against real GDI output this is under 1% of pixels for a 25-degree rotated `BitBlt` (`src/__fixtures__/gdi/rotate-bitblt-25deg`).
202
- - **Glyph rasterisation differs from GDI's font engine**: text placement, rotation, and shear match GDI, but glyphs are drawn by the host font engine, so rotated text still differs on about 2.4% of pixels (`src/__fixtures__/gdi/rotate-text-25deg`), mostly along glyph edges.
203
- - **Preset-colour linear gradients differ slightly from GDI+**: linear gradients with `InterpolationColors` preset stops differ from GDI+ on about 1.5% of pixels, along the preset stops. GDI+ appears to blend preset colours through a coarse lookup table that never quite reaches a peak stop colour, while this library interpolates them exactly; without the table's exact size there is nothing exact to model. All other linear gradients, including every `WrapMode`, are exact up to float colour rounding at period seams. Path gradients match within 0.15% of pixels (single pixels right on a curved or diagonal boundary).
204
- - **Texture and path-gradient brushes on strokes and text**: shape fills with these brushes are exact (see above), but strokes, pens, and text painted with a texture or path-gradient brush still go through a Canvas `CanvasPattern`, which every canvas backend filters, so their edges come out slightly soft. Texture brushes are sampled nearest-neighbour, so a texture scaled or rotated by its brush or world transform does not reproduce GDI+'s interpolated texture sampling.
205
- - **`DrawImage` Bicubic/HighQuality modes and clipping**: GDI+ resampling is modelled for the Default, LowQuality, Bilinear, and NearestNeighbor interpolation modes; Bicubic and the HighQuality modes, and embedded metafile images, fall back to the canvas's own scaling. In PNG output, deferred image draws (decoded asynchronously after replay) are painted on top without the clip region that was active when they were recorded; SVG output keeps each image at its recorded z-order and clip.
206
- - **SVG output**: SVG cannot read back what is already painted, so raster operations that depend on the destination (exact ROP3 blits, exact bitwise `SetROP2`) are evaluated against a hidden raster mirror when a canvas backend is available and embedded as image patches holding only the pixels they change; without a canvas backend (or with `exactRasterOps: false`) they fall back to `mix-blend-mode` equivalents (exact for the usual SRCAND/SRCPAINT/SRCINVERT mask idioms on black-and-white masks, approximate otherwise; ROP3 codes with no blend-mode equivalent are skipped). Embedded bitmaps are scaled by the SVG renderer's own image smoothing (they stay sharp when the SVG is scaled up) rather than baked at device resolution with GDI+'s resampler. Text is emitted as `<text>` and rendered with whatever fonts the viewer has, and rendering varies slightly between SVG renderers (measured in Chromium, SVG output matches the PNG output's parity on the real-GDI fixtures).
207
- - **Safety limits**: output is clamped to 8192×8192 and replay stops after 200,000 records (EMF/WMF) or 500,000 (EMF+). All three are overridable via `maxCanvasDimension` / `maxRecords`.
208
- - **Font rendering** uses the host Canvas font engine, so glyph metrics may differ from Windows GDI. Weight, italic, underline, and strike-out are honoured; supply `fontFamilyMap` to remap Windows face names to fonts available in your environment.
215
+ Everything is measured against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the exact per-fixture bounds.
216
+
217
+ - **Unhandled records**: `EMR_EXTTEXTOUTA`, `EMR_POLYTEXTOUTA`, `EMR_POLYTEXTOUTW` and `EMR_SMALLTEXTOUT` (ANSI, multi-string and small-glyph text records, rarely written by modern recorders) are skipped with a console warning. `EMR_SETTEXTJUSTIFICATION` and `EMR_SETCOLORADJUSTMENT` are read but not yet applied (Windows' own recorder bakes justification into `EMR_EXTTEXTOUTW` spacing arrays, so only other writers emit the former), and the `HALFTONE` stretch mode is not bit-exact. EMF+ image effects (`SerializableObject`: blur, sharpen, colour matrix and the like) are not applied, so the image is drawn without the effect, and an EMF+ pen's own transform is ignored.
218
+ - **Text without fonts**: without the `fonts` option, glyphs come from the host font engine, so text differs from Windows on a few percent of pixels (and depends on which fonts the host has). SVG text is `<text>`, rendered with the viewer's fonts.
219
+ - **Text with fonts**: non-antialiased text is within 0.03% of Windows' pixels (about 93% of glyphs bit-exact; the rest differ by a pixel on some diagonal stems, where GDI's hinting arithmetic is undocumented) and grayscale text within 0.13%. ClearType text, which is what `DEFAULT_QUALITY` fonts get on a default Windows install, is within about 4-5% (compatible-width glyph placement is not fully reproduced). Non-grid-fitted GDI+ AntiAlias and ClearType text differ by 11-13% of text pixels (GDI+'s own glyph placement and blend), rotated right/centre-aligned text can start a pixel off, and a few unusual raster-font sizes pick a different bitmap size than Windows.
220
+ - **Wide pens and paths**: flat-capped GDI pens 7 px and wider can differ by a few pixels at round joins, and dashed wide Bezier curves follow `WidenPath` (which Windows' direct drawing does not quite match); at most 0.2% of pixels on the fixtures. `EMR_WIDENPATH` does not reproduce the extra inner join triangles GDI's own `WidenPath` emits (visible only when the widened outline is itself stroked). EMF+ 1-pixel antialiased lines can differ by one antialiasing sample at their ends, some closed widened outlines by one sample along an edge, and Inset or compound pens on closed figures are approximate.
221
+ - **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.
222
+ - **Small EMF+ residuals**: rotated `HighQualityBicubic` `DrawImage` edge pixels (0.14%), one-level differences at exact half-level `Blend` knots, and a few pixels of a metafile nested in `DrawImage` under a scale.
223
+ - **WMF**: `PS_INSIDEFRAME` boxes can come out a pixel short at non-integer scales, right-to-left (`LAYOUT_RTL`) layouts differ by single pixels on mirrored diagonals, and metric map modes assume a 96 dpi reference device (Windows derives them from the physical display, so its own output varies per machine).
224
+ - **Smooth mode** (`gdiAntialias: true`, and SVG's default vector edges): only shape edges differ from Windows, by design; a flood fill reads the antialiased pixels it is given.
225
+ - **SVG renderers**: output is renderer-dependent in the usual ways: angled hard-stop gradient seams fall within each renderer's own precision, and a raster operation that reads pixels under text is exact wherever its result does not depend on the glyphs, otherwise expressed as a blend layer the SVG renderer applies to its own text.
226
+ - **PNG in plain Node.js** without `@napi-rs/canvas` and without `fonts` returns `null` for drawings that contain text, rather than an image missing its text.
227
+ - **Safety limits**: output is clamped to 8192×8192 and replay stops after 200,000 records (EMF/WMF) or 500,000 (EMF+); both are overridable via `maxCanvasDimension` / `maxRecords`.
209
228
 
210
229
  ## License
211
230
 
package/dist/index.d.mts CHANGED
@@ -136,15 +136,53 @@ interface EmfConvertOptions {
136
136
  */
137
137
  fontFamilyMap?: Record<string, string>;
138
138
  /**
139
- * Antialias plain-GDI (EMF) vector shapes: Rectangle, Ellipse, RoundRect,
140
- * Polygon, Polyline, arcs, and bracketed paths. Default `true`, Canvas's
141
- * own smooth edges. `false` rasterises their fills and strokes the way
142
- * Windows GDI does, without antialiasing and on GDI's own pixel grid, for
143
- * output that matches what Windows paints pixel for pixel. It reads back
144
- * and rewrites each shape's bounding box, so it is noticeably slower on
145
- * shape-heavy files. EMF+ drawing and text are unaffected.
139
+ * Smooth shape edges instead of reproducing Windows' own rasterisation.
140
+ *
141
+ * PNG output defaults to `false`: plain-GDI vector shapes (Rectangle,
142
+ * Ellipse, RoundRect, Polygon, Polyline, arcs, bracketed paths, wide
143
+ * pens) are rasterised the way Windows GDI does, with its 28.4
144
+ * fixed-point geometry, fill rule, line algorithm and Bezier flattening,
145
+ * and EMF+ fills, strokes and clips follow the GDI+ `SmoothingMode` the
146
+ * file records (None, Default and HighSpeed aliased on GDI+'s pixel grid,
147
+ * AntiAlias and HighQuality with GDI+'s 8 x 4-sample antialiasing), so the
148
+ * PNG matches what Windows paints pixel for pixel.
149
+ *
150
+ * `true` draws every edge with Canvas's own antialiasing instead (still
151
+ * with GDI's geometry, pen widths, caps, joins and dash patterns), which
152
+ * looks smoother than Windows, especially for files recorded without
153
+ * GDI+ antialiasing.
154
+ *
155
+ * SVG output defaults to smooth vector edges (an SVG is meant to scale);
156
+ * `false` there embeds Windows' aliased shape pixels as image patches.
146
157
  */
147
158
  gdiAntialias?: boolean;
159
+ /**
160
+ * Font files to render GDI and WMF text with, exactly as Windows GDI
161
+ * does: TrueType `.ttf` / `.ttc` and raster `.fon` / `.fnt` bytes
162
+ * (`loadSystemFonts()` reads the installed ones in Node.js). The
163
+ * LOGFONT is realised against these files (face, weight, slant,
164
+ * `lfHeight`/`lfWidth`, charset fallback), each TrueType glyph is grid-fitted by the font's own TrueType
165
+ * instructions and scan-converted with TrueType dropout control
166
+ * (non-antialiased, 4x4 grayscale or ClearType per the LOGFONT's
167
+ * `lfQuality`), and glyphs are placed on GDI's integer device grid with
168
+ * GDI's own advance widths, cell metrics, underline and strike-out.
169
+ * Raster faces (MS Sans Serif, Helv, System, Terminal, ...) are drawn
170
+ * from their bitmaps at the size and whole-number stretch GDI picks.
171
+ * Supply the fonts the metafile names (for Windows-authored files, the
172
+ * matching files from `C:\Windows\Fonts`); a face that is missing is
173
+ * substituted the way GDI's font mapper would (by pitch and family),
174
+ * and without this option text is drawn with the canvas font engine.
175
+ * Ignored by SVG output, which keeps text as `<text>` but takes its
176
+ * per-glyph positions and metrics from these fonts.
177
+ */
178
+ fonts?: Array<ArrayBuffer | ArrayBufferView>;
179
+ /**
180
+ * Windows' system-wide font smoothing, which GDI applies to fonts that
181
+ * ask for `DEFAULT_QUALITY`, `DRAFT_QUALITY` or `PROOF_QUALITY` (most
182
+ * metafiles): `'cleartype'` (Windows' default), `'gray'` (standard
183
+ * antialiasing) or `'mono'` (smoothing off). Only used with `fonts`.
184
+ */
185
+ fontSmoothing?: 'cleartype' | 'gray' | 'mono';
148
186
  }
149
187
  /**
150
188
  * Options for the SVG outputs ({@link convertMetafileToSvg} and friends).
@@ -156,14 +194,34 @@ interface EmfConvertOptions {
156
194
  interface SvgConvertOptions extends EmfConvertOptions {
157
195
  /**
158
196
  * Evaluate raster operations that read the destination (exact ROP3 blits,
159
- * exact bitwise ROP2) against a hidden raster mirror of the drawing, and
160
- * embed the pixels they change as image patches. Needs a canvas backend
161
- * (browser/worker canvas, or `@napi-rs/canvas` in Node.js); without one,
162
- * or when set to `false`, those operations fall back to SVG blend modes
197
+ * bitwise ROP2, pattern-brush fills combined through ROP2, and
198
+ * `gdiAntialias: false` shapes) against a hidden raster mirror of the
199
+ * drawing, and embed the pixels they change as image patches. The mirror
200
+ * is the canvas backend when one exists (browser/worker canvas, or
201
+ * `@napi-rs/canvas` in Node.js) and the built-in pure-JavaScript
202
+ * rasteriser otherwise, so this needs no canvas. The built-in mirror
203
+ * cannot draw glyphs: where a raster operation reads pixels under text,
204
+ * every pixel whose result does not depend on the glyphs stays exact and
205
+ * the rest is handed to the SVG renderer as a blend-mode layer (exact for
206
+ * inversion and for masks whose channels are all 0 or 255). With `false`,
207
+ * no mirror is kept and those operations fall back to SVG blend modes
163
208
  * (`mix-blend-mode`), which are exact for the common mask ROPs on
164
209
  * black/white masks and approximate otherwise. Default `true`.
165
210
  */
166
211
  exactRasterOps?: boolean;
212
+ /**
213
+ * How EMF+ `DrawImage` bitmaps are scaled. `'renderer'` (the default)
214
+ * embeds the original image and lets the SVG renderer scale it with its
215
+ * own smoothing, which stays sharp at any display size. `'exact'` bakes
216
+ * each draw at device resolution with the GDI+-matching resampler (the
217
+ * PNG output's `InterpolationMode`/`PixelOffsetMode` model), so the SVG
218
+ * shows what GDI+ painted pixel for pixel at its nominal size. PNG and
219
+ * BMP images are decoded in pure JavaScript; JPEG, GIF and other formats
220
+ * need a canvas backend to decode and otherwise keep `'renderer'`
221
+ * scaling, as do draws GDI+ scales with a filter the resampler does not
222
+ * model (bicubic, high-quality).
223
+ */
224
+ imageResampling?: 'renderer' | 'exact';
167
225
  /**
168
226
  * Emit `width`/`height` attributes on the root `<svg>` (the `viewBox` is
169
227
  * always emitted). Set `false` for a fluid SVG that fills its container.
@@ -191,8 +249,13 @@ interface SvgConvertOptions extends EmfConvertOptions {
191
249
  * - The buffer matches neither a valid EMF header nor a valid WMF header.
192
250
  * - The logical bounds are zero-sized or negative.
193
251
  * - No canvas API is available (browser/worker canvas, or the optional
194
- * `@napi-rs/canvas` package in plain Node.js). SVG output
195
- * ({@link convertMetafileToSvg}) needs no canvas at all.
252
+ * `@napi-rs/canvas` package in plain Node.js) AND the drawing contains
253
+ * text or an image format only a canvas can decode (JPEG, GIF, ...).
254
+ * Without a canvas the built-in pure-JavaScript rasteriser renders
255
+ * everything else (vectors, clipping, gradients, bitmaps, PNG/BMP
256
+ * images, every raster operation), but it has no font engine, so rather
257
+ * than return an image silently missing its text it returns `null`. SVG
258
+ * output ({@link convertMetafileToSvg}) needs no canvas at all.
196
259
  *
197
260
  * @param buffer - The raw EMF or WMF file bytes.
198
261
  * @param options - Optional {@link EmfConvertOptions} controlling output size,
@@ -201,6 +264,7 @@ interface SvgConvertOptions extends EmfConvertOptions {
201
264
  * @returns A `data:image/png;base64,...` string, or `null` on failure.
202
265
  */
203
266
  declare function convertMetafileToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
267
+
204
268
  /**
205
269
  * Converts an EMF or WMF buffer (format auto-detected) into an SVG document
206
270
  * tree, the common source for every SVG output form: serialise it with
@@ -208,11 +272,12 @@ declare function convertMetafileToDataUrl(buffer: ArrayBuffer, options?: EmfConv
208
272
  * with {@link svgTreeToReact}, or generate component source with
209
273
  * {@link svgTreeToJsx}.
210
274
  *
211
- * Unlike PNG output this needs no canvas implementation: vectors, text,
212
- * gradients, clipping, and bitmaps are all recorded in pure JavaScript. A
213
- * canvas backend, when present, is used only to evaluate destination-
214
- * reading raster operations exactly (see {@link SvgConvertOptions.exactRasterOps})
215
- * and to measure text.
275
+ * This needs no canvas implementation: vectors, text, gradients, clipping,
276
+ * and bitmaps are all recorded in pure JavaScript, and destination-reading
277
+ * raster operations are evaluated exactly against a raster mirror of the
278
+ * drawing (see {@link SvgConvertOptions.exactRasterOps}), which is the
279
+ * canvas backend when one exists (it also measures text) and the built-in
280
+ * pure-JavaScript rasteriser otherwise.
216
281
  *
217
282
  * @returns The root `<svg>` node, or `null` for an invalid/empty metafile.
218
283
  */
@@ -239,4 +304,54 @@ declare function convertMetafileToSvgDataUrl(buffer: ArrayBuffer, options?: SvgC
239
304
  */
240
305
  declare const DEFAULT_DPI_SCALE = 1;
241
306
 
242
- export { type CreateElement, DEFAULT_DPI_SCALE, type EmfConvertOptions, type SvgConvertOptions, type SvgJsxOptions, type SvgNode, convertMetafileToDataUrl, convertMetafileToSvg, convertMetafileToSvgDataUrl, convertMetafileToSvgTree, svgTreeToDataUrl, svgTreeToJsx, svgTreeToReact, svgTreeToString };
307
+ /**
308
+ * `loadSystemFonts()`: reads the operating system's installed font files
309
+ * (Node.js only) into buffers for the converter's `fonts` option.
310
+ *
311
+ * The Node.js modules are loaded with dynamic `import()` (and bundler
312
+ * ignore comments) only when the function is called, so the package stays
313
+ * browser-safe: nothing here is resolved or bundled for a browser build.
314
+ *
315
+ * @module load-system-fonts
316
+ */
317
+ /** Options for {@link loadSystemFonts}. */
318
+ interface LoadSystemFontsOptions {
319
+ /**
320
+ * Directories to scan instead of the platform defaults (Windows:
321
+ * `%WINDIR%\Fonts` and the per-user `%LOCALAPPDATA%\Microsoft\Windows\Fonts`;
322
+ * Linux: `/usr/share/fonts`, `/usr/local/share/fonts`, `~/.fonts`,
323
+ * `~/.local/share/fonts`; macOS: `/Library/Fonts`,
324
+ * `/System/Library/Fonts`, `~/Library/Fonts`). Missing directories are
325
+ * skipped.
326
+ */
327
+ dirs?: string[];
328
+ /**
329
+ * Keeps only the files this returns true for, given the full path and
330
+ * the lower-case file name (for example to load just the faces a
331
+ * metafile names: `(p, n) => /^(arial|times|cour)/.test(n)`).
332
+ */
333
+ filter?: (path: string, name: string) => boolean;
334
+ /** Subdirectory depth to descend (default 4; 0 scans only the directories themselves). */
335
+ maxDepth?: number;
336
+ }
337
+ /**
338
+ * Reads the installed TrueType (`.ttf`, `.ttc`) and raster (`.fon`,
339
+ * `.fnt`) font files, for `convertMetafileToDataUrl(data, { fonts })`
340
+ * and the SVG converters. Node.js only: in a browser (or any runtime
341
+ * without `process.versions.node`) it resolves to an empty array.
342
+ *
343
+ * Reading every installed font can take a few hundred megabytes on
344
+ * Windows; pass `filter` to load only what your metafiles use. Load once
345
+ * and reuse the array across conversions.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * import { convertMetafileToDataUrl, loadSystemFonts } from 'emf-converter';
350
+ *
351
+ * const fonts = await loadSystemFonts();
352
+ * const png = await convertMetafileToDataUrl(emfBytes, { fonts });
353
+ * ```
354
+ */
355
+ declare function loadSystemFonts(options?: LoadSystemFontsOptions): Promise<Uint8Array[]>;
356
+
357
+ export { type CreateElement, DEFAULT_DPI_SCALE, type EmfConvertOptions, type LoadSystemFontsOptions, type SvgConvertOptions, type SvgJsxOptions, type SvgNode, convertMetafileToDataUrl, convertMetafileToSvg, convertMetafileToSvgDataUrl, convertMetafileToSvgTree, loadSystemFonts, svgTreeToDataUrl, svgTreeToJsx, svgTreeToReact, svgTreeToString };