emf-converter 2.0.3 → 2.0.4

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
@@ -20,7 +20,7 @@ Windows Metafiles store a sequence of GDI drawing commands and are commonly embe
20
20
 
21
21
  ## Demo
22
22
 
23
- Try it right in your browser — drop in an `.emf` or `.wmf` file and see the rendered PNG, conversion time, and output size:
23
+ Try it right in your browser: drop in an `.emf` or `.wmf` file and see the rendered PNG, conversion time, and output size:
24
24
 
25
25
  **https://christophervr.github.io/emf-converter/**
26
26
 
@@ -30,32 +30,42 @@ Try it right in your browser — drop in an `.emf` or `.wmf` file and see the re
30
30
  npm install emf-converter
31
31
  ```
32
32
 
33
- No dependencies. Requires a Canvas API at runtime — `OffscreenCanvas` (Web Workers) or `HTMLCanvasElement`.
33
+ No required dependencies. Requires a Canvas API at runtime:
34
+
35
+ - **Browser / Web Worker**: nothing else to install, `OffscreenCanvas` or `HTMLCanvasElement` is used automatically.
36
+ - **Node.js** (no DOM, no Worker): install the optional [`@napi-rs/canvas`](https://www.npmjs.com/package/@napi-rs/canvas) package as well:
37
+
38
+ ```bash
39
+ npm install @napi-rs/canvas
40
+ ```
41
+
42
+ It's a prebuilt, Skia-based native module (no `node-gyp` required). When it's not installed, conversion in plain Node.js returns `null` instead of throwing.
34
43
 
35
44
  ## Quick start
36
45
 
37
46
  ```typescript
38
- import { convertEmfToDataUrl, convertWmfToDataUrl } from 'emf-converter';
47
+ import { convertMetafileToDataUrl } from 'emf-converter';
39
48
 
40
49
  const emfBuffer: ArrayBuffer = /* loaded from file or network */;
41
- const pngDataUrl = await convertEmfToDataUrl(emfBuffer);
50
+ const pngDataUrl = await convertMetafileToDataUrl(emfBuffer);
42
51
  // => "data:image/png;base64,iVBORw0KGgo..."
43
52
 
44
- const wmfPng = await convertWmfToDataUrl(wmfBuffer);
53
+ // Works the same for WMF, the format is auto-detected from the bytes.
54
+ const wmfPng = await convertMetafileToDataUrl(wmfBuffer);
45
55
 
46
56
  // Optional: limit output dimensions (aspect ratio preserved)
47
- const scaled = await convertEmfToDataUrl(emfBuffer, { maxWidth: 1024, maxHeight: 768 });
57
+ const scaled = await convertMetafileToDataUrl(emfBuffer, { maxWidth: 1024, maxHeight: 768 });
48
58
  ```
49
59
 
50
- Both functions return `Promise<string | null>` — `null` if the buffer is invalid or no Canvas API is available.
60
+ Returns `Promise<string | null>`, `null` if the buffer is invalid or no Canvas API is available.
51
61
 
52
62
  ## API
53
63
 
54
- ### `convertEmfToDataUrl(buffer, options?)` · `convertWmfToDataUrl(buffer, options?)`
64
+ ### `convertMetafileToDataUrl(buffer, options?)`
55
65
 
56
66
  | Parameter | Type | Description |
57
67
  | ----------- | ----------------------------- | ---------------------------------------------------- |
58
- | `buffer` | `ArrayBuffer` | Raw EMF/WMF file bytes |
68
+ | `buffer` | `ArrayBuffer` | Raw EMF or WMF file bytes (format is auto-detected) |
59
69
  | `options` | `EmfConvertOptions` (optional)| Output size, DPI scale, record limits, font mapping |
60
70
  | **Returns** | `Promise<string \| null>` | PNG data URL or `null` on failure |
61
71
 
@@ -63,15 +73,15 @@ Both functions return `Promise<string | null>` — `null` if the buffer is inval
63
73
 
64
74
  | Field | Type | Default | Description |
65
75
  | -------------------- | -------------------------- | -------------- | --------------------------------------------------------------------------- |
66
- | `maxWidth` | `number` | — | Maximum output width in pixels (aspect ratio preserved) |
67
- | `maxHeight` | `number` | — | Maximum output height in pixels |
76
+ | `maxWidth` | `number` | None | Maximum output width in pixels (aspect ratio preserved) |
77
+ | `maxHeight` | `number` | None | Maximum output height in pixels |
68
78
  | `dpiScale` | `number` | `1` | Resolution multiplier for sharper output; clamped to `4` |
69
79
  | `maxCanvasDimension` | `number` | `8192` | Hard cap on canvas width/height in pixels |
70
80
  | `maxRecords` | `number` | `200000`/`500000` | Cap on records processed per stream before replay stops (EMF+ uses the higher default unless overridden) |
71
- | `fontFamilyMap` | `Record<string, string>` | — | Maps Windows face names (case-insensitive) to fonts available locally, e.g. `{ calibri: 'Carlito' }` |
81
+ | `fontFamilyMap` | `Record<string, string>` | None | Maps Windows face names (case-insensitive) to fonts available locally, e.g. `{ calibri: 'Carlito' }` |
72
82
 
73
83
  ```ts
74
- const png = await convertEmfToDataUrl(buffer, {
84
+ const png = await convertMetafileToDataUrl(buffer, {
75
85
  dpiScale: 2,
76
86
  fontFamilyMap: { calibri: 'Carlito', 'ms shell dlg': 'Tahoma' },
77
87
  });
@@ -83,20 +93,20 @@ A three-phase pipeline: **parse → replay → export**. The header parser extra
83
93
 
84
94
  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:
85
95
 
86
- - **Clip regions with full boolean combine modes** — the converter tracks the active clip as a list of path shapes, so `Intersect`, `Union`, `Xor`, `Exclude`, `Complement`, and `Replace` combine modes work for `EMR_INTERSECTCLIPRECT` / `EMR_EXCLUDECLIPRECT` / `EMR_EXTSELECTCLIPRGN` (all `RGN_*` modes), the EMF+ `SetClipRect` / `SetClipPath` / `SetClipRegion` records (all `CombineMode` values, including nested region-node trees), and clip translation via `EMR_OFFSETCLIPRGN` / EMF+ `OffsetClip`. Subtraction and symmetric difference are expressed through even-odd fill-rule clipping, which Canvas 2D cannot do with plain `clip()` stacking.
87
- - **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 the gradient axis). Path gradients render as radial gradients from the centre colour to the surrounding colour across the boundary radius.
88
- - **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 remaining bitwise AND/OR/XOR-family modes are approximated with the nearest arithmetic composite (`difference` / `darken` / `lighten`), combined with pen-colour inversion for the NOT variants.
89
- - **GDI world transforms** — the scale and translation set by `EMR_SETWORLDTRANSFORM` / `EMR_MODIFYWORLDTRANSFORM` are applied to all GDI drawing, which is required for GDI+-exported EMF files (they record coordinates at 16× sub-pixel precision with a compensating transform). EMF+ records support the full affine transform set.
96
+ - **Clip regions with full boolean combine modes**: the converter tracks the active clip as a list of path shapes, so `Intersect`, `Union`, `Xor`, `Exclude`, `Complement`, and `Replace` combine modes work for `EMR_INTERSECTCLIPRECT` / `EMR_EXCLUDECLIPRECT` / `EMR_EXTSELECTCLIPRGN` (all `RGN_*` modes), the EMF+ `SetClipRect` / `SetClipPath` / `SetClipRegion` records (all `CombineMode` values, including nested region-node trees), and clip translation via `EMR_OFFSETCLIPRGN` / EMF+ `OffsetClip`. Subtraction and symmetric difference are expressed through even-odd fill-rule clipping, which Canvas 2D cannot do with plain `clip()` stacking.
97
+ - **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 the gradient axis). Path gradients render as radial gradients from the centre colour to the surrounding colour across the boundary radius.
98
+ - **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 remaining bitwise AND/OR/XOR-family modes are approximated with the nearest arithmetic composite (`difference` / `darken` / `lighten`), combined with pen-colour inversion for the NOT variants.
99
+ - **GDI world transforms**: the scale and translation set by `EMR_SETWORLDTRANSFORM` / `EMR_MODIFYWORLDTRANSFORM` are applied to all GDI drawing, which is required for GDI+-exported EMF files (they record coordinates at 16× sub-pixel precision with a compensating transform). EMF+ records support the full affine transform set.
90
100
 
91
101
  ## Limitations
92
102
 
