A pure-Rust HEIF/HEIC image decoder and encoder.
No C dependencies · no unsafe code · runs everywhere Rust runs, including the browser.
▶ 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.
Coming from libheif-rs? A call-by-call guide maps the two
APIs and fills in the diffs: docs/MIGRATION.md.
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, colour
management to sRGB (
decode_srgb, from the ICC profile ornclx). - 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
imageintegration, and it compiles to WebAssembly (≈ 250 KB).
Quick start
Command line: convert photos without installing libheif or ffmpeg.
Library:
[]
= "0.3"
Or try the examples on your own photos:
Decode a HEIC
let bytes = read?;
let image = decode?; // primary image, with alpha, crop/rotation/mirror applied
println!;
let rgba: = image.to_rgba8; // or to_rgb8(); image.data keeps 16-bit samples
let preview = decode_thumbnail?; // the embedded thumbnail, if any: much faster
The code values of decode are those of the file's own colour space: most iPhone and many camera
photos are Display P3 (tagged with an ICC profile) and would look washed out if shown as sRGB.
decode_srgb converts them, from the ICC profile when there is one and from nclx otherwise:
let srgb = decode_srgb?; // ICC / nclx applied, in sRGB
let small = decode_srgb_rgba8?; // 8-bit, half the memory
Files in a colour space heifer cannot convert (an ICC profile whose input space is not RGB) are
refused with Error::Unsupported. PQ and HLG files are HDR: decode_srgb refuses them, and
decode_hdr returns them in linear light (see the HDR section below).
Limit threads and image size, for example on a server that accepts uploads:
let options = Options ;
let image = decode_with_options?;
Encode a HEIC
let options = EncodeOptions ; // 0..=100
let heic: = encode_rgba8?; // or encode_rgb8
write?;
let again = encode?; // re-encode an Image
// Keep EXIF, XMP and the ICC profile of the original:
let meta = read_metadata?;
let smaller = encode_with_metadata?;
When the metadata carries an ICC profile, it is attached to every image item (tiles, grid and
thumbnail) and no nclx colour box is written, as in iPhone photos: the profile is the file's
colour space. Without one, files are tagged sRGB with nclx.
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.77 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 = read_metadata?;
if let Some = meta.exif_fields
let icc: = 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 = decode_hdr?; // f32 RGB, Display P3 primaries for iPhone photos
println!; // e.g. 4.00: highlights up to 4× SDR white
// Or apply the gain map yourself, e.g. on the GPU:
if let Some = read_gain_map?
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.
Files that carry a PQ (nclx transfer 16) or HLG (18) signal instead are linearised on the same
scale, where 1.0 is the BT.2408 reference white of 203 cd/m²: PQ reaches about 49.3 at peak and
HLG about 4.9. decode_hdr applies no tone mapping, so a display pipeline that cannot show that
range has to map it down itself. decode_srgb refuses these files and points to decode_hdr.
The reverse direction writes the same standard form: a monochrome, half-resolution gain map in a
hidden item, referenced by a tmap metadata item, with the base and tone-mapped items grouped as
alternatives. macOS (ImageIO) reads the result back as HDR, and it is the form recent Samsung
phones write.
// `hdr.data` is linear light in the base image's primaries; `hdr.headroom` is the ratio of
// HDR white to SDR white (4.0: highlights reach four times SDR white).
let bytes = encode_hdr?;
// Or keep EXIF, XMP and the ICC profile of the base image:
let bytes = encode_hdr_with_metadata?;
encode_hdr assumes that base is sRGB-encoded, as encode tags it, and that hdr uses the same
primaries. It does not convert between colour spaces, and clips values above hdr.headroom. The
gain map is coded at the same quality as the base image; the headroom itself is stored as the
ISO 21496-1 log2 gain range, so any headroom above 1 works.
image crate integration
= { = "0.3", = ["image"] }
register_image_decoder_hooks;
let img = open?; // .heic / .heif / .hif, plus content detection
let file = create?;
img.write_with_encoder?; // write a HEIC
Applications that already decode several images in parallel can register the hooks with
register_image_decoder_hooks_with_options(heifer::Options { max_threads: 1, ..Default::default() }).
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); matrix/TRC and LUT-based (A2B0/A2B1, lut8/lut16/lutAtoB) RGB ICC profiles (Display P3, Adobe RGB, camera LUT profiles, …) converted to sRGB by decode_srgb (matched to littleCMS) |
ICC profiles whose input space is not RGB; B2A output tables / soft proofing |
| HDR | Apple gain maps and ISO 21496-1 tmap gain maps; PQ (nclx transfer 16) and HLG (18) linearised by decode_hdr on the BT.2408 203 cd/m² scale; display-headroom limiting for gain maps |
tone mapping PQ/HLG to a display |
| 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; ISO 21496-1 HDR gain maps (tmap); HEIF writing (items, grids, alpha, thumbnails, metadata, clap, entity groups) |
4:4:4, 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)
- Container: the ISOBMFF boxes are parsed into items and properties: grids, alpha planes, transforms, metadata.
- Bitstream: each tile's HEVC data is extracted with its parameter sets.
- Decoding: CABAC, intra prediction, inverse transforms, deblocking and SAO; grid tiles are decoded in parallel.
- 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 = parse?;
let tiles = file.referenced_items;
let hevc = file.hevc_bitstream?; // Annex B, decodable by heifer-hevc-dec or ffmpeg
Replacing libheif-rs in an existing project? See docs/MIGRATION.md for a
side-by-side API map, behavioural differences and a migration checklist.
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;
ilocconstruction method 2 - 0.3.1:
imagecrate 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
slowand still265 Slow (SAO, 64×64 blocks, deeper transform trees, transform skip) - 0.3.15: HDR gain map encoding (ISO 21496-1
tmap) - 0.3.16: ICC colour management (
decode_srgb; an ICC profile replacesnclxwhen encoding) - Next: single-thread decoding speed (still slower than ffmpeg)
See CHANGELOG.md for details.
Development
Contributions are welcome. CONTRIBUTING.md lists the checks CI runs, the project's scope, and how changes are tested. The notes below cover building, testing and validating in more detail.
Inspecting files
Conformance (≈ 23 MB from itu.int, not committed)
Fuzzing (nightly toolchain and cargo install cargo-fuzz)
WebAssembly demo (rustup target add wasm32-unknown-unknown, cargo install wasm-bindgen-cli --version 0.2.129)
&&
Releasing — bump the version, update the CHANGELOG, push main, then tag:
The Release workflow creates the GitHub release, with the annotation as title and the matching
CHANGELOG section as notes. cargo publish is still run by hand, in dependency order:
heifer-isobmff, heifer-hevc-dec, heifer-hevc-enc, heifer, heifer-cli.
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.