emf-converter 2.0.2 → 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 +33 -23
- package/dist/index.d.mts +34 -41
- package/dist/index.d.ts +34 -41
- package/dist/index.js +161 -82
- package/dist/index.mjs +161 -81
- package/package.json +12 -2
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
|
|
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
|
|
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 {
|
|
47
|
+
import { convertMetafileToDataUrl } from 'emf-converter';
|
|
39
48
|
|
|
40
49
|
const emfBuffer: ArrayBuffer = /* loaded from file or network */;
|
|
41
|
-
const pngDataUrl = await
|
|
50
|
+
const pngDataUrl = await convertMetafileToDataUrl(emfBuffer);
|
|
42
51
|
// => "data:image/png;base64,iVBORw0KGgo..."
|
|
43
52
|
|
|
44
|
-
|
|
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
|
|
57
|
+
const scaled = await convertMetafileToDataUrl(emfBuffer, { maxWidth: 1024, maxHeight: 768 });
|
|
48
58
|
```
|
|
49
59
|
|
|
50
|
-
|
|
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
|
-
### `
|
|
64
|
+
### `convertMetafileToDataUrl(buffer, options?)`
|
|
55
65
|
|
|
56
66
|
| Parameter | Type | Description |
|
|
57
67
|
| ----------- | ----------------------------- | ---------------------------------------------------- |
|
|
58
|
-
| `buffer` | `ArrayBuffer` | Raw EMF
|
|
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` |
|
|
67
|
-
| `maxHeight` | `number` |
|
|
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>` |
|
|
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
|
|
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
|
|
87
|
-
- **Gradient brushes
|
|
88
|
-
- **Raster operations (ROP2)
|
|
89
|
-
- **GDI world transforms
|
|
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
|
|
94
|
-
- **Bitwise ROP2 modes are arithmetic approximations
|
|
95
|
-
- **Gradient details
|
|
96
|
-
- **GDI rotation/skew
|
|
97
|
-
- **Safety limits
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
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"
|
|
9
|
-
* async image decoding (via {@link createImageBitmap}
|
|
10
|
-
*
|
|
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)
|
|
48
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
71
|
-
*
|
|
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
|
|
75
|
-
*
|
|
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
|
|
79
|
-
* -
|
|
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
|
-
* @
|
|
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,
|
|
92
|
+
export { DEFAULT_DPI_SCALE, type EmfConvertOptions, convertMetafileToDataUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public API
|
|
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
|
-
*
|
|
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"
|
|
9
|
-
* async image decoding (via {@link createImageBitmap}
|
|
10
|
-
*
|
|
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)
|
|
48
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
71
|
-
*
|
|
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
|
|
75
|
-
*
|
|
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
|
|
79
|
-
* -
|
|
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
|
-
* @
|
|
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,
|
|
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
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
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
|
|
295
|
+
return { canvas, ctx, scaleX, scaleY };
|
|
276
296
|
}
|
|
277
|
-
if (typeof document
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
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
|
-
|
|
4898
|
-
)
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
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
|
|
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("===
|
|
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("
|
|
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(`
|
|
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("
|
|
5049
|
+
emfLog("convertEmfInternal: createCanvas returned null, returning null");
|
|
4964
5050
|
return null;
|
|
4965
5051
|
}
|
|
4966
5052
|
const { canvas, ctx } = setup;
|
|
4967
5053
|
emfLog(
|
|
4968
|
-
`
|
|
5054
|
+
`convertEmfInternal: canvas created ${canvas.width}\xD7${canvas.height} (dpiScale=${dpiScale})`
|
|
4969
5055
|
);
|
|
4970
5056
|
ctx.save();
|
|
4971
|
-
emfLog("
|
|
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("
|
|
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(`
|
|
5073
|
+
emfLog(`convertEmfInternal: SUCCESS, data URL length=${result.length}`);
|
|
4990
5074
|
} else {
|
|
4991
|
-
emfWarn("
|
|
5075
|
+
emfWarn("convertEmfInternal: exportCanvasToPngDataUrl returned null");
|
|
4992
5076
|
}
|
|
4993
|
-
emfLog("===
|
|
5077
|
+
emfLog("=== convertEmfInternal END ===");
|
|
4994
5078
|
return result;
|
|
4995
5079
|
} catch (err) {
|
|
4996
|
-
emfWarn("
|
|
4997
|
-
console.warn("[
|
|
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
|
|
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
|
-
"===
|
|
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("
|
|
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(`
|
|
5106
|
+
emfLog(`convertWmfInternal: logicalSize=${logicalW}\xD7${logicalH}`);
|
|
5027
5107
|
if (logicalW <= 0 || logicalH <= 0) {
|
|
5028
|
-
emfLog("
|
|
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(`
|
|
5048
|
-
emfLog("===
|
|
5127
|
+
emfLog(`convertWmfInternal: result=${result ? `dataUrl len=${result.length}` : "null"}`);
|
|
5128
|
+
emfLog("=== convertWmfInternal END ===");
|
|
5049
5129
|
return result;
|
|
5050
5130
|
} catch (err) {
|
|
5051
|
-
emfWarn("
|
|
5052
|
-
console.warn("[
|
|
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.
|
|
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
|
|
268
|
-
const
|
|
269
|
-
if (!
|
|
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
|
|
293
|
+
return { canvas, ctx, scaleX, scaleY };
|
|
274
294
|
}
|
|
275
|
-
if (typeof document
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
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
|
-
|
|
4896
|
-
)
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
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
|
|
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("===
|
|
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("
|
|
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(`
|
|
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("
|
|
5047
|
+
emfLog("convertEmfInternal: createCanvas returned null, returning null");
|
|
4962
5048
|
return null;
|
|
4963
5049
|
}
|
|
4964
5050
|
const { canvas, ctx } = setup;
|
|
4965
5051
|
emfLog(
|
|
4966
|
-
`
|
|
5052
|
+
`convertEmfInternal: canvas created ${canvas.width}\xD7${canvas.height} (dpiScale=${dpiScale})`
|
|
4967
5053
|
);
|
|
4968
5054
|
ctx.save();
|
|
4969
|
-
emfLog("
|
|
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("
|
|
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(`
|
|
5071
|
+
emfLog(`convertEmfInternal: SUCCESS, data URL length=${result.length}`);
|
|
4988
5072
|
} else {
|
|
4989
|
-
emfWarn("
|
|
5073
|
+
emfWarn("convertEmfInternal: exportCanvasToPngDataUrl returned null");
|
|
4990
5074
|
}
|
|
4991
|
-
emfLog("===
|
|
5075
|
+
emfLog("=== convertEmfInternal END ===");
|
|
4992
5076
|
return result;
|
|
4993
5077
|
} catch (err) {
|
|
4994
|
-
emfWarn("
|
|
4995
|
-
console.warn("[
|
|
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
|
|
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
|
-
"===
|
|
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("
|
|
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(`
|
|
5104
|
+
emfLog(`convertWmfInternal: logicalSize=${logicalW}\xD7${logicalH}`);
|
|
5025
5105
|
if (logicalW <= 0 || logicalH <= 0) {
|
|
5026
|
-
emfLog("
|
|
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(`
|
|
5046
|
-
emfLog("===
|
|
5125
|
+
emfLog(`convertWmfInternal: result=${result ? `dataUrl len=${result.length}` : "null"}`);
|
|
5126
|
+
emfLog("=== convertWmfInternal END ===");
|
|
5047
5127
|
return result;
|
|
5048
5128
|
} catch (err) {
|
|
5049
|
-
emfWarn("
|
|
5050
|
-
console.warn("[
|
|
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,
|
|
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
|
+
"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
|
-
"vitest": "^
|
|
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
|
}
|