heic

Pure Rust HEIC/HEIF image decoder. No C/C++ dependencies, no unsafe code.
#![forbid(unsafe_code)]— zero unsafe blocks in the entire codebaseno_std + alloccompatible (compiles for wasm32-unknown-unknown)- Multi-codec: HEVC (built-in), AV1 via rav1d-safe (
av1feature), uncompressed HEIF via zenflate (uncifeature) - AVX2/SSE4.1/NEON SIMD acceleration with automatic scalar fallback
- Cooperative cancellation via enough — all decode paths check
Stoptokens - Configurable resource limits (dimensions, pixel count, memory) enforced before allocation
- Optional tile-parallel decoding via rayon
Status
Decodes most HEIC files from iPhones and cameras. 118/162 HEIF test files decode successfully (with av1 + unci features). 49/49 ITU-T HEVC conformance vectors pass. Best quality: 77.3 dB PSNR (BT.709), 73% pixel-exact on example.heic.
What works
- HEIF container parsing (ISOBMFF boxes, grid images, overlays, identity-derived items)
- HEIF image sequences (msf1/moov — first I-frame extraction from movie structure)
- Multi-codec dispatch: HEVC, AV1 (
av1feature), uncompressed HEIF (uncifeature) - Full HEVC I-frame decoding (VPS/SPS/PPS, CABAC, intra prediction, transforms)
- AV1 still image decoding via rav1d-safe (
av1feature) - Uncompressed HEIF with deflate/zlib decompression via zenflate (
uncifeature) - Deblocking filter and SAO (Sample Adaptive Offset)
- YCbCr→RGB with BT.601/BT.709/BT.2020 matrices (full + limited range)
- CICP color info from HEVC VUI and HEIF colr nclx (container overrides codec)
- 10-bit HEVC with transparent 8-bit downconvert or 16-bit output preservation
- Alpha plane decoding from auxiliary images (HEVC and AV1)
- HDR gain map extraction (Apple
urn:com:apple:photo:2020:aux:hdrgainmap) - EXIF, XMP, and ICC profile extraction (zero-copy where possible)
- Thumbnail decode, image rotation/mirror transforms (ipma ordering)
- HEVC scaling lists (custom dequantization matrices)
- AVX2 SIMD: color conversion, IDCT 8/16/32, residual add, dequantize
- SSE4.1 SIMD: IDST 4x4
- NEON SIMD: color conversion, IDCT 8/16/32, IDST 4x4
- Tile-parallel grid decoding via rayon (
parallelfeature)
Known limitations
- HEVC I-slices only (sufficient for HEIC still images; no inter prediction for video)
- JPEG and H.264/AVC codecs in HEIF: detected but not decoded
- Brotli-compressed uncompressed HEIF: not yet supported (deflate/zlib only)
- PQ/HLG transfer functions: parsed and exposed via
ImageInfo, but no EOTF applied — callers handle tone-mapping
Features
| Feature | Default | Description |
|---|---|---|
std |
yes | Standard library support. Disable for no_std + alloc. |
parallel |
no | Parallel tile decoding via rayon. Implies std. |
av1 |
no | AV1 codec support via rav1d-safe. Implies std. |
unci |
no | Uncompressed HEIF (ISO 23001-17) with deflate/zlib decompression via zenflate. |
Usage
use ;
let data = read?;
let output = new.decode?;
println!;
Limits and cancellation
use ;
let mut limits = default;
limits.max_width = Some;
limits.max_height = Some;
limits.max_pixels = Some;
limits.max_memory_bytes = Some;
let output = new
.decode_request
.with_output_layout
.with_limits
.decode?;
Probe without decoding
use ImageInfo;
let info = from_bytes?;
println!;
// CICP color info also available:
// info.color_primaries, info.transfer_characteristics, info.matrix_coefficients, info.video_full_range
Zero-copy into pre-allocated buffer
let info = from_bytes?;
let mut buf = vec!;
let = new
.decode_request
.with_output_layout
.decode_into?;
For grid images (most iPhone photos), decode_into streams tile color conversion directly into the output buffer, avoiding the intermediate full-frame YCbCr allocation.
Metadata extraction
use Cow;
let decoder = new;
// Zero-copy for single-extent items, owned for multi-extent
let exif: = decoder.extract_exif?; // raw TIFF bytes
let xmp: = decoder.extract_xmp?; // raw XML bytes
let icc: = decoder.extract_icc?; // ICC profile bytes
let thumb = decoder.decode_thumbnail?; // smaller preview
HDR gain map
let gainmap = new.decode_gain_map?;
// gainmap.data: Vec<f32> (normalized 0.0–1.0), gainmap.width, gainmap.height
// Apply Apple HDR reconstruction:
// sdr_linear = sRGB_EOTF(sdr_pixel)
// gain_linear = sRGB_EOTF(gainmap_pixel)
// scale = 1.0 + (headroom - 1.0) * gain_linear
// hdr_linear = sdr_linear * scale
Performance
Benchmarked on AMD Ryzen 9 7950X, WSL2, Rust 1.93, release profile (thin LTO, codegen-units=1).
| Image | Time |
|---|---|
| 1280x854 (single tile) | 54 ms |
| 3024x4032 (48-tile, sequential) | 451 ms |
3024x4032 (48-tile, parallel) |
180 ms |
| Probe (metadata only) | 1.3 µs |
| EXIF extraction | 4.4 µs |
SIMD-accelerated on x86-64 (AVX2 for color conversion, IDCT 8/16/32, residual add, dequantize; SSE4.1 for IDST 4x4) and AArch64 (NEON for color conversion, IDCT 8/16/32, IDST 4x4). Scalar fallback when SIMD is unavailable.
Dependencies
4 runtime crates (default features), none with C/FFI:
heic
├── archmage — SIMD dispatch via CPU feature tokens
│ └── safe_unaligned_simd — safe wrappers over std::arch intrinsics
├── enough — cooperative cancellation (0 unsafe)
├── safe_unaligned_simd
└── whereat — error location tracking (deny(unsafe_code))
With parallel: adds rayon + crossbeam (6 more crates, all pure Rust).
With av1: adds rav1d-safe (pure Rust AV1 decoder with archmage SIMD).
With unci: adds zenflate (pure Rust DEFLATE/zlib).
Memory
Use DecoderConfig::estimate_memory() to check memory requirements before decoding. decode_into() uses a streaming path for grid images that reduces peak memory by ~60% compared to decode().
Security
Designed for untrusted input. All decode paths enforce resource limits before allocation, not after.
#![forbid(unsafe_code)]— the entire crate, including SIMD dispatch via archmage- Pre-decode dimension checks —
Limits(max_width, max_height, max_pixels, max_memory_bytes) checked against HEIF ispe box dimensions before any codec allocates frame buffers - AV1 frame_size_limit —
limits.max_pixelsfed directly into rav1d-safe'sSettings::frame_size_limit, rejecting oversized frames during OBU parsing before pixel buffer allocation - Decompression bomb protection — unci decompressor capped at
min(512 MiB, limits.max_memory_bytes)with expected size validated against declared dimensions - Cooperative cancellation —
enough::Stoptokens checked in all decode loops (per-CTU for HEVC, per-row for unci, per-tile for grids, inside rav1d, inside zenflate decompression) - Checked arithmetic — all dimension, offset, and size calculations use
checked_mul/checked_addwith explicit error returns - Fallible allocation —
try_reserve/try_vec!throughout; OOM returns an error, not a panic - Container parser hardening — resource limits on item count (64K), property count (64K), extents per item (1K), references (64K), sample table entries (1M), string lengths (4K), NAL unit size (16 MiB), ICC profile size (4 MiB)
- Fuzz targets — 5 libfuzzer targets covering decode, decode-with-limits, probe, AV1 decode, and unci decode
Use DecoderConfig::estimate_memory() to check memory requirements before decoding. Pass Limits to reject files that exceed your resource budget. Pass an enough::Stop token for cooperative cancellation of long-running decodes.
Image tech I maintain
| State of the art codecs* | zenjpeg · zenpng · zenwebp · zengif · zenavif (rav1d-safe · zenrav1e · zenavif-parse · zenavif-serialize) · zenjxl (jxl-encoder · zenjxl-decoder) · zentiff · zenbitmaps · heic · zenraw · zenpdf · ultrahdr · mozjpeg-rs · webpx |
| Compression | zenflate · zenzop |
| Processing | zenresize · zenfilters · zenquant · zenblend |
| Metrics | zensim · fast-ssim2 · butteraugli · resamplescope-rs · codec-eval · codec-corpus |
| Pixel types & color | zenpixels · zenpixels-convert · linear-srgb · garb |
| Pipeline | zenpipe · zencodec · zencodecs · zenlayout · zennode |
| ImageResizer | ImageResizer (C#) — 24M+ NuGet downloads across all packages |
| Imageflow | Image optimization engine (Rust) — .NET · node · go — 9M+ NuGet downloads across all packages |
| Imageflow Server | The fast, safe image server (Rust+C#) — 552K+ NuGet downloads, deployed by Fortune 500s and major brands |
* as of 2026
General Rust awesomeness
archmage · magetypes · enough · whereat · zenbench · cargo-copter
And other projects · GitHub @imazen · GitHub @lilith · lib.rs/~lilith · NuGet (over 30 million downloads / 87 packages)
License
Dual-licensed: AGPL-3.0 or commercial.
I've maintained and developed open-source image server software — and the 40+ library ecosystem it depends on — full-time since 2011. Fifteen years of continual maintenance, backwards compatibility, support, and the (very rare) security patch. That kind of stability requires sustainable funding, and dual-licensing is how we make it work without venture capital or rug-pulls. Support sustainable and secure software; swap patch tuesday for patch leap-year.
Your options:
- Startup license — $1 if your company has under $1M revenue and fewer than 5 employees. Get a key →
- Commercial subscription — Governed by the Imazen Site-wide Subscription License v1.1 or later. Apache 2.0-like terms, no source-sharing requirement. Sliding scale by company size. Pricing & 60-day free trial →
- AGPL v3 — Free and open. Share your source if you distribute.
See LICENSE-COMMERCIAL for details.
AI-Generated Code Notice
Developed with Claude (Anthropic). Not all code manually reviewed. Review critical paths before production use.