93
- - **Approximated edge cases in region ops** — all six combine modes are exact while the tracked clip is at most one composable shape (the overwhelmingly common case). When the clip is already an intersection of several shapes, or was set from a live path bracket (`EMR_SELECTCLIPPATH`), `Union` / `Xor` / `Complement` degrade to the nearest conservative approximation (a console log notes when this happens).
94
- - **Bitwise ROP2 modes are arithmetic approximations** — Canvas compositing cannot reproduce true bitwise AND/OR/XOR against the destination, so those modes use `darken` / `lighten` / `difference` stand-ins; only the modes listed above as exact are pixel-faithful.
95
- - **Gradient details** — gradient wrap/tile modes clamp instead of tiling, path gradients are radial approximations of the true boundary-shaped falloff, and texture (image) brushes fall back to solid black.
96
- - **GDI rotation/skew** — rotation and shear components of the *GDI* world transform are ignored (EMF+ transforms are unaffected); plain-GDI metafiles using rotated world transforms are rare.
97
- - **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`.
103
+ - **Approximated edge cases in region ops**: all six combine modes are exact while the tracked clip is at most one composable shape (the overwhelmingly common case). When the clip is already an intersection of several shapes, or was set from a live path bracket (`EMR_SELECTCLIPPATH`), `Union` / `Xor` / `Complement` degrade to the nearest conservative approximation (a console log notes when this happens).
104
+ - **Bitwise ROP2 modes are arithmetic approximations**: Canvas compositing cannot reproduce true bitwise AND/OR/XOR against the destination, so those modes use `darken` / `lighten` / `difference` stand-ins; only the modes listed above as exact are pixel-faithful.
105
+ - **Gradient details**: gradient wrap/tile modes clamp instead of tiling, path gradients are radial approximations of the true boundary-shaped falloff, and texture (image) brushes fall back to solid black.
106
+ - **GDI rotation/skew**: rotation and shear components of the *GDI* world transform are ignored (EMF+ transforms are unaffected); plain-GDI metafiles using rotated world transforms are rare.
107
+ - **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`.
98
108
  - **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.
99
109
 
100
110
  ## License
101
111
 
102
- [Apache-2.0](LICENSE) — free for commercial and closed-source use, with an explicit patent grant.
112
+ [Apache-2.0](LICENSE), free for commercial and closed-source use, with an explicit patent grant.
package/dist/index.d.mts CHANGED
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Public API — the two entry points consumed by the rest of the application.
2
+ * Public API, a single auto-detecting entry point consumed by the rest of
3
+ * the application.
3
4
  *
4
5
  * The conversion pipeline for both formats follows the same high-level steps:
5
- * 1. Parse the file header to determine logical bounds and canvas dimensions.
6
- * 2. Create an in-memory canvas (OffscreenCanvas preferred, HTMLCanvasElement fallback).
6
+ * 1. Parse the file header to determine logical bounds and canvas dimensions
7
+ * (tried as EMF first, then WMF, to auto-detect the format).
8
+ * 2. Create an in-memory canvas (OffscreenCanvas preferred, HTMLCanvasElement
9
+ * fallback, or the optional `@napi-rs/canvas` Node.js backend).
7
10
  * 3. Replay every metafile record onto the canvas context in order.
8
- * 4. Resolve "deferred images" — bitmap / embedded-metafile draws that require
9
- * async image decoding (via {@link createImageBitmap}).
10
- * 5. Export the canvas contents as a `data:image/png;base64,…` URL.
11
+ * 4. Resolve "deferred images", bitmap / embedded-metafile draws that require
12
+ * async image decoding (via {@link createImageBitmap} or, in Node.js, the
13
+ * `@napi-rs/canvas` `loadImage` helper).
14
+ * 5. Export the canvas contents as a `data:image/png;base64,...` URL.
11
15
  *
12
16
  * @module emf-converter
13
17
  */
@@ -44,50 +48,39 @@ interface EmfConvertOptions {
44
48
  fontFamilyMap?: Record<string, string>;
45
49
  }
46
50
  /**
47
- * Converts an EMF (Enhanced Metafile) binary buffer to a PNG data-URL string
48
- * by parsing the EMF header, iterating over all EMR records, and replaying
49
- * them onto an in-memory canvas. Embedded EMF+ (GDI+) records found inside
50
- * EMR_COMMENT payloads are handled transparently.
51
+ * Converts an EMF (Enhanced Metafile) or WMF (Windows Metafile) binary buffer
52
+ * to a PNG data-URL string, auto-detecting which of the two formats it is.
51
53
  *
52
- * The canvas is rendered at a configurable DPI scale (default 2x) to produce
53
- * sharper output when displayed at CSS logical-pixel sizes. This is important
54
- * for presentations viewed on HiDPI/Retina displays.
55
- *
56
- * Returns `null` when:
57
- * - The buffer does not begin with a valid EMR_HEADER record.
58
- * - The logical bounds are zero-sized or negative.
59
- * - No canvas API is available (e.g. headless test environment).
54
+ * Detection tries the EMF path first: {@link parseEmfHeader} is a cheap,
55
+ * side-effect-free probe that returns `null` immediately unless the buffer
56
+ * starts with a valid `EMR_HEADER` record, so it doubles as a safe format
57
+ * sniff. When that probe fails, the buffer is parsed as WMF instead.
60
58
  *
61
- * @param buffer - The raw EMF file bytes.
62
- * @param options - Optional {@link EmfConvertOptions} controlling output size,
63
- * DPI scale, record limits, and font mapping.
64
- * @returns A `data:image/png;base64,…` string, or `null` on failure.
65
- */
66
- declare function convertEmfToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
67
- /**
68
- * Converts a WMF (Windows Metafile) binary buffer to a PNG data-URL string.
59
+ * For EMF: parses the EMF header, iterates over all EMR records, and replays
60
+ * them onto an in-memory canvas. Embedded EMF+ (GDI+) records found inside
61
+ * EMR_COMMENT payloads are handled transparently.
69
62
  *
70
- * WMF is the older 16-bit metafile format with simpler record types than EMF.
71
- * This function parses the optional Aldus placeable header, the WMF header,
72
- * and then replays all META_* records onto a canvas.
63
+ * For WMF: parses the optional Aldus placeable header and the standard WMF
64
+ * header, then replays all META_* records onto a canvas.
73
65
  *
74
- * The canvas is rendered at a configurable DPI scale (default 2x) for
75
- * sharper output on HiDPI displays.
66
+ * The canvas is rendered at a configurable DPI scale (default 1x, 1:1 pixel
67
+ * mapping) via {@link EmfConvertOptions.dpiScale}. On the first call in a
68
+ * given process, the optional Node.js canvas backend (`@napi-rs/canvas`) is
69
+ * loaded once and cached; see {@link ensureNodeCanvasModule}.
76
70
  *
77
71
  * Returns `null` when:
78
- * - The header cannot be parsed or reports invalid dimensions.
79
- * - No canvas API is available.
72
+ * - The buffer matches neither a valid EMF header nor a valid WMF header.
73
+ * - The logical bounds are zero-sized or negative.
74
+ * - No canvas API is available (browser/worker canvas, or the optional
75
+ * `@napi-rs/canvas` package in plain Node.js).
80
76
  *
81
- * @param buffer - The raw WMF file bytes.
77
+ * @param buffer - The raw EMF or WMF file bytes.
82
78
  * @param options - Optional {@link EmfConvertOptions} controlling output size,
83
79
  * DPI scale, record limits, and font mapping.
84
- * @returns A `data:image/png;base64,…` string, or `null` on failure.
85
- */
86
- declare function convertWmfToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
87
-
88
- /**
89
- * Canvas creation, styling, string reading, stock objects, and export helpers.
80
+ * @param recursionDepth - Internal: tracks nested embedded-metafile depth.
81
+ * @returns A `data:image/png;base64,...` string, or `null` on failure.
90
82
  */
83
+ declare function convertMetafileToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
91
84
 
92
85
  /**
93
86
  * The default DPI scale factor used for EMF/WMF rendering.
@@ -96,4 +89,4 @@ declare function convertWmfToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOp
96
89
  */
97
90
  declare const DEFAULT_DPI_SCALE = 1;
98
91
 
99
- export { DEFAULT_DPI_SCALE, type EmfConvertOptions, convertEmfToDataUrl, convertWmfToDataUrl };
92
+ export { DEFAULT_DPI_SCALE, type EmfConvertOptions, convertMetafileToDataUrl };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Public API — the two entry points consumed by the rest of the application.
2
+ * Public API, a single auto-detecting entry point consumed by the rest of
3
+ * the application.
3
4
  *
4
5
  * The conversion pipeline for both formats follows the same high-level steps:
