@su-engineering/heic 0.1.0 → 0.2.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/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +7 -1
- package/README.md +25 -5
- package/dist/heic.global.js +1 -1
- package/dist/heic.global.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/{types-Bv9KPnri.d.ts → types-nCqKJJpp.d.ts} +11 -1
- package/dist/wasm.d.ts +2 -2
- package/dist/wasm.js +52 -44
- package/dist/wasm.js.map +1 -1
- package/docs/api.md +9 -1
- package/docs/benchmarks.md +66 -0
- package/docs/releasing.md +40 -25
- package/package.json +11 -6
|
@@ -71,6 +71,16 @@ interface DecodedImage {
|
|
|
71
71
|
/** Unsupported features that were detected and skipped. Usually empty. */
|
|
72
72
|
warnings: HeicWarning[];
|
|
73
73
|
}
|
|
74
|
+
interface ConvertOptions extends DecodeOptions {
|
|
75
|
+
/** Output format. Default 'image/jpeg'. */
|
|
76
|
+
type?: 'image/jpeg' | 'image/png';
|
|
77
|
+
/** JPEG encoder quality, from 0 to 1. Default 0.92; ignored for PNG. */
|
|
78
|
+
quality?: number;
|
|
79
|
+
}
|
|
80
|
+
/** Encoded pixels and decode metadata; no bitmap needs to be closed by the caller. */
|
|
81
|
+
interface ConvertedImage extends Omit<DecodedImage, 'image'> {
|
|
82
|
+
blob: Blob;
|
|
83
|
+
}
|
|
74
84
|
interface SupportReport {
|
|
75
85
|
/** `createImageBitmap` decodes HEIC directly (Safari, some Chrome builds). */
|
|
76
86
|
native: boolean;
|
|
@@ -109,4 +119,4 @@ interface DecoderAdapter {
|
|
|
109
119
|
decode(request: AdapterRequest): Promise<AdapterResult>;
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
export type { AdapterRequest as A, DecodeOptions as D, HeicWarning as H, OutputColorSpace as O, SourceColor as S, TransformsApplied as T, SupportReport as a,
|
|
122
|
+
export type { AdapterRequest as A, ConvertOptions as C, DecodeOptions as D, HeicWarning as H, OutputColorSpace as O, SourceColor as S, TransformsApplied as T, SupportReport as a, ConvertedImage as b, DecodedImage as c, DecoderAdapter as d, AdapterResult as e, Strategy as f };
|
package/dist/wasm.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { A as AdapterRequest,
|
|
1
|
+
import { d as DecoderAdapter } from './types-nCqKJJpp.js';
|
|
2
|
+
export { A as AdapterRequest, e as AdapterResult } from './types-nCqKJJpp.js';
|
|
3
3
|
|
|
4
4
|
interface WasmAdapterOptions {
|
|
5
5
|
/**
|
package/dist/wasm.js
CHANGED
|
@@ -17,54 +17,62 @@ function createWasmAdapter(options = {}) {
|
|
|
17
17
|
const libheif = await modulePromise;
|
|
18
18
|
if (request.signal?.aborted) throw new HeicAbortError();
|
|
19
19
|
const decoder = new libheif.HeifDecoder();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
throw new HeicDecodeError("Could not get a 2d context for libheif output", {
|
|
42
|
-
strategy: "wasm"
|
|
20
|
+
let images = [];
|
|
21
|
+
try {
|
|
22
|
+
images = decoder.decode(request.data);
|
|
23
|
+
if (!images || images.length === 0) {
|
|
24
|
+
throw new HeicDecodeError("libheif returned no images", { strategy: "wasm" });
|
|
25
|
+
}
|
|
26
|
+
const image = images[0];
|
|
27
|
+
const width = image.get_width();
|
|
28
|
+
const height = image.get_height();
|
|
29
|
+
if (width <= 0 || height <= 0) {
|
|
30
|
+
throw new HeicDecodeError(`libheif reported ${width}x${height}`, { strategy: "wasm" });
|
|
31
|
+
}
|
|
32
|
+
if (typeof OffscreenCanvas === "undefined") {
|
|
33
|
+
throw new HeicDecodeError("OffscreenCanvas is required to receive libheif output", {
|
|
34
|
+
strategy: "wasm"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
38
|
+
const ctx = canvas.getContext("2d", {
|
|
39
|
+
colorSpace: request.colorSpace,
|
|
40
|
+
alpha: false
|
|
43
41
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
image.display(imageData, (result) => {
|
|
49
|
-
if (!result) reject(new HeicDecodeError("libheif failed to render", { strategy: "wasm" }));
|
|
50
|
-
else resolve();
|
|
42
|
+
if (!ctx) {
|
|
43
|
+
throw new HeicDecodeError("Could not get a 2d context for libheif output", {
|
|
44
|
+
strategy: "wasm"
|
|
51
45
|
});
|
|
52
|
-
} catch (error) {
|
|
53
|
-
reject(
|
|
54
|
-
new HeicDecodeError(`libheif threw while rendering: ${String(error)}`, {
|
|
55
|
-
strategy: "wasm"
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
46
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
47
|
+
const imageData = ctx.createImageData(width, height, { colorSpace: request.colorSpace });
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
try {
|
|
50
|
+
image.display(imageData, (result) => {
|
|
51
|
+
if (!result) reject(new HeicDecodeError("libheif failed to render", { strategy: "wasm" }));
|
|
52
|
+
else resolve();
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
reject(
|
|
56
|
+
new HeicDecodeError(`libheif threw while rendering: ${String(error)}`, {
|
|
57
|
+
strategy: "wasm"
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
ctx.putImageData(imageData, 0, 0);
|
|
63
|
+
if (request.signal?.aborted) {
|
|
64
|
+
canvas.width = 0;
|
|
65
|
+
canvas.height = 0;
|
|
66
|
+
throw new HeicAbortError();
|
|
67
|
+
}
|
|
68
|
+
return { image: canvas, width, height };
|
|
69
|
+
} finally {
|
|
70
|
+
for (const image of images ?? []) image.free?.();
|
|
71
|
+
if (decoder.decoder && libheif.heif_context_free) {
|
|
72
|
+
libheif.heif_context_free(decoder.decoder);
|
|
73
|
+
decoder.decoder = null;
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
|
-
return { image: canvas, width, height };
|
|
68
76
|
}
|
|
69
77
|
};
|
|
70
78
|
}
|
package/dist/wasm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../wasm/index.ts"],"names":[],"mappings":"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","file":"wasm.js","sourcesContent":["/**\n * libheif-wasm fallback adapter.\n *\n * A **separate entry point** (`@su-engineering/heic/wasm`) so that bundlers\n * never pull roughly 1.2 MB of codec into the main chunk. Importing this module\n * is still cheap: the wasm itself is only instantiated on the first decode.\n *\n * The core package builds and passes its non-wasm tests with `libheif-js`\n * absent, which is why it is an optional peer dependency rather than a\n * dependency.\n */\nimport { HeicAbortError, HeicDecodeError } from '../src/errors.ts';\nimport type { AdapterRequest, AdapterResult, DecoderAdapter } from '../src/types.ts';\n\n/** The slice of libheif-js's API we use. Declared here so the package can be absent. */\ninterface LibheifImage {\n get_width(): number;\n get_height(): number;\n display(image: ImageData, callback: (result: ImageData | null) => void): void;\n free?(): void;\n}\ninterface LibheifDecoder {\n decode(buffer: Uint8Array | ArrayBuffer): LibheifImage[];\n}\ninterface LibheifModule {\n HeifDecoder: new () => LibheifDecoder;\n}\n\nexport interface WasmAdapterOptions {\n /**\n * Supplies the libheif module. Defaults to `import('libheif-js/wasm-bundle.js')`.\n *\n * Override it to pin a specific build, to serve the wasm from your own origin,\n * or to reuse an instance you already loaded. In a browser without a bundler,\n * this is required, because `libheif-js` is a bare specifier:\n *\n * ```ts\n * createWasmAdapter({\n * loadLibheif: () => import('/vendor/libheif-bundle.mjs'),\n * });\n * ```\n */\n loadLibheif?: () => Promise<unknown>;\n}\n\n/**\n * Builds a libheif-backed adapter.\n *\n * The module is loaded once, on first decode, and reused.\n */\nexport function createWasmAdapter(options: WasmAdapterOptions = {}): DecoderAdapter {\n const load = options.loadLibheif ?? ((): Promise<unknown> => import('libheif-js/wasm-bundle.js'));\n\n let modulePromise: Promise<LibheifModule> | undefined;\n\n return {\n name: 'libheif-wasm',\n\n // libheif applies irot / imir / clap itself: heif_decode_image honours the\n // transformative properties unless ignore_transformations is set, and\n // libheif-js does not expose that option. Declaring it here is what stops the\n // pipeline applying them a second time and rotating the image twice.\n appliesTransforms: true,\n\n async decode(request: AdapterRequest): Promise<AdapterResult> {\n if (request.signal?.aborted) throw new HeicAbortError();\n\n modulePromise ??= Promise.resolve(load()).then(normalizeLibheif);\n const libheif = await modulePromise;\n if (request.signal?.aborted) throw new HeicAbortError();\n\n const decoder = new libheif.HeifDecoder();\n const images = decoder.decode(request.data);\n if (!images || images.length === 0) {\n throw new HeicDecodeError('libheif returned no images', { strategy: 'wasm' });\n }\n\n // Index 0 is the primary image; any others are aux or sequence entries,\n // which v0.1 does not decode.\n const image = images[0]!;\n const width = image.get_width();\n const height = image.get_height();\n if (width <= 0 || height <= 0) {\n throw new HeicDecodeError(`libheif reported ${width}x${height}`, { strategy: 'wasm' });\n }\n\n if (typeof OffscreenCanvas === 'undefined') {\n throw new HeicDecodeError('OffscreenCanvas is required to receive libheif output', {\n strategy: 'wasm',\n });\n }\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d', {\n colorSpace: request.colorSpace,\n alpha: false,\n });\n if (!ctx) {\n throw new HeicDecodeError('Could not get a 2d context for libheif output', {\n strategy: 'wasm',\n });\n }\n\n const imageData = ctx.createImageData(width, height, { colorSpace: request.colorSpace });\n await new Promise<void>((resolve, reject) => {\n try {\n image.display(imageData, (result) => {\n if (!result) reject(new HeicDecodeError('libheif failed to render', { strategy: 'wasm' }));\n else resolve();\n });\n } catch (error) {\n reject(\n new HeicDecodeError(`libheif threw while rendering: ${String(error)}`, {\n strategy: 'wasm',\n }),\n );\n }\n });\n\n image.free?.();\n ctx.putImageData(imageData, 0, 0);\n\n if (request.signal?.aborted) {\n canvas.width = 0;\n canvas.height = 0;\n throw new HeicAbortError();\n }\n\n return { image: canvas, width, height };\n },\n };\n}\n\n/**\n * libheif-js is published in three shapes and bundlers add a fourth wrapper:\n * the CommonJS build exposes `HeifDecoder` directly, the wasm builds default-\n * export an emscripten *factory* that must be called, and interop may nest\n * either under `.default`. Normalising here means callers can hand us whatever\n * their toolchain produced without knowing which one it is.\n */\nasync function normalizeLibheif(imported: unknown): Promise<LibheifModule> {\n let candidate = imported;\n\n for (let depth = 0; depth < 4; depth++) {\n if (isLibheifModule(candidate)) return candidate;\n if (typeof candidate === 'function') {\n candidate = await (candidate as () => unknown)();\n continue;\n }\n if (candidate && typeof candidate === 'object' && 'default' in candidate) {\n candidate = (candidate as { default: unknown }).default;\n continue;\n }\n break;\n }\n\n throw new HeicDecodeError(\n 'The module supplied to the wasm adapter does not expose a HeifDecoder',\n { strategy: 'wasm' },\n );\n}\n\nfunction isLibheifModule(value: unknown): value is LibheifModule {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { HeifDecoder?: unknown }).HeifDecoder === 'function'\n );\n}\n\n/**\n * A ready-made adapter using the default `libheif-js` build.\n *\n * ```ts\n * import { decodeHeic } from '@su-engineering/heic';\n * import { wasmDecoder } from '@su-engineering/heic/wasm';\n *\n * await decodeHeic(file, { wasmLoader: async () => wasmDecoder });\n * ```\n */\nexport const wasmDecoder: DecoderAdapter = createWasmAdapter();\n\nexport type { DecoderAdapter, AdapterRequest, AdapterResult } from '../src/types.ts';\n"]}
|
|
1
|
+
{"version":3,"sources":["../wasm/index.ts"],"names":[],"mappings":"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","file":"wasm.js","sourcesContent":["/**\n * libheif-wasm fallback adapter.\n *\n * A **separate entry point** (`@su-engineering/heic/wasm`) so that bundlers\n * never pull roughly 1.2 MB of codec into the main chunk. Importing this module\n * is still cheap: the wasm itself is only instantiated on the first decode.\n *\n * The core package builds and passes its non-wasm tests with `libheif-js`\n * absent, which is why it is an optional peer dependency rather than a\n * dependency.\n */\nimport { HeicAbortError, HeicDecodeError } from '../src/errors.ts';\nimport type { AdapterRequest, AdapterResult, DecoderAdapter } from '../src/types.ts';\n\n/** The slice of libheif-js's API we use. Declared here so the package can be absent. */\ninterface LibheifImage {\n get_width(): number;\n get_height(): number;\n display(image: ImageData, callback: (result: ImageData | null) => void): void;\n free?(): void;\n}\ninterface LibheifDecoder {\n /** Context allocated by libheif-js's decode method. */\n decoder?: number | null;\n decode(buffer: Uint8Array | ArrayBuffer): LibheifImage[];\n}\ninterface LibheifModule {\n HeifDecoder: new () => LibheifDecoder;\n heif_context_free?: (context: number) => void;\n}\n\nexport interface WasmAdapterOptions {\n /**\n * Supplies the libheif module. Defaults to `import('libheif-js/wasm-bundle.js')`.\n *\n * Override it to pin a specific build, to serve the wasm from your own origin,\n * or to reuse an instance you already loaded. In a browser without a bundler,\n * this is required, because `libheif-js` is a bare specifier:\n *\n * ```ts\n * createWasmAdapter({\n * loadLibheif: () => import('/vendor/libheif-bundle.mjs'),\n * });\n * ```\n */\n loadLibheif?: () => Promise<unknown>;\n}\n\n/**\n * Builds a libheif-backed adapter.\n *\n * The module is loaded once, on first decode, and reused.\n */\nexport function createWasmAdapter(options: WasmAdapterOptions = {}): DecoderAdapter {\n const load = options.loadLibheif ?? ((): Promise<unknown> => import('libheif-js/wasm-bundle.js'));\n\n let modulePromise: Promise<LibheifModule> | undefined;\n\n return {\n name: 'libheif-wasm',\n\n // libheif applies irot / imir / clap itself: heif_decode_image honours the\n // transformative properties unless ignore_transformations is set, and\n // libheif-js does not expose that option. Declaring it here is what stops the\n // pipeline applying them a second time and rotating the image twice.\n appliesTransforms: true,\n\n async decode(request: AdapterRequest): Promise<AdapterResult> {\n if (request.signal?.aborted) throw new HeicAbortError();\n\n modulePromise ??= Promise.resolve(load()).then(normalizeLibheif);\n const libheif = await modulePromise;\n if (request.signal?.aborted) throw new HeicAbortError();\n\n const decoder = new libheif.HeifDecoder();\n let images: LibheifImage[] = [];\n try {\n images = decoder.decode(request.data);\n if (!images || images.length === 0) {\n throw new HeicDecodeError('libheif returned no images', { strategy: 'wasm' });\n }\n\n // Index 0 is the primary image; any others are aux or sequence entries,\n // which v0.1 does not decode.\n const image = images[0]!;\n const width = image.get_width();\n const height = image.get_height();\n if (width <= 0 || height <= 0) {\n throw new HeicDecodeError(`libheif reported ${width}x${height}`, { strategy: 'wasm' });\n }\n\n if (typeof OffscreenCanvas === 'undefined') {\n throw new HeicDecodeError('OffscreenCanvas is required to receive libheif output', {\n strategy: 'wasm',\n });\n }\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d', {\n colorSpace: request.colorSpace,\n alpha: false,\n });\n if (!ctx) {\n throw new HeicDecodeError('Could not get a 2d context for libheif output', {\n strategy: 'wasm',\n });\n }\n\n const imageData = ctx.createImageData(width, height, { colorSpace: request.colorSpace });\n await new Promise<void>((resolve, reject) => {\n try {\n image.display(imageData, (result) => {\n if (!result) reject(new HeicDecodeError('libheif failed to render', { strategy: 'wasm' }));\n else resolve();\n });\n } catch (error) {\n reject(\n new HeicDecodeError(`libheif threw while rendering: ${String(error)}`, {\n strategy: 'wasm',\n }),\n );\n }\n });\n\n ctx.putImageData(imageData, 0, 0);\n\n if (request.signal?.aborted) {\n canvas.width = 0;\n canvas.height = 0;\n throw new HeicAbortError();\n }\n\n return { image: canvas, width, height };\n } finally {\n // Each decode allocates a context. libheif-js only frees it on the next\n // decode on the same instance; this adapter creates a fresh instance.\n // Release every returned handle before its owning context, even on error.\n for (const image of images ?? []) image.free?.();\n if (decoder.decoder && libheif.heif_context_free) {\n libheif.heif_context_free(decoder.decoder);\n decoder.decoder = null;\n }\n }\n },\n };\n}\n\n/**\n * libheif-js is published in three shapes and bundlers add a fourth wrapper:\n * the CommonJS build exposes `HeifDecoder` directly, the wasm builds default-\n * export an emscripten *factory* that must be called, and interop may nest\n * either under `.default`. Normalising here means callers can hand us whatever\n * their toolchain produced without knowing which one it is.\n */\nasync function normalizeLibheif(imported: unknown): Promise<LibheifModule> {\n let candidate = imported;\n\n for (let depth = 0; depth < 4; depth++) {\n if (isLibheifModule(candidate)) return candidate;\n if (typeof candidate === 'function') {\n candidate = await (candidate as () => unknown)();\n continue;\n }\n if (candidate && typeof candidate === 'object' && 'default' in candidate) {\n candidate = (candidate as { default: unknown }).default;\n continue;\n }\n break;\n }\n\n throw new HeicDecodeError(\n 'The module supplied to the wasm adapter does not expose a HeifDecoder',\n { strategy: 'wasm' },\n );\n}\n\nfunction isLibheifModule(value: unknown): value is LibheifModule {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { HeifDecoder?: unknown }).HeifDecoder === 'function'\n );\n}\n\n/**\n * A ready-made adapter using the default `libheif-js` build.\n *\n * ```ts\n * import { decodeHeic } from '@su-engineering/heic';\n * import { wasmDecoder } from '@su-engineering/heic/wasm';\n *\n * await decodeHeic(file, { wasmLoader: async () => wasmDecoder });\n * ```\n */\nexport const wasmDecoder: DecoderAdapter = createWasmAdapter();\n\nexport type { DecoderAdapter, AdapterRequest, AdapterResult } from '../src/types.ts';\n"]}
|
package/docs/api.md
CHANGED
|
@@ -40,6 +40,14 @@ await pending; // Rejects with HeicAbortError at a cancellation checkpoint.
|
|
|
40
40
|
|
|
41
41
|
The decoder returns pixels, not a new HEIC file. EXIF and other source metadata are not serialized into a new output file. Warnings are advisory and do not enumerate every unsupported feature.
|
|
42
42
|
|
|
43
|
+
## `convertHeic(input, options?)`
|
|
44
|
+
|
|
45
|
+
Returns `Promise<ConvertedImage>` using the same inputs and decode options as `decodeHeic`. Adds `type: 'image/jpeg' | 'image/png'` (default JPEG) and `quality` (default `0.92`, a finite number from 0 to 1; ignored for PNG). Invalid type/quality values throw `TypeError`/`RangeError` before decoding.
|
|
46
|
+
|
|
47
|
+
The result contains `blob` and all `DecodedImage` metadata except `image`. Dimensions describe the encoded image. Transforms and `maxDimension` apply before encoding. Its bitmap and conversion canvas are released on success, cancellation, or encoder failure; callers do not need `close()`. Revoke any object URLs you create when finished.
|
|
48
|
+
|
|
49
|
+
Conversion uses `OffscreenCanvas.convertToBlob`, including in workers. Platform encoding errors can propagate. An empty Blob or unexpected MIME type throws `HeicDecodeError`. Cancellation is checked before and after encoding, but cannot interrupt an encoder already running. Quality and encoded bytes can differ across browsers. Source EXIF is not copied; full HDR and cross-browser color equivalence are not promised. The software fallback remains explicit through `wasmLoader` or adapter registration.
|
|
50
|
+
|
|
43
51
|
## `isHeic(input)`
|
|
44
52
|
|
|
45
53
|
Returns `Promise<IsHeicResult>` with `isHeic: boolean` and optional `brand`, `primaryItemType`, and `coding` (`'hevc'`, `'av1'`, or `'unknown'`). Inspects at most the first 64 KiB, including for a `Blob`.
|
|
@@ -107,4 +115,4 @@ These functions do not decode pixels and accept `ArrayBuffer`/`Uint8Array` where
|
|
|
107
115
|
| `parseHvcC`, `hvccToCodecString` | Parse HEVC configuration and produce a WebCodecs codec string. |
|
|
108
116
|
| `hvccToAnnexBPrologue`, `lengthPrefixedToAnnexB` | Prepare HEVC NAL data for Annex B decoding. |
|
|
109
117
|
|
|
110
|
-
Exported types include `BinaryInput`, `IsHeicResult`, `DecodeOptions`, `DecodedImage`, `Strategy`, `OutputColorSpace`, `SourceColor`, `TransformsApplied`, `HeicWarning`, `SupportReport`, `HeicErrorContext`, `DecoderAdapter`, `AdapterRequest`, `AdapterResult`, `HeifFile`, `ItemInfo`, `ItemLocation`, `ItemProperty`, `ItemProperties`, `ItemReferences`, `GridDescriptor`, `HvcC`, `ImagePlan`, `PlannedTile`, `TileGroup`, and `TransformOp`. The WASM entry point additionally exports `WasmAdapterOptions`.
|
|
118
|
+
Exported types include `BinaryInput`, `IsHeicResult`, `DecodeOptions`, `DecodedImage`, `ConvertOptions`, `ConvertedImage`, `Strategy`, `OutputColorSpace`, `SourceColor`, `TransformsApplied`, `HeicWarning`, `SupportReport`, `HeicErrorContext`, `DecoderAdapter`, `AdapterRequest`, `AdapterResult`, `HeifFile`, `ItemInfo`, `ItemLocation`, `ItemProperty`, `ItemProperties`, `ItemReferences`, `GridDescriptor`, `HvcC`, `ImagePlan`, `PlannedTile`, `TileGroup`, and `TransformOp`. The WASM entry point additionally exports `WasmAdapterOptions`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Benchmarking against heic-to
|
|
2
|
+
|
|
3
|
+
Compare full-resolution JPEG/PNG conversion with **heic-to 1.5.2**, using identical input bytes, MIME type, and JPEG quality. Photos are served only on `127.0.0.1`, never uploaded. The competitor is a pinned development dependency and is not shipped in the package.
|
|
4
|
+
|
|
5
|
+
## Run it
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm install --frozen-lockfile
|
|
9
|
+
pnpm exec playwright install chromium firefox
|
|
10
|
+
pnpm benchmark
|
|
11
|
+
|
|
12
|
+
# Use your own consented photos (quote paths containing spaces):
|
|
13
|
+
pnpm benchmark -- /path/to/one.heic /path/to/two.heic \
|
|
14
|
+
--iterations 10 --cold 5 --output benchmark-results/photos.json
|
|
15
|
+
|
|
16
|
+
pnpm benchmark -- /path/to/one.heic --browser firefox
|
|
17
|
+
pnpm benchmark -- /path/to/one.heic --type image/png
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Options: `--browser chromium|firefox|webkit`, `--iterations` (warm samples, default 5), `--cold` (first-use samples, default 3), `--type` (default `image/jpeg`), `--quality` (default `0.92`), and `--output`. With no files, two small committed synthetic fixtures smoke-test the harness; they are not representative phone-photo performance measurements.
|
|
21
|
+
|
|
22
|
+
For installed Chromium with platform HEVC, set `HEIC_CHROME=/path/to/browser`. This does not install codecs or guarantee hardware decoding. WebKit needs platform dependencies and does not represent released Safari. Close other CPU-intensive tasks and run benchmarks separately from tests.
|
|
23
|
+
|
|
24
|
+
## Methodology
|
|
25
|
+
|
|
26
|
+
| Variant | Decode behavior |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `ours-auto` | Native → WebCodecs → lazy libheif WASM fallback. |
|
|
29
|
+
| `ours-wasm` | Force the libheif-js 1.23.2 WASM fallback. |
|
|
30
|
+
| `heic-to` | Standard `heicTo` API with its bundled libheif build. |
|
|
31
|
+
|
|
32
|
+
- **Cold:** fresh context for every conversion; time module import, local bundle loading/parsing, codec initialization, decoding, and encoding. The browser process is reused. Loopback delivery is unthrottled and is not a mobile-network simulation.
|
|
33
|
+
- **Warm:** separate context per variant, one discarded warm-up, then interleaved sequential conversions with cached modules/codec. Variant order rotates between rounds.
|
|
34
|
+
- **Input fetch:** excluded for all variants. Neither side resizes. Output inspection is outside timing.
|
|
35
|
+
- **Validation:** every Blob must have the requested MIME type, nonzero size, and decodable pixels. Dimensions must match heic-to. A 64×64 thumbnail RGB mean absolute difference against heic-to is diagnostic, not independent proof of correct pixels or color.
|
|
36
|
+
- **Report:** raw samples, median/p95, actual strategy, dimensions, encoded bytes, loaded uncompressed JS bytes, browser/OS/CPU and dependency versions, and raw/gzip standalone core size. Small-sample p95 is usually the maximum, not a reliable population percentile.
|
|
37
|
+
|
|
38
|
+
Generated bundles/reports are ignored by Git. Reports use anonymous fixture IDs and omit filenames, source paths, and photo bytes. Keep private photos outside committed fixtures.
|
|
39
|
+
|
|
40
|
+
## Initial local measurements
|
|
41
|
+
|
|
42
|
+
September 18, 2026 conversion candidate, Linux x64, AMD Ryzen 7 PRO 8840U, Playwright Chromium 153.0.8010.12. Two consented iPhone photos, each 2268×4032 with a 5×8 tile grid, converted at full resolution to JPEG quality 0.92. Five cold samples and ten warm samples per variant; times below are medians. No other test suite ran during these measurements.
|
|
43
|
+
|
|
44
|
+
| Input | ours-auto cold | heic-to cold | ours-auto warm | heic-to warm |
|
|
45
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
46
|
+
| Photo 1 | 475 ms | 1,243 ms | 361 ms | 722 ms |
|
|
47
|
+
| Photo 2 | 587 ms | 1,503 ms | 471 ms | 848 ms |
|
|
48
|
+
|
|
49
|
+
Both `ours-auto` runs used **WASM**, not native/WebCodecs. Forced WASM warm medians were 362/475 ms. All outputs matched full-resolution dimensions; their 64×64 RGB thumbnail MAD against heic-to was zero in this browser. This is a narrow local result using private inputs, not an independently reproducible public corpus or a general performance guarantee. The harness is reproducible with the committed fixtures or your own photos.
|
|
50
|
+
|
|
51
|
+
An isolated Firefox 155 run on the same machine used three cold and five warm samples per variant, with the same photos and JPEG settings:
|
|
52
|
+
|
|
53
|
+
| Input | ours-auto cold | heic-to cold | ours-auto warm | heic-to warm |
|
|
54
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
55
|
+
| Photo 1 | 493 ms | 1,076 ms | 402 ms | 894 ms |
|
|
56
|
+
| Photo 2 | 607 ms | 1,350 ms | 518 ms | 1,117 ms |
|
|
57
|
+
|
|
58
|
+
These auto runs also used WASM; output dimensions matched and thumbnail RGB MAD was zero. Raw/gzip core size is reported separately from codec assets. In this bundled harness, our WASM path loaded approximately 2.02 MB of uncompressed JavaScript versus 3.00 MB for heic-to; production transfer sizes depend on bundling and compression.
|
|
59
|
+
|
|
60
|
+
## Interpretation
|
|
61
|
+
|
|
62
|
+
Our fallback stays unloaded when native or WebCodecs succeeds. `ours-auto` reports the actual successful strategy so a fallback result cannot be presented as a native/hardware speedup. Without platform HEVC, this compares software WASM against heic-to's bundled software implementation.
|
|
63
|
+
|
|
64
|
+
[heic-to 1.5.2](https://github.com/hoppergee/heic-to) documents libheif 1.22.2; this candidate tests libheif-js 1.23.2. Results compare complete package versions, including different codec builds and encoders; they do not isolate wrapper overhead.
|
|
65
|
+
|
|
66
|
+
Do not compare npm unpacked sizes as initial download sizes. `loadedJsBytes` is uncompressed local delivery, not gzip transfer cost. Core size excludes the optional codec. This script does not measure peak memory, UI blocking, constrained-network performance, or establish hardware acceleration. Two photos on one machine do not prove universal performance or compatibility. Test representative tiled, rotated, high-resolution, and color-diverse photos on target devices before making broad claims.
|
package/docs/releasing.md
CHANGED
|
@@ -1,42 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Releasing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The initial package is `@su-engineering/heic@0.1.0`. The `/wasm` export ships in the same package. Ongoing releases use Changesets, validated GitHub Actions, and npm trusted publishing (OIDC); no npm publishing secret is required.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## One-time npm configuration
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Confirm the MIT license and ownership; review optional codec license notices separately.
|
|
9
|
-
- Set the GitHub description, documentation homepage, and relevant topics. Keep visibility private until the owner chooses to open-source it.
|
|
10
|
-
- Enable issues and configure private vulnerability reporting if desired; ensure the security contact is monitored.
|
|
11
|
-
- Configure branch protection or a ruleset requiring the CI `check` and `browser` jobs, with appropriate maintainer access.
|
|
12
|
-
- Verify CI on the release commit and manually test platform HEVC and released Safari using representative, consented Apple photos, including grids.
|
|
7
|
+
In the npm package settings for `@su-engineering/heic`, add a GitHub Actions trusted publisher:
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
| Field | Value |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Organization or user | `su-engineering` |
|
|
12
|
+
| Repository | `heic-web` |
|
|
13
|
+
| Workflow filename | `release.yml` |
|
|
14
|
+
| Environment | Leave blank; this workflow does not use a GitHub environment. |
|
|
15
|
+
| Allowed actions | Enable direct `npm publish`. Stage-only permission does not support this workflow. |
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
The workflow file must exist on the default branch. The setting requires an interactive npm account/2FA operation. Never paste publishing credentials into an issue, pull request, or source file. Revoke the temporary initial-publish token after the launch.
|
|
18
|
+
|
|
19
|
+
See [npm's trusted-publishing documentation](https://docs.npmjs.com/trusted-publishers/). Publishing uses npm 11 on Node.js 24 and GitHub-hosted runners, with `id-token: write` scoped to the release job. The publish script explicitly invokes npm, rather than depending on a package manager's bundled publishing client.
|
|
20
|
+
|
|
21
|
+
GitHub Actions also needs permission to create pull requests. Under repository Settings → Actions → General, enable **Allow GitHub Actions to create and approve pull requests**. The workflow does not approve or auto-merge its own PRs.
|
|
22
|
+
|
|
23
|
+
## Normal release flow
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
1. Include a changeset with package changes: `pnpm exec changeset`. Choose patch, minor, or major and describe the observable change. Documentation/test-only changes can omit a changeset when no package release is needed.
|
|
26
|
+
2. Merge the contribution PR into `master` after CI passes.
|
|
27
|
+
3. `Release` runs the shared typecheck/build/package/unit and three-engine browser checks. If they pass, Changesets creates or updates a **Release: update package version** PR containing version/changelog changes.
|
|
28
|
+
4. The workflow explicitly dispatches CI on `changeset-release/master`. This is necessary because PRs created by `GITHUB_TOKEN` do not automatically trigger PR workflows. Verify the generated branch's successful checks before merging it.
|
|
29
|
+
5. Merge the version PR when ready. The release workflow validates that commit, publishes its new version publicly with provenance, and creates a `vX.Y.Z` tag and GitHub release.
|
|
30
|
+
|
|
31
|
+
Only the generated version PR determines a new package version; ordinary commits do not each publish. Never edit an already published version in place. Choose a new version through a changeset.
|
|
32
|
+
|
|
33
|
+
`tools/publish.mjs` queries the exact registry version and exits without publishing if it already exists. Unexpected registry errors fail closed. A newer `master` commit causes an older queued release to skip publishing so its successor can handle the release.
|
|
34
|
+
|
|
35
|
+
## Validation and package review
|
|
24
36
|
|
|
25
37
|
```sh
|
|
38
|
+
pnpm install --frozen-lockfile
|
|
39
|
+
pnpm test:all
|
|
26
40
|
mkdir -p /tmp/heic-pack
|
|
27
41
|
pnpm pack --pack-destination /tmp/heic-pack
|
|
28
42
|
```
|
|
29
43
|
|
|
30
|
-
Install the tarball
|
|
44
|
+
Install the tarball in a clean consumer. Verify ESM/types, the `heic.global.js` browser bundle, and lazy WASM integration. Published files must include built assets and documentation without private test photos or development dependencies.
|
|
31
45
|
|
|
32
|
-
`pnpm test:package`
|
|
46
|
+
`pnpm test:package` validates declared targets and creates a bundled browser consumer for default-adapter tests. `prepublishOnly` checks type safety, build/package targets, and unit tests; the release workflow runs the browser matrix before permitting publication.
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
CI does not establish released-Safari or hardware HEVC coverage. Test representative consented Apple photos, tiled grids, orientations, and color behavior on target devices before claiming device support. Record skipped tests.
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
## Recovery
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
- If npm rejects OIDC authentication, confirm the trusted-publisher fields exactly match this repository and `release.yml`, direct publishing is allowed, and the workflow uses a GitHub-hosted runner with Node/npm versions meeting npm's requirements.
|
|
53
|
+
- If npm publishing succeeds but tagging/release creation fails, do not republish or unpublish the version. Verify the registry's `gitHead`, create the missing `vX.Y.Z` tag on that commit, and create the GitHub release from its changelog.
|
|
54
|
+
- If a release run is interrupted before publishing, rerun `Release` on `master`. Already published versions are skipped.
|
|
55
|
+
- Rotate/revoke any fallback manual publishing token and keep it outside the repository. Trusted publishing needs no persistent npm token.
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
For an explicitly authorized emergency manual release, authenticate interactively and run `npm publish --access public` on the validated version commit. Create its tag/release afterward. Do not use the automated release script outside GitHub Actions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@su-engineering/heic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Browser-first HEIC/HEIF decoder. Decodes Apple HEIC photos without unconditionally downloading a WebAssembly codec.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
"dump": "node --experimental-strip-types tools/dump.ts",
|
|
41
41
|
"validate-corpus": "node --experimental-strip-types tools/validate-corpus.ts",
|
|
42
42
|
"prepublishOnly": "pnpm typecheck && pnpm build && pnpm test:package && pnpm test:unit",
|
|
43
|
-
"release": "
|
|
44
|
-
"test:package": "node tools/check-package.mjs"
|
|
43
|
+
"release": "node tools/publish.mjs",
|
|
44
|
+
"test:package": "node tools/check-package.mjs",
|
|
45
|
+
"benchmark": "pnpm build && node tools/benchmark.mjs"
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
47
48
|
"heic",
|
|
@@ -50,21 +51,25 @@
|
|
|
50
51
|
"webcodecs",
|
|
51
52
|
"image",
|
|
52
53
|
"browser",
|
|
53
|
-
"iphone"
|
|
54
|
+
"iphone",
|
|
55
|
+
"conversion",
|
|
56
|
+
"jpeg",
|
|
57
|
+
"png"
|
|
54
58
|
],
|
|
55
59
|
"devDependencies": {
|
|
56
60
|
"@changesets/cli": "^2.27.10",
|
|
57
61
|
"@playwright/test": "^1.62.1",
|
|
58
62
|
"@types/node": "^22.20.1",
|
|
59
63
|
"esbuild": "^0.28.1",
|
|
60
|
-
"
|
|
64
|
+
"heic-to": "1.5.2",
|
|
65
|
+
"libheif-js": "^1.23.2",
|
|
61
66
|
"tsup": "^8.5.1",
|
|
62
67
|
"typescript": "^5.9.3",
|
|
63
68
|
"vite": "^7.3.1",
|
|
64
69
|
"vitest": "^4.1.11"
|
|
65
70
|
},
|
|
66
71
|
"peerDependencies": {
|
|
67
|
-
"libheif-js": "^1.
|
|
72
|
+
"libheif-js": "^1.23.2"
|
|
68
73
|
},
|
|
69
74
|
"peerDependenciesMeta": {
|
|
70
75
|
"libheif-js": {
|