# Browser HEIC decoder build

Built 2026-09-15 from libheif 1.23.4 + libde265 1.1.3. Only the HEVC decoder is linked. Neither an encoder nor the experimental WebCodecs plugin is enabled. Threading and plugin loading are disabled. This was compiled with Emscripten 4.0.15, CMake 3.31.8, and Ninja 1.13.1.

## Application integration

Serve `libheif.mjs` and `libheif.wasm` from the same origin. Import the module only inside the application's dedicated Worker. Fetch the WASM into an ArrayBuffer and pass it as `wasmBinary` so initialization never performs synchronous network access:

```js
const {default: createHeif} = await import('/vendor/libheif/libheif.mjs');
const response = await fetch('/vendor/libheif/libheif.wasm');
if (!response.ok) throw new Error('Decoder could not load.');
const heif = await createHeif({wasmBinary: await response.arrayBuffer()});
const decoder = new heif.HeifDecoder({maxPixels: 32000000});
let images = [];
try {
  images = decoder.decode(new Uint8Array(inputArrayBuffer));
  if (!images.length) throw new Error('This image could not be read.');
  const image = images.find(item => item.is_primary()) || images[0];
  const width = image.get_width();
  const height = image.get_height();
  if (!width || !height || width * height > 32000000) throw new Error('Image is too large.');
  const destination = {width, height, data: new Uint8ClampedArray(width * height * 4)};
  const pixels = await new Promise((resolve, reject) => {
    image.display(destination, result => result ? resolve(result) : reject(new Error('Image decode failed.')));
  });
  // pixels.data contains 8-bit RGBA. Encode with OffscreenCanvas, or transfer
  // pixels.data.buffer to the main thread and use a canvas there.
} finally {
  for (const image of images) image.free();
  if (decoder.decoder) heif.heif_context_free(decoder.decoder);
  decoder.decoder = null;
}
```

`is_primary()` deliberately chooses the primary still; do not silently imply that burst images, auxiliary depth maps, or Live Photo video were exported. The number of top-level images is `images.length`. The decoder applies HEIF crop/rotation/mirror transforms; do not apply EXIF orientation again. No metadata is copied to an output file. The RGBA path is 8-bit; it does not preserve HDR gain maps, depth maps, wide-gamut ICC profiles, camera metadata, or original HEVC compression. Use explicit product copy for these limits.

## Limits and cancellation

A small LGPL-licensed wrapper tightens libheif limits before parsing. `new HeifDecoder({maxPixels,maxMemoryBytes,maxItems})` accepts optional lower limits. Hard ceilings are 50,000,000 pixels, 256 MiB of library-tracked memory, and 1,000 items. Additional limits are 4 MiB color profiles, 4,096 tiles, 1,000 sequence frames, and 64 file brands. WASM heap maximum is 512 MiB. Default pixel cap is 50,000,000; the application should explicitly select a lower limit such as 32,000,000 based on its supported devices.

The application must also cap compressed input size, batch totals, and retained output size. Process one input at a time. Web Worker termination provides actual cancellation and an external timeout even when native decode is synchronous. Terminate and recreate the Worker after a timeout or WASM trap. A cancelled JS Promise alone does not stop native decode. Browser total memory includes pixel copies outside the WASM heap and can exceed its limit.

The JS build has `DYNAMIC_EXECUTION=0`. For sites with CSP, allow WebAssembly compilation (`script-src 'self' 'wasm-unsafe-eval'`) and same-origin Worker/fetch resources. No JS `unsafe-eval` or blob Worker is needed by this module.

## Verification

`TEST-RESULTS.json` reports the completed fixture and limit smoke checks. Tests are Node-side WASM tests; browser integration, Safari/iOS memory behavior, and browser output encoders must also be tested by the host application. Official input fixtures are included in the source archive under `libheif-1.23.4/examples` and `libheif-1.23.4/tests/data`.

## Source and rebuilding

The exact original source archives and the complete modification/build scripts are provided. `bootstrap-tools.sh` downloads pinned portable macOS tools, verifies archive checksums, extracts sources, and calls `build-decoder.sh`. Run it in an empty directory containing the files from the source bundle. Output goes to `artifacts/`. Build tools occupy approximately 2 GB. The toolchain URLs are specified in the script; no administrator access is used. Source hashes and emitted-module hashes are in `SHA256SUMS`.

Sources:
- https://github.com/strukturag/libheif/releases/tag/v1.23.4
- https://github.com/strukturag/libde265/releases/tag/v1.1.3
- https://github.com/emscripten-core/emsdk/tree/4.0.15

## License notices

libheif and libde265 are LGPL-3.0-or-later. The small browser limit wrapper and modified `post.js` follow that license. Full LGPL and GPL text appears in both library license files. Copyright notices remain in the original source archives. Runtime notices for Emscripten, libc++, libc++abi, musl, compiler-rt, and libunwind accompany these files.

Keep the library module as a separate replaceable asset, publish the corresponding decoder source/build bundle and notices, and include a visible link to them in the website's open-source notices. The host application must also provide the corresponding application code or other means necessary to recombine it with a modified library as required by LGPL section 4; the decoder source bundle alone does not cover the site's application code. The software licenses should not be represented as a grant of all possible HEVC patent rights.
