# Browser image codecs

All user bytes stay in the browser. Static code/WASM/source archives are served from `/vendor/image-tools/codecs/`; there is no CDN or remote conversion API. The worker is `/image-codec-worker.js`. Assets are checksum-verified by `scripts/prepare-image-codecs.mjs` against `scripts/image-codec-assets.json`; package versions are exact in the lockfile. Sources were inspected on 2026-09-27.

## Processing contract

- JPG/PNG/WebP use the browser decoder, with EXIF orientation applied; AVIF uses the native decoder with orientation support, falling back to jSquash/libavif for untransformed images; HEIC/HEIF uses libheif. Pixels are redrawn before encoding, removing EXIF/XMP/comments and other input metadata. Input color profiles are not copied; output is 8-bit SDR. HDR, wide-gamut preservation, and animation conversion are not supported.
- Byte signatures are checked independently of names/MIME types; SVG is rejected. PNG/WebP animation and HEIF/AVIF sequences are rejected, as are multi-image HEIF collections. Dimensions from container headers are checked before decode and actual decoded dimensions are checked again.
- PNG palette mode uses UPNG quantization (2–256 colors), optional Floyd–Steinberg error diffusion in premultiplied RGB, then PNG encoding and OxiPNG level 2. It is lossy in color, retains alpha, and never claims guaranteed size savings or exact TinyPNG equivalence. Lossless mode skips quantization; OxiPNG preserves decoded pixels. Resizing is independent, and result cards show both original and output dimensions.
- AVIF encodes 8-bit YUV444 with full alpha quality and speed 8. Preview is decoded from the resulting AVIF and provided as PNG for browsers that cannot display AVIF.
- Heavy work runs sequentially in fresh workers. Cancel calls `terminate()`, unmount cancels too, and a later run creates a fresh worker. ZIP uses fflate inside a worker; ordinal prefixes keep duplicate names distinct. PDF wraps its PNG via pdf-lib in the same worker.
- Devices reporting ≤4 GB RAM or ≤4 logical cores: 20 MB/file, 12 MP decoded raster, 6 MP PNG/AVIF output and HEIF/AVIF input, 5 files / 60 MB per batch. Others: 40 MB/file, 24 MP raster, 12.5 MP heavy codecs (including standard 4032 × 3024 iPhone photos), 10 files / 120 MB. Unknown RAM uses reported cores; these caps reduce risk, not a promise that every device has sufficient memory. Operations time out at 3 minutes. No SharedArrayBuffer/COOP/COEP is required.

## Libraries, licenses, sources

- [jSquash](https://github.com/jamsinclair/jSquash): `@jsquash/avif@2.1.1`, `@jsquash/oxipng@2.3.0`, Apache-2.0 adapters; libavif BSD-2-Clause and its bundled license notices; OxiPNG MIT. The encoder adapter is patched only to disable multithreading. Runtime wrappers, WASM and licenses are served together.
- [UPNG.js](https://github.com/photopea/UPNG.js) `2.1.0`, MIT; pako `1.0.11`, MIT/Zlib. The build adapts their module wrappers to ESM/globalThis. Quantization is upstream UPNG, dithering is WiseLab code in `image-codec-core.mjs`.
- [libheif-js](https://github.com/catdad-experiments/libheif-js) `1.23.2`, LGPL-3.0, unmodified standalone replaceable ESM bundle. The upstream build uses `USE_UNSAFE_EVAL=0`, Emscripten 3.1.61, libheif commit `ac1cb05c39008f01525c991ff8b88f84ddf70fd2` (1.23.2), libde265 1.0.15. No HEVC encoder is included. LGPL licenses are supplied beside the module.
- fflate `0.8.3`, MIT, copied unchanged for ZIP. pdf-lib remains the existing MIT dependency.

AVIF adapters use libavif 1.0.1, libaom 3.7.0 and libsharpyuv from the commit recorded in the included AVIF Makefile. Their license and patent notices are served in `sources/`.

HEIF corresponding sources are served directly, with SHA-256 values in the public manifest:

- [libheif-js sources](/vendor/image-tools/codecs/sources/libheif-js-1.23.2.tar.gz)
- [upstream Emscripten build scripts](/vendor/image-tools/codecs/sources/libheif-emscripten-1.23.2.tar.gz)
- [libheif sources](/vendor/image-tools/codecs/sources/libheif-1.23.2.tar.gz)
- [libde265 sources](/vendor/image-tools/codecs/sources/libde265-1.0.15.tar.gz)

To rebuild a modified library, unpack the build scripts, unpack libheif into their `libheif/` directory, install Emscripten 3.1.61 and its system prerequisites as documented in libheif's `scripts/install-ci-linux.sh`, and follow `.github/workflows/emscripten.yml`. Place the supplied libde265 tarball in the build directory. Build with `USE_WASM=1 USE_UNSAFE_EVAL=0 USE_TYPESCRIPT=1` and run `dist-prep.sh wasm`. In the libheif-js sources, run `node scripts/install.js /path/to/libheif.tar.gz` as documented by that script, using its declared build dependencies. Replace the standalone `heif/libheif-bundle.mjs` with the resulting ESM module, retaining notices. The application uses only the documented `HeifDecoder`, `decode`, `get_width`, `get_height`, `display`, and `free` API; no application rebuild is needed to replace this module. Reverse engineering necessary to debug modifications to this LGPL component is permitted. These source archives exclude site code and user files.

## Recorded checks

`node --import ./tests/register.mjs --test tests/image-processing.test.mjs tests/image-codecs.test.mjs` checks byte admission, animation rejection, limits, settings, worker abort/re-run, PDF signature and page size. `WISE_PLAYWRIGHT_PATH=/path/to/playwright node scripts/image-codec-browser-smoke.mjs` serves real workers under strict CSP and verifies actual decoded pixels, HEIC alpha, AVIF signature/round-trip, EXIF rotation, PNG same-resolution compression, ZIP duplicate names, cancellation, and GET-only same-origin network traffic. Local fixtures, outputs and JSON evidence are in ignored `reports/local/image-codecs/`; they are not uploaded or distributed as site assets.

2026-09-27 Chrome measurements (illustrative fixtures, not a universal compression claim): gradient 640 × 480: 296,352 → 30,853 bytes, palette PSNR 55.11 dB; photo 800 × 533: 1,031,537 → 282,076 bytes, palette PSNR 36.09 dB. Lossless results: 52,014 and 776,365 bytes respectively, maximum decoded pixel difference 0. HEIC with alpha 129 decoded at 64 × 64; AVIF preserves transparent alpha 0.
