heifer 0.3.9

Pure-Rust HEIF/HEIC image decoder and encoder: no C dependencies, no unsafe code
Documentation

A pure-Rust HEIF/HEIC image decoder and encoder. No C dependencies · no unsafe code · runs everywhere Rust runs, including the browser.

crates.io docs.rs CI Fuzz License: MIT OR Apache-2.0

▶ Try it in your browser — drop a .heic photo to view it, or a PNG/JPEG to convert it to HEIC; everything runs locally in heifer compiled to WebAssembly.

Why Heifer?

HEIC is the default photo format on iPhones and many Android phones, yet reading and writing it from Rust has mostly meant leaning on C and C++.

Heifer is an independent implementation of HEIF and HEVC written entirely in safe Rust, validated against the official conformance bitstreams. Add one line to Cargo.toml and it builds anywhere Rust builds (Linux, macOS, Windows and WebAssembly are tested in CI), with no system library to install.

Features

  • Correct: identical to ffmpeg on 62/62 comparable HEVC conformance bitstreams; 41 also verified against the reference decoder's MD5 hashes.
  • Real photos: iPhone (48-tile grids, rotation, spatial photos), Samsung 200 MP, Xiaomi, Sony.
  • Safe: #![forbid(unsafe_code)], size limits against malicious files, fuzzed continuously on CI.
  • Complete: alpha, overlays, crop/rotation/mirror, 8–12 bit, EXIF/XMP/ICC metadata.
  • Encoder: writes HEIC files from RGB(A) pixels, with alpha, in parallel tiles like iPhone photos; checked with Apple's ImageIO and ffmpeg.
  • Portable: optional image integration, and it compiles to WebAssembly (≈ 250 KB).

Quick start

Command line: convert photos without installing libheif or ffmpeg.

cargo install heifer-cli
heifer IMG_1234.heic photo.jpg          # or: heifer *.HEIC --to jpg -o converted
heifer photo.png photo.heic -q 80       # to HEIC
heifer info IMG_1234.heic               # size, tiles, camera, date, metadata

Library:

[dependencies]
heifer = "0.3"

Or try the examples on your own photos:

cargo run --release -p heifer --example heic2png -- photo.heic photo.png
cargo run --release -p heifer --example to_heic -- photo.png photo.heic 80
cargo run --release -p heifer --example metadata -- photo.heic

Decode a HEIC

let bytes = std::fs::read("photo.heic")?;
let image = heifer::decode(&bytes)?;    // primary image, with alpha, crop/rotation/mirror applied

println!("{}×{}, {}-bit, alpha: {}", image.width, image.height, image.bit_depth, image.has_alpha);
let rgba: Vec<u8> = image.to_rgba8();   // or to_rgb8(); image.data keeps 16-bit samples

let preview = heifer::decode_thumbnail(&bytes)?;   // the embedded thumbnail, if any: much faster

Limit threads and image size, for example on a server that accepts uploads:

let options = heifer::Options {
    max_threads: 1,            // 0 = all cores (default); grid tiles are decoded in parallel
    max_pixels: 50_000_000,    // reject larger images (default: 2^28 pixels)
};
let image = heifer::decode_with_options(&bytes, &options)?;

Encode a HEIC

let options = heifer::EncodeOptions { quality: 80, ..Default::default() };   // 0..=100
let heic: Vec<u8> = heifer::encode_rgba8(width, height, &rgba, &options)?;    // or encode_rgb8
std::fs::write("photo.heic", heic)?;

let again = heifer::encode(&heifer::decode(&bytes)?, &options)?;              // re-encode an Image

// Keep EXIF, XMP and the ICC profile of the original:
let meta = heifer::read_metadata(&bytes)?;
let smaller = heifer::encode_with_metadata(&heifer::decode(&bytes)?, &meta, &options)?;

