heifer 0.3.12

Pure-Rust HEIF/HEIC image decoder and encoder: no C dependencies, no unsafe code
Documentation
<div align="center">

<img src="docs/images/banner.webp" alt="Heifer: HEIF/HEIC decoder and encoder in pure Rust" width="100%">

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

[![crates.io](https://img.shields.io/crates/v/heifer.svg)](https://crates.io/crates/heifer)
[![docs.rs](https://img.shields.io/docsrs/heifer)](https://docs.rs/heifer)
[![CI](https://github.com/HarpeLm/Heifer/actions/workflows/ci.yml/badge.svg)](https://github.com/HarpeLm/Heifer/actions/workflows/ci.yml)
[![Fuzz](https://github.com/HarpeLm/Heifer/actions/workflows/fuzz.yml/badge.svg)](https://github.com/HarpeLm/Heifer/actions/workflows/fuzz.yml)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

**[▶ Try it in your browser](https://harpelm.github.io/Heifer/)** — drop a `.heic` photo to view it, or a PNG/JPEG to convert it to HEIC; everything runs locally in heifer compiled to WebAssembly.


<table>
  <tr>
    <td align="center" width="25%"><h2>62 / 62</h2><sub>HEVC conformance streams<br>identical to ffmpeg</sub></td>
    <td align="center" width="25%"><h2>0</h2><sub>lines of <code>unsafe</code><br>or C code</sub></td>
    <td align="center" width="25%"><h2>200 MP</h2><sub>Galaxy S24 Ultra photos<br>decoded in 0.55 s</sub></td>
    <td align="center" width="25%"><h2>≈ 250 KB</h2><sub>WebAssembly build<br>for the browser</sub></td>
  </tr>
</table>

</div>

<br>

## <img src="docs/icons/problem.svg" width="24" height="24" align="top"> 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++.

<table width="100%">
  <tr>
    <td width="33%" valign="top">
      <h3 align="center"><img src="docs/icons/chain.svg" width="20" height="20" align="top"> C dependencies</h3>
      <p align="center">Existing crates bind to libheif and libde265: a system toolchain, painful cross-compilation and static linking.</p>
    </td>
    <td width="33%" valign="top">
      <h3 align="center"><img src="docs/icons/globe-off.svg" width="20" height="20" align="top"> No browser</h3>
      <p align="center">C bindings do not compile to <code>wasm32-unknown-unknown</code>, so HEIC photos cannot be decoded client-side.</p>
    </td>
    <td width="33%" valign="top">
      <h3 align="center"><img src="docs/icons/unlock.svg" width="20" height="20" align="top"> Untrusted input</h3>
      <p align="center">Image files come from anywhere. A video-codec-sized C parser is a large attack surface for a photo upload.</p>
    </td>
  </tr>
</table>

**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.

## <img src="docs/icons/solution.svg" width="24" height="24" align="top"> 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`]https://crates.io/crates/image integration, and it compiles to WebAssembly (≈ 250 KB).

## <img src="docs/icons/code.svg" width="24" height="24" align="top"> Quick start

**Command line**: convert photos without installing libheif or ffmpeg.

```sh
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**:

```toml
[dependencies]
heifer = "0.3"
```

Or try the examples on your own photos:

```sh
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
```

## <img src="docs/icons/code.svg" width="24" height="24" align="top"> Decode a HEIC

```rust
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:

```rust
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)?;
```

## <img src="docs/icons/code.svg" width="24" height="24" align="top"> Encode a HEIC

```rust
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.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](docs/BENCHMARKS.md)).

## <img src="docs/icons/code.svg" width="24" height="24" align="top"> Read metadata

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

```rust
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
```

## <img src="docs/icons/code.svg" width="24" height="24" align="top"> 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):

```rust
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.

## <img src="docs/icons/code.svg" width="24" height="24" align="top"> `image` crate integration

```toml
heifer = { version = "0.3", features = ["image"] }
```

```rust
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
```

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`.)

## <img src="docs/icons/layers.svg" width="24" height="24" align="top"> 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 |

## <img src="docs/icons/shield.svg" width="24" height="24" align="top"> Conformance

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

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/images/conformance-dark.svg">
  <img src="docs/images/conformance-light.svg" alt="HEVC conformance: 38 streams identical to ffmpeg and hash-verified, 24 identical to ffmpeg, 3 hash-verified that ffmpeg cannot decode, 1 unsupported" width="100%">
</picture>

| 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](https://github.com/rust-fuzz/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.

## <img src="docs/icons/shield.svg" width="24" height="24" align="top"> 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.

## <img src="docs/icons/gauge.svg" width="24" height="24" align="top"> Performance

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

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/images/bench-decode-dark.svg">
  <img src="docs/images/bench-decode-light.svg" alt="Decode time: Xiaomi 5 MP 22 ms (ffmpeg 56), iPhone 12 MP 40 ms (ffmpeg 96), 48 MP 196 ms (ffmpeg 582), Galaxy 200 MP 0.54 s (ffmpeg fails)" width="100%">
</picture>

| 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:

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/images/bench-scaling-dark.svg">
  <img src="docs/images/bench-scaling-light.svg" alt="Thread scaling on a 12 MP iPhone photo: 162 ms with 1 thread down to 32 ms with 10" width="100%">
</picture>

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](docs/BENCHMARKS.md)** (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`]https://crates.io/crates/heic-rs | [`heic`]https://crates.io/crates/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.

## <img src="docs/icons/workflow.svg" width="24" height="24" align="top"> Architecture

```text
 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

```text
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:

```rust
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
```

## <img src="docs/icons/map.svg" width="24" height="24" align="top"> Roadmap

- [x] **0.1 — Decoder**: container, HEVC, conformance, real photos, metadata, `image`, WebAssembly, fuzzing
- [x] **HDR**: Apple gain maps (iPhone photos)
- [x] **HDR**: ISO 21496-1 gain maps (`tmap`)
- [x] **0.3 — Encoder**: HEIF writing, HEVC intra encoder, alpha, parallel tiles; `iloc` construction method 2
- [x] **0.3.1**: `image` crate encoder, metadata when encoding
- [x] **0.3.2**: compression on par with x265 `medium` (4×4 blocks, transform splits, RDOQ, sign data hiding)
- [x] **0.3.3**: 10-bit encoding (Main 10)
- [x] **0.3.8**: thumbnails, public benchmarks
- [x] **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](CHANGELOG.md) for details.

## <img src="docs/icons/wrench.svg" width="24" height="24" align="top"> Development

<details>
<summary>Building, testing and validating</summary>

```sh
./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**

```sh
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)

```sh
./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`)

```sh
./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`)

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

</details>

## <img src="docs/icons/scale.svg" width="24" height="24" align="top"> License

Licensed under either of [Apache License 2.0](LICENSE-APACHE) or [MIT](LICENSE-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](https://github.com/bigcat88/pillow_heif) and [Nokia's HEIF samples](https://github.com/nokiatech/heif),
the fuzzing corpus of [libheif](https://github.com/strukturag/libheif), and [ffmpeg](https://ffmpeg.org) as a reference decoder.

---

<p align="center"><sub>Built with 💜 in safe Rust · <a href="https://harpelm.github.io/Heifer/">try the demo</a> · <a href="https://docs.rs/heifer">read the docs</a></sub></p>