This document is the practical API reference for integrating heic_decoder in apps and automation.
- Decode HEIF/HEIC/AVIF to RGBA (
u8oru16storage). - Decode from bytes,
Read,BufRead, and file paths. - Optional
imagecrate integration through hook registration. - Keep default decode output aligned with primary item transforms (
clap/irot/imir) and expose explicit EXIF orientation helpers.
default: no optional features.image-integration: enablesheic_decoder::image_integrationmodule and hook APIs.
Cargo.toml:
[dependencies]
heic_decoder = { path = "../heic-decoder" }With image integration:
[dependencies]
heic_decoder = { path = "../heic-decoder", features = ["image-integration"] }
image = { version = "0.25", default-features = false, features = ["png"] }DecodeGuardrailsmax_input_bytes: Option<u64>max_pixels: Option<u64>max_temp_spool_bytes: Option<u64>temp_spool_directory: Option<PathBuf>
DecodedRgbaImagewidth,heightsource_bit_depthpixels: DecodedRgbaPixels(U8(Vec<u8>)orU16(Vec<u16>))icc_profile: Option<Vec<u8>>
ExifOrientationHintexif_orientation: Option<u8>primary_item_has_orientation_transform: boolshould_apply_exif_orientation()orientation_to_apply()
DecodeError,DecodeErrorCategory,DecodeGuardrailError
Return type for RGBA decode functions:
Result<DecodedRgbaImage, DecodeError>decode_bytes_to_rgba_with_guardrails(input, guardrails)decode_read_to_rgba_with_guardrails(reader, guardrails)decode_bufread_to_rgba_with_guardrails(bufread, guardrails)decode_path_to_rgba_with_guardrails(path, guardrails)
Non-guardrailed convenience variants also exist (decode_*_to_rgba) and use DecodeGuardrails::default().
decode_bytes_to_png_with_guardrails(input, output_path, guardrails)decode_read_to_png_with_guardrails(reader, output_path, guardrails)decode_bufread_to_png_with_guardrails(bufread, output_path, guardrails)decode_path_to_png_with_guardrails(input_path, output_path, guardrails)
Legacy aliases:
decode_file_to_rgba->decode_path_to_rgbadecode_file_to_png->decode_path_to_png
These are useful for tooling and specialized pipelines, but most applications should use the core entry points above.
decode_primary_avif_to_image(input)->Result<DecodedAvifImage, DecodeAvifError>assemble_primary_heic_hevc_stream(input)->Result<Vec<u8>, DecodeHeicError>decode_primary_heic_to_image(input)->Result<DecodedHeicImage, DecodeHeicError>decode_primary_heic_to_metadata(input)->Result<DecodedHeicImageMetadata, DecodeHeicError>decode_primary_uncompressed_to_image(input)->Result<DecodedUncompressedImage, DecodeUncompressedError>
Also note:
isobmffandsourcemodules are public and available for advanced use.- These low-level APIs expose more container/codec details and have a larger surface for malformed-input handling.
Use this decision order:
- If you already have
&[u8], usedecode_bytes_to_rgba_with_guardrails. - If you have a file path, use
decode_path_to_rgba_with_guardrails. - If you have
Read/BufRead(non-seek stream), usedecode_read_to_rgba_with_guardrailsordecode_bufread_to_rgba_with_guardrails.
Important behavior:
Read/BufReadinputs are spooled once to a temporary file to enable random-access decode.Pathand seekable decode paths avoid full-file in-memory buffering.
Use explicit guardrails in production.
Example preset:
use heic_decoder::DecodeGuardrails;
let guardrails = DecodeGuardrails {
max_input_bytes: Some(128 * 1024 * 1024),
max_pixels: Some(64_000_000),
max_temp_spool_bytes: Some(128 * 1024 * 1024),
temp_spool_directory: None,
};Notes:
max_input_bytesapplies to all entry points.max_temp_spool_bytesprotects non-seekRead/BufReadingestion.max_pixelsblocks oversized decoded geometry before RGBA materialization.
Top-level errors are DecodeError. Use category() for stable handling:
use heic_decoder::{DecodeError, DecodeErrorCategory};
fn classify(err: &DecodeError) -> DecodeErrorCategory {
err.category()
}Stable categories:
IoParseMalformedInputUnsupportedFeatureResourceLimitDecoderBackendOutputEncoding
Typical policy mapping:
ResourceLimit: return413/ reject input.MalformedInputorParse: return422.UnsupportedFeature: return415.IoorDecoderBackend: retry/fallback/log depending on context.
Inspect storage and branch explicitly:
use heic_decoder::DecodedRgbaPixels;
match &decoded.pixels {
DecodedRgbaPixels::U8(p) => {
// p: &[u8], RGBA8 interleaved
}
DecodedRgbaPixels::U16(p) => {
// p: &[u16], RGBA16 interleaved
}
}Helpers on DecodedRgbaImage / DecodedRgbaPixels:
storage_bit_depth()as_rgba8(),as_rgba16()into_rgba8(),into_rgba16()apply_exif_orientation(exif_orientation)
Default decode behavior:
- Decode applies HEIF primary item transforms from
clap/irot/imir. - Decode does not implicitly apply EXIF orientation.
- This keeps output stable for libheif parity and avoids hidden double-rotation behavior.
Orientation APIs:
exif_orientation_hint(input_bytes)->ExifOrientationHintexif_orientation_hint_from_path(input_path)->Result<ExifOrientationHint, DecodeError>primary_exif_orientation(input_bytes)->Option<u8>primary_exif_orientation_from_path(input_path)->Result<Option<u8>, DecodeError>primary_item_has_orientation_transform(input_bytes)->boolpath_extension_is_heif(path)->boolpath_extension_is_heif_family(path)->boolDecodedRgbaImage::apply_exif_orientation(exif_orientation)->Result<DecodedRgbaImage, DecodeError>
Recommended pattern:
use heic_decoder::{decode_path_to_rgba, exif_orientation_hint};
use std::path::Path;
fn decode_with_explicit_orientation(path: &Path) -> Result<(), heic_decoder::DecodeError> {
let input = std::fs::read(path).map_err(heic_decoder::DecodeError::Io)?;
let hint = exif_orientation_hint(&input);
let decoded = decode_path_to_rgba(path)?;
let decoded = if let Some(orientation) = hint.orientation_to_apply() {
decoded.apply_exif_orientation(orientation)?
} else {
decoded
};
eprintln!("{}x{}", decoded.width, decoded.height);
Ok(())
}Path-only variant (no full input read, no image decode):
use heic_decoder::{exif_orientation_hint_from_path, path_extension_is_heif};
use std::path::Path;
fn orientation_to_apply(path: &Path) -> Result<Option<u8>, heic_decoder::DecodeError> {
if !path_extension_is_heif(path) {
return Ok(None);
}
Ok(exif_orientation_hint_from_path(path)?.orientation_to_apply())
}Requires image-integration feature.
Hook decodes buffer the entire encoded input in memory before decoding; in
exchange, pixels decode straight into the buffer image hands over, without
an additional full-frame owned RGBA allocation. Codec-native planes and a
single grid-tile scratch buffer may still be allocated.
register_image_decoder_hooks() caps the encoded input buffer at
DEFAULT_HOOK_MAX_INPUT_BYTES (128 MiB); register with explicit guardrails
to choose a different max_input_bytes bound (or None for unbounded).
use heic_decoder::DecodeGuardrails;
use heic_decoder::image_integration::register_image_decoder_hooks_with_guardrails;
let registration = register_image_decoder_hooks_with_guardrails(DecodeGuardrails {
max_input_bytes: Some(128 * 1024 * 1024),
max_pixels: Some(64_000_000),
max_temp_spool_bytes: Some(128 * 1024 * 1024),
temp_spool_directory: None,
});
assert!(registration.any_decoder_hook_registered());use image::ImageReader;
use heic_decoder::exif_orientation_hint;
use heic_decoder::image_integration::apply_exif_orientation_dynamic;
let bytes = std::fs::read("input.heic").map_err(image::ImageError::IoError)?;
let hint = exif_orientation_hint(&bytes);
let img = ImageReader::new(std::io::Cursor::new(&bytes))
.with_guessed_format()?
.decode()?;
let img = if let Some(orientation) = hint.orientation_to_apply() {
apply_exif_orientation_dynamic(img, orientation)
} else {
img
};The registered hook decoder also exposes the primary item's EXIF block through
the standard ImageDecoder::exif_metadata/ImageDecoder::orientation methods,
so generic image code (decoder.orientation() +
DynamicImage::apply_orientation) works without crate-specific helpers. EXIF
orientation stays unapplied in the pixels; applying it remains the caller's
choice. When the primary item carries irot/imir container transforms —
which decode does bake into the pixels — orientation() reports
NoTransforms (matching ExifOrientationHint::should_apply_exif_orientation)
so callers do not rotate twice; exif_metadata still returns the full block.
Under image-integration, DecodedRgbaImage provides conversion helpers:
into_image_buffer_with_metadata()into_image_buffer()into_dynamic_image_with_metadata()into_dynamic_image()
Metadata-preserving wrappers:
ImageBufferWithMetadataDynamicImageWithMetadata
Possible conversion failures:
ImageConversionError::SampleCountOverflowImageConversionError::SampleCountMismatch
Additional helper under image-integration:
apply_exif_orientation_dynamic(image, exif_orientation)
For reliable agent behavior:
- Always use
*_with_guardrailsAPIs for untrusted input. - Treat
Read/BufReadas temp-spooled random-access decode, not progressive decode. - Branch on
decoded.storage_bit_depth()and avoid implicit down-conversion. - Match
DecodeError::category()for stable retry/reject decisions. - Register image hooks exactly once during process startup.
- Keep guardrail values centralized in one config module for consistency.
use heic_decoder::{decode_path_to_rgba_with_guardrails, DecodeGuardrails};
use std::path::Path;
fn decode(path: &Path) -> Result<(), heic_decoder::DecodeError> {
let guardrails = DecodeGuardrails {
max_input_bytes: Some(128 * 1024 * 1024),
max_pixels: Some(64_000_000),
max_temp_spool_bytes: Some(128 * 1024 * 1024),
temp_spool_directory: None,
};
let decoded = decode_path_to_rgba_with_guardrails(path, guardrails)?;
eprintln!(
"decoded {}x{}, storage={}bit, source={}bit",
decoded.width,
decoded.height,
decoded.storage_bit_depth(),
decoded.source_bit_depth
);
Ok(())
}Enable the incremental-experiment Cargo feature to use
decode_incremental_experiment(BoundedInput, BoundedDecodeOptions). It returns
(BoundedRgbImage, [u64; 3]); the diagnostic counters are reconstructed rows,
SAO edge components, and SAO band components across all coded items. This
feature and entry point are experimental. Existing decode functions retain
their behavior and do not select this path automatically.
The incremental path supports opaque 8-bit 4:2:0 HEVC still images with one IDR slice per coded item, including non-grid images and uniform grids whose individual tiles exceed the output cap. WPP, HEVC tiles, multiple slices, higher bit depths, alpha, other chroma formats, and unsupported color profiles return errors. The output is display-oriented sRGB RGB8, using area averaging when reduced, with a maximum side from 1 through 6000. Known depth, semantic matte, and gain-map auxiliaries may accompany the SDR base image; this does not apply their effects or provide HDR output.
Reconstruction, deblocking, and SAO finish in rolling bands before conversion
and reduction. Large coded images use a bounded two-thread pipeline. The
working state grows with coded width and CTU size, while output storage is
capped by max_side; it does not retain a full source raster or use temporary
files or repeated reconstruction. A caller's borrowed compressed input is
separate from decoder-owned memory. Admission is conservative and may reject
budgets smaller than its reserved workspace, including a worker stack.
Allocator overhead and process RSS are not the same as requested allocation
payloads. Use Path input when avoiding a caller-owned compressed input buffer
is also important.
This first subset retains limits of 16384 per coded side, 256 million display
pixels, bounded metadata, and a 64 KiB slice-header prefix. It supports one
clean-aperture crop before orientation, with chroma interpolation for odd
origins on a single coded image or one-tile grid. Odd-origin crops across
multiple grid items and repeated crops or crops after orientation are
unsupported. Exif orientation applies when no effective container orientation
is present. decoder-tracing disables bounded decoding.