Images are stored as YCbCr 4:2:0 (sRGB, BT.601 full range), in 10 bits (HEVC Main 10) when the image has more than 8 bits per sample (e.g. 16-bit PNGs), which keeps gradients free of banding, and split into 512×512 tiles that are encoded in parallel, like iPhone photos; transparency is kept as an alpha plane. A 12 MP image takes about 0.9 s on 10 cores. At quality 80, a re-encoded Apple photo has a PSNR of 42.0 dB against the original, both decoded by Apple (1.0 bit per pixel).

The files are decoded by Apple's ImageIO (used by Preview and sips) and by ffmpeg. The encoder chooses block sizes (64×64 down to 4×4), transform splits and prediction modes by rate-distortion cost, with RDOQ, sign data hiding, transform skip and SAO. At equal quality its files are 5% smaller than those of x265's medium preset and 2.7% smaller than slow (PSNR, 6:1:1), and slightly smaller than still265's Slow preset (0.3 to 0.6%, benchmarks).

Read metadata

Read EXIF, XMP and the ICC profile without decoding pixels:

let meta = heifer::read_metadata(&bytes)?;
if let Some(exif) = meta.exif_fields() {
    println!("{:?} {:?} taken {:?}", exif.make(), exif.model(), exif.date_time());
}
let icc: Option<Vec<u8>> = meta.icc_profile;   // also meta.exif (raw TIFF) and meta.xmp

HDR photos

Recent iPhones store an HDR gain map next to the normal image. decode_hdr applies it and returns linear-light RGB where 1.0 is SDR white and highlights go up to the photo's headroom, matching Apple's own decoder (within 0.3% on average on an iPhone 12 Pro photo):

let hdr = heifer::decode_hdr(&bytes)?;          // f32 RGB, Display P3 primaries for iPhone photos
println!("headroom {:.2}×", hdr.headroom);      // e.g. 4.00: highlights up to 4× SDR white

// Or apply the gain map yourself, e.g. on the GPU:
if let Some(gain) = heifer::read_gain_map(&bytes)? {
    println!("{}×{} gain map, headroom {:.2}", gain.width, gain.height, gain.headroom);
}

decode_hdr also reads ISO 21496-1 gain maps stored as HEIF tmap items, including their per-channel gain, gamma, offset, and headroom metadata. ISO gain maps take precedence when both formats are present; read_gain_map exposes the legacy Apple map.

image crate integration

heifer = { version = "0.3", features = ["image"] }
heifer::image_crate::register_image_decoder_hooks();
let img = image::open("photo.heic")?;   // .heic / .heif / .hif, plus content detection

let file = std::fs::File::create("photo.heic")?;
img.write_with_encoder(heifer::image_crate::HeifEncoder::new(file))?;   // write a HEIC

HeifDecoder implements image::ImageDecoder (8- and 16-bit RGB/RGBA, EXIF/XMP/ICC). The HEIF orientation is already applied, so it is never applied twice.

HeifEncoder implements image::ImageEncoder: 8- and 16-bit grey and RGB, with or without alpha, and the ICC profile and EXIF set by image. (image::save only knows the formats built into image, so writing goes through write_with_encoder.)

Supported formats / features

Supported Not yet
Container grids, overlays (iovl), alpha planes, clap/irot/imir, thumbnails, multiple images, all iloc construction methods —
HEVC Main, Main 10, Main Still Picture, intra Range Extensions: 8–12 bit, 4:0:0 / 4:2:0 / 4:2:2 / 4:4:4, tiles, WPP, PCM, scaling lists, deblocking, SAO extended precision (16-bit), image sequences (inter prediction)
Colour YCbCr → RGB, BT.601 / 709 / 2020, full and limited range (colr nclx, else HEVC VUI); chroma upsampled like Apple's decoder (≈ 50 dB from Apple's output on Apple photos) applying ICC profiles
HDR Apple gain maps and ISO 21496-1 tmap gain maps; display-headroom limiting —
Metadata EXIF (with a small reader for camera, date, orientation, GPS), XMP, ICC IPTC
Encoding HEIC from 8- to 16-bit RGB(A): HEVC Main and Main 10 intra, 4:2:0, alpha, 512×512 grid tiles, 320-pixel thumbnails, any size, EXIF/XMP/ICC; HEIF writing (items, grids, alpha, thumbnails, metadata, clap) 4:4:4, HDR gain maps, lossless

