npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pixagram/heic

v0.1.1

Published

Detect HEIC/HEIF files and decode them to RGBA, ImageData, PNG or JPEG in the browser — pure-Rust `heic` decoder compiled to WebAssembly (scalar + SIMD builds).

Readme

@pixagram/heic

Detect HEIC/HEIF files and decode them in the browser — to raw RGBA, an ImageData, or a PNG/JPEG/WebP Blob/File. Powered by the pure-Rust heic decoder (v0.1.6, #![forbid(unsafe_code)]) compiled to WebAssembly. No native dependencies, no server round-trip.

License: the decoder is AGPL-3.0-only OR Imazen commercial, and HEVC is patent-encumbered. Read LICENSE.md before shipping.

import { isHeic, toBlob, toImageData } from '@pixagram/heic';

input.addEventListener('change', async () => {
  const file = input.files[0];
  if (await isHeic(file)) {                       // reads 272 bytes, no wasm loaded
    const jpeg = await toBlob(file, { type: 'image/jpeg', quality: 0.9 });
    // …upload `jpeg`, or:
    const pixels = await toImageData(file);       // draw it, feed it to a pipeline…
  }
});

Install

npm install @pixagram/heic

ESM only. Ships two wasm builds (wasm/scalar, wasm/simd); the loader picks one at runtime and the other is never fetched.

What it does

| Function | Returns | Loads wasm? | |---|---|---| | sniff(bytes) / isHeicSync(bytes) | brand-level classification of an ISOBMFF header | no | | isHeic(input) | Promise<boolean> — HEVC-coded HEIF, from content (reads only the header of a Blob) | no | | isHeicFilename(name) | extension hint (.heic .heif .heics .heifs .hif) | no | | probe(input) | dimensions, bit depth, alpha, metadata presence, CICP colour info — no pixels decoded | yes | | decode(input, options?) | { width, height, data: Uint8ClampedArray, hasAlpha, color } — RGBA8, rotation applied | yes | | toImageData(input, options?) | ImageData sharing the decoded buffer | yes | | toBlob(input, options?) | PNG (default) / JPEG / WebP Blob via the platform canvas encoder | yes | | toFile(input, options?) | same as toBlob, as a File named after the input | yes | | decodeThumbnail(input) | the embedded preview, or null | yes | | extractExif / extractXmp / extractIcc(input) | raw bytes or null | yes | | init(options?), resetWasm(), decoderInfo(), isSimdSupported() | loader control and diagnostics | — |

input is a Blob/File, ArrayBuffer or any typed-array view. Views are used in place; a Blob is read once.

Detection semantics

sniff parses the ftyp box and reports { isHeif, isHeic, codec, majorBrand, compatibleBrands }.

  • isHeic is true for HEVC brands (heic heix hevc hevx heim heis hevm hevs) and for generically-branded HEIF (mif1/msf1) that is not AVIF. Those files carry HEVC in practice; probe() is the definitive check.
  • AVIF (avif/avis) is isHeif: true, isHeic: false, codec: 'av1' — same container, not decodable here.
  • Do not trust File.type: browsers frequently report '' for .heic.

Decode options

await decode(file, {
  limits: { maxWidth: 16384, maxHeight: 16384, maxPixels: 100_000_000, maxMemoryBytes: 2 * 2 ** 30 },
});

Those are the defaults; 0 disables a limit. Limits are checked against the header before any pixel buffer is allocated (LIMIT_EXCEEDED). A 48 MP iPhone photo is 48,000,000 pixels and needs roughly 260 MB of wasm memory.

Encode options (toBlob, toFile, encodeToBlob)

await toBlob(file, {
  type: 'image/jpeg',   // 'image/png' (default) | 'image/jpeg' | 'image/webp'
  quality: 0.9,         // lossy formats, 0–1, default 0.92
  background: '#fff',   // composited under alpha; default '#ffffff' for JPEG, null otherwise
  colorSpace: 'auto',   // 'auto' | 'srgb' | 'display-p3' — see Colour below
});

If the browser cannot encode the requested type it would normally hand back a PNG silently; this package detects that and throws UNSUPPORTED_OUTPUT_TYPE.

Errors

Everything throws HeicError with a stable code:

import { isHeicError } from '@pixagram/heic';

try { await decode(file); }
catch (e) {
  if (isHeicError(e) && e.code === 'NOT_HEIF') …       // not a HEIF container
  if (isHeicError(e) && e.code === 'UNSUPPORTED_CODEC') … // AVIF / JPEG-in-HEIF
  if (isHeicError(e) && e.code === 'LIMIT_EXCEEDED') …
}

Decoder codes: NOT_HEIF NEED_MORE_DATA INVALID_CONTAINER INVALID_DATA UNSUPPORTED UNSUPPORTED_CODEC NO_PRIMARY_IMAGE HEVC_DECODE BUFFER_TOO_SMALL LIMIT_EXCEEDED OUT_OF_MEMORY CANCELLED SINK DECODE_ERROR PROBE_ERROR. Wrapper codes: INVALID_INPUT WASM_LOAD_FAILED WASM_TRAP NO_CANVAS NO_IMAGEDATA UNSUPPORTED_OUTPUT_TYPE ENCODE_FAILED.

Corrupt input returns an error; it does not trap. If the module ever does trap (WASM_TRAP), the instance is discarded and the next call reloads it.

Loading the wasm

By default the glue resolves heic_wasm_bg.wasm with new URL('…', import.meta.url) and fetches it — this works unbundled, in webpack 5 and in Vite builds. Two knobs exist for setups that need them:

import { init } from '@pixagram/heic';
import wasmUrl from '@pixagram/heic/wasm/simd/heic_wasm_bg.wasm?url'; // Vite

await init({ variant: 'simd', wasmUrl });          // explicit URL (must match the variant)
await init({ wasmSource: fetch('/static/heic.wasm') }); // or bytes / Response / WebAssembly.Module
  • webpack 5: works out of the box — both .wasm files are emitted as hashed assets and one is fetched at runtime. The package contains no computed import()/require, so there is no "Critical dependency" warning; the remaining "asset size limit" hint is informational (raise performance.maxAssetSize or filter .wasm in performance.assetFilter).
  • Vite: add optimizeDeps: { exclude: ['@pixagram/heic'] } so the dev server does not pre-bundle the glue away from its .wasm.
  • Node ≥ 20.16 / 22.3: the loader reads the file from disk itself via process.getBuiltinModule (no fetch(file:), no import('node:fs') that would leak into browser bundles). Older Node: pass wasmSource. decode/probe/metadata work, toBlob/toImageData need a canvas (NO_CANVAS / NO_IMAGEDATA).
  • init() is idempotent; calling it up front hides the ~50 ms load from the first conversion. Pass wasmUrl/wasmSource on the first call (or after resetWasm()); once an instance exists they are ignored. Calling init({ variant: 'scalar' }) after simd loaded switches builds.

Workers

Decoding a 12 MP photo takes a few hundred milliseconds of pure CPU and the PNG encode adds more; do it off the main thread. Every function works in a Worker (encoding uses OffscreenCanvas):

// worker.ts
import { toBlob } from '@pixagram/heic';
self.onmessage = async ({ data: file }) => {
  self.postMessage(await toBlob(file, { type: 'image/jpeg' }));
};

decode() results are plain objects; data.buffer can be transferred.

Performance and memory

Node 22 / V8, median of 7 runs, this package's fixtures:

| image | scalar | simd | |---|---|---| | 1280×854 (libheif example) | 154 ms | 151 ms | | 2560×1440 | 318 ms | 337 ms | | 1440×960 | 176 ms | 170 ms |

The SIMD build is within noise of scalar with heic 0.1.6: its wasm128 kernels cover colour conversion, IDCT/IDST and dequantisation, but HEVC intra decode time is dominated by CABAC entropy decoding and intra prediction, which are scalar. The dual-build plumbing is kept because it costs nothing at runtime and upstream is actively adding wasm128 kernels. npm run bench reproduces the table.

Wasm linear memory only grows. After a large decode, decoderInfo().memoryBytes shows the high-water mark; resetWasm() drops the instance so the next call starts from a fresh, small one.

Grid images (most iPhone photos) use the decoder's streaming path, which skips the full-frame YCbCr intermediate (~60 % lower peak memory).

Colour

  • decode() applies the file's rotation/mirror transforms; output is display-oriented.
  • YCbCr→RGB uses the matrix and range the file declares (BT.601/709/2020, full/limited).
  • Recent iPhones tag Display P3 primaries (CICP 12). With colorSpace: 'auto' the ImageData/canvas is created as display-p3 where the browser supports it (Chrome, Safari), so PNG/JPEG exports carry the right profile. Where it does not, pixels are still correct but wide-gamut colours look slightly duller when interpreted as sRGB. decoded.color exposes the CICP codes and extractIcc() the embedded profile for callers who colour-manage themselves.
  • 10-bit sources are downconverted to 8-bit. HDR transfer functions (PQ/HLG) are reported in color.transfer but not tone-mapped.

Limitations

  • HEVC I-frames only — fine for stills, no video.
  • AVIF, JPEG-in-HEIF and H.264-in-HEIF are detected (UNSUPPORTED_CODEC) but not decoded.
  • No cancellation mid-decode (the call is synchronous inside wasm); use a Worker and terminate it if you need to abort.
  • PNG/JPEG encoding relies on the platform canvas. A pure-wasm encoder can be added behind a build flag if server-side conversion is needed.
  • The upstream decoder reports 118/162 HEIF test files and 49/49 HEVC conformance vectors passing; some exotic files may fail with UNSUPPORTED.

Building from source

rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version "$(awk '/^name = "wasm-bindgen"$/{getline; gsub(/version = |"/,""); print; exit}' rust/Cargo.lock)"
# optional but recommended: wasm-opt from binaryen ≥ 119 (older releases reject
# the bulk-memory / multivalue opcodes current rustc emits)
npm install
npm run build        # scripts/build-wasm.sh (both variants) + tsc
npm test             # node --test; encode tests need @napi-rs/canvas (dev dep) or skip
npm run bench

scripts/build-wasm.sh refuses to run if the wasm-bindgen CLI version differs from the crate version in rust/Cargo.lock, and it patches the generated glue to make init() re-entrant (the loader owns idempotency; see comments there).

Layout:

rust/            wasm-bindgen binding crate (probe / decode / thumbnail / metadata, typed error codes)
scripts/         build-wasm.sh, bench.mjs
src/             detect.ts · loader.ts · encode.ts · errors.ts · types.ts · index.ts
wasm/{scalar,simd}/  generated glue + .wasm (committed build artefacts)
dist/            compiled ESM + .d.ts
test/            node:test suites and fixtures