#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
extern crate alloc;
whereat::define_at_crate_info!();
macro_rules! try_vec {
($val:expr; $len:expr) => {{
#[cfg(feature = "fallible-alloc")]
{
$crate::alloc_vec_fallible($len, $val)
}
#[cfg(not(feature = "fallible-alloc"))]
{
Ok::<::alloc::vec::Vec<_>, $crate::error::HevcError>(::alloc::vec![$val; $len])
}
}};
}
#[cfg(feature = "fallible-alloc")]
#[inline]
pub(crate) fn alloc_vec_fallible<T: Clone>(
len: usize,
val: T,
) -> core::result::Result<alloc::vec::Vec<T>, error::HevcError> {
let mut v = alloc::vec::Vec::new();
v.try_reserve(len)
.map_err(|_| error::HevcError::AllocationFailed)?;
v.resize(len, val);
Ok(v)
}
mod auxiliary;
mod decode;
mod error;
#[doc(hidden)]
pub mod heif;
#[doc(hidden)]
pub mod hevc;
#[cfg(feature = "zencodec")]
mod codec;
#[cfg(feature = "zencodec")]
pub use codec::{
HeicAuxiliaryInfo, HeicDecodeJob, HeicDecoder as HeicZenDecoder, HeicDecoderConfig,
HeicStreamDecoder,
};
pub use auxiliary::{
AuxiliaryImageDescriptor, AuxiliaryImageType, DepthMap, DepthRepresentationInfo,
DepthRepresentationType, SegmentationMatte,
};
pub use error::{HeicError, HevcError, ProbeError, Result};
pub use hevc::{DecodedFrame, VideoDecoder};
#[cfg(feature = "std")]
pub fn enable_deblock_trace() {
hevc::enable_deblock_trace();
}
#[cfg(feature = "std")]
pub fn cabac_bin_trace(limit: u32) {
hevc::cabac::BIN_TRACE_LIMIT.store(limit, core::sync::atomic::Ordering::Relaxed);
hevc::cabac::BIN_TRACE_COUNTER.store(0, core::sync::atomic::Ordering::Relaxed);
}
pub use enough::{Stop, StopReason, Unstoppable};
pub use whereat::{At, at};
use alloc::borrow::Cow;
use alloc::vec::Vec;
use heif::{FourCC, ItemType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PixelLayout {
Rgb8,
Rgba8,
Bgr8,
Bgra8,
}
impl PixelLayout {
#[must_use]
pub const fn bytes_per_pixel(self) -> usize {
match self {
Self::Rgb8 | Self::Bgr8 => 3,
Self::Rgba8 | Self::Bgra8 => 4,
}
}
#[must_use]
pub const fn has_alpha(self) -> bool {
matches!(self, Self::Rgba8 | Self::Bgra8)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Limits {
pub max_width: Option<u64>,
pub max_height: Option<u64>,
pub max_pixels: Option<u64>,
pub max_memory_bytes: Option<u64>,
}
impl Limits {
pub(crate) fn check_dimensions(&self, width: u32, height: u32) -> Result<()> {
if let Some(max_w) = self.max_width
&& u64::from(width) > max_w
{
return Err(at!(HeicError::LimitExceeded("image width exceeds limit")));
}
if let Some(max_h) = self.max_height
&& u64::from(height) > max_h
{
return Err(at!(HeicError::LimitExceeded("image height exceeds limit")));
}
if let Some(max_px) = self.max_pixels
&& u64::from(width) * u64::from(height) > max_px
{
return Err(at!(HeicError::LimitExceeded("pixel count exceeds limit")));
}
Ok(())
}
pub(crate) fn check_memory(&self, estimated_bytes: u64) -> Result<()> {
if let Some(max_mem) = self.max_memory_bytes
&& estimated_bytes > max_mem
{
return Err(at!(HeicError::LimitExceeded(
"estimated memory exceeds limit"
)));
}
Ok(())
}
}
pub trait RowSink {
fn demand(&mut self, y: u32, height: u32, min_bytes: usize) -> &mut [u8];
}
#[derive(Debug, Clone)]
#[must_use]
pub struct DecodeOutput {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
pub layout: PixelLayout,
}
#[derive(Debug, Clone)]
#[must_use]
pub struct ImageInfo {
pub width: u32,
pub height: u32,
pub has_alpha: bool,
pub bit_depth: u8,
pub chroma_format: u8,
pub has_exif: bool,
pub has_xmp: bool,
pub has_thumbnail: bool,
pub color_primaries: u16,
pub transfer_characteristics: u16,
pub matrix_coefficients: u16,
pub video_full_range: bool,
pub has_icc_profile: bool,
pub has_depth: bool,
pub has_gain_map: bool,
pub exif: Option<Vec<u8>>,
pub xmp: Option<Vec<u8>>,
pub icc_profile: Option<Vec<u8>>,
}
fn apply_transform_dimensions(
mut w: u32,
mut h: u32,
transforms: &[heif::Transform],
) -> (u32, u32) {
for t in transforms {
match t {
heif::Transform::Rotation(rot) if rot.angle == 90 || rot.angle == 270 => {
core::mem::swap(&mut w, &mut h);
}
heif::Transform::CleanAperture(clap) if clap.width_d > 0 && clap.height_d > 0 => {
w = clap.width_n / clap.width_d;
h = clap.height_n / clap.height_d;
}
_ => {} }
}
(w, h)
}
impl ImageInfo {
pub const PROBE_BYTES: usize = 4096;
pub fn from_bytes(data: &[u8]) -> core::result::Result<Self, ProbeError> {
if data.len() < 12 {
return Err(ProbeError::NeedMoreData);
}
let box_type = &data[4..8];
if box_type != b"ftyp" {
return Err(ProbeError::InvalidFormat);
}
let container = heif::parse(data, &Unstoppable).map_err(ProbeError::Corrupt)?;
let primary_item = container
.primary_item()
.ok_or_else(|| ProbeError::Corrupt(at!(HeicError::NoPrimaryImage)))?;
let has_alpha = !container
.find_auxiliary_items(primary_item.id, "urn:mpeg:hevc:2015:auxid:1")
.is_empty()
|| !container
.find_auxiliary_items(
primary_item.id,
"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
)
.is_empty();
let has_exif = container
.item_infos
.iter()
.any(|i| i.item_type == FourCC(*b"Exif"));
let has_xmp = container.item_infos.iter().any(|i| {
i.item_type == FourCC(*b"mime")
&& (i.content_type.contains("xmp") || i.content_type.contains("rdf+xml"))
});
let has_thumbnail = !container.find_thumbnails(primary_item.id).is_empty();
let has_depth = !container
.find_auxiliary_items(primary_item.id, "urn:mpeg:hevc:2015:auxid:2")
.is_empty()
|| !container
.find_auxiliary_items(
primary_item.id,
"urn:mpeg:mpegB:cicp:systems:auxiliary:depth",
)
.is_empty();
let has_gain_map = !container
.find_auxiliary_items(primary_item.id, "urn:com:apple:photo:2020:aux:hdrgainmap")
.is_empty();
let (color_primaries, transfer_characteristics, matrix_coefficients, video_full_range) =
match &primary_item.color_info {
Some(heif::ColorInfo::Nclx {
color_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
}) => (
*color_primaries,
*transfer_characteristics,
*matrix_coefficients,
*full_range,
),
_ => (2, 2, 2, false), };
let has_icc_profile = matches!(
&primary_item.color_info,
Some(heif::ColorInfo::IccProfile(_))
);
let exif = Self::extract_exif_from_container(&container);
let xmp = Self::extract_xmp_from_container(&container);
let icc_profile = match &primary_item.color_info {
Some(heif::ColorInfo::IccProfile(icc)) => Some(icc.clone()),
_ => None,
};
if let Some(ref config) = primary_item.hevc_config
&& let Ok(hevc_info) = hevc::get_info_from_config(config)
{
let bit_depth = config.bit_depth_luma_minus8 + 8;
let chroma_format = config.chroma_format;
let (width, height) = apply_transform_dimensions(
hevc_info.width,
hevc_info.height,
&primary_item.transforms,
);
return Ok(ImageInfo {
width,
height,
has_alpha,
bit_depth,
chroma_format,
has_exif,
has_xmp,
has_thumbnail,
color_primaries,
transfer_characteristics,
matrix_coefficients,
video_full_range,
has_icc_profile,
has_depth,
has_gain_map,
exif: exif.clone(),
xmp: xmp.clone(),
icc_profile: icc_profile.clone(),
});
}
if let Some(ref config) = primary_item.av1_config
&& let Some((w, h)) = primary_item.dimensions
{
let bit_depth = config.bit_depth();
let chroma_format = config.chroma_format();
let (width, height) = apply_transform_dimensions(w, h, &primary_item.transforms);
return Ok(ImageInfo {
width,
height,
has_alpha,
bit_depth,
chroma_format,
has_exif,
has_xmp,
has_thumbnail,
color_primaries,
transfer_characteristics,
matrix_coefficients,
video_full_range,
has_icc_profile,
has_depth,
has_gain_map,
exif: exif.clone(),
xmp: xmp.clone(),
icc_profile: icc_profile.clone(),
});
}
if primary_item.uncompressed_config.is_some()
&& let Some((w, h)) = primary_item.dimensions
{
let bit_depth = primary_item
.uncompressed_config
.as_ref()
.and_then(|c| c.components.first())
.map(|c| c.component_bit_depth_minus_one + 1)
.unwrap_or(8);
let (width, height) = apply_transform_dimensions(w, h, &primary_item.transforms);
return Ok(ImageInfo {
width,
height,
has_alpha,
bit_depth,
chroma_format: 3, has_exif,
has_xmp,
has_thumbnail,
color_primaries,
transfer_characteristics,
matrix_coefficients,
video_full_range,
has_icc_profile,
has_depth,
has_gain_map,
exif: exif.clone(),
xmp: xmp.clone(),
icc_profile: icc_profile.clone(),
});
}
if primary_item.item_type != ItemType::Hvc1
&& let Some((w, h)) = primary_item.dimensions
{
let mut bit_depth = 8u8;
let mut chroma_format = 1u8;
for r in &container.item_references {
if r.reference_type == FourCC::DIMG
&& r.from_item_id == primary_item.id
&& let Some(&tile_id) = r.to_item_ids.first()
&& let Some(tile) = container.get_item(tile_id)
{
if let Some(ref config) = tile.hevc_config {
bit_depth = config.bit_depth_luma_minus8 + 8;
chroma_format = config.chroma_format;
break;
} else if let Some(ref config) = tile.av1_config {
bit_depth = config.bit_depth();
chroma_format = config.chroma_format();
break;
}
}
}
let (width, height) = apply_transform_dimensions(w, h, &primary_item.transforms);
return Ok(ImageInfo {
width,
height,
has_alpha,
bit_depth,
chroma_format,
has_exif,
has_xmp,
has_thumbnail,
color_primaries,
transfer_characteristics,
matrix_coefficients,
video_full_range,
has_icc_profile,
has_depth,
has_gain_map,
exif: exif.clone(),
xmp: xmp.clone(),
icc_profile: icc_profile.clone(),
});
}
let image_data = container
.get_item_data(primary_item.id)
.map_err(ProbeError::Corrupt)?;
let hevc_info = hevc::get_info(&image_data)
.map_err(|e| ProbeError::Corrupt(at!(HeicError::from(e))))?;
let (width, height) =
apply_transform_dimensions(hevc_info.width, hevc_info.height, &primary_item.transforms);
Ok(ImageInfo {
width,
height,
has_alpha,
bit_depth: 8,
chroma_format: 1,
has_exif,
has_xmp,
has_thumbnail,
color_primaries,
transfer_characteristics,
matrix_coefficients,
video_full_range,
has_icc_profile,
has_depth,
has_gain_map,
exif,
xmp,
icc_profile,
})
}
fn extract_exif_from_container(container: &heif::HeifContainer<'_>) -> Option<Vec<u8>> {
for info in &container.item_infos {
if info.item_type != FourCC(*b"Exif") {
continue;
}
let Ok(exif_data) = container.get_item_data(info.item_id) else {
continue;
};
if exif_data.len() < 4 {
continue;
}
let tiff_offset =
u32::from_be_bytes([exif_data[0], exif_data[1], exif_data[2], exif_data[3]])
as usize;
let tiff_start = 4 + tiff_offset;
if tiff_start < exif_data.len() {
return Some(exif_data[tiff_start..].to_vec());
}
}
None
}
fn extract_xmp_from_container(container: &heif::HeifContainer<'_>) -> Option<Vec<u8>> {
for info in &container.item_infos {
if info.item_type == FourCC(*b"mime")
&& (info.content_type.contains("xmp")
|| info.content_type.contains("rdf+xml")
|| info.content_type == "application/rdf+xml")
&& let Ok(xmp_data) = container.get_item_data(info.item_id)
{
return Some(xmp_data.into_owned());
}
}
None
}
#[must_use]
pub fn output_buffer_size(self, layout: PixelLayout) -> Option<usize> {
(self.width as usize)
.checked_mul(self.height as usize)?
.checked_mul(layout.bytes_per_pixel())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct HdrGainMap {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
pub bit_depth: u8,
pub xmp: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct DecoderConfig {
_private: (),
}
impl Default for DecoderConfig {
fn default() -> Self {
Self::new()
}
}
impl DecoderConfig {
#[must_use]
pub fn new() -> Self {
Self { _private: () }
}
pub fn decode(&self, data: &[u8], layout: PixelLayout) -> Result<DecodeOutput> {
self.decode_request(data)
.with_output_layout(layout)
.decode()
}
pub fn decode_request<'a>(&'a self, data: &'a [u8]) -> DecodeRequest<'a> {
DecodeRequest {
_config: self,
data,
layout: PixelLayout::Rgba8,
limits: None,
stop: None,
max_threads: None,
}
}
pub fn decode_to_frame(&self, data: &[u8]) -> Result<hevc::DecodedFrame> {
decode::decode_to_frame(data, None, &Unstoppable, None)
}
#[must_use]
pub fn estimate_memory(width: u32, height: u32, layout: PixelLayout) -> u64 {
let w = u64::from(width);
let h = u64::from(height);
let pixels = w * h;
let luma_bytes = pixels * 2;
let chroma_w = w.div_ceil(2);
let chroma_h = h.div_ceil(2);
let chroma_bytes = chroma_w * chroma_h * 2 * 2;
let output_bytes = pixels * layout.bytes_per_pixel() as u64;
let blocks_w = w.div_ceil(4);
let blocks_h = h.div_ceil(4);
let deblock_bytes = blocks_w * blocks_h * 2;
luma_bytes + chroma_bytes + output_bytes + deblock_bytes
}
pub fn has_gain_map(&self, data: &[u8]) -> Result<bool> {
decode::has_gain_map(data)
}
pub fn decode_gain_map(&self, data: &[u8]) -> Result<HdrGainMap> {
decode::decode_gain_map(data)
}
pub fn extract_exif<'a>(&self, data: &'a [u8]) -> Result<Option<Cow<'a, [u8]>>> {
decode::extract_exif(data)
}
pub fn extract_xmp<'a>(&self, data: &'a [u8]) -> Result<Option<Cow<'a, [u8]>>> {
decode::extract_xmp(data)
}
pub fn extract_icc(&self, data: &[u8]) -> Result<Option<Vec<u8>>> {
let container = heif::parse(data, &Unstoppable)?;
let primary_item = container
.primary_item()
.ok_or_else(|| at!(HeicError::NoPrimaryImage))?;
match &primary_item.color_info {
Some(heif::ColorInfo::IccProfile(icc)) => Ok(Some(icc.clone())),
_ => Ok(None),
}
}
pub fn decode_thumbnail(
&self,
data: &[u8],
layout: PixelLayout,
) -> Result<Option<DecodeOutput>> {
decode::decode_thumbnail(data, layout)
}
pub fn auxiliary_images(&self, data: &[u8]) -> Result<Vec<AuxiliaryImageDescriptor>> {
decode::list_auxiliary_images(data)
}
pub fn auxiliary_types(&self, data: &[u8]) -> Result<Vec<AuxiliaryImageType>> {
Ok(decode::list_auxiliary_images(data)?
.into_iter()
.map(|d| d.aux_type)
.collect())
}
pub fn has_depth(&self, data: &[u8]) -> Result<bool> {
decode::has_depth(data)
}
pub fn decode_depth(&self, data: &[u8]) -> Result<DepthMap> {
decode::decode_depth(data)
}
pub fn decode_auxiliary(
&self,
data: &[u8],
item_id: u32,
layout: PixelLayout,
) -> Result<DecodeOutput> {
decode::decode_auxiliary_item(data, item_id, layout)
}
pub fn decode_mattes(&self, data: &[u8]) -> Result<Vec<SegmentationMatte>> {
decode::decode_mattes(data)
}
pub fn decode_matte(
&self,
data: &[u8],
matte_type: &AuxiliaryImageType,
) -> Result<Option<SegmentationMatte>> {
decode::decode_matte(data, matte_type)
}
}
#[must_use]
pub struct DecodeRequest<'a> {
_config: &'a DecoderConfig,
data: &'a [u8],
layout: PixelLayout,
limits: Option<&'a Limits>,
stop: Option<&'a dyn Stop>,
max_threads: Option<usize>,
}
impl<'a> DecodeRequest<'a> {
pub fn with_output_layout(mut self, layout: PixelLayout) -> Self {
self.layout = layout;
self
}
pub fn with_limits(mut self, limits: &'a Limits) -> Self {
self.limits = Some(limits);
self
}
pub fn with_stop(mut self, stop: &'a dyn Stop) -> Self {
self.stop = Some(stop);
self
}
pub fn with_max_threads(mut self, max_threads: usize) -> Self {
self.max_threads = Some(max_threads);
self
}
pub fn decode(self) -> Result<DecodeOutput> {
let stop: &dyn Stop = self.stop.unwrap_or(&Unstoppable);
let frame = decode::decode_to_frame(self.data, self.limits, stop, self.max_threads)?;
let width = frame.cropped_width();
let height = frame.cropped_height();
if let Some(limits) = self.limits {
limits.check_dimensions(width, height)?;
let output_bytes =
u64::from(width) * u64::from(height) * self.layout.bytes_per_pixel() as u64;
limits.check_memory(output_bytes)?;
}
let data = match self.layout {
PixelLayout::Rgb8 => frame.to_rgb()?,
PixelLayout::Rgba8 => frame.to_rgba()?,
PixelLayout::Bgr8 => frame.to_bgr()?,
PixelLayout::Bgra8 => frame.to_bgra()?,
};
Ok(DecodeOutput {
data,
width,
height,
layout: self.layout,
})
}
pub fn decode_into(self, output: &mut [u8]) -> Result<(u32, u32)> {
let stop: &dyn Stop = self.stop.unwrap_or(&Unstoppable);
if let Some(result) = decode::try_decode_grid_streaming(
self.data,
self.limits,
stop,
self.layout,
output,
self.max_threads,
)? {
return Ok(result);
}
let frame = decode::decode_to_frame(self.data, self.limits, stop, self.max_threads)?;
let width = frame.cropped_width();
let height = frame.cropped_height();
let required = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(self.layout.bytes_per_pixel()))
.ok_or_else(|| {
at!(HeicError::LimitExceeded(
"output buffer size overflows usize",
))
})?;
if output.len() < required {
return Err(at!(HeicError::BufferTooSmall {
required,
actual: output.len(),
}));
}
match self.layout {
PixelLayout::Rgb8 => {
frame.write_rgb_into(output);
}
PixelLayout::Rgba8 => {
frame.write_rgba_into(output);
}
PixelLayout::Bgr8 => {
frame.write_bgr_into(output);
}
PixelLayout::Bgra8 => {
frame.write_bgra_into(output);
}
}
Ok((width, height))
}
pub fn decode_rows(self, sink: &mut dyn RowSink) -> Result<(u32, u32)> {
let stop: &dyn Stop = self.stop.unwrap_or(&Unstoppable);
if let Some(result) = decode::try_decode_grid_to_sink(
self.data,
self.limits,
stop,
self.layout,
sink,
self.max_threads,
)? {
return Ok(result);
}
let frame = decode::decode_to_frame(self.data, self.limits, stop, self.max_threads)?;
let width = frame.cropped_width();
let height = frame.cropped_height();
if let Some(limits) = self.limits {
limits.check_dimensions(width, height)?;
let output_bytes =
u64::from(width) * u64::from(height) * self.layout.bytes_per_pixel() as u64;
limits.check_memory(output_bytes)?;
}
let data = match self.layout {
PixelLayout::Rgb8 => frame.to_rgb()?,
PixelLayout::Rgba8 => frame.to_rgba()?,
PixelLayout::Bgr8 => frame.to_bgr()?,
PixelLayout::Bgra8 => frame.to_bgra()?,
};
let bpp = self.layout.bytes_per_pixel();
let row_bytes = width as usize * bpp;
let min_bytes = row_bytes * height as usize;
let buf = sink.demand(0, height, min_bytes);
buf[..data.len()].copy_from_slice(&data);
Ok((width, height))
}
pub fn decode_yuv(self) -> Result<hevc::DecodedFrame> {
let stop: &dyn Stop = self.stop.unwrap_or(&Unstoppable);
decode::decode_to_frame(self.data, self.limits, stop, self.max_threads)
}
}
#[inline]
fn floor_f64(x: f64) -> f64 {
let i = x as i64;
let f = i as f64;
if f > x { f - 1.0 } else { f }
}
#[inline]
fn round_f64(x: f64) -> f64 {
if x >= 0.0 {
floor_f64(x + 0.5)
} else {
-floor_f64(-x + 0.5)
}
}