@keeratita/heic-converter 0.4.2 โ 0.5.1
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 +370 -44
- package/dist/chunk-FKEXCXR4.js +1 -0
- package/dist/chunk-PTRQ7ZFB.mjs +1 -0
- package/dist/exif-H2Q7BO7Y.js +1 -0
- package/dist/exif-MBEKII5G.mjs +1 -0
- package/dist/heic-decoder-43VH2M3C.mjs +1 -0
- package/dist/heic-decoder-RXDCJDOS.js +1 -0
- package/dist/heic-decoder.wasm +0 -0
- package/dist/heic-decoder.wasm.br +0 -0
- package/dist/heic-decoder.wasm.gz +0 -0
- package/dist/index.d.mts +384 -40
- package/dist/index.d.ts +384 -40
- package/dist/index.js +1 -3
- package/dist/index.mjs +1 -3
- package/dist/worker-FNMN2SJV.mjs +1 -0
- package/dist/worker-Z3UQ6WL7.js +1 -0
- package/package.json +38 -14
- package/dist/heic-decoder-AAMWPCW2.mjs +0 -5
- package/dist/heic-decoder-L3ME46HT.js +0 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @keeratita/heic-converter
|
|
2
2
|
|
|
3
|
-
A modern, lightweight TypeScript library to convert `.heic` and `.heif` images to standard web formats (JPEG, PNG, WebP, SVG) client-side in the browser or on the backend in Node.js.
|
|
3
|
+
A modern, lightweight TypeScript library to convert `.heic` and `.heif` images to standard web formats (JPEG, PNG, WebP, AVIF, SVG) client-side in the browser or on the backend in Node.js.
|
|
4
4
|
|
|
5
5
|
Designed specifically for environments with strict **Content Security Policy (CSP)** rules, it is built with WebAssembly compiled **without** dynamic code execution (`eval()` or `new Function()`).
|
|
6
6
|
|
|
@@ -10,13 +10,16 @@ Designed specifically for environments with strict **Content Security Policy (CS
|
|
|
10
10
|
|
|
11
11
|
- ๐ **CSP Compliant**: Emscripten glue code is compiled with `-s DYNAMIC_EXECUTION=0`. Safe to run without `'unsafe-eval'`.
|
|
12
12
|
- ๐งฉ **Dependency Injection Architecture**: Swap the decoder module easily by implementing a simple `IHeicDecoder` interface.
|
|
13
|
-
- โก **Optimized Performance**: A fresh decoder instance is created and released per conversion โ memory is reclaimed promptly and concurrent conversions never share mutable WASM state.
|
|
13
|
+
- โก **Optimized Performance**: A fresh decoder instance is created and released per conversion โ memory is reclaimed promptly and concurrent conversions never share mutable WASM state. `convertMany({ reuseDecoders: true })` amortizes WASM init across batch items.
|
|
14
14
|
- ๐ **Isomorphic / Universal**: Runs in Node.js (decoding) and browser (decoding & canvas-based encoding).
|
|
15
15
|
- ๐ฆ **No Bloat**: Zero external production dependencies. Small footprint.
|
|
16
|
-
- ๐จ **Format Support**: Convert to `jpeg` (with quality configuration), `png`, `webp`, and `svg` (
|
|
17
|
-
- ๐ **Resize
|
|
18
|
-
-
|
|
19
|
-
-
|
|
16
|
+
- ๐จ **Format Support**: Convert to `jpeg` (with quality configuration), `png`, `webp`, `avif`, and `svg` (the encoded raster wrapped in an SVG document). AVIF degrades gracefully with a clear `format_unsupported` error where the canvas cannot encode it.
|
|
17
|
+
- ๐ **Resize & Crop**: Downscale with `maxWidth`/`maxHeight` or apply a uniform `scale` factor; cut any region with `crop` (in display pixels, composed with orientation).
|
|
18
|
+
- ๐งญ **EXIF-Orientation Aware**: Images stored rotated with an EXIF orientation flag come out upright, matching what viewers display. Opt out with `applyOrientation: false`.
|
|
19
|
+
- ๐ **Metadata Preservation**: `preserveExif: true` keeps the source EXIF block in JPEG (APP1) and PNG (`eXIf`) output; raw-decode consumers get the block via `DecodedImage.exif`.
|
|
20
|
+
- ๐ **Batch Conversion**: Convert many images at once with bounded concurrency via `convertMany` โ with per-item results (`continueOnError`) and pooled decoders (`reuseDecoders`) for large batches.
|
|
21
|
+
- ๐งต **Web Worker Helpers**: Offload single conversions (`convertHeicInWorker`) or whole batches (`convertManyInWorker`) to Web Workers to keep the UI thread responsive.
|
|
22
|
+
- ๐ **Output Shapes & Cancellation**: Return Blobs, base64 data URLs, or ArrayBuffers (`output`), and cancel in-flight work with an `AbortSignal` (`signal`) โ worker conversions terminate immediately.
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
@@ -48,15 +51,17 @@ In the browser, you can pass a `File` or `Blob` and get a converted `Blob` back:
|
|
|
48
51
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
49
52
|
|
|
50
53
|
// Convert input File/Blob to JPEG
|
|
51
|
-
const
|
|
52
|
-
const jpegBlob = await convertHeic(
|
|
54
|
+
const input = document.querySelector<HTMLInputElement>('#fileInput')!.files!.item(0)!;
|
|
55
|
+
const jpegBlob = await convertHeic(input, {
|
|
53
56
|
to: 'jpeg',
|
|
54
|
-
quality: 0.9
|
|
57
|
+
quality: 0.9,
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
// Create preview URL
|
|
58
|
-
const
|
|
59
|
-
|
|
61
|
+
const preview = document.querySelector('img');
|
|
62
|
+
if (preview) {
|
|
63
|
+
preview.src = URL.createObjectURL(jpegBlob);
|
|
64
|
+
}
|
|
60
65
|
```
|
|
61
66
|
|
|
62
67
|
### 2. Browser: WebP Output
|
|
@@ -66,6 +71,8 @@ WebP offers excellent compression with quality configuration:
|
|
|
66
71
|
```typescript
|
|
67
72
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
68
73
|
|
|
74
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
75
|
+
|
|
69
76
|
const webpBlob = await convertHeic(heicBlob, {
|
|
70
77
|
to: 'webp',
|
|
71
78
|
quality: 0.8,
|
|
@@ -79,6 +86,8 @@ SVG wraps the raster image as an embedded lossless PNG inside an SVG container:
|
|
|
79
86
|
```typescript
|
|
80
87
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
81
88
|
|
|
89
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
90
|
+
|
|
82
91
|
const svgBlob = await convertHeic(heicBlob, {
|
|
83
92
|
to: 'svg',
|
|
84
93
|
});
|
|
@@ -93,6 +102,8 @@ If your bundler places files in a custom assets folder or CDN, you can configure
|
|
|
93
102
|
```typescript
|
|
94
103
|
import { convertHeic, LibheifDecoder } from '@keeratita/heic-converter';
|
|
95
104
|
|
|
105
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
106
|
+
|
|
96
107
|
// Create decoder with custom asset paths
|
|
97
108
|
const decoder = new LibheifDecoder({
|
|
98
109
|
locateFile: (path, prefix) => `https://cdn.example.com/assets/${path}`,
|
|
@@ -110,6 +121,8 @@ Alternatively, if you prefer to load the WASM binary manually as an `ArrayBuffer
|
|
|
110
121
|
```typescript
|
|
111
122
|
import { convertHeic, LibheifDecoder } from '@keeratita/heic-converter';
|
|
112
123
|
|
|
124
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
125
|
+
|
|
113
126
|
const wasmResponse = await fetch('/assets/heic-decoder.wasm');
|
|
114
127
|
const wasmBinary = await wasmResponse.arrayBuffer();
|
|
115
128
|
|
|
@@ -121,11 +134,47 @@ const jpegBlob = await convertHeic(heicBlob, {
|
|
|
121
134
|
});
|
|
122
135
|
```
|
|
123
136
|
|
|
137
|
+
#### Files you must serve
|
|
138
|
+
|
|
139
|
+
`dist/` is no longer a single file. The JS is code-split, so a hand-hosted deploy (copying files into an assets folder or a CDN, with no bundler rewriting specifiers) needs **all** of these, under the chunk names the build produced:
|
|
140
|
+
|
|
141
|
+
| File | Needed for |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `index.mjs` / `index.js` | the entry (ESM / CJS) |
|
|
144
|
+
| `chunk-*.mjs` / `chunk-*.js` | the eagerly imported shared code |
|
|
145
|
+
| `heic-decoder-*.mjs` / `heic-decoder-*.js` | the Emscripten glue, fetched on first decode |
|
|
146
|
+
| `heic-decoder.wasm` (+ `.gz` / `.br`) | the decoder binary |
|
|
147
|
+
| `worker-*.mjs` / `worker-*.js` | `convertHeicInWorker` / `convertManyInWorker` |
|
|
148
|
+
| `exif-*.mjs` / `exif-*.js` | `preserveExif: true` |
|
|
149
|
+
|
|
150
|
+
- Chunk names are **content-hashed and hard-referenced by the entry**, so a partial upload โ or a `dist/` mixed across releases โ 404s instead of degrading quietly. Deploy the whole directory per release.
|
|
151
|
+
- Serve chunks as `text/javascript`. A wrong MIME type surfaces as `worker_load_failed` from the worker APIs, and as `preserveExif` silently dropping metadata (it warns once).
|
|
152
|
+
- `heic-decoder.wasm` and the glue chunk come from **one** `emcc` invocation and are ABI-coupled. If you host the binary separately (`locateFile`, an artifact mirror, an immutable `/assets/heic-decoder.wasm`), re-upload *and* cache-bust it together with the glue, or instantiation fails with `decoder_init_failed`.
|
|
153
|
+
|
|
154
|
+
A bundler handles all of this: the dynamic imports are literal specifiers, so your bundler rewrites and emits the chunks for you.
|
|
155
|
+
|
|
156
|
+
#### Using a bundler (Vite, webpack, Rollup, esbuild)
|
|
157
|
+
|
|
158
|
+
The main entry is a small lazy loader: the Emscripten glue ships as a dynamic-import chunk and the `.wasm` binary is **not** inlined โ you must make sure `dist/heic-decoder.wasm` is served and resolvable at runtime. Bundlers that rewrite asset URLs usually handle this automatically because the glue resolves the binary relative to its own chunk. If yours doesn't, copy the binary into your assets (the `@keeratita/heic-converter/wasm` subpath export resolves to the file itself, handy as a copy source) and point the decoder at it:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { convertHeic, LibheifDecoder } from '@keeratita/heic-converter';
|
|
162
|
+
|
|
163
|
+
declare const heicBlob: Blob;
|
|
164
|
+
|
|
165
|
+
// Serve a copy of @keeratita/heic-converter/dist/heic-decoder.wasm there.
|
|
166
|
+
const blob = await convertHeic(heicBlob, {
|
|
167
|
+
decoder: new LibheifDecoder({ locateFile: () => '/assets/heic-decoder.wasm' }),
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Fetching a `.gz`/`.br` variant URL directly will not work โ see the compression note below.
|
|
172
|
+
|
|
124
173
|
#### Reducing the download size
|
|
125
174
|
|
|
126
|
-
The WASM binary (~
|
|
175
|
+
The WASM binary (~794 KB, ~288 KB gzipped) dominates the payload; everything else is small and lazy. The Emscripten glue is a separate chunk fetched only on the first decode, and the Web Worker implementation and the opt-in EXIF injectors are separate chunks fetched only when you first call `convertHeicInWorker`/`convertManyInWorker` or pass `preserveExif: true`. What every consumer pays for up front is a few kilobytes of gzipped JavaScript โ `test/unit/bundle.test.ts` fails the build if one of those boundaries moves or the eager budget grows, so the numbers live in the tests rather than here.
|
|
127
176
|
|
|
128
|
-
`npm run build` also emits pre-compressed copies โ `dist/heic-decoder.wasm.gz` (~
|
|
177
|
+
`npm run build` also emits pre-compressed copies โ `dist/heic-decoder.wasm.gz` (~288 KB) and `dist/heic-decoder.wasm.br` (~230 KB) โ and verifies each one decompresses back to the binary, so a corrupt sidecar cannot reach a CDN. Most static hosts and CDNs (GitHub Pages, Netlify, Vercel, Cloudflare) already compress `application/wasm` automatically when the browser sends `Accept-Encoding`; verify with:
|
|
129
178
|
|
|
130
179
|
```bash
|
|
131
180
|
curl -sI -H 'Accept-Encoding: br' https://your-site/heic-decoder.wasm | grep -i content-encoding
|
|
@@ -133,7 +182,7 @@ curl -sI -H 'Accept-Encoding: br' https://your-site/heic-decoder.wasm | grep -i
|
|
|
133
182
|
|
|
134
183
|
If your server does not compress automatically, serve the pre-compressed files with the matching `Content-Encoding` header (nginx: `gzip_static on;` / `brotli_static on;`). Do **not** point `locateFile` at a `.br`/`.gz` URL โ the browser only decompresses responses tagged with `Content-Encoding`.
|
|
135
184
|
|
|
136
|
-
For long-lived caching, serve the WASM with `Cache-Control: public, max-age=31536000, immutable` and rename it per release (e.g. `heic-decoder-1.2.3.wasm` via `locateFile`) so clients pick up upgrades. GitHub Pages caps asset cache lifetime at 600s; put a CDN in front if that matters.
|
|
185
|
+
For long-lived caching, serve the WASM with `Cache-Control: public, max-age=31536000, immutable` and rename it per release (e.g. `heic-decoder-1.2.3.wasm` via `locateFile`) so clients pick up upgrades โ and treat the binary and its glue chunk as one unit when you do, per the note above. GitHub Pages caps asset cache lifetime at 600s; put a CDN in front if that matters.
|
|
137
186
|
|
|
138
187
|
### 5. Node.js: Decoding Raw Pixel Data
|
|
139
188
|
|
|
@@ -149,19 +198,34 @@ import sharp from 'sharp'; // external node image library
|
|
|
149
198
|
async function convertNode() {
|
|
150
199
|
const heicData = new Uint8Array(fs.readFileSync('input.heic'));
|
|
151
200
|
|
|
152
|
-
|
|
201
|
+
// Passing wasmBinary explicitly avoids relying on default path resolution
|
|
202
|
+
// inside the package; fs.readFileSync returns a Buffer, which the option
|
|
203
|
+
// accepts (it takes ArrayBuffer or any ArrayBufferView).
|
|
204
|
+
const decoder = new LibheifDecoder({
|
|
205
|
+
wasmBinary: fs.readFileSync(
|
|
206
|
+
'node_modules/@keeratita/heic-converter/dist/heic-decoder.wasm',
|
|
207
|
+
),
|
|
208
|
+
});
|
|
153
209
|
await decoder.initialize();
|
|
154
210
|
|
|
155
|
-
// Decodes to { width, height, data: Uint8ClampedArray (RGBA) }.
|
|
211
|
+
// Decodes to { width, height, data: Uint8ClampedArray (RGBA), orientation?, exif? }.
|
|
156
212
|
// data is an independent copy โ safe to use after decoder.free().
|
|
157
|
-
|
|
213
|
+
// orientation 2-8 means the stored pixels need a rotation/flip for display
|
|
214
|
+
// (convertHeic applies it automatically; raw-decode consumers must do it
|
|
215
|
+
// themselves โ rotate the buffer, or set the orientation tag in `exif`
|
|
216
|
+
// before handing it to sharp so viewers apply it).
|
|
217
|
+
// exif is the source EXIF block ("Exif\0\0" + TIFF) when the file carries
|
|
218
|
+
// one โ pass it to sharp.withMetadata({ exif }) to keep camera metadata.
|
|
219
|
+
const { width, height, data, exif } = await decoder.decode(heicData);
|
|
158
220
|
|
|
159
221
|
// Process raw pixels using sharp
|
|
160
|
-
|
|
222
|
+
let pipeline = sharp(Buffer.from(data), {
|
|
161
223
|
raw: { width, height, channels: 4 },
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
.
|
|
224
|
+
});
|
|
225
|
+
if (exif) {
|
|
226
|
+
pipeline = pipeline.withMetadata({ exif: Buffer.from(exif) });
|
|
227
|
+
}
|
|
228
|
+
await pipeline.toFormat('jpeg').toFile('output.jpg');
|
|
165
229
|
|
|
166
230
|
// Clean up WASM memory
|
|
167
231
|
decoder.free();
|
|
@@ -175,7 +239,8 @@ For large images, you can pass an `onProgress` callback to track the conversion
|
|
|
175
239
|
```typescript
|
|
176
240
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
177
241
|
|
|
178
|
-
const heicBlob
|
|
242
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
243
|
+
|
|
179
244
|
const jpegBlob = await convertHeic(heicBlob, {
|
|
180
245
|
to: 'jpeg',
|
|
181
246
|
onProgress: (percent) => {
|
|
@@ -185,6 +250,9 @@ const jpegBlob = await convertHeic(heicBlob, {
|
|
|
185
250
|
});
|
|
186
251
|
```
|
|
187
252
|
|
|
253
|
+
> [!NOTE]
|
|
254
|
+
> The callback contract is enforced by the library: `percent` is always a finite number clamped to `0`โ`100` (first call `0`, last call `100`). If your callback throws, the conversion rejects with a `progress_callback_failed` error naming your message โ the failure is attributed to the callback instead of crashing the WASM module.
|
|
255
|
+
|
|
188
256
|
> [!TIP]
|
|
189
257
|
> Since the WebAssembly module runs on the main browser thread, the UI thread will be occupied during conversion. For maximum responsiveness when converting large images, it is highly recommended to run this library inside a standard JS **Web Worker** and communicate progress back to the main thread.
|
|
190
258
|
|
|
@@ -195,6 +263,8 @@ Downscale to fit within maximum dimensions (aspect ratio is preserved, images sm
|
|
|
195
263
|
```typescript
|
|
196
264
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
197
265
|
|
|
266
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
267
|
+
|
|
198
268
|
const thumbnailBlob = await convertHeic(heicBlob, {
|
|
199
269
|
to: 'jpeg',
|
|
200
270
|
maxWidth: 800,
|
|
@@ -211,13 +281,97 @@ const halfSizeBlob = await convertHeic(heicBlob, {
|
|
|
211
281
|
});
|
|
212
282
|
```
|
|
213
283
|
|
|
214
|
-
|
|
284
|
+
> [!NOTE]
|
|
285
|
+
> Target dimensions are validated before any decoding work: `scale`, `maxWidth`, and `maxHeight` must be positive finite numbers (otherwise an `invalid_resize` error is thrown), and the resulting target size must not exceed **16384 px** on either side โ the canvas/WASM ceiling shared with the decoder's own security limits.
|
|
286
|
+
|
|
287
|
+
### 8. Orientation (EXIF) Handling
|
|
288
|
+
|
|
289
|
+
Some HEIC files are stored rotated, with the intended display rotation recorded in the EXIF orientation tag (274). Browsers, Photos, and most image viewers apply that rotation when displaying โ so `convertHeic` uprights the output for you during rendering:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { convertHeic } from '@keeratita/heic-converter';
|
|
293
|
+
|
|
294
|
+
// Upright according to the source's EXIF orientation (default: applyOrientation: true)
|
|
295
|
+
const jpeg = await convertHeic(heicFile);
|
|
296
|
+
|
|
297
|
+
// Keep the exact stored-pixel geometry instead (the pre-0.5 behavior):
|
|
298
|
+
const unrotated = await convertHeic(heicFile, { applyOrientation: false });
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
How it works:
|
|
302
|
+
|
|
303
|
+
- HEIF's native display-transform boxes (`irot`/`imir`) are applied by the decoder itself during `decode()` โ `convertHeic` never stacks a second rotation on top, so Apple-style files are never double-rotated.
|
|
304
|
+
- When a file carries only the EXIF tag (the class written by tools that just rewrite metadata), `convertHeic` applies the rotation/flip via a canvas transform.
|
|
305
|
+
- `LibheifDecoder.decode()` returns the raw **stored** pixels and reports a pending rotation via the optional `orientation` field (EXIF semantics `1`โ`8`; absent or `1` means nothing is pending). If you encode the pixels yourself (e.g. Node.js + `sharp`), apply the rotation yourself.
|
|
306
|
+
- A non-boolean `applyOrientation` value is rejected up front with an `invalid_input` error.
|
|
307
|
+
|
|
308
|
+
### 9. Cropping
|
|
309
|
+
|
|
310
|
+
Cut a rectangle out of the image during conversion. Crop coordinates are in **post-orientation display pixels** โ the geometry the image is shown in (after any EXIF/irot correction), which is what "top-left corner" means to a user. The crop is applied before `scale`/`maxWidth`/`maxHeight`, which then downscale the cropped region:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { convertHeic } from '@keeratita/heic-converter';
|
|
314
|
+
|
|
315
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
316
|
+
|
|
317
|
+
const thumbnail = await convertHeic(heicBlob, {
|
|
318
|
+
to: 'jpeg',
|
|
319
|
+
crop: { x: 200, y: 100, width: 640, height: 480 }, // x/y default to 0
|
|
320
|
+
maxWidth: 320, // downscale the crop, not the whole image
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Cropping semantics:
|
|
325
|
+
|
|
326
|
+
- `width`/`height` are required positive integers; `x`/`y` are optional non-negative integers. Malformed values reject with `invalid_crop` **before** any decoding; a rectangle that exceeds the image rejects with `invalid_crop` naming the actual display size.
|
|
327
|
+
- Crop, orientation, and resize compose into a single canvas transform โ no intermediate copies.
|
|
328
|
+
- With `applyOrientation: false`, crop coordinates address the **stored** pixels instead (geometry as decoded).
|
|
329
|
+
|
|
330
|
+
### 10. Output Shapes (Blob, data URL, ArrayBuffer)
|
|
331
|
+
|
|
332
|
+
By default conversions return a `Blob`. Choose another representation with `output` โ the return type follows the option:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const dataUrl = await convertHeic(heicBlob, { to: 'png', output: 'dataUrl' });
|
|
336
|
+
// string: "data:image/png;base64,โฆ" โ ready for <img src>
|
|
337
|
+
|
|
338
|
+
const bytes = await convertHeic(heicBlob, { to: 'webp', output: 'arrayBuffer' });
|
|
339
|
+
// ArrayBuffer โ for uploads (fetch body), WebCodecs, custom pipelines
|
|
340
|
+
|
|
341
|
+
const blob = await convertHeic(heicBlob, { to: 'jpeg' }); // default Blob
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`output` applies to `convertMany` and `convertManyInWorker` too (in the worker case the chosen representation travels back through the worker protocol โ `arrayBuffer` results arrive as a structured-clone copy, never a base64 round-trip; a custom worker script can additionally transfer the buffer with `postMessage(msg, [msg.blob])`). Invalid values reject with `invalid_input`.
|
|
345
|
+
|
|
346
|
+
### 11. Canceling a Conversion (AbortSignal)
|
|
347
|
+
|
|
348
|
+
Pass an `AbortSignal` to cancel pending work. Cancellation is checked at stage boundaries (after input read, before/after decode, before encode), so a queued or in-flight conversion stops promptly and rejects with the `aborted` code:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { convertHeic, convertHeicInWorker } from '@keeratita/heic-converter';
|
|
352
|
+
|
|
353
|
+
const controller = new AbortController();
|
|
354
|
+
setTimeout(() => controller.abort(), 2000); // give up after 2s
|
|
355
|
+
|
|
356
|
+
await convertHeic(heicBlob, { signal: controller.signal }); // rejects with code 'aborted'
|
|
357
|
+
await convertMany(files, { signal: controller.signal }); // aborts remaining items
|
|
358
|
+
await convertHeicInWorker(heicBlob, { workerUrl, signal: controller.signal }); // terminates the worker
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
> [!NOTE]
|
|
362
|
+
> - Worker-based conversions abort **immediately**: the worker is terminated, since it runs off the main thread.
|
|
363
|
+
> - The synchronous WASM `decode()` on the main thread cannot be preempted mid-call โ cancellation takes effect at the next boundary after it finishes.
|
|
364
|
+
> - In `convertMany`, an aborted batch rejects with `aborted` (cancellation is the outcome you asked for, and it wins over item-failure aggregation). With `continueOnError`, already-started items record `aborted` errors per item.
|
|
365
|
+
|
|
366
|
+
### 12. Batch Conversion
|
|
215
367
|
|
|
216
368
|
Convert many images at once with bounded concurrency (default `4`). Results are returned in input order; if any conversion fails, the promise rejects as soon as the failure is known with an error that identifies the failing item:
|
|
217
369
|
|
|
218
370
|
```typescript
|
|
219
371
|
import { convertMany } from '@keeratita/heic-converter';
|
|
220
372
|
|
|
373
|
+
declare const heicFiles: File[]; // your HEIC files/blobs
|
|
374
|
+
|
|
221
375
|
const blobs = await convertMany(heicFiles, {
|
|
222
376
|
to: 'png',
|
|
223
377
|
concurrency: 3,
|
|
@@ -227,7 +381,40 @@ const blobs = await convertMany(heicFiles, {
|
|
|
227
381
|
});
|
|
228
382
|
```
|
|
229
383
|
|
|
230
|
-
|
|
384
|
+
Batch semantics worth knowing:
|
|
385
|
+
|
|
386
|
+
- The promise rejects with the **first failure in time** (not the lowest index). All items are still allowed to finish; decoders of in-flight items are released when they settle.
|
|
387
|
+
- The rejection is a `batch_item_failed` error carrying structured fields: `itemIndex` (0-based index of the first failing item), `itemTotal`, `failedCount` (total items that failed), and `cause` (the underlying error). The message names the failing item and, when several items failed, summarizes the count and the other errors.
|
|
388
|
+
- `onProgress` only fires for items that succeed; a failing item never reports 100%.
|
|
389
|
+
- `concurrency` must be a positive integer; the default is `4`.
|
|
390
|
+
|
|
391
|
+
**Keeping going after failures** โ with `continueOnError: true` the batch never rejects for item failures. Every item runs, and you get a result entry per item in input order:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const results = await convertMany(files, { to: 'png', continueOnError: true });
|
|
395
|
+
// Array<{ index: number; ok: true; result: Blob } | { index: number; ok: false; error: Error }>
|
|
396
|
+
|
|
397
|
+
const failures = results.filter((r) => !r.ok);
|
|
398
|
+
if (failures.length > 0) {
|
|
399
|
+
console.warn(`${failures.length} file(s) failed`, failures.map((f) => f.index));
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Up-front validation mistakes (bad format, bad `concurrency`, โฆ) still reject normally โ a typo is a caller bug, not an item failure.
|
|
404
|
+
|
|
405
|
+
**Pooling decoders for big batches** โ by default each item gets its own decoder instance (created, WASM-initialized, freed per item). For batches much larger than the concurrency, `reuseDecoders: true` hands each batch runner one pooled instance that is reused across items โ the WASM module load is amortized while the exclusive-use invariant holds (an instance never decodes two things at once):
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
const blobs = await convertMany(thousandFiles, {
|
|
409
|
+
to: 'webp',
|
|
410
|
+
concurrency: 4,
|
|
411
|
+
reuseDecoders: true, // one LibheifDecoder per runner instead of one per item
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Ignored when you inject your own `decoder` (yours is used for all items, per the injection contract).
|
|
416
|
+
|
|
417
|
+
### 13. Web Worker Conversion
|
|
231
418
|
|
|
232
419
|
Run the conversion inside a Web Worker so the main thread stays responsive. Create a worker script that uses this library:
|
|
233
420
|
|
|
@@ -242,7 +429,11 @@ self.onmessage = async (event) => {
|
|
|
242
429
|
...options,
|
|
243
430
|
onProgress: (percent) => self.postMessage({ type: 'progress', percent }),
|
|
244
431
|
});
|
|
245
|
-
|
|
432
|
+
// With output: 'arrayBuffer' the payload is zero-copy via a transfer list.
|
|
433
|
+
self.postMessage(
|
|
434
|
+
{ type: 'result', ok: true, blob },
|
|
435
|
+
blob instanceof ArrayBuffer ? [blob] : []
|
|
436
|
+
);
|
|
246
437
|
} catch (error) {
|
|
247
438
|
self.postMessage({ type: 'result', ok: false, error: error?.stack ?? error?.message ?? String(error) });
|
|
248
439
|
}
|
|
@@ -254,6 +445,8 @@ Then convert from the main thread:
|
|
|
254
445
|
```typescript
|
|
255
446
|
import { convertHeicInWorker } from '@keeratita/heic-converter';
|
|
256
447
|
|
|
448
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
449
|
+
|
|
257
450
|
const jpegBlob = await convertHeicInWorker(heicBlob, {
|
|
258
451
|
workerUrl: new URL('./converter.worker.js', import.meta.url),
|
|
259
452
|
workerType: 'module',
|
|
@@ -266,11 +459,28 @@ const jpegBlob = await convertHeicInWorker(heicBlob, {
|
|
|
266
459
|
> [!NOTE]
|
|
267
460
|
> - Use `workerType: 'module'` when the worker script uses ES module imports (as in the example above). The default is `'classic'`, which requires the script to be pre-bundled (e.g. by Vite or webpack) โ static ES imports are not supported in classic workers.
|
|
268
461
|
> - `workerUrl` should be a compile-time constant; the script runs with the page's privileges.
|
|
269
|
-
> - Only `progress` and `result` messages are understood; any other message type is ignored.
|
|
270
|
-
> - `decoder
|
|
271
|
-
> -
|
|
462
|
+
> - Only `progress` and `result` messages are understood; any other message type is ignored โ but ignored messages are counted and surfaced in timeout diagnostics (see below).
|
|
463
|
+
> - `decoder` cannot cross the worker boundary: passing it rejects immediately with an `invalid_input` error (it is also absent from `WorkerConvertOptions` at compile time). `onProgress` and `workerUrl` are stripped from the posted message; progress is forwarded through `{ type: 'progress', percent }` messages instead.
|
|
464
|
+
> - Concurrent calls are bounded per worker script: calls beyond `maxConcurrentWorkers` queue and start as slots free. Must be a positive integer (`invalid_concurrency` otherwise). Default: `navigator.hardwareConcurrency` clamped to 1โ8 (4 when unavailable).
|
|
465
|
+
> - `timeoutMs` (default `60000`) bounds how long the promise waits for a result; must be a finite number โฅ 0, `0` disables. The deadline starts when the call is made, so time spent queued behind `maxConcurrentWorkers` counts toward it โ raise `timeoutMs` or `maxConcurrentWorkers` for very large batches. A timeout rejects with a `worker_timeout` error whose message includes diagnostics โ how many progress messages arrived, the last percent, and any unrecognized message type (the usual sign the worker script doesn't implement the protocol).
|
|
272
466
|
> - This helper is **browser-only**: it rejects in Node.js, where there is no global `Worker`.
|
|
273
467
|
|
|
468
|
+
**Batch inside workers** โ `convertManyInWorker` mirrors `convertMany` (input order, `batch_item_failed` aggregation or `continueOnError` per-item results) while each item runs in a worker; real concurrency is bounded by `maxConcurrentWorkers`:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { convertManyInWorker } from '@keeratita/heic-converter';
|
|
472
|
+
|
|
473
|
+
const blobs = await convertManyInWorker(files, {
|
|
474
|
+
workerUrl: new URL('./converter.worker.js', import.meta.url),
|
|
475
|
+
workerType: 'module',
|
|
476
|
+
to: 'webp',
|
|
477
|
+
maxConcurrentWorkers: 3,
|
|
478
|
+
onProgress: (index, percent) => console.log(`Image ${index}: ${Math.round(percent)}%`),
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Every option of `convertHeicInWorker` applies (`output`, `signal`, `crop`, `preserveExif`, `timeoutMs`, โฆ); `onProgress` receives the item `index` in addition to the percent, and a mid-batch abort terminates the running workers.
|
|
483
|
+
|
|
274
484
|
---
|
|
275
485
|
|
|
276
486
|
## ๐ Content Security Policy (CSP)
|
|
@@ -284,6 +494,14 @@ Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval
|
|
|
284
494
|
> [!NOTE]
|
|
285
495
|
> `'wasm-unsafe-eval'` is a CSP Level 3 directive that allows compiling and executing WebAssembly modules without opening the security risks of general JavaScript `'unsafe-eval'`.
|
|
286
496
|
|
|
497
|
+
Two additions to that header are easy to miss:
|
|
498
|
+
|
|
499
|
+
- **Serving the WASM from another origin** (`locateFile` pointing at a CDN) needs that origin in `connect-src` โ the glue downloads the binary with `fetch()`/`XMLHttpRequest`, so `connect-src 'self'` blocks it and you get `decoder_init_failed` blaming the binary itself:
|
|
500
|
+
```http
|
|
501
|
+
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https://cdn.example.com; img-src 'self' blob: data:;
|
|
502
|
+
```
|
|
503
|
+
- **The worker helpers** are real `Worker` scripts, so they follow `worker-src` (falling back to `script-src`). With `workerUrl` on your own origin, `worker-src 'self'` is enough; if your bundler inlines the worker as a blob URL, use `worker-src 'self' blob:`. A CSP-blocked worker surfaces as the generic `worker_failed`.
|
|
504
|
+
|
|
287
505
|
---
|
|
288
506
|
|
|
289
507
|
## ๐ API Reference
|
|
@@ -294,36 +512,55 @@ Converts a HEIC image file to a standard web format.
|
|
|
294
512
|
|
|
295
513
|
- **`input`**: `Blob | File | ArrayBuffer | Uint8Array`
|
|
296
514
|
- **`options`**: (optional) `ConvertOptions`
|
|
297
|
-
- `to`: `'jpeg' | 'jpg' | 'png' | 'webp' | 'svg'` (Default: `'jpeg'`)
|
|
298
|
-
- `quality`: `number` (0.0 to 1.0, applicable to JPEG and
|
|
515
|
+
- `to`: `'jpeg' | 'jpg' | 'png' | 'webp' | 'avif' | 'svg'` (Default: `'jpeg'`; `avif` rejects with `format_unsupported` where the canvas cannot encode it)
|
|
516
|
+
- `quality`: `number` (0.0 to 1.0, applicable to JPEG, WebP, and AVIF. Default: `0.92`)
|
|
299
517
|
- `decoder`: `IHeicDecoder` (Inject custom decoder instance)
|
|
300
518
|
- `onProgress`: `(percent: number) => void` (Optional callback, receives progress percentage from `0` to `100` during decoding)
|
|
301
519
|
- `maxWidth`: `number` (Downscale to fit within this width, preserving aspect ratio. Never upscales)
|
|
302
520
|
- `maxHeight`: `number` (Downscale to fit within this height, preserving aspect ratio. Never upscales)
|
|
303
521
|
- `scale`: `number` (Uniform scale factor, e.g. `0.5` halves the image. Takes precedence over `maxWidth`/`maxHeight`)
|
|
304
|
-
-
|
|
522
|
+
- `applyOrientation`: `boolean` (Rotate/flip the output to match the source's EXIF orientation. `irot`/`imir` transforms are already applied by the decoder and never stacked. Default: `true`)
|
|
523
|
+
- `crop`: `{ x?, y?, width, height }` (Cut a rectangle in post-orientation display pixels before resize. Default: none)
|
|
524
|
+
- `preserveExif`: `boolean` (Re-inject the source EXIF block into JPEG (APP1) / PNG (`eXIf`) output. Metadata may contain GPS โ opt-in. Default: `false`. Fail-safe: unparsable encoder output, a malformed block, or an oversized payload returns the image without metadata rather than failing โ as does a missing `dist/exif-*.mjs` chunk, which logs a one-time warning so a deployment problem is distinguishable from a file that simply had no Exif item)
|
|
525
|
+
- `output`: `'blob' | 'dataUrl' | 'arrayBuffer'` (Result representation. Default: `'blob'`; see [Usage section 10](#10-output-shapes-blob-data-url-arraybuffer))
|
|
526
|
+
- `signal`: `AbortSignal` (Cancel pending work; rejects with `aborted`. Default: none)
|
|
527
|
+
- **Returns**: `Promise<Blob>` โ or `Promise<string>` / `Promise<ArrayBuffer>` with the typed `output` overloads
|
|
305
528
|
|
|
306
529
|
### `convertMany(inputs, options?)`
|
|
307
530
|
|
|
308
|
-
Converts multiple HEIC images with bounded concurrency. Results are returned in input order; rejects
|
|
531
|
+
Converts multiple HEIC images with bounded concurrency. Results are returned in input order; rejects with the first failure in time โ a `batch_item_failed` error whose `message` names the failing item and whose fields describe the batch (`itemIndex` 0-based, `itemTotal`, `failedCount`, `cause`). With `continueOnError` the promise fulfills with per-item results instead.
|
|
309
532
|
|
|
310
533
|
- **`inputs`**: `Array<Blob | File | ArrayBuffer | Uint8Array>`
|
|
311
534
|
- **`options`**: (optional) `ConvertManyOptions` โ same as `ConvertOptions`, except `onProgress` uses the batch signature below; plus:
|
|
312
535
|
- `concurrency`: `number` (Maximum concurrent conversions. Default: `4`)
|
|
313
|
-
- `onProgress`: `(index: number, percent: number) => void` (Per-item progress callback)
|
|
314
|
-
- `decoder`: `IHeicDecoder` (Optional. When provided, the same instance is shared by all concurrent conversions and must be safe for concurrent `decode()` calls)
|
|
315
|
-
-
|
|
536
|
+
- `onProgress`: `(index: number, percent: number) => void` (Per-item progress callback; fires only for successful items)
|
|
537
|
+
- `decoder`: `IHeicDecoder` (Optional. When provided, the same instance is shared by all concurrent conversions and must be safe for concurrent `decode()` calls; the library never frees an injected decoder; `reuseDecoders` is ignored)
|
|
538
|
+
- `continueOnError`: `boolean` (Return per-item `{ index, ok, result | error }` entries instead of rejecting on the first failure. Default: `false`)
|
|
539
|
+
- `reuseDecoders`: `boolean` (Hand each batch runner one pooled `LibheifDecoder` reused across items โ amortizes WASM init for large batches while keeping exclusive use. Default: `false`)
|
|
540
|
+
- **Returns**: `Promise<Blob[]>` โ or `Promise<string[]>` / `Promise<ArrayBuffer[]>` with the typed `output` overloads; `Promise<ConvertItemResult[]>` with `continueOnError` (also typed by `output`)
|
|
316
541
|
|
|
317
542
|
### `convertHeicInWorker(input, options)`
|
|
318
543
|
|
|
319
|
-
Converts a HEIC image inside a Web Worker. The worker script must implement the message protocol shown in [Usage section
|
|
544
|
+
Converts a HEIC image inside a Web Worker. The worker script must implement the message protocol shown in [Usage section 13](#13-web-worker-conversion). Browser-only; rejects in Node.js.
|
|
320
545
|
|
|
321
546
|
- **`input`**: `Blob | File | ArrayBuffer | Uint8Array`
|
|
322
|
-
- **`options`**: `WorkerConvertOptions` โ same as `ConvertOptions
|
|
547
|
+
- **`options`**: `WorkerConvertOptions` โ same as `ConvertOptions` but without `decoder` (cannot be structured-cloned; passing one rejects with `invalid_input`), plus:
|
|
323
548
|
- `workerUrl`: `string | URL` (URL of the worker script; should be a compile-time constant)
|
|
324
549
|
- `workerType`: `'classic' | 'module'` (Worker script type. Default: `'classic'`; use `'module'` for scripts with ES imports)
|
|
325
|
-
- `timeoutMs`: `number` (Maximum wait for the result in milliseconds.
|
|
326
|
-
-
|
|
550
|
+
- `timeoutMs`: `number` (Maximum wait for the result in milliseconds, including time spent queued. Finite, โฅ 0; `0` disables. Default: `60000`)
|
|
551
|
+
- `maxConcurrentWorkers`: `number` (Positive integer. Concurrent workers per `workerUrl` + type; extra calls queue. Default: `navigator.hardwareConcurrency` clamped to 1โ8)
|
|
552
|
+
- **Returns**: `Promise<Blob>` โ or `Promise<string>` / `Promise<ArrayBuffer>` with the typed `output` overloads. Aborting via `signal` terminates the worker immediately.
|
|
553
|
+
|
|
554
|
+
### `convertManyInWorker(inputs, options)`
|
|
555
|
+
|
|
556
|
+
Converts multiple HEIC images, each inside a Web Worker, with `convertMany` semantics (input order, `batch_item_failed` aggregation, `continueOnError` per-item results). Browser-only; rejects in Node.js.
|
|
557
|
+
|
|
558
|
+
- **`inputs`**: `Array<Blob | File | ArrayBuffer | Uint8Array>`
|
|
559
|
+
- **`options`**: `WorkerBatchOptions` โ same as `convertHeicInWorker`'s `WorkerConvertOptions` (including `output`, `crop`, `preserveExif`, `signal`), except `onProgress` uses the batch signature below; plus:
|
|
560
|
+
- `onProgress`: `(index: number, percent: number) => void` (Per-item progress callback)
|
|
561
|
+
- `maxConcurrentWorkers`: `number` (Positive integer. Real concurrency โ each in-flight item occupies one semaphore slot. Default: `navigator.hardwareConcurrency` clamped to 1โ8)
|
|
562
|
+
- `continueOnError`: `boolean` (Per-item `{ index, ok, result | error }` entries instead of rejecting on the first failure. Default: `false`)
|
|
563
|
+
- **Returns**: `Promise<Blob[]>` โ typed overloads mirror `convertMany` (`output` / `continueOnError` combinations)
|
|
327
564
|
|
|
328
565
|
### `LibheifDecoder(options?)`
|
|
329
566
|
|
|
@@ -331,11 +568,57 @@ The default WASM-based implementation of `IHeicDecoder`.
|
|
|
331
568
|
|
|
332
569
|
- **`options`**: (optional) `LibheifDecoderOptions`
|
|
333
570
|
- `locateFile`: `(path: string, prefix: string) => string`
|
|
334
|
-
- `wasmBinary`: `ArrayBuffer`
|
|
571
|
+
- `wasmBinary`: `ArrayBuffer | ArrayBufferView` (e.g. a Node.js `Buffer` from `fs.readFileSync`)
|
|
572
|
+
- `moduleOverrides`: `Record<string, unknown>` (Advanced: merged into the Emscripten module arguments, e.g. `instantiateWasm`)
|
|
335
573
|
- **Methods**:
|
|
336
574
|
- `initialize(): Promise<void>`: Loads and initializes the WASM wrapper. Safe to call concurrently; the module loads at most once per instance.
|
|
337
|
-
- `decode(data: Uint8Array, onProgress?: (percent: number) => void): Promise<DecodedImage>`: Decodes the HEIC bytes to raw RGBA, with optional progress callback
|
|
338
|
-
- `free(): void`: Releases
|
|
575
|
+
- `decode(data: Uint8Array, onProgress?: (percent: number) => void): Promise<DecodedImage>`: Decodes the HEIC bytes to raw RGBA, with optional progress callback (normalized to finite `0`โ`100`). Returns JS-owned pixel data โ safe to use after `free()`. The result carries an optional `orientation` field (`2`โ`8`, EXIF semantics) when the stored pixels need a rotation for display that the decoder did not itself apply (EXIF-tag-only files); absent or `1` means the pixels are upright. Safe to call after `free()`: the instance transparently re-initializes.
|
|
576
|
+
- `free(): void`: Releases the WASM module and decoder instance. Idempotent, and safe to call while `initialize()` is in flight (the in-flight load is discarded).
|
|
577
|
+
|
|
578
|
+
### Error handling
|
|
579
|
+
|
|
580
|
+
All errors thrown by this library are `HeicConverterError` instances (`extends Error`) with a machine-readable `code`, and โ where relevant โ a `cause` and batch fields:
|
|
581
|
+
|
|
582
|
+
| `code` | Thrown by | Meaning |
|
|
583
|
+
| --- | --- | --- |
|
|
584
|
+
| `invalid_input` | all conversion APIs | Unsupported input type; non-boolean `applyOrientation`/`continueOnError`/`reuseDecoders`/`preserveExif`, unknown `output`, malformed `signal`; or worker helper called with a `decoder` |
|
|
585
|
+
| `invalid_quality` | all conversion APIs | `quality` outside `0.0`โ`1.0` or not a finite number |
|
|
586
|
+
| `invalid_resize` | all conversion APIs | `scale`/`maxWidth`/`maxHeight` not positive finite numbers, or target size exceeds 16384 px |
|
|
587
|
+
| `invalid_format` | all conversion APIs | Unknown `to` value |
|
|
588
|
+
| `invalid_crop` | all conversion APIs | `crop` with non-integer/non-positive dimensions, negative offsets, or a rectangle beyond the image's display size |
|
|
589
|
+
| `invalid_concurrency` | all conversion APIs | `concurrency` (batch) or `maxConcurrentWorkers` (worker APIs) not a positive integer |
|
|
590
|
+
| `decoder_init_failed` | `convertHeic`, `convertMany` | WASM module could not be loaded (missing asset, CSP block) |
|
|
591
|
+
| `decode_failed` | `convertHeic`, `convertMany` | Invalid/corrupt HEIC bytes |
|
|
592
|
+
| `unsupported_environment` | `convertHeic`, `convertMany` | No canvas APIs (e.g. Node.js); the worker APIs reject with `worker_unsupported` there instead. Decode raw RGBA via `LibheifDecoder` |
|
|
593
|
+
| `render_encode_failed` | `convertHeic`, `convertMany` | Canvas render/encode failure (bad dimensions, `toBlob` returned null) |
|
|
594
|
+
| `format_unsupported` | `convertHeic`, `convertMany` | The canvas cannot encode the requested format (e.g. `avif` on Safari) |
|
|
595
|
+
| `aborted` | all conversion APIs | The `AbortSignal` was aborted before the work completed |
|
|
596
|
+
| `progress_callback_failed` | all conversion APIs | The host `onProgress` callback threw; message attributes the failure |
|
|
597
|
+
| `worker_unsupported` | `convertHeicInWorker`, `convertManyInWorker` | No global `Worker` (e.g. Node.js) |
|
|
598
|
+
| `worker_create_failed` | `convertHeicInWorker`, `convertManyInWorker` | `new Worker(...)` threw (wrong URL, MIME type) |
|
|
599
|
+
| `worker_post_failed` | `convertHeicInWorker`, `convertManyInWorker` | `postMessage` threw (non-cloneable option) |
|
|
600
|
+
| `worker_timeout` | `convertHeicInWorker`, `convertManyInWorker` | No result within `timeoutMs`; message includes progress/protocol diagnostics |
|
|
601
|
+
| `worker_failed` | `convertHeicInWorker`, `convertManyInWorker` | Worker reported `{ type: 'result', ok: false, error }` |
|
|
602
|
+
| `worker_load_failed` | `convertHeicInWorker`, `convertManyInWorker` | The lazily imported worker chunk (`dist/worker-*.mjs`) could not be fetched โ deploy every file in `dist/` together, check your bundler emitted the chunk, and inspect `error.cause`; use `convertHeic` to convert in the current thread |
|
|
603
|
+
| `batch_item_failed` | `convertMany`, `convertManyInWorker` | One or more items failed; see `itemIndex`/`itemTotal`/`failedCount`/`cause` |
|
|
604
|
+
|
|
605
|
+
Option validation for **all four** conversion APIs runs on the main thread before any worker is created, so a bad option keeps its own code. One ordering note: `convertHeicInWorker`/`convertManyInWorker` resolve the worker chunk before the implementation runs, so when that chunk itself cannot be fetched the rejection is `worker_load_failed` regardless of other option problems โ the original failure stays on `cause`. Stage errors raised *inside* a worker (decode, render, unsupported format) come back stringified under `worker_failed` โ the numeric `code` does not survive the worker boundary; match on the message instead.
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
import { convertHeic, type HeicConverterErrorCode } from '@keeratita/heic-converter';
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await convertHeic(input, { to: 'png' });
|
|
612
|
+
} catch (error) {
|
|
613
|
+
const code: HeicConverterErrorCode | undefined =
|
|
614
|
+
error && typeof error === 'object' && 'code' in error
|
|
615
|
+
? (error as { code?: HeicConverterErrorCode }).code
|
|
616
|
+
: undefined;
|
|
617
|
+
if (code === 'unsupported_environment') {
|
|
618
|
+
// e.g. Node.js: decode raw RGBA with LibheifDecoder and encode externally
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
```
|
|
339
622
|
|
|
340
623
|
### `freeSharedDecoder()`
|
|
341
624
|
|
|
@@ -362,7 +645,14 @@ To compile the underlying [`libheif`](https://github.com/strukturag/libheif) and
|
|
|
362
645
|
npm run build:wasm
|
|
363
646
|
```
|
|
364
647
|
|
|
365
|
-
|
|
648
|
+
That rewrites **both** generated artifacts together โ `src/wasm/public/heic-decoder.wasm` and `src/wasm/wrapper/heic-decoder.js` come from a single `emcc` invocation, so never replace one alone โ and you must then refresh the pinned hashes or CI's verification step fails:
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
npm run wasm:hashes # regenerates build-scripts/wasm-artifacts.json โ commit it with the artifacts
|
|
652
|
+
npm run verify:wasm # confirms the committed pair matches, and scans the glue for eval/new Function
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Never hand-edit either generated file. See [WASM_DEPENDENCIES.md](WASM_DEPENDENCIES.md) for the pinned upstream library versions.
|
|
366
656
|
|
|
367
657
|
### Build JS & TS Typings
|
|
368
658
|
|
|
@@ -374,15 +664,20 @@ npm run build
|
|
|
374
664
|
|
|
375
665
|
### Run Unit Tests
|
|
376
666
|
|
|
667
|
+
Build first: the suites that decode real HEIC files gate on `dist/heic-decoder.wasm` (a stale `dist/` is detected and reported rather than silently passing).
|
|
668
|
+
|
|
377
669
|
```bash
|
|
670
|
+
npm run build
|
|
378
671
|
npm run test
|
|
672
|
+
npm run typecheck # tsc over src/ *and* test/ โ tests are excluded from tsconfig.json
|
|
379
673
|
```
|
|
380
674
|
|
|
381
675
|
### Run Browser E2E Tests
|
|
382
676
|
|
|
383
|
-
Real browser conversions against the CSP sandbox and the GitHub Pages demo (`docs/`)
|
|
677
|
+
Real browser conversions against the CSP sandbox and the GitHub Pages demo (`docs/`). Build first โ both servers serve `dist/`:
|
|
384
678
|
|
|
385
679
|
```bash
|
|
680
|
+
npm run build
|
|
386
681
|
npx playwright install chromium # one-time
|
|
387
682
|
npm run test:e2e
|
|
388
683
|
```
|
|
@@ -418,6 +713,37 @@ This script will automatically run the linter, build the TS library, run the uni
|
|
|
418
713
|
|
|
419
714
|
---
|
|
420
715
|
|
|
716
|
+
## ๐ก๏ธ Supply Chain & Trust
|
|
717
|
+
|
|
718
|
+
| Property | Status |
|
|
719
|
+
| -------- | ------ |
|
|
720
|
+
| Runtime dependencies | **none** โ the package ships no `dependencies` at all |
|
|
721
|
+
| Provenance | SLSA provenance attestation, from OIDC trusted publishing in `.github/workflows/publish.yml` |
|
|
722
|
+
| Install-time code | two dev-tree packages โ `esbuild` (links its platform binary) and `fsevents` (macOS-only native addon) โ pinned in an allowlist that CI enforces (`npm run check:scripts`) |
|
|
723
|
+
| `eval()` / `new Function()` | never generated (`-s DYNAMIC_EXECUTION=0`); CI scans the emitted glue for both |
|
|
724
|
+
| Decoder artifacts | committed WASM + glue pinned to SHA-256 and re-verified on every CI run (`npm run verify:wasm`) |
|
|
725
|
+
| Publish path | tag pushes only; the job asserts the attestation landed (`npm run verify:provenance`) |
|
|
726
|
+
|
|
727
|
+
After installing, you can verify the attestation yourself in your own project:
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
npm audit signatures
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Scanner signals you will see
|
|
734
|
+
|
|
735
|
+
Automated scanners report three signals on this package. All three are expected; they are documented here rather than engineered around:
|
|
736
|
+
|
|
737
|
+
| Signal | What it actually is |
|
|
738
|
+
| ------ | ------------------- |
|
|
739
|
+
| **Network access** | The generated Emscripten glue calls `fetch()` / `XMLHttpRequest` to download `dist/heic-decoder.wasm` (~800 KB). That download is how a browser gets the decoder. Nothing else in the package touches the network โ `src/` contains no `fetch`, `XMLHttpRequest`, or socket code. |
|
|
740
|
+
| **URL strings** | The literal `http://www.w3.org/2000/svg`, the XML namespace on the root element of SVG output. It is a format identifier and is never fetched. |
|
|
741
|
+
| **Minified code** | `dist/` is Terser-minified deliberately (~6% smaller gzipped, more on the glue). The published bytes come from the tagged commit through CI, with provenance attached. |
|
|
742
|
+
|
|
743
|
+
If one of these ever becomes actually exploitable rather than merely noisy, that is a bug โ see [SECURITY.md](SECURITY.md).
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
421
747
|
## ๐ License
|
|
422
748
|
|
|
423
749
|
MIT ยฉ Keerati Tansawatcharoen
|