5
- * 1. Parse the file header to determine logical bounds and canvas dimensions.
6
- * 2. Create an in-memory canvas (OffscreenCanvas preferred, HTMLCanvasElement fallback).
6
+ * 1. Parse the file header to determine logical bounds and canvas dimensions
7
+ * (tried as EMF first, then WMF, to auto-detect the format).
8
+ * 2. Create an in-memory canvas (OffscreenCanvas preferred, HTMLCanvasElement
9
+ * fallback, or the optional `@napi-rs/canvas` Node.js backend).
7
10
  * 3. Replay every metafile record onto the canvas context in order.
8
- * 4. Resolve "deferred images" — bitmap / embedded-metafile draws that require
9
- * async image decoding (via {@link createImageBitmap}).
10
- * 5. Export the canvas contents as a `data:image/png;base64,…` URL.
11
+ * 4. Resolve "deferred images", bitmap / embedded-metafile draws that require
12
+ * async image decoding (via {@link createImageBitmap} or, in Node.js, the
13
+ * `@napi-rs/canvas` `loadImage` helper).
14
+ * 5. Export the canvas contents as a `data:image/png;base64,...` URL.
11
15
  *
12
16
  * @module emf-converter
13
17
  */
@@ -44,50 +48,39 @@ interface EmfConvertOptions {
44
48
  fontFamilyMap?: Record<string, string>;
45
49
  }
46
50
  /**
47
- * Converts an EMF (Enhanced Metafile) binary buffer to a PNG data-URL string
48
- * by parsing the EMF header, iterating over all EMR records, and replaying
49
- * them onto an in-memory canvas. Embedded EMF+ (GDI+) records found inside
50
- * EMR_COMMENT payloads are handled transparently.
51
+ * Converts an EMF (Enhanced Metafile) or WMF (Windows Metafile) binary buffer
52
+ * to a PNG data-URL string, auto-detecting which of the two formats it is.
51
53
  *
52
- * The canvas is rendered at a configurable DPI scale (default 2x) to produce
53
- * sharper output when displayed at CSS logical-pixel sizes. This is important
54
- * for presentations viewed on HiDPI/Retina displays.
55
- *
56
- * Returns `null` when:
57
- * - The buffer does not begin with a valid EMR_HEADER record.
58
- * - The logical bounds are zero-sized or negative.
59
- * - No canvas API is available (e.g. headless test environment).
54
+ * Detection tries the EMF path first: {@link parseEmfHeader} is a cheap,
55
+ * side-effect-free probe that returns `null` immediately unless the buffer
56
+ * starts with a valid `EMR_HEADER` record, so it doubles as a safe format
57
+ * sniff. When that probe fails, the buffer is parsed as WMF instead.
60
58
  *
61
- * @param buffer - The raw EMF file bytes.
62
- * @param options - Optional {@link EmfConvertOptions} controlling output size,
63
- * DPI scale, record limits, and font mapping.
64
- * @returns A `data:image/png;base64,…` string, or `null` on failure.
65
- */
66
- declare function convertEmfToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
67
- /**
68
- * Converts a WMF (Windows Metafile) binary buffer to a PNG data-URL string.
59
+ * For EMF: parses the EMF header, iterates over all EMR records, and replays
60
+ * them onto an in-memory canvas. Embedded EMF+ (GDI+) records found inside
61
+ * EMR_COMMENT payloads are handled transparently.
69
62
  *
70
- * WMF is the older 16-bit metafile format with simpler record types than EMF.
71
- * This function parses the optional Aldus placeable header, the WMF header,
72
- * and then replays all META_* records onto a canvas.
63
+ * For WMF: parses the optional Aldus placeable header and the standard WMF
64
+ * header, then replays all META_* records onto a canvas.
73
65
  *
74
- * The canvas is rendered at a configurable DPI scale (default 2x) for
75
- * sharper output on HiDPI displays.
66
+ * The canvas is rendered at a configurable DPI scale (default 1x, 1:1 pixel
67
+ * mapping) via {@link EmfConvertOptions.dpiScale}. On the first call in a
68
+ * given process, the optional Node.js canvas backend (`@napi-rs/canvas`) is
69
+ * loaded once and cached; see {@link ensureNodeCanvasModule}.
76
70
  *
77
71
  * Returns `null` when:
78
- * - The header cannot be parsed or reports invalid dimensions.
79
- * - No canvas API is available.
72
+ * - The buffer matches neither a valid EMF header nor a valid WMF header.
73
+ * - The logical bounds are zero-sized or negative.
74
+ * - No canvas API is available (browser/worker canvas, or the optional
75
+ * `@napi-rs/canvas` package in plain Node.js).
80
76
  *
81
- * @param buffer - The raw WMF file bytes.
77
+ * @param buffer - The raw EMF or WMF file bytes.
82
78
  * @param options - Optional {@link EmfConvertOptions} controlling output size,
83
79
  * DPI scale, record limits, and font mapping.
84
- * @returns A `data:image/png;base64,…` string, or `null` on failure.
85
- */
86
- declare function convertWmfToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
87
-
88
- /**
89
- * Canvas creation, styling, string reading, stock objects, and export helpers.
80
+ * @param recursionDepth - Internal: tracks nested embedded-metafile depth.
81
+ * @returns A `data:image/png;base64,...` string, or `null` on failure.
90
82
  */
83
+ declare function convertMetafileToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOptions, recursionDepth?: number): Promise<string | null>;
91
84
 
92
85
  /**
93
86
  * The default DPI scale factor used for EMF/WMF rendering.
@@ -96,4 +89,4 @@ declare function convertWmfToDataUrl(buffer: ArrayBuffer, options?: EmfConvertOp
96
89
  */
97
90
  declare const DEFAULT_DPI_SCALE = 1;
98
91
 
99
- export { DEFAULT_DPI_SCALE, type EmfConvertOptions, convertEmfToDataUrl, convertWmfToDataUrl };
92
+ export { DEFAULT_DPI_SCALE, type EmfConvertOptions, convertMetafileToDataUrl };
package/dist/index.js CHANGED
@@ -230,6 +230,26 @@ var emfWarn = (...args) => {
230
230
  };
231
231
 
232
232
  // src/emf-canvas-helpers.ts
233
+ var nodeCanvasModule;
234
+ async function ensureNodeCanvasModule() {
235
+ if (nodeCanvasModule !== void 0) {
236
+ return nodeCanvasModule;
237
+ }
238
+ if (typeof OffscreenCanvas !== "undefined" || typeof document !== "undefined") {
239
+ nodeCanvasModule = null;
240
+ return nodeCanvasModule;
241
+ }
242
+ if (typeof process === "undefined" || !process.versions?.node) {
243
+ nodeCanvasModule = null;
244
+ return nodeCanvasModule;
245
+ }
246
+ try {
247
+ nodeCanvasModule = await import('@napi-rs/canvas');
248
+ } catch {
249
+ nodeCanvasModule = null;
250
+ }
251
+ return nodeCanvasModule;
252
+ }
233
253
  var DEFAULT_DPI_SCALE = 1;
234
254
  function createCanvas(width, height, maxWidth, maxHeight, dpiScale = DEFAULT_DPI_SCALE, maxCanvasDimension = MAX_CANVAS_DIMENSION) {
235
255
  const effectiveScale = Math.max(1, Math.min(dpiScale, 4));
@@ -266,30 +286,44 @@ function createCanvas(width, height, maxWidth, maxHeight, dpiScale = DEFAULT_DPI
266
286
  emfLog(
267
287
  `createCanvas: using OffscreenCanvas ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
268
288
  );
269
- const canvas2 = new OffscreenCanvas(w, h);
270
- const ctx2 = canvas2.getContext("2d");
271
- if (!ctx2) {
289
+ const canvas = new OffscreenCanvas(w, h);
290
+ const ctx = canvas.getContext("2d");
291
+ if (!ctx) {
272
292
  emfWarn('createCanvas: OffscreenCanvas.getContext("2d") returned null');
273
293
  return null;
274
294
  }
275
- return { canvas: canvas2, ctx: ctx2, scaleX, scaleY };
295
+ return { canvas, ctx, scaleX, scaleY };
276
296
  }
277
- if (typeof document === "undefined") {
278
- emfWarn("createCanvas: no OffscreenCanvas and no document \u2014 cannot create canvas");
279
- return null;
297
+ if (typeof document !== "undefined") {
298
+ emfLog(
299
+ `createCanvas: using HTMLCanvasElement ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
300
+ );
301
+ const canvas = document.createElement("canvas");
302
+ canvas.width = w;
303
+ canvas.height = h;
304
+ const ctx = canvas.getContext("2d");
305
+ if (!ctx) {
306
+ emfWarn('createCanvas: HTMLCanvasElement.getContext("2d") returned null');
307
+ return null;
308
+ }
309
+ return { canvas, ctx, scaleX, scaleY };
280
310
  }
281
- emfLog(
282
- `createCanvas: using HTMLCanvasElement ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
283
- );
284
- const canvas = document.createElement("canvas");
285
- canvas.width = w;
286
- canvas.height = h;
287
- const ctx = canvas.getContext("2d");
288
- if (!ctx) {
289
- emfWarn('createCanvas: HTMLCanvasElement.getContext("2d") returned null');
290
- return null;
311
+ if (nodeCanvasModule) {
312
+ emfLog(
313
+ `createCanvas: using @napi-rs/canvas ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
314
+ );
315
+ const canvas = nodeCanvasModule.createCanvas(w, h);
316
+ const ctx = canvas.getContext("2d");
317
+ if (!ctx) {
318
+ emfWarn('createCanvas: @napi-rs/canvas getContext("2d") returned null');
319
+ return null;
320
+ }
321
+ return { canvas, ctx, scaleX, scaleY };
291
322
  }