Conformance

The first picture of 66 official conformance bitstreams (ITU-T H.265.1) covering Main, Main 10 and the Range Extensions:

Check Result
Identical to ffmpeg, sample for sample 62 / 62 comparable streams
Verified against the reference decoder's picture hash (MD5 / checksum SEI) 41 streams, including 3 that ffmpeg cannot decode
Not supported 1 (16-bit extended precision tools)

Robustness

Six cargo-fuzz targets with HEIF/HEVC dictionaries run around the clock on GitHub Actions (5h15 sessions every 6 hours): full decoding, container parsing, HEVC decoding, a parallel-vs-sequential differential check, a CABAC round trip and an encode-then-decode round trip. Fuzzing already found and fixed a memory exhaustion bug.

The encoder reconstructs pictures with the decoder's own code, and its tests check that the decoder reproduces the encoder's reconstruction sample for sample.

Real-world compatibility

Photo Content Result
iPhone 13 Pro 4032×3024, 48-tile grid, HDR gain map, depth map ✓
iPhone 8 Plus rotated 270° correct orientation
iPhone 15 Pro spatial (stereo) photo ✓
Samsung Galaxy S24 Ultra 200 MP, 768 tiles decoded (ffmpeg fails)
Xiaomi, Sony, others grids, 8/10/12-bit, alpha ✓
Corrupted / truncated files clean errors, never a panic

The remaining differences with ffmpeg are in heifer's favour: it honours the declared image sizes and grid crops, and decodes the file's primary image rather than the largest one.

Performance

Decode time on a 10-core Apple Silicon Mac (release build, median of several runs), compared with ffmpeg decoding the same file:

Image heifer heifer, 1 thread ffmpeg
Xiaomi, 5 MP 22 ms 120 ms 56 ms
iPhone 13 Pro, 12 MP (48 tiles) 40 ms 167 ms 96 ms
8000×6000, 48 MP 196 ms 0.97 s 582 ms
Galaxy S24 Ultra, 200 MP (768 tiles) 0.54 s 2.1 s fails
In the browser (WebAssembly, 1 thread), 1280×854 ~80 ms

Grid tiles are decoded in parallel; the iPhone photo scales from 162 ms on 1 thread to 32 ms on 10:

On multi-core machines heifer decodes these photos faster than ffmpeg. Single-threaded decoding is still slower than ffmpeg. The hot loops are written so that the compiler vectorizes them (NEON, SSE/AVX), without any unsafe code.

Benchmarks against every other HEIC decoder and encoder (heic-rs, heic, heic-decoder, zune-heic, still265, ffmpeg, x265, Apple), reproducible with python3 benchmarks/run.py.

Other pure-Rust decoders

Two other pure-Rust HEIC decoders appeared in 2026. Measured on the same machine, decoding to 8-bit RGBA, on the files of this repository (October 2026, heic 0.1.6 and heic-rs 0.1.1):

heifer heic-rs heic
License MIT OR Apache-2.0 MIT OR Apache-2.0 AGPL-3.0 or commercial
Test files decoded (of 53) 50 49 (no iovl overlays) 50 (refuses 200 MP by default)
Colours vs Apple's decoder (median of 13 Apple and other files) 50 dB 27 dB (full-range video decoded as limited range) 40 dB
Apple HDR gain maps applied ✓ – extraction only
Speed, 12 MP iPhone photo, 1 thread 165 ms 128 ms 227 ms

heic-rs is currently about 1.3× faster than heifer; heifer is more accurate and complete.

