@su-engineering/heic 0.1.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 +83 -0
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/SECURITY.md +70 -0
- package/dist/chunk-Y5FFG5J7.js +49 -0
- package/dist/chunk-Y5FFG5J7.js.map +1 -0
- package/dist/heic.global.js +2 -0
- package/dist/heic.global.js.map +1 -0
- package/dist/index.d.ts +445 -0
- package/dist/index.js +1648 -0
- package/dist/index.js.map +1 -0
- package/dist/types-Bv9KPnri.d.ts +112 -0
- package/dist/wasm.d.ts +38 -0
- package/dist/wasm.js +97 -0
- package/dist/wasm.js.map +1 -0
- package/docs/api.md +110 -0
- package/docs/compatibility.md +53 -0
- package/docs/releasing.md +42 -0
- package/package.json +95 -0
package/dist/wasm.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { HeicAbortError, HeicDecodeError } from './chunk-Y5FFG5J7.js';
|
|
2
|
+
|
|
3
|
+
// wasm/index.ts
|
|
4
|
+
function createWasmAdapter(options = {}) {
|
|
5
|
+
const load = options.loadLibheif ?? (() => import('libheif-js/wasm-bundle.js'));
|
|
6
|
+
let modulePromise;
|
|
7
|
+
return {
|
|
8
|
+
name: "libheif-wasm",
|
|
9
|
+
// libheif applies irot / imir / clap itself: heif_decode_image honours the
|
|
10
|
+
// transformative properties unless ignore_transformations is set, and
|
|
11
|
+
// libheif-js does not expose that option. Declaring it here is what stops the
|
|
12
|
+
// pipeline applying them a second time and rotating the image twice.
|
|
13
|
+
appliesTransforms: true,
|
|
14
|
+
async decode(request) {
|
|
15
|
+
if (request.signal?.aborted) throw new HeicAbortError();
|
|
16
|
+
modulePromise ??= Promise.resolve(load()).then(normalizeLibheif);
|
|
17
|
+
const libheif = await modulePromise;
|
|
18
|
+
if (request.signal?.aborted) throw new HeicAbortError();
|
|
19
|
+
const decoder = new libheif.HeifDecoder();
|
|
20
|
+
const images = decoder.decode(request.data);
|
|
21
|
+
if (!images || images.length === 0) {
|
|
22
|
+
throw new HeicDecodeError("libheif returned no images", { strategy: "wasm" });
|
|
23
|
+
}
|
|
24
|
+
const image = images[0];
|
|
25
|
+
const width = image.get_width();
|
|
26
|
+
const height = image.get_height();
|
|
27
|
+
if (width <= 0 || height <= 0) {
|
|
28
|
+
throw new HeicDecodeError(`libheif reported ${width}x${height}`, { strategy: "wasm" });
|
|
29
|
+
}
|
|
30
|
+
if (typeof OffscreenCanvas === "undefined") {
|
|
31
|
+
throw new HeicDecodeError("OffscreenCanvas is required to receive libheif output", {
|
|
32
|
+
strategy: "wasm"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
36
|
+
const ctx = canvas.getContext("2d", {
|
|
37
|
+
colorSpace: request.colorSpace,
|
|
38
|
+
alpha: false
|
|
39
|
+
});
|
|
40
|
+
if (!ctx) {
|
|
41
|
+
throw new HeicDecodeError("Could not get a 2d context for libheif output", {
|
|
42
|
+
strategy: "wasm"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const imageData = ctx.createImageData(width, height, { colorSpace: request.colorSpace });
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
try {
|
|
48
|
+
image.display(imageData, (result) => {
|
|
49
|
+
if (!result) reject(new HeicDecodeError("libheif failed to render", { strategy: "wasm" }));
|
|
50
|
+
else resolve();
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
reject(
|
|
54
|
+
new HeicDecodeError(`libheif threw while rendering: ${String(error)}`, {
|
|
55
|
+
strategy: "wasm"
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
image.free?.();
|
|
61
|
+
ctx.putImageData(imageData, 0, 0);
|
|
62
|
+
if (request.signal?.aborted) {
|
|
63
|
+
canvas.width = 0;
|
|
64
|
+
canvas.height = 0;
|
|
65
|
+
throw new HeicAbortError();
|
|
66
|
+
}
|
|
67
|
+
return { image: canvas, width, height };
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function normalizeLibheif(imported) {
|
|
72
|
+
let candidate = imported;
|
|
73
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
74
|
+
if (isLibheifModule(candidate)) return candidate;
|
|
75
|
+
if (typeof candidate === "function") {
|
|
76
|
+
candidate = await candidate();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (candidate && typeof candidate === "object" && "default" in candidate) {
|
|
80
|
+
candidate = candidate.default;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
throw new HeicDecodeError(
|
|
86
|
+
"The module supplied to the wasm adapter does not expose a HeifDecoder",
|
|
87
|
+
{ strategy: "wasm" }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
function isLibheifModule(value) {
|
|
91
|
+
return typeof value === "object" && value !== null && typeof value.HeifDecoder === "function";
|
|
92
|
+
}
|
|
93
|
+
var wasmDecoder = createWasmAdapter();
|
|
94
|
+
|
|
95
|
+
export { createWasmAdapter, wasmDecoder };
|
|
96
|
+
//# sourceMappingURL=wasm.js.map
|
|
97
|
+
//# sourceMappingURL=wasm.js.map
|
package/dist/wasm.js.map
ADDED
|
@@ -0,0 +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"]}
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Import core exports from `@su-engineering/heic`. Import the optional adapter from `@su-engineering/heic/wasm`. The package is ESM; generated declarations are the authoritative TypeScript contract.
|
|
4
|
+
|
|
5
|
+
## `decodeHeic(input, options?)`
|
|
6
|
+
|
|
7
|
+
Returns `Promise<DecodedImage>`. Input is a `Blob` (including `File`), `ArrayBuffer`, or `Uint8Array`. The complete input is read and parsed before a decoding strategy is attempted. Do not modify an input buffer during decoding.
|
|
8
|
+
|
|
9
|
+
| Option | Default | Behavior |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `strategy` | `'auto'` | Try native, then WebCodecs, then a supplied adapter. Set `'native'`, `'webcodecs'`, or `'wasm'` to restrict decoding to that path. |
|
|
12
|
+
| `colorSpace` | `'srgb'` | Request `'srgb'` or `'display-p3'` for compositing canvases. Native decoding uses browser defaults. |
|
|
13
|
+
| `maxDimension` | Unset | Downscale the final bitmap's longest side to this many pixels; never upscale. Use a positive finite number. Does not constrain peak memory. |
|
|
14
|
+
| `signal` | Unset | Cooperatively cancel via `AbortSignal`. Synchronous parsing/codec work and browser operations cannot always be interrupted immediately. |
|
|
15
|
+
| `wasmLoader` | Unset | Async function returning a `DecoderAdapter`, called only when the cascade reaches the fallback. A globally registered adapter takes precedence. |
|
|
16
|
+
|
|
17
|
+
Cancellation example:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const pending = decodeHeic(file, { signal: controller.signal });
|
|
22
|
+
// When the user cancels:
|
|
23
|
+
controller.abort();
|
|
24
|
+
await pending; // Rejects with HeicAbortError at a cancellation checkpoint.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Result and ownership
|
|
28
|
+
|
|
29
|
+
| Field | Meaning |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `image` | Caller-owned `ImageBitmap`; call `close()` after rendering or otherwise consuming it. |
|
|
32
|
+
| `width`, `height` | Returned bitmap size, after optional downscaling. |
|
|
33
|
+
| `sourceWidth`, `sourceHeight` | Intrinsic display size after container transforms, before downscaling. |
|
|
34
|
+
| `strategy` | Successful path: `'native'`, `'webcodecs'`, or `'wasm'`. |
|
|
35
|
+
| `bitDepth` | Source bit depth from container/codec metadata; not a promise of output precision. |
|
|
36
|
+
| `isGrid`, `tileCount` | Whether the primary image is tiled, and its planned tile count. |
|
|
37
|
+
| `sourceColor` | `null`, ICC bytes (`{ type: 'icc', profile }`), or nclx primaries/transfer/matrix/full-range metadata. |
|
|
38
|
+
| `transformsApplied` | Counter-clockwise rotation, mirror direction, and whether clean-aperture cropping was applied. |
|
|
39
|
+
| `warnings` | Structured `{ code, message }` diagnostics for recognized unsupported features and unusual layouts. |
|
|
40
|
+
|
|
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
|
+
|
|
43
|
+
## `isHeic(input)`
|
|
44
|
+
|
|
45
|
+
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`.
|
|
46
|
+
|
|
47
|
+
A malformed, ambiguous, or incomplete prefix can yield limited information. A positive identification does not guarantee a valid file or successful decode. AVIF shares HEIF brands; an AV1 result should be routed elsewhere.
|
|
48
|
+
|
|
49
|
+
## `probeSupport()`
|
|
50
|
+
|
|
51
|
+
Returns `Promise<SupportReport>`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
{
|
|
55
|
+
native: boolean,
|
|
56
|
+
webcodecs: boolean,
|
|
57
|
+
hevcCodecStrings: string[],
|
|
58
|
+
recommended: 'native' | 'webcodecs' | 'wasm' | 'none',
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The current implementation recommends `'wasm'` when both browser paths are unavailable, even when no adapter is installed. It does not load libheif. It tests a small inline native image and queries accepted WebCodecs configurations; it does not certify every file, profile, or output color space.
|
|
63
|
+
|
|
64
|
+
## Errors
|
|
65
|
+
|
|
66
|
+
`HeicError` extends `Error` and provides `context` (available brand, item ID/type, strategy, codec, offset, or box). Subclasses:
|
|
67
|
+
|
|
68
|
+
| Error | Meaning |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `HeicParseError` | Malformed/truncated input or a container structure the parser refuses. |
|
|
71
|
+
| `HeicUnsupportedError` | Unsupported image features, resource limits, or no successful strategy; includes `attempts`. |
|
|
72
|
+
| `HeicDecodeError` | A decoder or rendering operation failed. |
|
|
73
|
+
| `HeicAbortError` | Cancellation detected. |
|
|
74
|
+
|
|
75
|
+
Auto mode collects strategy failures and can end with `HeicUnsupportedError` even when a decoder failed on corrupt data. Forced paths may expose a more direct decode error. Custom adapter/loader and platform errors can also propagate; always handle an unknown error in application code.
|
|
76
|
+
|
|
77
|
+
## WASM adapters
|
|
78
|
+
|
|
79
|
+
`wasmDecoder` is a reusable default adapter. `createWasmAdapter(options?)` creates an independent instance, loading and normalizing a libheif module once on first use.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createWasmAdapter } from '@su-engineering/heic/wasm';
|
|
83
|
+
|
|
84
|
+
const adapter = createWasmAdapter({
|
|
85
|
+
loadLibheif: () => import('/vendor/libheif-bundle.mjs'),
|
|
86
|
+
});
|
|
87
|
+
await decodeHeic(file, { wasmLoader: async () => adapter });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The URL above is an application-provided, self-hosted ESM WASM bundle, not an asset shipped by this package. For direct browser use, serve compatible adapter assets and their relative imports too. The default loader uses `libheif-js/wasm-bundle.js`; bare specifiers require a bundler or suitable import mapping.
|
|
91
|
+
|
|
92
|
+
A custom `DecoderAdapter` has `name`, `appliesTransforms`, and an async `decode(request)` method. The request contains the complete `Uint8Array` data, requested color space, and optional signal. Return `{ image, width, height }`, where `image` is an `ImageBitmap` or `OffscreenCanvas`. Set `appliesTransforms: true` only if the pixels already include container `irot`, `imir`, and `clap`; otherwise the core applies them. Returned pixel resources are handed to the core; do not reuse them after returning.
|
|
93
|
+
|
|
94
|
+
`registerDecoderAdapter(adapter)` sets a realm-wide fallback. Pass `undefined` to clear it. `getRegisteredAdapter()` retrieves it. Registration takes precedence over per-call loaders; prefer per-call loading for independent consumers.
|
|
95
|
+
|
|
96
|
+
## Parser and inspection exports
|
|
97
|
+
|
|
98
|
+
These functions do not decode pixels and accept `ArrayBuffer`/`Uint8Array` where a buffer is required. Their structures are exposed for diagnostics and advanced integrations; consult generated declarations before depending on them.
|
|
99
|
+
|
|
100
|
+
| Export | Purpose |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `parseHeif` | Parse the file into `HeifFile`: brands, primary item, items, locations, properties, and references. |
|
|
103
|
+
| `propertiesForItem`, `findProperty` | Resolve associated properties and find a typed property. |
|
|
104
|
+
| `readItemData` | Extract an item's payload from its extents. |
|
|
105
|
+
| `readGrid`, `parseGridPayload` | Read grid references and grid descriptors. |
|
|
106
|
+
| `planDecode` | Resolve an `ImagePlan` with tiles, groups, display size, transforms, and warnings. |
|
|
107
|
+
| `parseHvcC`, `hvccToCodecString` | Parse HEVC configuration and produce a WebCodecs codec string. |
|
|
108
|
+
| `hvccToAnnexBPrologue`, `lengthPrefixedToAnnexB` | Prepare HEVC NAL data for Annex B decoding. |
|
|
109
|
+
|
|
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`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Compatibility and limitations
|
|
2
|
+
|
|
3
|
+
## Runtime requirements
|
|
4
|
+
|
|
5
|
+
Pixel decoding targets modern browsers and dedicated browser workers. It uses `ImageBitmap` and `createImageBitmap`; software and WebCodecs compositing also require `OffscreenCanvas` with a 2D context. WebCodecs requires `VideoDecoder` and `EncodedVideoChunk`, normally in a secure context (HTTPS or localhost).
|
|
6
|
+
|
|
7
|
+
The Node.js engine field describes package tooling compatibility, not a server pixel-decoding implementation. Parser/inspection functions can be used without browser rendering APIs. Development uses Node.js 22.12+.
|
|
8
|
+
|
|
9
|
+
## Strategy selection
|
|
10
|
+
|
|
11
|
+
| Strategy | Requirement | Fallback behavior |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `native` | Browser image decoder accepts the HEIC file and returns plausible primary dimensions. | Auto mode continues if unavailable or rejected. |
|
|
14
|
+
| `webcodecs` | Browser exposes WebCodecs and accepts the file's HEVC configuration. | Auto mode continues after decode failure. |
|
|
15
|
+
| `wasm` | Caller supplies/registers an adapter; default adapter needs `libheif-js`. | No implicit codec download; absence/failure ends the cascade. |
|
|
16
|
+
|
|
17
|
+
Do not infer support from a browser name or version alone. Platform codecs, hardware, browser builds, and the input profile can change the outcome. `probeSupport()` is an advisory probe; the successful `decoded.strategy` reports what actually happened.
|
|
18
|
+
|
|
19
|
+
## File support
|
|
20
|
+
|
|
21
|
+
Supported primary items are HEVC `hvc1`/`hev1` images and supported HEVC tiled grids. Container `irot`, `imir`, and valid `clap` transforms are planned; native/libheif paths are expected to apply those themselves. Unsupported essential properties cause an error.
|
|
22
|
+
|
|
23
|
+
The library decodes one primary image. Recognized alpha, depth/disparity, and HDR gain-map auxiliary items can generate warnings but are not composited. It does not decode AVIF/AV1, arbitrary derived-image types, animation, image sequences, Live Photo motion, or all HEIF extensions. Sequence and motion features are not exhaustively detected, so no warning does not establish their absence.
|
|
24
|
+
|
|
25
|
+
## Color and HDR
|
|
26
|
+
|
|
27
|
+
`sourceColor` describes parsed ICC or nclx metadata. Reporting metadata does not mean applying the ICC profile. `colorSpace: 'display-p3'` requests a compositing canvas space; native decoding follows browser defaults, and the libheif adapter writes decoded RGBA bytes into the requested canvas. Cross-strategy color equivalence and full wide-gamut/HDR preservation are not guaranteed.
|
|
28
|
+
|
|
29
|
+
Source bit depth is metadata, not output precision. The default libheif path uses 8-bit RGBA output. Gain-map reconstruction and alpha compositing are outside current scope. Test color-critical workflows against independently rendered references on your target devices.
|
|
30
|
+
|
|
31
|
+
## Memory and cancellation
|
|
32
|
+
|
|
33
|
+
The planner caps the primary coded image at 256 million pixels and grid references at 4,096 tiles. These are rejection thresholds, not a memory budget or a guarantee that an accepted file will fit on a device. Tile buffers, decoder state, transforms, and canvases add overhead.
|
|
34
|
+
|
|
35
|
+
`maxDimension` downsizes the output after decoding/compositing; it does not prevent full-resolution allocations. Enforce input-size and application dimension limits, limit concurrent jobs, and use a worker when blocking the UI is unacceptable.
|
|
36
|
+
|
|
37
|
+
Cancellation is cooperative. An `AbortSignal` is checked around parts of the pipeline; synchronous work and an in-flight platform/codec operation may finish before cancellation is observed. Terminating an application-owned worker can provide a stronger boundary for expensive tasks.
|
|
38
|
+
|
|
39
|
+
## What the tests establish
|
|
40
|
+
|
|
41
|
+
- Unit tests cover parser behavior, bounds checks, HEVC configuration, and deterministic fixture mutations.
|
|
42
|
+
- Browser tests exercise the public built package, software decoding, transforms, dimensions, and workers across Playwright engines.
|
|
43
|
+
- Native and WebCodecs tests skip when capability probes fail. Headless Linux generally cannot validate platform HEVC decoding; some WebKit builds accept limited profiles through software decoders. Tests query each fixture configuration.
|
|
44
|
+
- The optional `chrome-hevc` project can exercise an installed browser with platform decoding. Released Safari still needs Apple-device testing.
|
|
45
|
+
- Committed fixtures include a tiny single-image file and generated asymmetric transform variants. They do not provide a broad real-world Apple grid/device corpus. Private local fixtures improve local coverage but are absent from CI.
|
|
46
|
+
|
|
47
|
+
A green CI run establishes only the tests that executed. Review skips and the input corpus before making support claims. See [fixture documentation](https://github.com/su-engineering/heic-web/blob/master/test/fixtures/README.md).
|
|
48
|
+
|
|
49
|
+
## Third-party code
|
|
50
|
+
|
|
51
|
+
The core library and repository contributions are [MIT licensed](../LICENSE). `libheif-js` is an optional, separately distributed dependency; its package declares LGPL-3.0 and includes upstream codec code with separate notices. The browser's platform decoder is supplied by the browser/OS.
|
|
52
|
+
|
|
53
|
+
Keep upstream license notices with any redistributed codec assets and review the licenses of the exact build you ship. This repository's MIT license does not relicense libheif or its bundled codecs.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Release checklist
|
|
2
|
+
|
|
3
|
+
Releases are maintainer-operated. The repository does not automatically publish to npm on push. Changesets is configured for public package publishing with `master` as the base branch.
|
|
4
|
+
|
|
5
|
+
## Before making the repository public
|
|
6
|
+
|
|
7
|
+
- Review the complete Git history and tracked files for credentials, private photos, sensitive metadata, and code/assets you cannot redistribute. Ignoring a file now does not remove historical copies.
|
|
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.
|
|
13
|
+
|
|
14
|
+
## Prepare a version
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pnpm install --frozen-lockfile
|
|
18
|
+
pnpm test:all
|
|
19
|
+
pnpm exec changeset
|
|
20
|
+
pnpm exec changeset version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Review version/changelog changes and commit them. For an initial release already at the intended version, avoid an accidental extra version bump. Inspect the package before publishing:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
mkdir -p /tmp/heic-pack
|
|
27
|
+
pnpm pack --pack-destination /tmp/heic-pack
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Install the tarball into a clean consumer project. Verify the ESM and declaration entry points, the `heic.global.js` browser bundle, and lazy fallback integration with the supported libheif build. The package should contain built assets, docs, README, security policy, and license, without test photographs or local dependencies.
|
|
31
|
+
|
|
32
|
+
`pnpm test:package` checks declared build targets and executes the standalone bundle in a sandbox. `prepublishOnly` checks type safety, build targets, and unit tests; it does not run the browser suite. Browser checks and device testing remain release requirements.
|
|
33
|
+
|
|
34
|
+
## Publish
|
|
35
|
+
|
|
36
|
+
Confirm npm organization access and your authenticated publishing identity. When explicitly authorized to publish:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm release
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This invokes `changeset publish`. Review the published version and registry access, then push any generated tags and create release notes summarizing behavior changes and known compatibility limits. Add provenance/trusted publishing through a separately reviewed release workflow if needed; never commit registry tokens.
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@su-engineering/heic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-first HEIC/HEIF decoder. Decodes Apple HEIC photos without unconditionally downloading a WebAssembly codec.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"docs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"CONTRIBUTING.md",
|
|
14
|
+
"SECURITY.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./wasm": {
|
|
23
|
+
"types": "./dist/wasm.d.ts",
|
|
24
|
+
"import": "./dist/wasm.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"unpkg": "./dist/heic.global.js",
|
|
31
|
+
"jsdelivr": "./dist/heic.global.js",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:unit": "vitest run",
|
|
37
|
+
"test:browser": "playwright test",
|
|
38
|
+
"test:all": "pnpm typecheck && pnpm build && pnpm test:package && pnpm test:unit && pnpm test:browser",
|
|
39
|
+
"fixtures": "sh tools/make-fixtures.sh",
|
|
40
|
+
"dump": "node --experimental-strip-types tools/dump.ts",
|
|
41
|
+
"validate-corpus": "node --experimental-strip-types tools/validate-corpus.ts",
|
|
42
|
+
"prepublishOnly": "pnpm typecheck && pnpm build && pnpm test:package && pnpm test:unit",
|
|
43
|
+
"release": "changeset publish",
|
|
44
|
+
"test:package": "node tools/check-package.mjs"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"heic",
|
|
48
|
+
"heif",
|
|
49
|
+
"decode",
|
|
50
|
+
"webcodecs",
|
|
51
|
+
"image",
|
|
52
|
+
"browser",
|
|
53
|
+
"iphone"
|
|
54
|
+
],
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@changesets/cli": "^2.27.10",
|
|
57
|
+
"@playwright/test": "^1.62.1",
|
|
58
|
+
"@types/node": "^22.20.1",
|
|
59
|
+
"esbuild": "^0.28.1",
|
|
60
|
+
"libheif-js": "^1.19.8",
|
|
61
|
+
"tsup": "^8.5.1",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vite": "^7.3.1",
|
|
64
|
+
"vitest": "^4.1.11"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"libheif-js": "^1.18.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"libheif-js": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "git+https://github.com/su-engineering/heic-web.git"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=18"
|
|
80
|
+
},
|
|
81
|
+
"packageManager": "pnpm@9.15.9",
|
|
82
|
+
"homepage": "https://github.com/su-engineering/heic-web#readme",
|
|
83
|
+
"bugs": {
|
|
84
|
+
"url": "https://github.com/su-engineering/heic-web/issues"
|
|
85
|
+
},
|
|
86
|
+
"pnpm": {
|
|
87
|
+
"overrides": {
|
|
88
|
+
"esbuild": "^0.28.1"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"publishConfig": {
|
|
92
|
+
"access": "public",
|
|
93
|
+
"registry": "https://registry.npmjs.org/"
|
|
94
|
+
}
|
|
95
|
+
}
|