292
- return { canvas, ctx, scaleX, scaleY };
323
+ emfWarn(
324
+ "createCanvas: no OffscreenCanvas, no document, and no @napi-rs/canvas, cannot create canvas"
325
+ );
326
+ return null;
293
327
  } catch (err) {
294
328
  return null;
295
329
  }
@@ -318,6 +352,14 @@ function createTempCanvas(width, height) {
318
352
  }
319
353
  return { canvas, ctx };
320
354
  }
355
+ if (nodeCanvasModule) {
356
+ const canvas = nodeCanvasModule.createCanvas(width, height);
357
+ const ctx = canvas.getContext("2d");
358
+ if (!ctx) {
359
+ return null;
360
+ }
361
+ return { canvas, ctx };
362
+ }
321
363
  return null;
322
364
  }
323
365
  function rop2Paint(rop2) {
@@ -551,8 +593,31 @@ async function exportCanvasToPngDataUrl(canvas) {
551
593
  );
552
594
  return canvas.toDataURL("image/png");
553
595
  }
596
+ if (nodeCanvasModule && canvas instanceof nodeCanvasModule.Canvas) {
597
+ emfLog(
598
+ `exportCanvasToPngDataUrl: using @napi-rs/canvas toDataURLAsync (${canvas.width}\xD7${canvas.height})`
599
+ );
600
+ return canvas.toDataURLAsync();
601
+ }
554
602
  return null;
555
603
  }
604
+ async function decodeDeferredImageBytes(bytes, mime) {
605
+ if (nodeCanvasModule) {
606
+ const image = await nodeCanvasModule.loadImage(new Uint8Array(bytes));
607
+ return { drawable: image, width: image.width, height: image.height, close: () => {
608
+ } };
609
+ }
610
+ if (typeof createImageBitmap !== "function") {
611
+ return null;
612
+ }
613
+ const blob = mime !== void 0 ? new Blob([bytes], { type: mime }) : new Blob([bytes]);
614
+ const bitmap = await createImageBitmap(blob);
615
+ return { drawable: bitmap, width: bitmap.width, height: bitmap.height, close: () => bitmap.close() };
616
+ }
617
+ function canvasDrawImage(ctx, image, dx, dy, dw, dh) {
618
+ const draw = ctx.drawImage;
619
+ draw.call(ctx, image, dx, dy, dw, dh);
620
+ }
556
621
 
557
622
  // src/emf-header-parser.ts