Architecture

 photo.heic ──▶ container ──▶ HEVC bitstream ──▶ HEVC decoder ──▶ assembly ──▶ RGB(A)
              (heifer-isobmff)   per tile        (heifer-hevc-dec)   (heifer)

 RGB(A) ──▶ YCbCr 4:2:0 tiles ──▶ HEVC encoder ──▶ HEIF writer ──▶ photo.heic
               (heifer)          (heifer-hevc-enc)  (heifer-isobmff)
  1. Container: the ISOBMFF boxes are parsed into items and properties: grids, alpha planes, transforms, metadata.
  2. Bitstream: each tile's HEVC data is extracted with its parameter sets.
  3. Decoding: CABAC, intra prediction, inverse transforms, deblocking and SAO; grid tiles are decoded in parallel.
  4. Assembly: tiles are stitched, alpha attached, YCbCr converted to RGB, and crop/rotation/mirror applied.

Crates

heifer-cli           the `heifer` command: conversions to and from HEIC
heifer               decode(), encode(), read_metadata(), image crate integration
├── heifer-isobmff   HEIF container: reading and writing of boxes, items, properties, grids
├── heifer-hevc-dec  HEVC decoder: CABAC, intra prediction, transforms, deblocking, SAO
└── heifer-hevc-enc  HEVC intra encoder: mode decisions, transforms, CABAC

The lower-level crates can be used directly, for example to extract an HEVC stream:

let file = heifer_isobmff::HeifFile::parse(&bytes)?;
let tiles = file.referenced_items(file.primary_id, b"dimg");
let hevc = file.hevc_bitstream(tiles[0])?;   // Annex B, decodable by heifer-hevc-dec or ffmpeg

Roadmap

  • 0.1 — Decoder: container, HEVC, conformance, real photos, metadata, image, WebAssembly, fuzzing
  • HDR: Apple gain maps (iPhone photos)
  • HDR: ISO 21496-1 gain maps (tmap)
  • 0.3 — Encoder: HEIF writing, HEVC intra encoder, alpha, parallel tiles; iloc construction method 2
  • 0.3.1: image crate encoder, metadata when encoding
  • 0.3.2: compression on par with x265 medium (4×4 blocks, transform splits, RDOQ, sign data hiding)
  • 0.3.3: 10-bit encoding (Main 10)
  • 0.3.8: thumbnails, public benchmarks
  • 0.3.9: files smaller than x265 slow and still265 Slow (SAO, 64×64 blocks, deeper transform trees, transform skip)
  • Next: HDR gain map encoding
  • ICC colour management

See CHANGELOG.md for details.

Development

./scripts/fetch-fixtures.sh      # sample HEIC files (not committed)
./scripts/fetch-real.sh          # real-world photos from pillow-heif's test suite (not committed)
cargo test --workspace --all-features

Inspecting files

cargo run -p heifer-isobmff --example dump -- photo.heic                       # box tree
cargo run -p heifer-isobmff --example info -- photo.heic --extract 1002 t.h265 # items, HEVC stream
cargo run -p heifer-hevc-dec --example params -- t.h265                        # parameter sets
cargo run --release -p heifer-hevc-dec --example decode -- t.h265 t.yuv        # raw YUV

Conformance (≈ 23 MB from itu.int, not committed)

./scripts/fetch-conformance.sh
cargo test --release -p heifer-hevc-dec --test conformance   # reference picture hashes
cargo build --release -p heifer-hevc-dec --example decode
./scripts/conformance.py                                     # comparison with ffmpeg

Fuzzing (nightly toolchain and cargo install cargo-fuzz)

./scripts/fetch-fuzz-corpus.sh   # libheif's fuzzing corpus as extra seeds
./scripts/fuzz.sh 8h             # all targets on all cores; Ctrl+C to stop

WebAssembly demo (rustup target add wasm32-unknown-unknown, cargo install wasm-bindgen-cli --version 0.2.129)

./scripts/build-demo.sh && python3 -m http.server -d demo/web 8000

License

Licensed under either of Apache License 2.0 or MIT, at your option.

Patents. HEVC is covered by patents. Heifer is an independent implementation for research and interoperability; users are responsible for complying with applicable patent licensing in their jurisdiction.

Acknowledgements. Validation relies on the ITU-T HEVC conformance bitstreams, the test files of pillow-heif and Nokia's HEIF samples, the fuzzing corpus of libheif, and ffmpeg as a reference decoder.