@keeratita/heic-converter 0.4.1 โ 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -40
- package/dist/heic-decoder-7BA7NJOA.js +5 -0
- package/dist/heic-decoder-WNTZ4MAN.mjs +5 -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 +361 -36
- package/dist/index.d.ts +361 -36
- package/dist/index.js +3 -7
- package/dist/index.mjs +3 -7
- package/package.json +17 -13
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,6 +134,37 @@ const jpegBlob = await convertHeic(heicBlob, {
|
|
|
121
134
|
});
|
|
122
135
|
```
|
|
123
136
|
|
|
137
|
+
#### Using a bundler (Vite, webpack, Rollup, esbuild)
|
|
138
|
+
|
|
139
|
+
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:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { convertHeic, LibheifDecoder } from '@keeratita/heic-converter';
|
|
143
|
+
|
|
144
|
+
declare const heicBlob: Blob;
|
|
145
|
+
|
|
146
|
+
// Serve a copy of @keeratita/heic-converter/dist/heic-decoder.wasm there.
|
|
147
|
+
const blob = await convertHeic(heicBlob, {
|
|
148
|
+
decoder: new LibheifDecoder({ locateFile: () => '/assets/heic-decoder.wasm' }),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Fetching a `.gz`/`.br` variant URL directly will not work โ see the compression note below.
|
|
153
|
+
|
|
154
|
+
#### Reducing the download size
|
|
155
|
+
|
|
156
|
+
The WASM binary (~1.3 MB) dominates the payload. Everything else is small and lazy: the Emscripten glue (~70 KB) is a separate chunk fetched only on the first decode, and the main entry is ~26 KB.
|
|
157
|
+
|
|
158
|
+
`npm run build` also emits pre-compressed copies โ `dist/heic-decoder.wasm.gz` (~421 KB) and `dist/heic-decoder.wasm.br` (~312 KB). Most static hosts and CDNs (GitHub Pages, Netlify, Vercel, Cloudflare) already compress `application/wasm` automatically when the browser sends `Accept-Encoding`; verify with:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
curl -sI -H 'Accept-Encoding: br' https://your-site/heic-decoder.wasm | grep -i content-encoding
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
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`.
|
|
165
|
+
|
|
166
|
+
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.
|
|
167
|
+
|
|
124
168
|
### 5. Node.js: Decoding Raw Pixel Data
|
|
125
169
|
|
|
126
170
|
Since Node.js lacks the native browser Canvas API, `convertHeic` (which relies on Canvas to encode raster formats) will throw an error on the backend.
|
|
@@ -135,19 +179,34 @@ import sharp from 'sharp'; // external node image library
|
|
|
135
179
|
async function convertNode() {
|
|
136
180
|
const heicData = new Uint8Array(fs.readFileSync('input.heic'));
|
|
137
181
|
|
|
138
|
-
|
|
182
|
+
// Passing wasmBinary explicitly avoids relying on default path resolution
|
|
183
|
+
// inside the package; fs.readFileSync returns a Buffer, which the option
|
|
184
|
+
// accepts (it takes ArrayBuffer or any ArrayBufferView).
|
|
185
|
+
const decoder = new LibheifDecoder({
|
|
186
|
+
wasmBinary: fs.readFileSync(
|
|
187
|
+
'node_modules/@keeratita/heic-converter/dist/heic-decoder.wasm',
|
|
188
|
+
),
|
|
189
|
+
});
|
|
139
190
|
await decoder.initialize();
|
|
140
191
|
|
|
141
|
-
// Decodes to { width, height, data: Uint8ClampedArray (RGBA) }.
|
|
192
|
+
// Decodes to { width, height, data: Uint8ClampedArray (RGBA), orientation?, exif? }.
|
|
142
193
|
// data is an independent copy โ safe to use after decoder.free().
|
|
143
|
-
|
|
194
|
+
// orientation 2-8 means the stored pixels need a rotation/flip for display
|
|
195
|
+
// (convertHeic applies it automatically; raw-decode consumers must do it
|
|
196
|
+
// themselves โ rotate the buffer, or set the orientation tag in `exif`
|
|
197
|
+
// before handing it to sharp so viewers apply it).
|
|
198
|
+
// exif is the source EXIF block ("Exif\0\0" + TIFF) when the file carries
|
|
199
|
+
// one โ pass it to sharp.withMetadata({ exif }) to keep camera metadata.
|
|
200
|
+
const { width, height, data, exif } = await decoder.decode(heicData);
|
|
144
201
|
|
|
145
202
|
// Process raw pixels using sharp
|
|
146
|
-
|
|
203
|
+
let pipeline = sharp(Buffer.from(data), {
|
|
147
204
|
raw: { width, height, channels: 4 },
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
.
|
|
205
|
+
});
|
|
206
|
+
if (exif) {
|
|
207
|
+
pipeline = pipeline.withMetadata({ exif: Buffer.from(exif) });
|
|
208
|
+
}
|
|
209
|
+
await pipeline.toFormat('jpeg').toFile('output.jpg');
|
|
151
210
|
|
|
152
211
|
// Clean up WASM memory
|
|
153
212
|
decoder.free();
|
|
@@ -161,7 +220,8 @@ For large images, you can pass an `onProgress` callback to track the conversion
|
|
|
161
220
|
```typescript
|
|
162
221
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
163
222
|
|
|
164
|
-
const heicBlob
|
|
223
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
224
|
+
|
|
165
225
|
const jpegBlob = await convertHeic(heicBlob, {
|
|
166
226
|
to: 'jpeg',
|
|
167
227
|
onProgress: (percent) => {
|
|
@@ -171,6 +231,9 @@ const jpegBlob = await convertHeic(heicBlob, {
|
|
|
171
231
|
});
|
|
172
232
|
```
|
|
173
233
|
|
|
234
|
+
> [!NOTE]
|
|
235
|
+
> 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.
|
|
236
|
+
|
|
174
237
|
> [!TIP]
|
|
175
238
|
> 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.
|
|
176
239
|
|
|
@@ -181,6 +244,8 @@ Downscale to fit within maximum dimensions (aspect ratio is preserved, images sm
|
|
|
181
244
|
```typescript
|
|
182
245
|
import { convertHeic } from '@keeratita/heic-converter';
|
|
183
246
|
|
|
247
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
248
|
+
|
|
184
249
|
const thumbnailBlob = await convertHeic(heicBlob, {
|
|
185
250
|
to: 'jpeg',
|
|
186
251
|
maxWidth: 800,
|
|
@@ -197,13 +262,97 @@ const halfSizeBlob = await convertHeic(heicBlob, {
|
|
|
197
262
|
});
|
|
198
263
|
```
|
|
199
264
|
|
|
200
|
-
|
|
265
|
+
> [!NOTE]
|
|
266
|
+
> 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.
|
|
267
|
+
|
|
268
|
+
### 8. Orientation (EXIF) Handling
|
|
269
|
+
|
|
270
|
+
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:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { convertHeic } from '@keeratita/heic-converter';
|
|
274
|
+
|
|
275
|
+
// Upright according to the source's EXIF orientation (default: applyOrientation: true)
|
|
276
|
+
const jpeg = await convertHeic(heicFile);
|
|
277
|
+
|
|
278
|
+
// Keep the exact stored-pixel geometry instead (the pre-0.5 behavior):
|
|
279
|
+
const unrotated = await convertHeic(heicFile, { applyOrientation: false });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
How it works:
|
|
283
|
+
|
|
284
|
+
- 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.
|
|
285
|
+
- 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.
|
|
286
|
+
- `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.
|
|
287
|
+
- A non-boolean `applyOrientation` value is rejected up front with an `invalid_input` error.
|
|
288
|
+
|
|
289
|
+
### 9. Cropping
|
|
290
|
+
|
|
291
|
+
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:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { convertHeic } from '@keeratita/heic-converter';
|
|
295
|
+
|
|
296
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
297
|
+
|
|
298
|
+
const thumbnail = await convertHeic(heicBlob, {
|
|
299
|
+
to: 'jpeg',
|
|
300
|
+
crop: { x: 200, y: 100, width: 640, height: 480 }, // x/y default to 0
|
|
301
|
+
maxWidth: 320, // downscale the crop, not the whole image
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Cropping semantics:
|
|
306
|
+
|
|
307
|
+
- `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.
|
|
308
|
+
- Crop, orientation, and resize compose into a single canvas transform โ no intermediate copies.
|
|
309
|
+
- With `applyOrientation: false`, crop coordinates address the **stored** pixels instead (geometry as decoded).
|
|
310
|
+
|
|
311
|
+
### 10. Output Shapes (Blob, data URL, ArrayBuffer)
|
|
312
|
+
|
|
313
|
+
By default conversions return a `Blob`. Choose another representation with `output` โ the return type follows the option:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const dataUrl = await convertHeic(heicBlob, { to: 'png', output: 'dataUrl' });
|
|
317
|
+
// string: "data:image/png;base64,โฆ" โ ready for <img src>
|
|
318
|
+
|
|
319
|
+
const bytes = await convertHeic(heicBlob, { to: 'webp', output: 'arrayBuffer' });
|
|
320
|
+
// ArrayBuffer โ for uploads (fetch body), WebCodecs, custom pipelines
|
|
321
|
+
|
|
322
|
+
const blob = await convertHeic(heicBlob, { to: 'jpeg' }); // default Blob
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`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`.
|
|
326
|
+
|
|
327
|
+
### 11. Canceling a Conversion (AbortSignal)
|
|
328
|
+
|
|
329
|
+
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:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { convertHeic, convertHeicInWorker } from '@keeratita/heic-converter';
|
|
333
|
+
|
|
334
|
+
const controller = new AbortController();
|
|
335
|
+
setTimeout(() => controller.abort(), 2000); // give up after 2s
|
|
336
|
+
|
|
337
|
+
await convertHeic(heicBlob, { signal: controller.signal }); // rejects with code 'aborted'
|
|
338
|
+
await convertMany(files, { signal: controller.signal }); // aborts remaining items
|
|
339
|
+
await convertHeicInWorker(heicBlob, { workerUrl, signal: controller.signal }); // terminates the worker
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
> [!NOTE]
|
|
343
|
+
> - Worker-based conversions abort **immediately**: the worker is terminated, since it runs off the main thread.
|
|
344
|
+
> - The synchronous WASM `decode()` on the main thread cannot be preempted mid-call โ cancellation takes effect at the next boundary after it finishes.
|
|
345
|
+
> - 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.
|
|
346
|
+
|
|
347
|
+
### 12. Batch Conversion
|
|
201
348
|
|
|
202
349
|
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:
|
|
203
350
|
|
|
204
351
|
```typescript
|
|
205
352
|
import { convertMany } from '@keeratita/heic-converter';
|
|
206
353
|
|
|
354
|
+
declare const heicFiles: File[]; // your HEIC files/blobs
|
|
355
|
+
|
|
207
356
|
const blobs = await convertMany(heicFiles, {
|
|
208
357
|
to: 'png',
|
|
209
358
|
concurrency: 3,
|
|
@@ -213,7 +362,40 @@ const blobs = await convertMany(heicFiles, {
|
|
|
213
362
|
});
|
|
214
363
|
```
|
|
215
364
|
|
|
216
|
-
|
|
365
|
+
Batch semantics worth knowing:
|
|
366
|
+
|
|
367
|
+
- 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.
|
|
368
|
+
- 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.
|
|
369
|
+
- `onProgress` only fires for items that succeed; a failing item never reports 100%.
|
|
370
|
+
- `concurrency` must be a positive integer; the default is `4`.
|
|
371
|
+
|
|
372
|
+
**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:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
const results = await convertMany(files, { to: 'png', continueOnError: true });
|
|
376
|
+
// Array<{ index: number; ok: true; result: Blob } | { index: number; ok: false; error: Error }>
|
|
377
|
+
|
|
378
|
+
const failures = results.filter((r) => !r.ok);
|
|
379
|
+
if (failures.length > 0) {
|
|
380
|
+
console.warn(`${failures.length} file(s) failed`, failures.map((f) => f.index));
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Up-front validation mistakes (bad format, bad `concurrency`, โฆ) still reject normally โ a typo is a caller bug, not an item failure.
|
|
385
|
+
|
|
386
|
+
**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):
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
const blobs = await convertMany(thousandFiles, {
|
|
390
|
+
to: 'webp',
|
|
391
|
+
concurrency: 4,
|
|
392
|
+
reuseDecoders: true, // one LibheifDecoder per runner instead of one per item
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Ignored when you inject your own `decoder` (yours is used for all items, per the injection contract).
|
|
397
|
+
|
|
398
|
+
### 13. Web Worker Conversion
|
|
217
399
|
|
|
218
400
|
Run the conversion inside a Web Worker so the main thread stays responsive. Create a worker script that uses this library:
|
|
219
401
|
|
|
@@ -228,7 +410,11 @@ self.onmessage = async (event) => {
|
|
|
228
410
|
...options,
|
|
229
411
|
onProgress: (percent) => self.postMessage({ type: 'progress', percent }),
|
|
230
412
|
});
|
|
231
|
-
|
|
413
|
+
// With output: 'arrayBuffer' the payload is zero-copy via a transfer list.
|
|
414
|
+
self.postMessage(
|
|
415
|
+
{ type: 'result', ok: true, blob },
|
|
416
|
+
blob instanceof ArrayBuffer ? [blob] : []
|
|
417
|
+
);
|
|
232
418
|
} catch (error) {
|
|
233
419
|
self.postMessage({ type: 'result', ok: false, error: error?.stack ?? error?.message ?? String(error) });
|
|
234
420
|
}
|
|
@@ -240,6 +426,8 @@ Then convert from the main thread:
|
|
|
240
426
|
```typescript
|
|
241
427
|
import { convertHeicInWorker } from '@keeratita/heic-converter';
|
|
242
428
|
|
|
429
|
+
declare const heicBlob: Blob; // your HEIC file/blob
|
|
430
|
+
|
|
243
431
|
const jpegBlob = await convertHeicInWorker(heicBlob, {
|
|
244
432
|
workerUrl: new URL('./converter.worker.js', import.meta.url),
|
|
245
433
|
workerType: 'module',
|
|
@@ -252,11 +440,28 @@ const jpegBlob = await convertHeicInWorker(heicBlob, {
|
|
|
252
440
|
> [!NOTE]
|
|
253
441
|
> - 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.
|
|
254
442
|
> - `workerUrl` should be a compile-time constant; the script runs with the page's privileges.
|
|
255
|
-
> - Only `progress` and `result` messages are understood; any other message type is ignored.
|
|
256
|
-
> - `decoder
|
|
257
|
-
> -
|
|
443
|
+
> - 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).
|
|
444
|
+
> - `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.
|
|
445
|
+
> - 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).
|
|
446
|
+
> - `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).
|
|
258
447
|
> - This helper is **browser-only**: it rejects in Node.js, where there is no global `Worker`.
|
|
259
448
|
|
|
449
|
+
**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`:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import { convertManyInWorker } from '@keeratita/heic-converter';
|
|
453
|
+
|
|
454
|
+
const blobs = await convertManyInWorker(files, {
|
|
455
|
+
workerUrl: new URL('./converter.worker.js', import.meta.url),
|
|
456
|
+
workerType: 'module',
|
|
457
|
+
to: 'webp',
|
|
458
|
+
maxConcurrentWorkers: 3,
|
|
459
|
+
onProgress: (index, percent) => console.log(`Image ${index}: ${Math.round(percent)}%`),
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
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.
|
|
464
|
+
|
|
260
465
|
---
|
|
261
466
|
|
|
262
467
|
## ๐ Content Security Policy (CSP)
|
|
@@ -280,36 +485,55 @@ Converts a HEIC image file to a standard web format.
|
|
|
280
485
|
|
|
281
486
|
- **`input`**: `Blob | File | ArrayBuffer | Uint8Array`
|
|
282
487
|
- **`options`**: (optional) `ConvertOptions`
|
|
283
|
-
- `to`: `'jpeg' | 'jpg' | 'png' | 'webp' | 'svg'` (Default: `'jpeg'`)
|
|
284
|
-
- `quality`: `number` (0.0 to 1.0, applicable to JPEG and
|
|
488
|
+
- `to`: `'jpeg' | 'jpg' | 'png' | 'webp' | 'avif' | 'svg'` (Default: `'jpeg'`; `avif` rejects with `format_unsupported` where the canvas cannot encode it)
|
|
489
|
+
- `quality`: `number` (0.0 to 1.0, applicable to JPEG, WebP, and AVIF. Default: `0.92`)
|
|
285
490
|
- `decoder`: `IHeicDecoder` (Inject custom decoder instance)
|
|
286
491
|
- `onProgress`: `(percent: number) => void` (Optional callback, receives progress percentage from `0` to `100` during decoding)
|
|
287
492
|
- `maxWidth`: `number` (Downscale to fit within this width, preserving aspect ratio. Never upscales)
|
|
288
493
|
- `maxHeight`: `number` (Downscale to fit within this height, preserving aspect ratio. Never upscales)
|
|
289
494
|
- `scale`: `number` (Uniform scale factor, e.g. `0.5` halves the image. Takes precedence over `maxWidth`/`maxHeight`)
|
|
290
|
-
-
|
|
495
|
+
- `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`)
|
|
496
|
+
- `crop`: `{ x?, y?, width, height }` (Cut a rectangle in post-orientation display pixels before resize. Default: none)
|
|
497
|
+
- `preserveExif`: `boolean` (Re-inject the source EXIF block into JPEG (APP1) / PNG (`eXIf`) output. Metadata may contain GPS โ opt-in. Default: `false`)
|
|
498
|
+
- `output`: `'blob' | 'dataUrl' | 'arrayBuffer'` (Result representation. Default: `'blob'`; see [Usage section 10](#10-output-shapes-blob-data-url-arraybuffer))
|
|
499
|
+
- `signal`: `AbortSignal` (Cancel pending work; rejects with `aborted`. Default: none)
|
|
500
|
+
- **Returns**: `Promise<Blob>` โ or `Promise<string>` / `Promise<ArrayBuffer>` with the typed `output` overloads
|
|
291
501
|
|
|
292
502
|
### `convertMany(inputs, options?)`
|
|
293
503
|
|
|
294
|
-
Converts multiple HEIC images with bounded concurrency. Results are returned in input order; rejects
|
|
504
|
+
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.
|
|
295
505
|
|
|
296
506
|
- **`inputs`**: `Array<Blob | File | ArrayBuffer | Uint8Array>`
|
|
297
507
|
- **`options`**: (optional) `ConvertManyOptions` โ same as `ConvertOptions`, except `onProgress` uses the batch signature below; plus:
|
|
298
508
|
- `concurrency`: `number` (Maximum concurrent conversions. Default: `4`)
|
|
299
|
-
- `onProgress`: `(index: number, percent: number) => void` (Per-item progress callback)
|
|
300
|
-
- `decoder`: `IHeicDecoder` (Optional. When provided, the same instance is shared by all concurrent conversions and must be safe for concurrent `decode()` calls)
|
|
301
|
-
-
|
|
509
|
+
- `onProgress`: `(index: number, percent: number) => void` (Per-item progress callback; fires only for successful items)
|
|
510
|
+
- `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)
|
|
511
|
+
- `continueOnError`: `boolean` (Return per-item `{ index, ok, result | error }` entries instead of rejecting on the first failure. Default: `false`)
|
|
512
|
+
- `reuseDecoders`: `boolean` (Hand each batch runner one pooled `LibheifDecoder` reused across items โ amortizes WASM init for large batches while keeping exclusive use. Default: `false`)
|
|
513
|
+
- **Returns**: `Promise<Blob[]>` โ or `Promise<string[]>` / `Promise<ArrayBuffer[]>` with the typed `output` overloads; `Promise<ConvertItemResult[]>` with `continueOnError` (also typed by `output`)
|
|
302
514
|
|
|
303
515
|
### `convertHeicInWorker(input, options)`
|
|
304
516
|
|
|
305
|
-
Converts a HEIC image inside a Web Worker. The worker script must implement the message protocol shown in [Usage section
|
|
517
|
+
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.
|
|
306
518
|
|
|
307
519
|
- **`input`**: `Blob | File | ArrayBuffer | Uint8Array`
|
|
308
|
-
- **`options`**: `WorkerConvertOptions` โ same as `ConvertOptions
|
|
520
|
+
- **`options`**: `WorkerConvertOptions` โ same as `ConvertOptions` but without `decoder` (cannot be structured-cloned; passing one rejects with `invalid_input`), plus:
|
|
309
521
|
- `workerUrl`: `string | URL` (URL of the worker script; should be a compile-time constant)
|
|
310
522
|
- `workerType`: `'classic' | 'module'` (Worker script type. Default: `'classic'`; use `'module'` for scripts with ES imports)
|
|
311
|
-
- `timeoutMs`: `number` (Maximum wait for the result in milliseconds.
|
|
312
|
-
-
|
|
523
|
+
- `timeoutMs`: `number` (Maximum wait for the result in milliseconds, including time spent queued. Finite, โฅ 0; `0` disables. Default: `60000`)
|
|
524
|
+
- `maxConcurrentWorkers`: `number` (Positive integer. Concurrent workers per `workerUrl` + type; extra calls queue. Default: `navigator.hardwareConcurrency` clamped to 1โ8)
|
|
525
|
+
- **Returns**: `Promise<Blob>` โ or `Promise<string>` / `Promise<ArrayBuffer>` with the typed `output` overloads. Aborting via `signal` terminates the worker immediately.
|
|
526
|
+
|
|
527
|
+
### `convertManyInWorker(inputs, options)`
|
|
528
|
+
|
|
529
|
+
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.
|
|
530
|
+
|
|
531
|
+
- **`inputs`**: `Array<Blob | File | ArrayBuffer | Uint8Array>`
|
|
532
|
+
- **`options`**: `WorkerBatchOptions` โ same as `convertHeicInWorker`'s `WorkerConvertOptions` (including `output`, `crop`, `preserveExif`, `signal`), except `onProgress` uses the batch signature below; plus:
|
|
533
|
+
- `onProgress`: `(index: number, percent: number) => void` (Per-item progress callback)
|
|
534
|
+
- `maxConcurrentWorkers`: `number` (Positive integer. Real concurrency โ each in-flight item occupies one semaphore slot. Default: `navigator.hardwareConcurrency` clamped to 1โ8)
|
|
535
|
+
- `continueOnError`: `boolean` (Per-item `{ index, ok, result | error }` entries instead of rejecting on the first failure. Default: `false`)
|
|
536
|
+
- **Returns**: `Promise<Blob[]>` โ typed overloads mirror `convertMany` (`output` / `continueOnError` combinations)
|
|
313
537
|
|
|
314
538
|
### `LibheifDecoder(options?)`
|
|
315
539
|
|
|
@@ -317,11 +541,56 @@ The default WASM-based implementation of `IHeicDecoder`.
|
|
|
317
541
|
|
|
318
542
|
- **`options`**: (optional) `LibheifDecoderOptions`
|
|
319
543
|
- `locateFile`: `(path: string, prefix: string) => string`
|
|
320
|
-
- `wasmBinary`: `ArrayBuffer`
|
|
544
|
+
- `wasmBinary`: `ArrayBuffer | ArrayBufferView` (e.g. a Node.js `Buffer` from `fs.readFileSync`)
|
|
545
|
+
- `moduleOverrides`: `Record<string, unknown>` (Advanced: merged into the Emscripten module arguments, e.g. `instantiateWasm`)
|
|
321
546
|
- **Methods**:
|
|
322
547
|
- `initialize(): Promise<void>`: Loads and initializes the WASM wrapper. Safe to call concurrently; the module loads at most once per instance.
|
|
323
|
-
- `decode(data: Uint8Array, onProgress?: (percent: number) => void): Promise<DecodedImage>`: Decodes the HEIC bytes to raw RGBA, with optional progress callback
|
|
324
|
-
- `free(): void`: Releases
|
|
548
|
+
- `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.
|
|
549
|
+
- `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).
|
|
550
|
+
|
|
551
|
+
### Error handling
|
|
552
|
+
|
|
553
|
+
All errors thrown by this library are `HeicConverterError` instances (`extends Error`) with a machine-readable `code`, and โ where relevant โ a `cause` and batch fields:
|
|
554
|
+
|
|
555
|
+
| `code` | Thrown by | Meaning |
|
|
556
|
+
| --- | --- | --- |
|
|
557
|
+
| `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` |
|
|
558
|
+
| `invalid_quality` | all conversion APIs | `quality` outside `0.0`โ`1.0` or not a finite number |
|
|
559
|
+
| `invalid_resize` | all conversion APIs | `scale`/`maxWidth`/`maxHeight` not positive finite numbers, or target size exceeds 16384 px |
|
|
560
|
+
| `invalid_format` | all conversion APIs | Unknown `to` value |
|
|
561
|
+
| `invalid_crop` | all conversion APIs | `crop` with non-integer/non-positive dimensions, negative offsets, or a rectangle beyond the image's display size |
|
|
562
|
+
| `invalid_concurrency` | all conversion APIs | `concurrency` (batch) or `maxConcurrentWorkers` (worker APIs) not a positive integer |
|
|
563
|
+
| `decoder_init_failed` | `convertHeic`, `convertMany` | WASM module could not be loaded (missing asset, CSP block) |
|
|
564
|
+
| `decode_failed` | `convertHeic`, `convertMany` | Invalid/corrupt HEIC bytes |
|
|
565
|
+
| `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` |
|
|
566
|
+
| `render_encode_failed` | `convertHeic`, `convertMany` | Canvas render/encode failure (bad dimensions, `toBlob` returned null) |
|
|
567
|
+
| `format_unsupported` | `convertHeic`, `convertMany` | The canvas cannot encode the requested format (e.g. `avif` on Safari) |
|
|
568
|
+
| `aborted` | all conversion APIs | The `AbortSignal` was aborted before the work completed |
|
|
569
|
+
| `progress_callback_failed` | all conversion APIs | The host `onProgress` callback threw; message attributes the failure |
|
|
570
|
+
| `worker_unsupported` | `convertHeicInWorker`, `convertManyInWorker` | No global `Worker` (e.g. Node.js) |
|
|
571
|
+
| `worker_create_failed` | `convertHeicInWorker`, `convertManyInWorker` | `new Worker(...)` threw (wrong URL, MIME type) |
|
|
572
|
+
| `worker_post_failed` | `convertHeicInWorker`, `convertManyInWorker` | `postMessage` threw (non-cloneable option) |
|
|
573
|
+
| `worker_timeout` | `convertHeicInWorker`, `convertManyInWorker` | No result within `timeoutMs`; message includes progress/protocol diagnostics |
|
|
574
|
+
| `worker_failed` | `convertHeicInWorker`, `convertManyInWorker` | Worker reported `{ type: 'result', ok: false, error }` |
|
|
575
|
+
|
|
576
|
+
Option validation for **all four** conversion APIs runs on the main thread before any worker is created, so a bad option always keeps its own code. 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.
|
|
577
|
+
| `batch_item_failed` | `convertMany`, `convertManyInWorker` | One or more items failed; see `itemIndex`/`itemTotal`/`failedCount`/`cause` |
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { convertHeic, type HeicConverterErrorCode } from '@keeratita/heic-converter';
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
await convertHeic(input, { to: 'png' });
|
|
584
|
+
} catch (error) {
|
|
585
|
+
const code: HeicConverterErrorCode | undefined =
|
|
586
|
+
error && typeof error === 'object' && 'code' in error
|
|
587
|
+
? (error as { code?: HeicConverterErrorCode }).code
|
|
588
|
+
: undefined;
|
|
589
|
+
if (code === 'unsupported_environment') {
|
|
590
|
+
// e.g. Node.js: decode raw RGBA with LibheifDecoder and encode externally
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
325
594
|
|
|
326
595
|
### `freeSharedDecoder()`
|
|
327
596
|
|
|
@@ -366,9 +635,10 @@ npm run test
|
|
|
366
635
|
|
|
367
636
|
### Run Browser E2E Tests
|
|
368
637
|
|
|
369
|
-
Real browser conversions against the CSP sandbox and the GitHub Pages demo (`docs/`)
|
|
638
|
+
Real browser conversions against the CSP sandbox and the GitHub Pages demo (`docs/`). Build first โ both servers serve `dist/`:
|
|
370
639
|
|
|
371
640
|
```bash
|
|
641
|
+
npm run build
|
|
372
642
|
npx playwright install chromium # one-time
|
|
373
643
|
npm run test:e2e
|
|
374
644
|
```
|