558
623
  function parseEmfHeader(view) {
@@ -1470,7 +1535,8 @@ function handleBitBlt(rCtx, offset, dataOff, recSize) {
1470
1535
  const temp = createTempCanvas(imageData.width, imageData.height);
1471
1536
  if (temp) {
1472
1537
  temp.ctx.putImageData(imageData, 0, 0);
1473
- ctx.drawImage(
1538
+ canvasDrawImage(
1539
+ ctx,
1474
1540
  temp.canvas,
1475
1541
  gmx(rCtx, dstX),
1476
1542
  gmy(rCtx, dstY),
@@ -1505,7 +1571,8 @@ function handleStretchDibits(rCtx, offset, dataOff, recSize) {
1505
1571
  const temp = createTempCanvas(imageData.width, imageData.height);
1506
1572
  if (temp) {
1507
1573
  temp.ctx.putImageData(imageData, 0, 0);
1508
- ctx.drawImage(
1574
+ canvasDrawImage(
1575
+ ctx,
1509
1576
  temp.canvas,
1510
1577
  gmx(rCtx, dstX),
1511
1578
  gmy(rCtx, dstY),
@@ -4863,12 +4930,16 @@ async function processDeferredImages(ctx, deferredImages, recursionDepth = 0) {
4863
4930
  if (img.isMetafile) {
4864
4931
  if (recursionDepth >= MAX_METAFILE_RECURSION) {
4865
4932
  emfWarn(
4866
- ` Deferred image [${idx}]: skipping embedded metafile \u2014 recursion depth ${recursionDepth} >= ${MAX_METAFILE_RECURSION}`
4933
+ ` Deferred image [${idx}]: skipping embedded metafile, recursion depth ${recursionDepth} >= ${MAX_METAFILE_RECURSION}`
4867
4934
  );
4868
4935
  continue;
4869
4936
  }
4870
4937
  emfLog(` Deferred image [${idx}]: recursively converting embedded metafile...`);
4871
- const metafileDataUrl = await convertEmfToDataUrl(plainBuffer, void 0, recursionDepth + 1) ?? await convertWmfToDataUrl(plainBuffer, void 0, recursionDepth + 1);
4938
+ const metafileDataUrl = await convertMetafileToDataUrl(
4939
+ plainBuffer,
4940
+ void 0,
4941
+ recursionDepth + 1
4942
+ );
4872
4943
  if (metafileDataUrl) {
4873
4944
  emfLog(
4874
4945
  ` Deferred image [${idx}]: metafile converted, dataUrl length=${metafileDataUrl.length}`
@@ -4881,26 +4952,28 @@ async function processDeferredImages(ctx, deferredImages, recursionDepth = 0) {
4881
4952
  for (let i = 0; i < byteString.length; i++) {
4882
4953
  ia[i] = byteString.charCodeAt(i);
4883
4954
  }
4884
- const metaBlob = new Blob([ab], { type: mime });
4885
- emfLog(
4886
- ` Deferred image [${idx}]: creating ImageBitmap from ${metaBlob.size} byte blob (${mime})...`
4887
- );
4888
- const bitmap = await createImageBitmap(metaBlob);
4889
- emfLog(` Deferred image [${idx}]: ImageBitmap created ${bitmap.width}\xD7${bitmap.height}`);
4890
- ctx.drawImage(bitmap, img.dx, img.dy, img.dw, img.dh);
4891
- bitmap.close();
4955
+ emfLog(` Deferred image [${idx}]: decoding ${ab.byteLength} byte image (${mime})...`);
4956
+ const decoded = await decodeDeferredImageBytes(ab, mime);
4957
+ if (decoded) {
4958
+ emfLog(` Deferred image [${idx}]: decoded ${decoded.width}\xD7${decoded.height}`);
4959
+ canvasDrawImage(ctx, decoded.drawable, img.dx, img.dy, img.dw, img.dh);
4960
+ decoded.close();
4961
+ } else {
4962
+ emfWarn(` Deferred image [${idx}]: no image decoder available`);
4963
+ }
4892
4964
  } else {
4893
4965
  emfWarn(` Deferred image [${idx}]: metafile conversion returned null`);
4894
4966
  }
4895
4967
  } else {
4896
- emfLog(
4897
- ` Deferred image [${idx}]: creating ImageBitmap from ${plainBuffer.byteLength} byte blob...`
4898
- );
4899
- const blob = new Blob([plainBuffer]);
4900
- const bitmap = await createImageBitmap(blob);
4901
- emfLog(` Deferred image [${idx}]: ImageBitmap created ${bitmap.width}\xD7${bitmap.height}`);
4902
- ctx.drawImage(bitmap, img.dx, img.dy, img.dw, img.dh);
4903
- bitmap.close();
4968
+ emfLog(` Deferred image [${idx}]: decoding ${plainBuffer.byteLength} byte image...`);
4969
+ const decoded = await decodeDeferredImageBytes(plainBuffer);
4970
+ if (decoded) {
4971
+ emfLog(` Deferred image [${idx}]: decoded ${decoded.width}\xD7${decoded.height}`);
4972
+ canvasDrawImage(ctx, decoded.drawable, img.dx, img.dy, img.dw, img.dh);
4973
+ decoded.close();
4974
+ } else {
4975
+ emfWarn(` Deferred image [${idx}]: no image decoder available`);
4976
+ }
4904
4977
  }
4905
4978
  } catch (imgErr) {
4906
4979
  imgErr instanceof Error ? imgErr.message : String(imgErr);
@@ -4913,10 +4986,35 @@ async function processDeferredImages(ctx, deferredImages, recursionDepth = 0) {
4913
4986
  }
4914
4987
  ctx.setTransform(1, 0, 0, 1, 0, 0);
4915
4988
  }
4916
- async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4989
+ async function convertMetafileToDataUrl(buffer, options, recursionDepth = 0) {
4990
+ await ensureNodeCanvasModule();
4917
4991
  if (recursionDepth > MAX_METAFILE_RECURSION) {
4918
4992
  return null;
4919
4993
  }
4994
+ emfLog(`convertMetafileToDataUrl: input buffer ${buffer.byteLength} bytes`);
4995
+ if (buffer.byteLength >= 16) {
4996
+ const hdrBytes = new Uint8Array(buffer, 0, 16);
4997
+ emfLog(
4998
+ `convertMetafileToDataUrl: first 16 bytes: [${Array.from(hdrBytes).map((b) => b.toString(16).padStart(2, "0")).join(" ")}]`
4999
+ );
5000
+ }
5001
+ const view = new DataView(buffer);
5002
+ let emfHeader = null;
5003
+ try {
5004
+ emfHeader = parseEmfHeader(view);
5005
+ } catch (err) {
5006
+ emfWarn(
5007
+ "convertMetafileToDataUrl: parseEmfHeader threw during detection:",
5008
+ err instanceof Error ? err.message : err
5009
+ );
5010
+ emfHeader = null;
5011
+ }
5012
+ if (emfHeader) {
5013
+ return convertEmfInternal(buffer, view, emfHeader, options, recursionDepth);
5014
+ }
5015
+ return convertWmfInternal(buffer, view, options);
5016
+ }
5017
+ async function convertEmfInternal(buffer, view, header, options, recursionDepth) {
4920
5018
  const opts = options ?? {};
4921
5019
  const dpiScale = opts.dpiScale ?? DEFAULT_DPI_SCALE;
4922
5020
  const effectiveMaxWidth = opts.maxWidth;
@@ -4927,30 +5025,18 @@ async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4927
5025
  fontFamilyMap: opts.fontFamilyMap
4928
5026
  };
4929
5027
  try {
4930
- emfLog("=== convertEmfToDataUrl START ===");
5028
+ emfLog("=== convertEmfInternal START ===");
4931
5029
  emfLog(
4932
5030
  `Input buffer: ${buffer.byteLength} bytes, maxWidth=${effectiveMaxWidth}, maxHeight=${effectiveMaxHeight}, dpiScale=${dpiScale}`
4933
5031
  );
4934
- if (buffer.byteLength >= 16) {
4935
- const hdrBytes = new Uint8Array(buffer, 0, 16);
4936
- emfLog(
4937
- `First 16 bytes: [${Array.from(hdrBytes).map((b) => b.toString(16).padStart(2, "0")).join(" ")}]`
4938
- );
4939
- }
4940
- const view = new DataView(buffer);
4941
- const header = parseEmfHeader(view);
4942
- if (!header) {
4943
- emfLog("convertEmfToDataUrl: parseEmfHeader returned null \u2014 returning null");
4944
- return null;
4945
- }
4946
5032
  const renderBounds = getRenderableEmfBounds(header);
4947
5033
  if (!renderBounds) {
4948
- emfLog("convertEmfToDataUrl: getRenderableEmfBounds returned null \u2014 returning null");
5034
+ emfLog("convertEmfInternal: getRenderableEmfBounds returned null, returning null");
4949
5035
  return null;
4950
5036
  }
4951
5037
  const logicalW = renderBounds.right - renderBounds.left;
4952
5038
  const logicalH = renderBounds.bottom - renderBounds.top;
4953
- emfLog(`convertEmfToDataUrl: logicalSize=${logicalW}\xD7${logicalH}`);
5039
+ emfLog(`convertEmfInternal: logicalSize=${logicalW}\xD7${logicalH}`);
4954
5040
  const setup = createCanvas(
4955
5041
  logicalW,
4956
5042
  logicalH,
@@ -4960,15 +5046,15 @@ async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4960
5046
  opts.maxCanvasDimension
4961
5047
  );
4962
5048
  if (!setup) {
4963
- emfLog("convertEmfToDataUrl: createCanvas returned null \u2014 returning null");
5049
+ emfLog("convertEmfInternal: createCanvas returned null, returning null");
4964
5050
  return null;
4965
5051
  }
4966
5052
  const { canvas, ctx } = setup;
4967
5053
  emfLog(
4968
- `convertEmfToDataUrl: canvas created ${canvas.width}\xD7${canvas.height} (dpiScale=${dpiScale})`
5054
+ `convertEmfInternal: canvas created ${canvas.width}\xD7${canvas.height} (dpiScale=${dpiScale})`
4969
5055
  );
4970
5056
  ctx.save();
4971
- emfLog("convertEmfToDataUrl: starting replayEmfRecords...");
5057
+ emfLog("convertEmfInternal: starting replayEmfRecords...");
4972
5058
  const deferredImages = replayEmfRecords(
4973
5059
  view,
4974
5060
  ctx,
@@ -4978,30 +5064,25 @@ async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4978
5064
  dpiScale,
4979
5065
  replayOptions
4980
5066
  );
4981
- emfLog(
4982
- `convertEmfToDataUrl: replayEmfRecords returned ${deferredImages.length} deferred images`
4983
- );
5067
+ emfLog(`convertEmfInternal: replayEmfRecords returned ${deferredImages.length} deferred images`);
4984
5068
  ctx.restore();
4985
- await processDeferredImages(ctx, deferredImages);
4986
- emfLog("convertEmfToDataUrl: exporting canvas to PNG data URL...");
5069
+ await processDeferredImages(ctx, deferredImages, recursionDepth);
5070
+ emfLog("convertEmfInternal: exporting canvas to PNG data URL...");
4987
5071
  const result = await exportCanvasToPngDataUrl(canvas);
4988
5072
  if (result) {
4989
- emfLog(`convertEmfToDataUrl: SUCCESS \u2014 data URL length=${result.length}`);
5073
+ emfLog(`convertEmfInternal: SUCCESS, data URL length=${result.length}`);
4990
5074
  } else {
4991
- emfWarn("convertEmfToDataUrl: exportCanvasToPngDataUrl returned null");
5075
+ emfWarn("convertEmfInternal: exportCanvasToPngDataUrl returned null");
4992
5076
  }
4993
- emfLog("=== convertEmfToDataUrl END ===");
5077
+ emfLog("=== convertEmfInternal END ===");
4994
5078
  return result;
4995
5079
  } catch (err) {
4996
- emfWarn("convertEmfToDataUrl: EXCEPTION:", err instanceof Error ? err.message : err);
4997
- console.warn("[pptx-editor] EMF conversion failed:", err instanceof Error ? err.message : err);
5080
+ emfWarn("convertEmfInternal: EXCEPTION:", err instanceof Error ? err.message : err);
5081
+ console.warn("[emf-converter] EMF conversion failed:", err instanceof Error ? err.message : err);
4998
5082
  return null;
4999
5083
  }
5000
5084
  }
5001
- async function convertWmfToDataUrl(buffer, options, recursionDepth = 0) {
5002
- if (recursionDepth > MAX_METAFILE_RECURSION) {
5003
- return null;
5004
- }
5085
+ async function convertWmfInternal(buffer, view, options, recursionDepth) {
5005
5086
  const opts = options ?? {};
5006
5087
  const dpiScale = opts.dpiScale ?? DEFAULT_DPI_SCALE;
5007
5088
  const effectiveMaxWidth = opts.maxWidth;
@@ -5012,20 +5093,19 @@ async function convertWmfToDataUrl(buffer, options, recursionDepth = 0) {
5012
5093
  };
5013
5094
  try {
5014
5095
  emfLog(
5015
- "=== convertWmfToDataUrl START ===",
5096
+ "=== convertWmfInternal START ===",
5016
5097
  `buffer=${buffer.byteLength} bytes, dpiScale=${dpiScale}`
5017
5098
  );
5018
- const view = new DataView(buffer);
5019
5099
  const header = parseWmfHeader(view);
5020
5100
  if (!header) {
5021
- emfLog("convertWmfToDataUrl: parseWmfHeader returned null");
5101
+ emfLog("convertWmfInternal: parseWmfHeader returned null");
5022
5102
  return null;
5023
5103
  }
5024
5104
  const logicalW = header.boundsRight - header.boundsLeft;
5025
5105
  const logicalH = header.boundsBottom - header.boundsTop;
5026
- emfLog(`convertWmfToDataUrl: logicalSize=${logicalW}\xD7${logicalH}`);
5106
+ emfLog(`convertWmfInternal: logicalSize=${logicalW}\xD7${logicalH}`);
5027
5107
  if (logicalW <= 0 || logicalH <= 0) {
5028
- emfLog("convertWmfToDataUrl: invalid dimensions \u2014 returning null");
5108
+ emfLog("convertWmfInternal: invalid dimensions, returning null");
5029
5109
  return null;
5030
5110
  }
5031
5111
  const setup = createCanvas(
@@ -5044,16 +5124,15 @@ async function convertWmfToDataUrl(buffer, options, recursionDepth = 0) {
5044
5124
  replayWmfRecords(view, ctx, header, canvas.width, canvas.height, replayOptions);
5045
5125
  ctx.restore();
5046
5126
  const result = await exportCanvasToPngDataUrl(canvas);
5047
- emfLog(`convertWmfToDataUrl: result=${result ? `dataUrl len=${result.length}` : "null"}`);
5048
- emfLog("=== convertWmfToDataUrl END ===");
5127
+ emfLog(`convertWmfInternal: result=${result ? `dataUrl len=${result.length}` : "null"}`);
5128
+ emfLog("=== convertWmfInternal END ===");
5049
5129
  return result;
5050
5130
  } catch (err) {
5051
- emfWarn("convertWmfToDataUrl: EXCEPTION:", err instanceof Error ? err.message : err);
5052
- console.warn("[pptx-editor] WMF conversion failed:", err instanceof Error ? err.message : err);
5131
+ emfWarn("convertWmfInternal: EXCEPTION:", err instanceof Error ? err.message : err);
5132
+ console.warn("[emf-converter] WMF conversion failed:", err instanceof Error ? err.message : err);
5053
5133
  return null;
5054
5134
  }
5055
5135
  }
5056
5136
 
5057
5137
  exports.DEFAULT_DPI_SCALE = DEFAULT_DPI_SCALE;
5058
- exports.convertEmfToDataUrl = convertEmfToDataUrl;
5059
- exports.convertWmfToDataUrl = convertWmfToDataUrl;
5138
+ exports.convertMetafileToDataUrl = convertMetafileToDataUrl;
package/dist/index.mjs CHANGED
@@ -228,6 +228,26 @@ var emfWarn = (...args) => {
228
228
  };
229
229
 
230
230
  // src/emf-canvas-helpers.ts
231
+ var nodeCanvasModule;
232
+ async function ensureNodeCanvasModule() {
233
+ if (nodeCanvasModule !== void 0) {
234
+ return nodeCanvasModule;
235
+ }
236
+ if (typeof OffscreenCanvas !== "undefined" || typeof document !== "undefined") {
237
+ nodeCanvasModule = null;
238
+ return nodeCanvasModule;
239
+ }
240
+ if (typeof process === "undefined" || !process.versions?.node) {
241
+ nodeCanvasModule = null;
242
+ return nodeCanvasModule;
243
+ }
244
+ try {
245
+ nodeCanvasModule = await import('@napi-rs/canvas');
246
+ } catch {
247
+ nodeCanvasModule = null;
248
+ }
249
+ return nodeCanvasModule;
250
+ }
231
251
  var DEFAULT_DPI_SCALE = 1;
232
252
  function createCanvas(width, height, maxWidth, maxHeight, dpiScale = DEFAULT_DPI_SCALE, maxCanvasDimension = MAX_CANVAS_DIMENSION) {
233
253
  const effectiveScale = Math.max(1, Math.min(dpiScale, 4));
@@ -264,30 +284,44 @@ function createCanvas(width, height, maxWidth, maxHeight, dpiScale = DEFAULT_DPI
264
284
  emfLog(
265
285
  `createCanvas: using OffscreenCanvas ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
266
286
  );
267
- const canvas2 = new OffscreenCanvas(w, h);
268
- const ctx2 = canvas2.getContext("2d");
269
- if (!ctx2) {
287
+ const canvas = new OffscreenCanvas(w, h);
288
+ const ctx = canvas.getContext("2d");
289
+ if (!ctx) {
270
290
  emfWarn('createCanvas: OffscreenCanvas.getContext("2d") returned null');
271
291
  return null;
272
292
  }
273
- return { canvas: canvas2, ctx: ctx2, scaleX, scaleY };
293
+ return { canvas, ctx, scaleX, scaleY };
274
294
  }
275
- if (typeof document === "undefined") {
276
- emfWarn("createCanvas: no OffscreenCanvas and no document \u2014 cannot create canvas");
277
- return null;
295
+ if (typeof document !== "undefined") {
296
+ emfLog(
297
+ `createCanvas: using HTMLCanvasElement ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
298
+ );
299
+ const canvas = document.createElement("canvas");
300
+ canvas.width = w;
301
+ canvas.height = h;
302
+ const ctx = canvas.getContext("2d");
303
+ if (!ctx) {
304
+ emfWarn('createCanvas: HTMLCanvasElement.getContext("2d") returned null');
305
+ return null;
306
+ }
307
+ return { canvas, ctx, scaleX, scaleY };
278
308
  }
279
- emfLog(
280
- `createCanvas: using HTMLCanvasElement ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
281
- );
282
- const canvas = document.createElement("canvas");
283
- canvas.width = w;
284
- canvas.height = h;
285
- const ctx = canvas.getContext("2d");
286
- if (!ctx) {
287
- emfWarn('createCanvas: HTMLCanvasElement.getContext("2d") returned null');
288
- return null;
309
+ if (nodeCanvasModule) {
310
+ emfLog(
311
+ `createCanvas: using @napi-rs/canvas ${w}\xD7${h}, scale=(${scaleX.toFixed(3)},${scaleY.toFixed(3)})`
312
+ );
313
+ const canvas = nodeCanvasModule.createCanvas(w, h);
314
+ const ctx = canvas.getContext("2d");
315
+ if (!ctx) {
316
+ emfWarn('createCanvas: @napi-rs/canvas getContext("2d") returned null');
317
+ return null;
318
+ }
319
+ return { canvas, ctx, scaleX, scaleY };
289
320
  }
290
- return { canvas, ctx, scaleX, scaleY };
321
+ emfWarn(
322
+ "createCanvas: no OffscreenCanvas, no document, and no @napi-rs/canvas, cannot create canvas"
323
+ );
324
+ return null;
291
325
  } catch (err) {
292
326
  return null;
293
327
  }
@@ -316,6 +350,14 @@ function createTempCanvas(width, height) {
316
350
  }
317
351
  return { canvas, ctx };
318
352
  }
353
+ if (nodeCanvasModule) {
354
+ const canvas = nodeCanvasModule.createCanvas(width, height);
355
+ const ctx = canvas.getContext("2d");
356
+ if (!ctx) {
357
+ return null;
358
+ }
359
+ return { canvas, ctx };
360
+ }
319
361
  return null;
320
362
  }
321
363
  function rop2Paint(rop2) {
@@ -549,8 +591,31 @@ async function exportCanvasToPngDataUrl(canvas) {
549
591
  );
550
592
  return canvas.toDataURL("image/png");
551
593
  }
594
+ if (nodeCanvasModule && canvas instanceof nodeCanvasModule.Canvas) {
595
+ emfLog(
596
+ `exportCanvasToPngDataUrl: using @napi-rs/canvas toDataURLAsync (${canvas.width}\xD7${canvas.height})`
597
+ );
598
+ return canvas.toDataURLAsync();
599
+ }
552
600
  return null;
553
601
  }
602
+ async function decodeDeferredImageBytes(bytes, mime) {
603
+ if (nodeCanvasModule) {
604
+ const image = await nodeCanvasModule.loadImage(new Uint8Array(bytes));
605
+ return { drawable: image, width: image.width, height: image.height, close: () => {
606
+ } };
607
+ }
608
+ if (typeof createImageBitmap !== "function") {
609
+ return null;
610
+ }
611
+ const blob = mime !== void 0 ? new Blob([bytes], { type: mime }) : new Blob([bytes]);
612
+ const bitmap = await createImageBitmap(blob);
613
+ return { drawable: bitmap, width: bitmap.width, height: bitmap.height, close: () => bitmap.close() };
614
+ }
615
+ function canvasDrawImage(ctx, image, dx, dy, dw, dh) {
616
+ const draw = ctx.drawImage;
617
+ draw.call(ctx, image, dx, dy, dw, dh);
618
+ }
554
619
 
555
620
  // src/emf-header-parser.ts
556
621
  function parseEmfHeader(view) {
@@ -1468,7 +1533,8 @@ function handleBitBlt(rCtx, offset, dataOff, recSize) {
1468
1533
  const temp = createTempCanvas(imageData.width, imageData.height);
1469
1534
  if (temp) {
1470
1535
  temp.ctx.putImageData(imageData, 0, 0);
1471
- ctx.drawImage(
1536
+ canvasDrawImage(
1537
+ ctx,
1472
1538
  temp.canvas,
1473
1539
  gmx(rCtx, dstX),
1474
1540
  gmy(rCtx, dstY),
@@ -1503,7 +1569,8 @@ function handleStretchDibits(rCtx, offset, dataOff, recSize) {
1503
1569
  const temp = createTempCanvas(imageData.width, imageData.height);
1504
1570
  if (temp) {
1505
1571
  temp.ctx.putImageData(imageData, 0, 0);
1506
- ctx.drawImage(
1572
+ canvasDrawImage(
1573
+ ctx,
1507
1574
  temp.canvas,
1508
1575
  gmx(rCtx, dstX),
1509
1576
  gmy(rCtx, dstY),
@@ -4861,12 +4928,16 @@ async function processDeferredImages(ctx, deferredImages, recursionDepth = 0) {
4861
4928
  if (img.isMetafile) {
4862
4929
  if (recursionDepth >= MAX_METAFILE_RECURSION) {
4863
4930
  emfWarn(
4864
- ` Deferred image [${idx}]: skipping embedded metafile \u2014 recursion depth ${recursionDepth} >= ${MAX_METAFILE_RECURSION}`
4931
+ ` Deferred image [${idx}]: skipping embedded metafile, recursion depth ${recursionDepth} >= ${MAX_METAFILE_RECURSION}`
4865
4932
  );
4866
4933
  continue;
4867
4934
  }
4868
4935
  emfLog(` Deferred image [${idx}]: recursively converting embedded metafile...`);
4869
- const metafileDataUrl = await convertEmfToDataUrl(plainBuffer, void 0, recursionDepth + 1) ?? await convertWmfToDataUrl(plainBuffer, void 0, recursionDepth + 1);
4936
+ const metafileDataUrl = await convertMetafileToDataUrl(
4937
+ plainBuffer,
4938
+ void 0,
4939
+ recursionDepth + 1
4940
+ );
4870
4941
  if (metafileDataUrl) {
4871
4942
  emfLog(
4872
4943
  ` Deferred image [${idx}]: metafile converted, dataUrl length=${metafileDataUrl.length}`
@@ -4879,26 +4950,28 @@ async function processDeferredImages(ctx, deferredImages, recursionDepth = 0) {
4879
4950
  for (let i = 0; i < byteString.length; i++) {
4880
4951
  ia[i] = byteString.charCodeAt(i);
4881
4952
  }
4882
- const metaBlob = new Blob([ab], { type: mime });
4883
- emfLog(
4884
- ` Deferred image [${idx}]: creating ImageBitmap from ${metaBlob.size} byte blob (${mime})...`
4885
- );
4886
- const bitmap = await createImageBitmap(metaBlob);
4887
- emfLog(` Deferred image [${idx}]: ImageBitmap created ${bitmap.width}\xD7${bitmap.height}`);
4888
- ctx.drawImage(bitmap, img.dx, img.dy, img.dw, img.dh);
4889
- bitmap.close();
4953
+ emfLog(` Deferred image [${idx}]: decoding ${ab.byteLength} byte image (${mime})...`);
4954
+ const decoded = await decodeDeferredImageBytes(ab, mime);
4955
+ if (decoded) {
4956
+ emfLog(` Deferred image [${idx}]: decoded ${decoded.width}\xD7${decoded.height}`);
4957
+ canvasDrawImage(ctx, decoded.drawable, img.dx, img.dy, img.dw, img.dh);
4958
+ decoded.close();
4959
+ } else {
4960
+ emfWarn(` Deferred image [${idx}]: no image decoder available`);
4961
+ }
4890
4962
  } else {
4891
4963
  emfWarn(` Deferred image [${idx}]: metafile conversion returned null`);
4892
4964
  }
4893
4965
  } else {
4894
- emfLog(
4895
- ` Deferred image [${idx}]: creating ImageBitmap from ${plainBuffer.byteLength} byte blob...`
4896
- );
4897
- const blob = new Blob([plainBuffer]);
4898
- const bitmap = await createImageBitmap(blob);
4899
- emfLog(` Deferred image [${idx}]: ImageBitmap created ${bitmap.width}\xD7${bitmap.height}`);
4900
- ctx.drawImage(bitmap, img.dx, img.dy, img.dw, img.dh);
4901
- bitmap.close();
4966
+ emfLog(` Deferred image [${idx}]: decoding ${plainBuffer.byteLength} byte image...`);
4967
+ const decoded = await decodeDeferredImageBytes(plainBuffer);
4968
+ if (decoded) {
4969
+ emfLog(` Deferred image [${idx}]: decoded ${decoded.width}\xD7${decoded.height}`);
4970
+ canvasDrawImage(ctx, decoded.drawable, img.dx, img.dy, img.dw, img.dh);
4971
+ decoded.close();
4972
+ } else {
4973
+ emfWarn(` Deferred image [${idx}]: no image decoder available`);
4974
+ }
4902
4975
  }
4903
4976
  } catch (imgErr) {
4904
4977
  imgErr instanceof Error ? imgErr.message : String(imgErr);
@@ -4911,10 +4984,35 @@ async function processDeferredImages(ctx, deferredImages, recursionDepth = 0) {
4911
4984
  }
4912
4985
  ctx.setTransform(1, 0, 0, 1, 0, 0);
4913
4986
  }
4914
- async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4987
+ async function convertMetafileToDataUrl(buffer, options, recursionDepth = 0) {
4988
+ await ensureNodeCanvasModule();
4915
4989
  if (recursionDepth > MAX_METAFILE_RECURSION) {
4916
4990
  return null;
4917
4991
  }
4992
+ emfLog(`convertMetafileToDataUrl: input buffer ${buffer.byteLength} bytes`);
4993
+ if (buffer.byteLength >= 16) {
4994
+ const hdrBytes = new Uint8Array(buffer, 0, 16);
4995
+ emfLog(
4996
+ `convertMetafileToDataUrl: first 16 bytes: [${Array.from(hdrBytes).map((b) => b.toString(16).padStart(2, "0")).join(" ")}]`
4997
+ );
4998
+ }
4999
+ const view = new DataView(buffer);
5000
+ let emfHeader = null;
5001
+ try {
5002
+ emfHeader = parseEmfHeader(view);
5003
+ } catch (err) {
5004
+ emfWarn(
5005
+ "convertMetafileToDataUrl: parseEmfHeader threw during detection:",
5006
+ err instanceof Error ? err.message : err
5007
+ );
5008
+ emfHeader = null;
5009
+ }
5010
+ if (emfHeader) {
5011
+ return convertEmfInternal(buffer, view, emfHeader, options, recursionDepth);
5012
+ }
5013
+ return convertWmfInternal(buffer, view, options);
5014
+ }
5015
+ async function convertEmfInternal(buffer, view, header, options, recursionDepth) {
4918
5016
  const opts = options ?? {};
4919
5017
  const dpiScale = opts.dpiScale ?? DEFAULT_DPI_SCALE;
4920
5018
  const effectiveMaxWidth = opts.maxWidth;
@@ -4925,30 +5023,18 @@ async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4925
5023
  fontFamilyMap: opts.fontFamilyMap
4926
5024
  };
4927
5025
  try {
4928
- emfLog("=== convertEmfToDataUrl START ===");
5026
+ emfLog("=== convertEmfInternal START ===");
4929
5027
  emfLog(
4930
5028
  `Input buffer: ${buffer.byteLength} bytes, maxWidth=${effectiveMaxWidth}, maxHeight=${effectiveMaxHeight}, dpiScale=${dpiScale}`
4931
5029
  );
4932
- if (buffer.byteLength >= 16) {
4933
- const hdrBytes = new Uint8Array(buffer, 0, 16);
4934
- emfLog(
4935
- `First 16 bytes: [${Array.from(hdrBytes).map((b) => b.toString(16).padStart(2, "0")).join(" ")}]`
4936
- );
4937
- }
4938
- const view = new DataView(buffer);
4939
- const header = parseEmfHeader(view);
4940
- if (!header) {
4941
- emfLog("convertEmfToDataUrl: parseEmfHeader returned null \u2014 returning null");
4942
- return null;
4943
- }
4944
5030
  const renderBounds = getRenderableEmfBounds(header);
4945
5031
  if (!renderBounds) {
4946
- emfLog("convertEmfToDataUrl: getRenderableEmfBounds returned null \u2014 returning null");
5032
+ emfLog("convertEmfInternal: getRenderableEmfBounds returned null, returning null");
4947
5033
  return null;
4948
5034
  }
4949
5035
  const logicalW = renderBounds.right - renderBounds.left;
4950
5036
  const logicalH = renderBounds.bottom - renderBounds.top;
4951
- emfLog(`convertEmfToDataUrl: logicalSize=${logicalW}\xD7${logicalH}`);
5037
+ emfLog(`convertEmfInternal: logicalSize=${logicalW}\xD7${logicalH}`);
4952
5038
  const setup = createCanvas(
4953
5039
  logicalW,
4954
5040
  logicalH,
@@ -4958,15 +5044,15 @@ async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4958
5044
  opts.maxCanvasDimension
4959
5045
  );
4960
5046
  if (!setup) {
4961
- emfLog("convertEmfToDataUrl: createCanvas returned null \u2014 returning null");
5047
+ emfLog("convertEmfInternal: createCanvas returned null, returning null");
4962
5048
  return null;
4963
5049
  }
4964
5050
  const { canvas, ctx } = setup;
4965
5051
  emfLog(
4966
- `convertEmfToDataUrl: canvas created ${canvas.width}\xD7${canvas.height} (dpiScale=${dpiScale})`
5052
+ `convertEmfInternal: canvas created ${canvas.width}\xD7${canvas.height} (dpiScale=${dpiScale})`
4967
5053
  );
4968
5054
  ctx.save();
4969
- emfLog("convertEmfToDataUrl: starting replayEmfRecords...");
5055
+ emfLog("convertEmfInternal: starting replayEmfRecords...");
4970
5056
  const deferredImages = replayEmfRecords(
4971
5057
  view,
4972
5058
  ctx,
@@ -4976,30 +5062,25 @@ async function convertEmfToDataUrl(buffer, options, recursionDepth = 0) {
4976
5062
  dpiScale,
4977
5063
  replayOptions
4978
5064
  );
4979
- emfLog(
4980
- `convertEmfToDataUrl: replayEmfRecords returned ${deferredImages.length} deferred images`
4981
- );
5065
+ emfLog(`convertEmfInternal: replayEmfRecords returned ${deferredImages.length} deferred images`);
4982
5066
  ctx.restore();
4983
- await processDeferredImages(ctx, deferredImages);
4984
- emfLog("convertEmfToDataUrl: exporting canvas to PNG data URL...");
5067
+ await processDeferredImages(ctx, deferredImages, recursionDepth);
5068
+ emfLog("convertEmfInternal: exporting canvas to PNG data URL...");
4985
5069
  const result = await exportCanvasToPngDataUrl(canvas);
4986
5070
  if (result) {
4987
- emfLog(`convertEmfToDataUrl: SUCCESS \u2014 data URL length=${result.length}`);
5071
+ emfLog(`convertEmfInternal: SUCCESS, data URL length=${result.length}`);
4988
5072
  } else {
4989
- emfWarn("convertEmfToDataUrl: exportCanvasToPngDataUrl returned null");
5073
+ emfWarn("convertEmfInternal: exportCanvasToPngDataUrl returned null");
4990
5074
  }
4991
- emfLog("=== convertEmfToDataUrl END ===");
5075
+ emfLog("=== convertEmfInternal END ===");
4992
5076
  return result;
4993
5077
  } catch (err) {
4994
- emfWarn("convertEmfToDataUrl: EXCEPTION:", err instanceof Error ? err.message : err);
4995
- console.warn("[pptx-editor] EMF conversion failed:", err instanceof Error ? err.message : err);
5078
+ emfWarn("convertEmfInternal: EXCEPTION:", err instanceof Error ? err.message : err);
5079
+ console.warn("[emf-converter] EMF conversion failed:", err instanceof Error ? err.message : err);
4996
5080
  return null;
4997
5081
  }
4998
5082
  }
4999
- async function convertWmfToDataUrl(buffer, options, recursionDepth = 0) {
5000
- if (recursionDepth > MAX_METAFILE_RECURSION) {
5001
- return null;
5002
- }
5083
+ async function convertWmfInternal(buffer, view, options, recursionDepth) {
5003
5084
  const opts = options ?? {};
5004
5085
  const dpiScale = opts.dpiScale ?? DEFAULT_DPI_SCALE;
5005
5086
  const effectiveMaxWidth = opts.maxWidth;
@@ -5010,20 +5091,19 @@ async function convertWmfToDataUrl(buffer, options, recursionDepth = 0) {
5010
5091
  };
5011
5092
  try {
5012
5093
  emfLog(
5013
- "=== convertWmfToDataUrl START ===",
5094
+ "=== convertWmfInternal START ===",
5014
5095
  `buffer=${buffer.byteLength} bytes, dpiScale=${dpiScale}`
5015
5096
  );
5016
- const view = new DataView(buffer);
5017
5097
  const header = parseWmfHeader(view);
5018
5098
  if (!header) {
5019
- emfLog("convertWmfToDataUrl: parseWmfHeader returned null");
5099
+ emfLog("convertWmfInternal: parseWmfHeader returned null");
5020
5100
  return null;
5021
5101
  }
5022
5102
  const logicalW = header.boundsRight - header.boundsLeft;
5023
5103
  const logicalH = header.boundsBottom - header.boundsTop;
5024
- emfLog(`convertWmfToDataUrl: logicalSize=${logicalW}\xD7${logicalH}`);
5104
+ emfLog(`convertWmfInternal: logicalSize=${logicalW}\xD7${logicalH}`);
5025
5105
  if (logicalW <= 0 || logicalH <= 0) {
5026
- emfLog("convertWmfToDataUrl: invalid dimensions \u2014 returning null");
5106
+ emfLog("convertWmfInternal: invalid dimensions, returning null");
5027
5107
  return null;
5028
5108
  }
5029
5109
  const setup = createCanvas(
@@ -5042,14 +5122,14 @@ async function convertWmfToDataUrl(buffer, options, recursionDepth = 0) {
5042
5122
  replayWmfRecords(view, ctx, header, canvas.width, canvas.height, replayOptions);
5043
5123
  ctx.restore();
5044
5124
  const result = await exportCanvasToPngDataUrl(canvas);
5045
- emfLog(`convertWmfToDataUrl: result=${result ? `dataUrl len=${result.length}` : "null"}`);
5046
- emfLog("=== convertWmfToDataUrl END ===");
5125
+ emfLog(`convertWmfInternal: result=${result ? `dataUrl len=${result.length}` : "null"}`);
5126
+ emfLog("=== convertWmfInternal END ===");
5047
5127
  return result;
5048
5128
  } catch (err) {
5049
- emfWarn("convertWmfToDataUrl: EXCEPTION:", err instanceof Error ? err.message : err);
5050
- console.warn("[pptx-editor] WMF conversion failed:", err instanceof Error ? err.message : err);
5129
+ emfWarn("convertWmfInternal: EXCEPTION:", err instanceof Error ? err.message : err);
5130
+ console.warn("[emf-converter] WMF conversion failed:", err instanceof Error ? err.message : err);
5051
5131
  return null;
5052
5132
  }
5053
5133
  }
5054
5134
 
5055
- export { DEFAULT_DPI_SCALE, convertEmfToDataUrl, convertWmfToDataUrl };
5135
+ export { DEFAULT_DPI_SCALE, convertMetafileToDataUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Convert EMF/WMF metafile binaries to PNG data URLs using Canvas",
5
5
  "keywords": [
6
6
  "canvas",
@@ -45,8 +45,18 @@
45
45
  "pack": "bun run build && bun pm pack"
46
46
  },
47
47
  "devDependencies": {
48
+ "@napi-rs/canvas": "^1.0.9",
49
+ "@types/node": "^22.20.3",
48
50
  "tsup": "^8.5.1",
49
51
  "typescript": "^6.0.3",
50
52
  "vitest": "^5.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@napi-rs/canvas": "^1.0.9"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@napi-rs/canvas": {
59
+ "optional": true
60
+ }
51
61
  }
52
62
  }