use alloc::borrow::Cow;
use rgb::{Rgb, Rgba};
use zencodec::decode::{
DecodeCapabilities, DecodeOutput, DecodePolicy, DecodeRowSink, OutputInfo,
negotiate_pixel_format,
};
use zencodec::{
ContentLightLevel, GainMapInfo, GainMapPresence, ImageFormat, ImageInfo, ImageSequence,
MasteringDisplay, Orientation, ResourceLimits, Supplements, ThreadingPolicy, Unsupported,
};
use zenpixels::{Cicp, ColorPrimaries, PixelBuffer, PixelDescriptor, TransferFunction};
use enough::Stop as _;
use whereat::{At, ResultAtExt, at};
use crate::auxiliary::AuxiliaryImageType;
use crate::error::HeicError;
#[derive(Debug, Clone)]
pub struct HeicAuxiliaryInfo {
pub has_depth: bool,
pub has_gain_map: bool,
pub auxiliary_types: alloc::vec::Vec<AuxiliaryImageType>,
}
#[derive(Debug, Clone, Copy)]
pub struct HeicSourceEncoding;
impl zencodec::SourceEncodingDetails for HeicSourceEncoding {
fn source_generic_quality(&self) -> Option<f32> {
None }
fn is_lossless(&self) -> bool {
false }
}
fn policy_to_threads(policy: ThreadingPolicy) -> usize {
if policy.is_parallel() { 0 } else { 1 }
}
static HEIC_DECODE_CAPS: DecodeCapabilities = DecodeCapabilities::new()
.with_icc(true)
.with_exif(true)
.with_xmp(true)
.with_cicp(true)
.with_stop(true)
.with_cheap_probe(true)
.with_decode_into(true)
.with_streaming(true)
.with_hdr(true)
.with_native_16bit(true)
.with_native_alpha(true)
.with_enforces_max_pixels(true)
.with_enforces_max_memory(true)
.with_enforces_max_input_bytes(true)
.with_gain_map(true)
.with_threads_supported_range(1, if cfg!(feature = "parallel") { 256 } else { 1 });
static DECODE_DESCRIPTORS: &[PixelDescriptor] = &[
PixelDescriptor::RGB8_SRGB,
PixelDescriptor::RGBA8_SRGB,
PixelDescriptor::BGRA8_SRGB,
PixelDescriptor::RGB16_SRGB,
PixelDescriptor::RGBA16_SRGB,
];
#[derive(Clone, Debug)]
pub struct HeicDecoderConfig {
inner: crate::DecoderConfig,
pub extract_gain_map: bool,
pub extract_depth: bool,
}
impl HeicDecoderConfig {
#[must_use]
pub fn new() -> Self {
Self {
inner: crate::DecoderConfig::new(),
extract_gain_map: false,
extract_depth: false,
}
}
#[must_use]
pub fn inner(&self) -> &crate::DecoderConfig {
&self.inner
}
#[must_use]
pub fn with_extract_gain_map(mut self, extract: bool) -> Self {
self.extract_gain_map = extract;
self
}
#[must_use]
pub fn with_extract_depth(mut self, extract: bool) -> Self {
self.extract_depth = extract;
self
}
}
impl Default for HeicDecoderConfig {
fn default() -> Self {
Self::new()
}
}
impl zencodec::decode::DecoderConfig for HeicDecoderConfig {
type Error = At<HeicError>;
type Job<'a> = HeicDecodeJob;
fn formats() -> &'static [ImageFormat] {
&[ImageFormat::Heic]
}
fn supported_descriptors() -> &'static [PixelDescriptor] {
DECODE_DESCRIPTORS
}
fn capabilities() -> &'static DecodeCapabilities {
&HEIC_DECODE_CAPS
}
fn job<'a>(self) -> Self::Job<'a> {
let extract_gain_map = self.extract_gain_map;
let extract_depth = self.extract_depth;
HeicDecodeJob {
config: self,
stop: None,
limits: ResourceLimits::none(),
policy: None,
extract_gain_map,
extract_depth,
}
}
}
pub struct HeicDecodeJob {
config: HeicDecoderConfig,
stop: Option<zencodec::StopToken>,
limits: ResourceLimits,
policy: Option<DecodePolicy>,
pub extract_gain_map: bool,
pub extract_depth: bool,
}
fn apply_policy(policy: Option<&DecodePolicy>, mut info: ImageInfo) -> ImageInfo {
if let Some(policy) = policy {
if !policy.resolve_icc(true) {
info.source_color.icc_profile = None;
}
if !policy.resolve_exif(true) {
info.embedded_metadata.exif = None;
}
if !policy.resolve_xmp(true) {
info.embedded_metadata.xmp = None;
}
}
info
}
impl HeicDecodeJob {
fn native_limits(&self) -> Option<crate::Limits> {
if !self.limits.has_any() {
return None;
}
let mut limits = crate::Limits::default();
limits.max_width = self.limits.max_width.map(u64::from);
limits.max_height = self.limits.max_height.map(u64::from);
limits.max_pixels = self.limits.max_pixels;
limits.max_memory_bytes = self.limits.max_memory_bytes;
Some(limits)
}
}
fn available_descriptors(has_alpha: bool, bit_depth: u8) -> alloc::vec::Vec<PixelDescriptor> {
let mut available = alloc::vec::Vec::with_capacity(5);
if bit_depth > 8 {
if has_alpha {
available.push(PixelDescriptor::RGBA16_SRGB);
available.push(PixelDescriptor::RGB16_SRGB);
} else {
available.push(PixelDescriptor::RGB16_SRGB);
available.push(PixelDescriptor::RGBA16_SRGB);
}
}
if has_alpha {
available.push(PixelDescriptor::RGBA8_SRGB);
available.push(PixelDescriptor::BGRA8_SRGB);
available.push(PixelDescriptor::RGB8_SRGB);
} else {
available.push(PixelDescriptor::RGB8_SRGB);
available.push(PixelDescriptor::RGBA8_SRGB);
available.push(PixelDescriptor::BGRA8_SRGB);
}
available
}
fn is_16bit(desc: PixelDescriptor) -> bool {
desc == PixelDescriptor::RGB16_SRGB || desc == PixelDescriptor::RGBA16_SRGB
}
fn descriptor_to_layout(desc: PixelDescriptor) -> crate::PixelLayout {
if desc.pixel_format() == PixelDescriptor::BGRA8_SRGB.pixel_format() {
crate::PixelLayout::Bgra8
} else if desc.pixel_format() == PixelDescriptor::RGBA8_SRGB.pixel_format() {
crate::PixelLayout::Rgba8
} else {
crate::PixelLayout::Rgb8
}
}
impl<'a> zencodec::decode::DecodeJob<'a> for HeicDecodeJob {
type Error = At<HeicError>;
type Dec = HeicDecoder<'a>;
type StreamDec = HeicStreamDecoder;
type AnimationFrameDec = Unsupported<At<HeicError>>;
fn with_stop(mut self, stop: zencodec::StopToken) -> Self {
self.stop = Some(stop);
self
}
fn with_limits(mut self, limits: ResourceLimits) -> Self {
self.limits = limits;
self
}
fn with_policy(mut self, policy: DecodePolicy) -> Self {
self.policy = Some(policy);
self
}
fn probe(&self, data: &[u8]) -> Result<ImageInfo, At<HeicError>> {
self.limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(HeicError::LimitExceeded(limit_exceeded_msg(e))))?;
let native = crate::ImageInfo::from_bytes(data).map_err(probe_error_to_heic)?;
Ok(apply_policy(
self.policy.as_ref(),
build_image_info_lightweight(&native),
))
}
fn probe_full(&self, data: &[u8]) -> Result<ImageInfo, At<HeicError>> {
self.limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(HeicError::LimitExceeded(limit_exceeded_msg(e))))?;
let native = crate::ImageInfo::from_bytes(data).map_err(probe_error_to_heic)?;
let stop_ref: &dyn enough::Stop = match self.stop {
Some(ref s) => s,
None => &enough::Unstoppable,
};
let container = crate::heif::parse(data, stop_ref).ok();
Ok(apply_policy(
self.policy.as_ref(),
build_image_info_full(&native, container.as_ref(), native.width, native.height),
))
}
fn output_info(&self, data: &[u8]) -> Result<OutputInfo, At<HeicError>> {
self.limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(HeicError::LimitExceeded(limit_exceeded_msg(e))))?;
let native = crate::ImageInfo::from_bytes(data).map_err(probe_error_to_heic)?;
let available = available_descriptors(native.has_alpha, native.bit_depth);
let base_desc = available[0]; let desc = cicp_descriptor(
base_desc,
native.color_primaries,
native.transfer_characteristics,
);
Ok(OutputInfo::full_decode(native.width, native.height, desc))
}
fn decoder(
mut self,
data: Cow<'a, [u8]>,
preferred: &[PixelDescriptor],
) -> Result<HeicDecoder<'a>, At<HeicError>> {
self.limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(HeicError::LimitExceeded(limit_exceeded_msg(e))))?;
let thread_count = policy_to_threads(self.limits.threading());
let stop = self.stop.take();
let limits = self.native_limits();
Ok(HeicDecoder {
config: self.config,
data,
preferred: preferred.to_vec(),
stop,
limits,
thread_count,
policy: self.policy,
extract_gain_map: self.extract_gain_map,
extract_depth: self.extract_depth,
})
}
fn push_decoder(
self,
data: Cow<'a, [u8]>,
sink: &mut dyn DecodeRowSink,
preferred: &[PixelDescriptor],
) -> Result<OutputInfo, At<HeicError>> {
self.limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(HeicError::LimitExceeded(limit_exceeded_msg(e))))?;
let probe_info = crate::ImageInfo::from_bytes(&data).ok();
let has_alpha = probe_info.as_ref().is_some_and(|pi| pi.has_alpha);
let bit_depth = probe_info.as_ref().map_or(8, |pi| pi.bit_depth);
let available = available_descriptors(has_alpha, bit_depth);
let negotiated = negotiate_pixel_format(preferred, &available)
.ok_or_else(|| at!(HeicError::InvalidData("pixel format negotiation failed")))?;
if is_16bit(negotiated) {
let dec = self.decoder(data, preferred)?;
let output = <HeicDecoder<'_> as zencodec::decode::Decode>::decode(dec)?;
let ps = output.pixels();
let desc = ps.descriptor();
let w = ps.width();
let h = ps.rows();
sink.begin(w, h, desc)
.map_err(|e| at!(HeicError::Sink(e)))?;
let mut dst = sink
.provide_next_buffer(0, h, w, desc)
.map_err(|e| at!(HeicError::Sink(e)))?;
for row in 0..h {
dst.row_mut(row).copy_from_slice(ps.row(row));
}
drop(dst);
sink.finish().map_err(|e| at!(HeicError::Sink(e)))?;
let info = output.info();
return Ok(OutputInfo::full_decode(info.width, info.height, desc));
}
let layout = descriptor_to_layout(negotiated);
let desc = if let Some(ref pi) = probe_info {
cicp_descriptor(
layout_to_descriptor(layout),
pi.color_primaries,
pi.transfer_characteristics,
)
} else {
layout_to_descriptor(layout)
};
let probe_width = probe_info.as_ref().map_or(0, |pi| pi.width);
let mut adapter = RowSinkAdapter {
inner: sink,
descriptor: desc,
width: probe_width,
strip_buf: alloc::vec::Vec::new(),
pending_y: None,
pending_height: 0,
deferred_error: None,
};
let thread_count = policy_to_threads(self.limits.threading());
let native_limits = self.native_limits();
let mut req = self
.config
.inner
.decode_request(&data)
.with_output_layout(layout);
if let Some(ref limits) = native_limits {
req = req.with_limits(limits);
}
if let Some(ref stop) = self.stop {
req = req.with_stop(stop);
}
if thread_count > 0 {
req = req.with_max_threads(thread_count);
}
let probe_height = probe_info.as_ref().map_or(0, |pi| pi.height);
adapter
.inner
.begin(probe_width, probe_height, desc)
.map_err(|e| at!(HeicError::Sink(e)))?;
let (w, h) = req.decode_rows(&mut adapter)?;
adapter.take_deferred_error()?;
adapter.flush_pending()?;
adapter
.inner
.finish()
.map_err(|e| at!(HeicError::Sink(e)))?;
Ok(OutputInfo::full_decode(w, h, desc))
}
fn streaming_decoder(
self,
data: Cow<'a, [u8]>,
preferred: &[PixelDescriptor],
) -> Result<HeicStreamDecoder, At<HeicError>> {
self.limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(HeicError::LimitExceeded(limit_exceeded_msg(e))))?;
let thread_count = policy_to_threads(self.limits.threading());
HeicStreamDecoder::new(
&data,
preferred,
self.native_limits().as_ref(),
self.stop,
thread_count,
)
}
fn animation_frame_decoder(
self,
_data: Cow<'a, [u8]>,
_preferred: &[PixelDescriptor],
) -> Result<Unsupported<At<HeicError>>, At<HeicError>> {
Err(at!(HeicError::Unsupported(
"HEIC does not support animation decoding",
)))
}
}
struct RowSinkAdapter<'a> {
inner: &'a mut dyn DecodeRowSink,
descriptor: PixelDescriptor,
width: u32,
strip_buf: alloc::vec::Vec<u8>,
pending_y: Option<u32>,
pending_height: u32,
deferred_error: Option<At<HeicError>>,
}
impl RowSinkAdapter<'_> {
fn flush_pending(&mut self) -> Result<(), At<HeicError>> {
if let Some(y) = self.pending_y.take() {
let bpp = self.descriptor.bytes_per_pixel();
let row_bytes = self.width as usize * bpp;
let mut dst = self
.inner
.provide_next_buffer(y, self.pending_height, self.width, self.descriptor)
.map_err(|e| at!(HeicError::Sink(e)))?;
for row in 0..self.pending_height {
let src_start = row as usize * row_bytes;
dst.row_mut(row)
.copy_from_slice(&self.strip_buf[src_start..src_start + row_bytes]);
}
}
Ok(())
}
fn flush_pending_deferred(&mut self) {
if let Err(e) = self.flush_pending() {
self.deferred_error = Some(e);
}
}
fn take_deferred_error(&mut self) -> Result<(), At<HeicError>> {
match self.deferred_error.take() {
Some(e) => Err(e),
None => Ok(()),
}
}
}
impl crate::RowSink for RowSinkAdapter<'_> {
fn demand(&mut self, y: u32, height: u32, min_bytes: usize) -> &mut [u8] {
if self.width == 0 {
let bpp = self.descriptor.bytes_per_pixel();
if height > 0 && bpp > 0 {
self.width = (min_bytes / height as usize / bpp) as u32;
}
}
self.flush_pending_deferred();
self.pending_y = Some(y);
self.pending_height = height;
self.strip_buf.resize(min_bytes, 0);
&mut self.strip_buf[..min_bytes]
}
}
pub struct HeicDecoder<'a> {
config: HeicDecoderConfig,
data: Cow<'a, [u8]>,
preferred: alloc::vec::Vec<PixelDescriptor>,
stop: Option<zencodec::StopToken>,
limits: Option<crate::Limits>,
thread_count: usize,
policy: Option<DecodePolicy>,
extract_gain_map: bool,
extract_depth: bool,
}
impl zencodec::decode::Decode for HeicDecoder<'_> {
type Error = At<HeicError>;
fn decode(self) -> Result<DecodeOutput, At<HeicError>> {
let data: &[u8] = &self.data;
let preferred = &self.preferred;
let probe_info = crate::ImageInfo::from_bytes(data).ok();
let bit_depth = probe_info.as_ref().map_or(8, |pi| pi.bit_depth);
let has_alpha = probe_info.as_ref().is_some_and(|pi| pi.has_alpha);
let available = available_descriptors(has_alpha, bit_depth);
let negotiated = negotiate_pixel_format(preferred, &available)
.ok_or_else(|| at!(HeicError::InvalidData("pixel format negotiation failed")))?;
let (buf, width, height, has_alpha): (PixelBuffer, u32, u32, bool) = if is_16bit(negotiated)
{
let mut req = self.config.inner.decode_request(data);
if let Some(ref limits) = self.limits {
req = req.with_limits(limits);
}
if let Some(ref stop) = self.stop {
req = req.with_stop(stop);
}
if self.thread_count > 0 {
req = req.with_max_threads(self.thread_count);
}
let frame = req.decode_yuv()?;
let has_alpha = frame.alpha_plane.is_some();
let w = frame.cropped_width();
let h = frame.cropped_height();
let wants_alpha = negotiated == PixelDescriptor::RGBA16_SRGB;
if has_alpha || wants_alpha {
let desc = cicp_descriptor(
PixelDescriptor::RGBA16_SRGB,
frame.color_primaries as u16,
frame.transfer_characteristics as u16,
);
let rgba_data = frame.to_rgba16()?;
let pixels = u16_vec_to_rgba(rgba_data);
let pb = PixelBuffer::from_pixels_erased(pixels, w, h)
.map_err_at(|_| HeicError::InvalidData("pixel count mismatch"))?
.with_descriptor(desc);
(pb, w, h, true)
} else {
let desc = cicp_descriptor(
PixelDescriptor::RGB16_SRGB,
frame.color_primaries as u16,
frame.transfer_characteristics as u16,
);
let rgb_data = frame.to_rgb16()?;
let pixels = u16_vec_to_rgb(rgb_data);
let pb = PixelBuffer::from_pixels_erased(pixels, w, h)
.map_err_at(|_| HeicError::InvalidData("pixel count mismatch"))?
.with_descriptor(desc);
(pb, w, h, false)
}
} else {
let layout = descriptor_to_layout(negotiated);
let mut req = self
.config
.inner
.decode_request(data)
.with_output_layout(layout);
if let Some(ref limits) = self.limits {
req = req.with_limits(limits);
}
if let Some(ref stop) = self.stop {
req = req.with_stop(stop);
}
if self.thread_count > 0 {
req = req.with_max_threads(self.thread_count);
}
let native_output = req.decode()?;
let has_alpha =
layout == crate::PixelLayout::Rgba8 || layout == crate::PixelLayout::Bgra8;
let w = native_output.width;
let h = native_output.height;
let mut pb = raw_to_pixel_buffer(native_output.data, w, h, native_output.layout)?;
if let Some(ref pi) = probe_info {
let desc = cicp_descriptor(
pb.descriptor(),
pi.color_primaries,
pi.transfer_characteristics,
);
pb = pb.with_descriptor(desc);
}
(pb, w, h, has_alpha)
};
let stop_ref: &dyn enough::Stop = self
.stop
.as_ref()
.map_or(&enough::Unstoppable as &dyn enough::Stop, |s| s);
let container = crate::heif::parse(data, stop_ref).ok();
let fallback_info = crate::ImageInfo {
width,
height,
has_alpha,
bit_depth: 8,
chroma_format: 1,
has_exif: false,
has_xmp: false,
has_thumbnail: false,
color_primaries: 2,
transfer_characteristics: 2,
matrix_coefficients: 2,
video_full_range: false,
has_icc_profile: false,
has_depth: false,
has_gain_map: false,
exif: None,
xmp: None,
icc_profile: None,
};
let pi_ref = probe_info.as_ref().unwrap_or(&fallback_info);
let info = apply_policy(
self.policy.as_ref(),
build_image_info_full(pi_ref, container.as_ref(), width, height),
);
let mut output =
DecodeOutput::new(buf, info).with_source_encoding_details(HeicSourceEncoding);
if let Some(ref pi) = probe_info {
let aux_types = if let Some(ref c) = container {
let primary_id = c.primary_item_id;
c.find_all_auxiliary_items(primary_id)
.into_iter()
.map(|(_id, urn)| AuxiliaryImageType::from_urn(&urn))
.collect()
} else {
alloc::vec::Vec::new()
};
output.extensions_mut().insert(HeicAuxiliaryInfo {
has_depth: pi.has_depth,
has_gain_map: pi.has_gain_map,
auxiliary_types: aux_types,
});
if self.extract_gain_map
&& pi.has_gain_map
&& let Ok(gain_map) = crate::decode::decode_gain_map(data)
{
output.extensions_mut().insert(gain_map);
}
if self.extract_depth
&& pi.has_depth
&& let Ok(depth_map) = crate::decode::decode_depth(data)
{
output.extensions_mut().insert(depth_map);
}
}
Ok(output)
}
}
struct GridState {
tile_data: alloc::vec::Vec<alloc::vec::Vec<u8>>,
tile_config: crate::heif::HevcDecoderConfig,
rows: u32,
cols: u32,
tile_width: u32,
tile_height: u32,
output_width: u32,
output_height: u32,
color_override: Option<(bool, u8)>,
layout: crate::PixelLayout,
}
pub struct HeicStreamDecoder {
info: ImageInfo,
descriptor: PixelDescriptor,
y_offset: u32,
grid: Option<GridState>,
current_grid_row: u32,
strip_buffer: alloc::vec::Vec<u8>,
full_pixels: Option<PixelBuffer>,
stop: Option<zencodec::StopToken>,
}
impl HeicStreamDecoder {
const FALLBACK_STRIP_HEIGHT: u32 = 64;
fn new(
data: &[u8],
preferred: &[PixelDescriptor],
limits: Option<&crate::Limits>,
owned_stop: Option<zencodec::StopToken>,
thread_count: usize,
) -> Result<Self, At<HeicError>> {
let stop_ref: &dyn enough::Stop = match owned_stop {
Some(ref s) => s,
None => &enough::Unstoppable,
};
let probe_info = crate::ImageInfo::from_bytes(data).ok();
let config = crate::DecoderConfig::new();
let pi = probe_info
.as_ref()
.ok_or_else(|| at!(HeicError::InvalidData("cannot probe HEIC header")))?;
let container = crate::heif::parse(data, stop_ref).ok();
let info = build_image_info_full(pi, container.as_ref(), pi.width, pi.height);
if pi.bit_depth <= 8
&& !pi.has_alpha
&& let Some(grid_state) =
Self::try_init_grid(container.as_ref(), data, preferred, limits, stop_ref, pi)?
{
let descriptor = cicp_descriptor(
layout_to_descriptor(grid_state.layout),
pi.color_primaries,
pi.transfer_characteristics,
);
return Ok(Self {
info,
descriptor,
y_offset: 0,
grid: Some(grid_state),
current_grid_row: 0,
strip_buffer: alloc::vec::Vec::new(),
full_pixels: None,
stop: owned_stop,
});
}
let available = available_descriptors(pi.has_alpha, pi.bit_depth);
let negotiated = negotiate_pixel_format(preferred, &available)
.ok_or_else(|| at!(HeicError::InvalidData("pixel format negotiation failed")))?;
let pixels: PixelBuffer = if is_16bit(negotiated) {
let mut req = config.decode_request(data);
if let Some(lim) = limits {
req = req.with_limits(lim);
}
req = req.with_stop(stop_ref);
if thread_count > 0 {
req = req.with_max_threads(thread_count);
}
let frame = req.decode_yuv()?;
let has_alpha = frame.alpha_plane.is_some();
let wants_alpha = negotiated == PixelDescriptor::RGBA16_SRGB;
if has_alpha || wants_alpha {
let desc = cicp_descriptor(
PixelDescriptor::RGBA16_SRGB,
frame.color_primaries as u16,
frame.transfer_characteristics as u16,
);
let rgba_data = frame.to_rgba16()?;
let pixels: alloc::vec::Vec<Rgba<u16>> = rgba_data
.chunks_exact(4)
.map(|c| Rgba {
r: c[0],
g: c[1],
b: c[2],
a: c[3],
})
.collect();
let w = frame.cropped_width();
let h = frame.cropped_height();
PixelBuffer::from_pixels_erased(pixels, w, h)
.map_err_at(|_| HeicError::InvalidData("pixel count mismatch"))?
.with_descriptor(desc)
} else {
let desc = cicp_descriptor(
PixelDescriptor::RGB16_SRGB,
frame.color_primaries as u16,
frame.transfer_characteristics as u16,
);
let rgb_data = frame.to_rgb16()?;
let pixels = u16_vec_to_rgb(rgb_data);
let w = frame.cropped_width();
let h = frame.cropped_height();
PixelBuffer::from_pixels_erased(pixels, w, h)
.map_err_at(|_| HeicError::InvalidData("pixel count mismatch"))?
.with_descriptor(desc)
}
} else {
let layout = descriptor_to_layout(negotiated);
let mut req = config.decode_request(data).with_output_layout(layout);
if let Some(lim) = limits {
req = req.with_limits(lim);
}
req = req.with_stop(stop_ref);
if thread_count > 0 {
req = req.with_max_threads(thread_count);
}
let native_output = req.decode()?;
let mut pb = raw_to_pixel_buffer(
native_output.data,
native_output.width,
native_output.height,
native_output.layout,
)?;
let desc = cicp_descriptor(
pb.descriptor(),
pi.color_primaries,
pi.transfer_characteristics,
);
pb = pb.with_descriptor(desc);
pb
};
let descriptor = pixels.descriptor();
Ok(Self {
info,
descriptor,
y_offset: 0,
grid: None,
current_grid_row: 0,
strip_buffer: alloc::vec::Vec::new(),
full_pixels: Some(pixels),
stop: owned_stop,
})
}
fn try_init_grid(
pre_parsed: Option<&crate::heif::HeifContainer<'_>>,
data: &[u8],
preferred: &[PixelDescriptor],
limits: Option<&crate::Limits>,
stop: &dyn enough::Stop,
_probe_info: &crate::ImageInfo,
) -> Result<Option<GridState>, At<HeicError>> {
use crate::heif::{self, ColorInfo, FourCC, ItemType};
stop.check().map_err(|r| at!(HeicError::Cancelled(r)))?;
let owned;
let container: &heif::HeifContainer<'_> = match pre_parsed {
Some(c) => c,
None => {
owned = heif::parse(data, stop)?;
&owned
}
};
let primary_item = container
.primary_item()
.ok_or_else(|| at!(HeicError::NoPrimaryImage))?;
if primary_item.item_type != ItemType::Grid {
return Ok(None);
}
if !primary_item.transforms.is_empty() {
return Ok(None);
}
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();
if has_alpha {
return Ok(None);
}
let grid_data = container.get_item_data(primary_item.id)?;
if grid_data.len() < 8 {
return Err(at!(HeicError::InvalidData("Grid descriptor too short")));
}
let flags = grid_data[1];
let rows = grid_data[2] as u32 + 1;
let cols = grid_data[3] as u32 + 1;
let (output_width, output_height) = if (flags & 1) != 0 {
if grid_data.len() < 12 {
return Err(at!(HeicError::InvalidData(
"Grid descriptor too short for 32-bit dims",
)));
}
(
u32::from_be_bytes([grid_data[4], grid_data[5], grid_data[6], grid_data[7]]),
u32::from_be_bytes([grid_data[8], grid_data[9], grid_data[10], grid_data[11]]),
)
} else {
(
u16::from_be_bytes([grid_data[4], grid_data[5]]) as u32,
u16::from_be_bytes([grid_data[6], grid_data[7]]) as u32,
)
};
if let Some(lim) = limits {
lim.check_dimensions(output_width, output_height)?;
}
let tile_ids = container.get_item_references(primary_item.id, FourCC::DIMG);
let expected_tiles = (rows * cols) as usize;
if tile_ids.len() != expected_tiles {
return Err(at!(HeicError::InvalidData("Grid tile count mismatch")));
}
let first_tile = container
.get_item(tile_ids[0])
.ok_or_else(|| at!(HeicError::InvalidData("Missing tile item")))?;
let tile_config = first_tile
.hevc_config
.as_ref()
.ok_or_else(|| at!(HeicError::InvalidData("Missing tile hvcC config")))?
.clone();
let (tile_width, tile_height) = first_tile
.dimensions
.ok_or_else(|| at!(HeicError::InvalidData("Missing tile dimensions")))?;
let color_override = match &primary_item.color_info {
Some(ColorInfo::Nclx {
full_range,
matrix_coefficients,
..
}) => Some((*full_range, *matrix_coefficients as u8)),
_ => None,
};
let tile_data: alloc::vec::Vec<alloc::vec::Vec<u8>> = tile_ids
.iter()
.map(|&tid| container.get_item_data(tid).map(|cow| cow.into_owned()))
.collect::<Result<_, _>>()?;
let available = available_descriptors(false, 8);
let negotiated = negotiate_pixel_format(preferred, &available)
.ok_or_else(|| at!(HeicError::InvalidData("pixel format negotiation failed")))?;
let layout = descriptor_to_layout(negotiated);
Ok(Some(GridState {
tile_data,
tile_config,
rows,
cols,
tile_width,
tile_height,
output_width,
output_height,
color_override,
layout,
}))
}
fn decode_grid_row(&mut self) -> Result<Option<(u32, u32, u32)>, At<HeicError>> {
if let Some(ref stop) = self.stop {
stop.check().map_err(|r| at!(HeicError::Cancelled(r)))?;
}
let grid = self.grid.as_ref().ok_or_else(|| {
at!(HeicError::InvalidData(
"grid not initialized for streaming decode"
))
})?;
let row = self.current_grid_row;
if row >= grid.rows {
return Ok(None);
}
let strip_h = grid
.tile_height
.min(grid.output_height.saturating_sub(row * grid.tile_height));
if strip_h == 0 {
return Ok(None);
}
let y_offset = row * grid.tile_height;
let bpp = grid.layout.bytes_per_pixel();
let strip_bytes = grid.output_width as usize * strip_h as usize * bpp;
self.strip_buffer.resize(strip_bytes, 0);
let cols = grid.cols as usize;
let row_start = row as usize * cols;
for col in 0..cols {
let tile_idx = row_start + col;
if tile_idx >= grid.tile_data.len() {
break;
}
let mut tile_frame =
crate::hevc::decode_with_config(&grid.tile_config, &grid.tile_data[tile_idx])?;
if let Some((fr, mc)) = grid.color_override {
tile_frame.full_range = fr;
tile_frame.matrix_coeffs = mc;
}
let dst_x = col as u32 * grid.tile_width;
let copy_w = tile_frame
.cropped_width()
.min(grid.output_width.saturating_sub(dst_x));
let copy_h = tile_frame.cropped_height().min(strip_h);
crate::decode::convert_tile_to_output(
&tile_frame,
&mut self.strip_buffer,
grid.layout,
dst_x,
0,
copy_w,
copy_h,
grid.output_width,
);
}
self.current_grid_row += 1;
Ok(Some((y_offset, grid.output_width, strip_h)))
}
}
impl zencodec::decode::StreamingDecode for HeicStreamDecoder {
type Error = At<HeicError>;
fn next_batch(&mut self) -> Result<Option<(u32, zenpixels::PixelSlice<'_>)>, At<HeicError>> {
if self.grid.is_some() {
let result = self.decode_grid_row()?;
match result {
None => Ok(None),
Some((y, width, height)) => {
let bpp = self.descriptor.bytes_per_pixel();
let stride = width as usize * bpp;
let slice = zenpixels::PixelSlice::new(
&self.strip_buffer,
width,
height,
stride,
self.descriptor,
)
.map_err(|_| at!(HeicError::InvalidData("failed to create pixel slice")))?;
Ok(Some((y, slice)))
}
}
} else if let Some(ref pixels) = self.full_pixels {
let height = pixels.height();
if self.y_offset >= height {
return Ok(None);
}
let h = Self::FALLBACK_STRIP_HEIGHT.min(height - self.y_offset);
let slice = pixels.rows(self.y_offset, h).erase();
let y = self.y_offset;
self.y_offset += h;
Ok(Some((y, slice)))
} else {
Ok(None)
}
}
fn info(&self) -> &ImageInfo {
&self.info
}
}
fn raw_to_pixel_buffer(
mut raw: alloc::vec::Vec<u8>,
w: u32,
h: u32,
layout: crate::PixelLayout,
) -> Result<PixelBuffer, At<HeicError>> {
match layout {
crate::PixelLayout::Rgb8 => {
Ok(PixelBuffer::from_vec(raw, w, h, PixelDescriptor::RGB8_SRGB)
.map_err_at(|_| HeicError::InvalidData("pixel buffer size mismatch"))?)
}
crate::PixelLayout::Rgba8 => {
Ok(
PixelBuffer::from_vec(raw, w, h, PixelDescriptor::RGBA8_SRGB)
.map_err_at(|_| HeicError::InvalidData("pixel buffer size mismatch"))?,
)
}
crate::PixelLayout::Bgr8 => {
garb::bytes::rgb_to_bgr_inplace(&mut raw)
.map_err(|_| at!(HeicError::InvalidData("BGR swizzle size mismatch")))?;
Ok(PixelBuffer::from_vec(raw, w, h, PixelDescriptor::RGB8_SRGB)
.map_err_at(|_| HeicError::InvalidData("pixel buffer size mismatch"))?)
}
crate::PixelLayout::Bgra8 => {
Ok(
PixelBuffer::from_vec(raw, w, h, PixelDescriptor::BGRA8_SRGB)
.map_err_at(|_| HeicError::InvalidData("pixel buffer size mismatch"))?,
)
}
}
}
fn build_image_info_lightweight(pi: &crate::ImageInfo) -> ImageInfo {
let mut info = ImageInfo::new(pi.width, pi.height, ImageFormat::Heic)
.with_sequence(ImageSequence::Multi {
image_count: Some(1),
random_access: true,
})
.with_orientation(Orientation::Identity) .with_alpha(pi.has_alpha)
.with_bit_depth(pi.bit_depth)
.with_channel_count(if pi.has_alpha { 4 } else { 3 })
.with_source_encoding_details(HeicSourceEncoding)
.with_supplements({
let mut s = Supplements::default();
s.gain_map = pi.has_gain_map;
s.depth_map = pi.has_depth;
s
});
if pi.color_primaries != 2 || pi.transfer_characteristics != 2 || pi.matrix_coefficients != 2 {
info = info
.with_cicp(Cicp::new(
pi.color_primaries as u8,
pi.transfer_characteristics as u8,
pi.matrix_coefficients as u8,
pi.video_full_range,
))
.with_color_authority(zencodec::ColorAuthority::Cicp);
}
if pi.has_gain_map {
info.gain_map = GainMapPresence::Unknown;
} else {
info.gain_map = GainMapPresence::Absent;
}
info
}
fn build_image_info_full(
pi: &crate::ImageInfo,
container: Option<&crate::heif::HeifContainer<'_>>,
width: u32,
height: u32,
) -> ImageInfo {
let mut info = ImageInfo::new(width, height, ImageFormat::Heic)
.with_sequence(ImageSequence::Multi {
image_count: Some(1),
random_access: true,
})
.with_orientation(Orientation::Identity) .with_alpha(pi.has_alpha)
.with_bit_depth(pi.bit_depth)
.with_channel_count(if pi.has_alpha { 4 } else { 3 })
.with_source_encoding_details(HeicSourceEncoding)
.with_supplements({
let mut s = Supplements::default();
s.gain_map = pi.has_gain_map;
s.depth_map = pi.has_depth;
s
});
if pi.has_gain_map {
info.gain_map = GainMapPresence::Unknown;
} else {
info.gain_map = GainMapPresence::Absent;
}
if pi.color_primaries != 2 || pi.transfer_characteristics != 2 || pi.matrix_coefficients != 2 {
info = info
.with_cicp(Cicp::new(
pi.color_primaries as u8,
pi.transfer_characteristics as u8,
pi.matrix_coefficients as u8,
pi.video_full_range,
))
.with_color_authority(zencodec::ColorAuthority::Cicp);
}
if let Some(container) = container {
let primary_item = container.primary_item();
if pi.has_gain_map
&& let Some(ref pri) = primary_item
&& let Some(gm_info) = extract_apple_gain_map_info(container, pri.id)
{
info.gain_map = GainMapPresence::Available(alloc::boxed::Box::new(gm_info));
}
if pi.has_icc_profile
&& let Some(ref item) = primary_item
&& let Some(crate::heif::ColorInfo::IccProfile(icc)) = &item.color_info
{
info = info.with_icc_profile(icc.clone());
}
if let Some(exif) = extract_exif_from_container(container) {
info = info.with_exif(exif);
}
if let Some(xmp) = extract_xmp_from_container(container) {
info = info.with_xmp(xmp);
}
if let Some(ref item) = primary_item
&& let Some(clli) = &item.content_light_level
{
info = info.with_content_light_level(ContentLightLevel::new(
clli.max_content_light_level,
clli.max_frame_average_light_level,
));
}
if let Some(ref item) = primary_item
&& let Some(mdcv) = &item.mastering_display
{
let xy = |v: u16| v as f32 / 50_000.0;
let primaries_xy = [
[xy(mdcv.primaries_xy[0].0), xy(mdcv.primaries_xy[0].1)],
[xy(mdcv.primaries_xy[1].0), xy(mdcv.primaries_xy[1].1)],
[xy(mdcv.primaries_xy[2].0), xy(mdcv.primaries_xy[2].1)],
];
let white_point_xy = [xy(mdcv.white_point_xy.0), xy(mdcv.white_point_xy.1)];
let max_luminance = mdcv.max_luminance as f32 / 10_000.0;
let min_luminance = mdcv.min_luminance as f32 / 10_000.0;
info = info.with_mastering_display(MasteringDisplay::new(
primaries_xy,
white_point_xy,
max_luminance,
min_luminance,
));
}
}
info
}
fn extract_exif_from_container(
container: &crate::heif::HeifContainer<'_>,
) -> Option<alloc::vec::Vec<u8>> {
use crate::heif::FourCC;
for item_info in &container.item_infos {
if item_info.item_type != FourCC(*b"Exif") {
continue;
}
let Ok(exif_data) = container.get_item_data(item_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: &crate::heif::HeifContainer<'_>,
) -> Option<alloc::vec::Vec<u8>> {
use crate::heif::FourCC;
for item_info in &container.item_infos {
if item_info.item_type == FourCC(*b"mime")
&& (item_info.content_type.contains("xmp")
|| item_info.content_type.contains("rdf+xml")
|| item_info.content_type == "application/rdf+xml")
&& let Ok(xmp_data) = container.get_item_data(item_info.item_id)
{
return Some(xmp_data.into_owned());
}
}
None
}
fn limit_exceeded_msg(_e: zencodec::LimitExceeded) -> &'static str {
"input data size exceeds max_input_bytes"
}
fn extract_apple_gain_map_info(
container: &crate::heif::HeifContainer<'_>,
primary_id: u32,
) -> Option<GainMapInfo> {
let aux_ids =
container.find_auxiliary_items(primary_id, "urn:com:apple:photo:2020:aux:hdrgainmap");
let &gainmap_id = aux_ids.first()?;
let aux_item = container.get_item(gainmap_id)?;
let (width, height) = aux_item.dimensions?;
let xmp_bytes = container.find_xmp_for_item(gainmap_id)?;
let xmp_str = core::str::from_utf8(&xmp_bytes).ok()?;
let (uhdr_md, _len) = ultrahdr_core::metadata::parse_xmp(xmp_str).ok()?;
let params = uhdr_metadata_to_zencodec(&uhdr_md);
Some(GainMapInfo::new(params, width, height, 1))
}
fn uhdr_metadata_to_zencodec(md: &ultrahdr_core::GainMapMetadata) -> zencodec::GainMapParams {
let mut params = zencodec::GainMapParams::default();
for i in 0..3 {
params.channels[i] = zencodec::GainMapChannel {
min: md.gain_map_min[i],
max: md.gain_map_max[i],
gamma: md.gamma[i],
base_offset: md.base_offset[i],
alternate_offset: md.alternate_offset[i],
};
}
params.base_hdr_headroom = md.base_hdr_headroom;
params.alternate_hdr_headroom = md.alternate_hdr_headroom;
params.use_base_color_space = md.use_base_color_space;
params.backward_direction = md.backward_direction;
params
}
fn cicp_descriptor(
base: PixelDescriptor,
color_primaries: u16,
transfer_characteristics: u16,
) -> PixelDescriptor {
let tf = TransferFunction::from_cicp(transfer_characteristics as u8).unwrap_or(base.transfer());
let primaries = ColorPrimaries::from_cicp(color_primaries as u8).unwrap_or(base.primaries);
base.with_transfer(tf).with_primaries(primaries)
}
fn layout_to_descriptor(layout: crate::PixelLayout) -> PixelDescriptor {
match layout {
crate::PixelLayout::Rgb8 => PixelDescriptor::RGB8_SRGB,
crate::PixelLayout::Rgba8 => PixelDescriptor::RGBA8_SRGB,
crate::PixelLayout::Bgr8 => PixelDescriptor::RGB8_SRGB,
crate::PixelLayout::Bgra8 => PixelDescriptor::BGRA8_SRGB,
}
}
fn probe_error_to_heic(e: crate::ProbeError) -> At<HeicError> {
match e {
crate::ProbeError::NeedMoreData => at!(HeicError::InvalidData("not enough data to probe")),
crate::ProbeError::InvalidFormat => {
at!(HeicError::InvalidData("not a valid HEIC/HEIF file"))
}
crate::ProbeError::Corrupt(inner) => inner,
}
}
fn u16_vec_to_rgb(data: alloc::vec::Vec<u16>) -> alloc::vec::Vec<Rgb<u16>> {
match bytemuck::try_cast_vec(data) {
Ok(pixels) => pixels,
Err((_err, data)) => bytemuck::cast_slice::<u16, Rgb<u16>>(&data).to_vec(),
}
}
fn u16_vec_to_rgba(data: alloc::vec::Vec<u16>) -> alloc::vec::Vec<Rgba<u16>> {
match bytemuck::try_cast_vec(data) {
Ok(pixels) => pixels,
Err((_err, data)) => bytemuck::cast_slice::<u16, Rgba<u16>>(&data).to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_creation() {
let config = HeicDecoderConfig::new();
assert_eq!(
<HeicDecoderConfig as zencodec::decode::DecoderConfig>::formats(),
&[ImageFormat::Heic]
);
let descriptors =
<HeicDecoderConfig as zencodec::decode::DecoderConfig>::supported_descriptors();
assert!(!descriptors.is_empty());
assert!(descriptors.contains(&PixelDescriptor::RGB8_SRGB));
assert!(descriptors.contains(&PixelDescriptor::RGBA8_SRGB));
assert!(descriptors.contains(&PixelDescriptor::BGRA8_SRGB));
let _ = config;
}
#[test]
fn default_config() {
let config = HeicDecoderConfig::default();
assert_eq!(
<HeicDecoderConfig as zencodec::decode::DecoderConfig>::formats(),
&[ImageFormat::Heic]
);
let _ = config;
}
#[test]
fn capabilities_reported() {
let caps = <HeicDecoderConfig as zencodec::decode::DecoderConfig>::capabilities();
assert!(caps.icc());
assert!(caps.exif());
assert!(caps.xmp());
assert!(caps.cicp());
assert!(caps.stop());
assert!(caps.cheap_probe());
assert!(caps.native_16bit());
assert!(caps.native_alpha());
assert!(caps.hdr());
assert!(caps.enforces_max_input_bytes());
}
#[test]
fn job_creation() {
use zencodec::decode::DecoderConfig as _;
let config = HeicDecoderConfig::new();
let _job = config.job();
}
#[test]
fn animation_frame_decoder_returns_unsupported() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let config = HeicDecoderConfig::new();
let result = config
.job()
.animation_frame_decoder(Cow::Borrowed(&[]), &[]);
assert!(result.is_err());
}
#[test]
fn probe_invalid_data() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let config = HeicDecoderConfig::new();
let result = config.job().probe(b"not a heic file");
assert!(result.is_err());
}
#[test]
fn negotiate_no_preference_no_alpha() {
let available = available_descriptors(false, 8);
let desc = negotiate_pixel_format(&[], &available);
assert_eq!(desc, Some(PixelDescriptor::RGB8_SRGB));
}
#[test]
fn negotiate_no_preference_with_alpha() {
let available = available_descriptors(true, 8);
let desc = negotiate_pixel_format(&[], &available);
assert_eq!(desc, Some(PixelDescriptor::RGBA8_SRGB));
}
#[test]
fn negotiate_rgba_preference() {
let available = available_descriptors(false, 8);
let desc = negotiate_pixel_format(&[PixelDescriptor::RGBA8_SRGB], &available);
assert_eq!(desc, Some(PixelDescriptor::RGBA8_SRGB));
}
#[test]
fn negotiate_bgra_preference() {
let available = available_descriptors(false, 8);
let desc = negotiate_pixel_format(&[PixelDescriptor::BGRA8_SRGB], &available);
assert_eq!(desc, Some(PixelDescriptor::BGRA8_SRGB));
}
#[test]
fn negotiate_16bit_source_no_preference() {
let available = available_descriptors(false, 10);
let desc = negotiate_pixel_format(&[], &available);
assert_eq!(desc, Some(PixelDescriptor::RGB16_SRGB));
}
#[test]
fn negotiate_16bit_source_8bit_preference() {
let available = available_descriptors(false, 10);
let desc = negotiate_pixel_format(&[PixelDescriptor::RGB8_SRGB], &available);
assert_eq!(desc, Some(PixelDescriptor::RGB8_SRGB));
}
#[test]
fn raw_to_pixel_buffer_rgb8() {
let raw = alloc::vec![10, 20, 30, 40, 50, 60];
let buf = raw_to_pixel_buffer(raw, 2, 1, crate::PixelLayout::Rgb8).unwrap();
assert_eq!(buf.width(), 2);
assert_eq!(buf.height(), 1);
let img: imgref::ImgRef<'_, Rgb<u8>> = buf.try_as_imgref().expect("expected RGB8");
assert_eq!(
img.buf()[0],
Rgb {
r: 10,
g: 20,
b: 30
}
);
assert_eq!(
img.buf()[1],
Rgb {
r: 40,
g: 50,
b: 60
}
);
}
#[test]
fn raw_to_pixel_buffer_rgba8() {
let raw = alloc::vec![10, 20, 30, 255, 40, 50, 60, 128];
let buf = raw_to_pixel_buffer(raw, 2, 1, crate::PixelLayout::Rgba8).unwrap();
assert_eq!(buf.width(), 2);
assert_eq!(buf.height(), 1);
let img: imgref::ImgRef<'_, Rgba<u8>> = buf.try_as_imgref().expect("expected RGBA8");
assert_eq!(
img.buf()[0],
Rgba {
r: 10,
g: 20,
b: 30,
a: 255
}
);
}
#[test]
fn raw_to_pixel_buffer_bgr8() {
let raw = alloc::vec![30, 20, 10];
let buf = raw_to_pixel_buffer(raw, 1, 1, crate::PixelLayout::Bgr8).unwrap();
let img: imgref::ImgRef<'_, Rgb<u8>> = buf.try_as_imgref().expect("expected RGB8");
assert_eq!(
img.buf()[0],
Rgb {
r: 10,
g: 20,
b: 30
}
);
}
#[test]
fn raw_to_pixel_buffer_bgra8() {
let raw = alloc::vec![30, 20, 10, 255];
let buf = raw_to_pixel_buffer(raw, 1, 1, crate::PixelLayout::Bgra8).unwrap();
let img: imgref::ImgRef<'_, rgb::alt::BGRA<u8>> =
buf.try_as_imgref().expect("expected BGRA8");
let px = &img.buf()[0];
assert_eq!(px.b, 30);
assert_eq!(px.g, 20);
assert_eq!(px.r, 10);
assert_eq!(px.a, 255);
}
#[test]
fn probe_error_conversion() {
let e = probe_error_to_heic(crate::ProbeError::NeedMoreData);
assert!(matches!(e.error(), HeicError::InvalidData(_)));
let e = probe_error_to_heic(crate::ProbeError::InvalidFormat);
assert!(matches!(e.error(), HeicError::InvalidData(_)));
let e = probe_error_to_heic(crate::ProbeError::Corrupt(at!(HeicError::NoPrimaryImage)));
assert!(matches!(e.error(), HeicError::NoPrimaryImage));
}
#[test]
fn descriptor_to_layout_mapping() {
assert_eq!(
descriptor_to_layout(PixelDescriptor::RGB8_SRGB),
crate::PixelLayout::Rgb8
);
assert_eq!(
descriptor_to_layout(PixelDescriptor::RGBA8_SRGB),
crate::PixelLayout::Rgba8
);
assert_eq!(
descriptor_to_layout(PixelDescriptor::BGRA8_SRGB),
crate::PixelLayout::Bgra8
);
}
#[test]
fn policy_to_threads_sequential() {
assert_eq!(policy_to_threads(ThreadingPolicy::Sequential), 1);
}
#[test]
fn policy_to_threads_parallel() {
assert_eq!(policy_to_threads(ThreadingPolicy::Parallel), 0);
}
#[test]
fn single_thread_decode_via_adapter() {
use zencodec::decode::{Decode, DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let limits = ResourceLimits::none().with_threading(ThreadingPolicy::Sequential);
let job = config.job().with_limits(limits);
let decoder = job
.decoder(Cow::Borrowed(&data), &[PixelDescriptor::RGB8_SRGB])
.expect("decoder creation");
let output = decoder.decode().expect("single-thread decode");
let info = output.info();
assert_eq!(info.width, 1280);
assert_eq!(info.height, 854);
let pixels = output.pixels();
assert_eq!(pixels.width(), 1280);
assert_eq!(pixels.rows(), 854);
assert!(pixels.row(0).iter().any(|&b| b != 0));
}
#[test]
fn single_thread_native_decode() {
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = crate::DecoderConfig::new();
let output = config
.decode_request(&data)
.with_output_layout(crate::PixelLayout::Rgb8)
.with_max_threads(1)
.decode()
.expect("single-thread native decode");
assert_eq!(output.width, 1280);
assert_eq!(output.height, 854);
assert!(output.data.iter().any(|&b| b != 0));
}
#[test]
fn probe_enforces_max_input_bytes() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let limits = ResourceLimits::none().with_max_input_bytes(100);
let job = config.job().with_limits(limits);
let result = job.probe(&data);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err.error(), HeicError::LimitExceeded(_)),
"expected LimitExceeded, got {err:?}"
);
}
#[test]
fn probe_allows_within_max_input_bytes() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let limits = ResourceLimits::none().with_max_input_bytes(data.len() as u64 + 1000);
let job = config.job().with_limits(limits);
let result = job.probe(&data);
assert!(result.is_ok());
}
#[test]
fn decoder_enforces_max_input_bytes() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let limits = ResourceLimits::none().with_max_input_bytes(100);
let job = config.job().with_limits(limits);
let result = job.decoder(Cow::Borrowed(&data), &[PixelDescriptor::RGB8_SRGB]);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
matches!(err.error(), HeicError::LimitExceeded(_)),
"expected LimitExceeded, got {err:?}"
);
}
#[test]
fn probe_full_enforces_max_input_bytes() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let limits = ResourceLimits::none().with_max_input_bytes(100);
let job = config.job().with_limits(limits);
let result = job.probe_full(&data);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err.error(), HeicError::LimitExceeded(_)),
"expected LimitExceeded, got {err:?}"
);
}
#[test]
fn probe_returns_lightweight_info() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let job = config.job();
let info = job.probe(&data).expect("probe should succeed");
assert_eq!(info.width, 1280);
assert_eq!(info.height, 854);
assert_eq!(info.format, ImageFormat::Heic);
assert_eq!(info.frame_count(), Some(1));
assert!(
info.embedded_metadata.exif.is_none(),
"probe() should not extract EXIF"
);
assert!(
info.embedded_metadata.xmp.is_none(),
"probe() should not extract XMP"
);
assert!(
info.source_color.icc_profile.is_none(),
"probe() should not extract ICC profile"
);
}
#[test]
fn probe_full_returns_complete_info() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/test-images/classic-car-iphone12pro.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let job = config.job();
let info = job.probe_full(&data).expect("probe_full should succeed");
assert_eq!(info.width, 3024);
assert_eq!(info.height, 4032);
assert_eq!(info.format, ImageFormat::Heic);
assert!(
info.embedded_metadata.exif.is_some(),
"probe_full() should extract EXIF from iPhone HEIC"
);
}
#[test]
fn probe_and_probe_full_agree_on_dimensions() {
use zencodec::decode::{DecodeJob as _, DecoderConfig as _};
let path =
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into());
let file = format!("{path}/libheif/examples/example.heic");
let Ok(data) = std::fs::read(&file) else {
eprintln!("Skipping test: {file} not found");
return;
};
let config = HeicDecoderConfig::new();
let job_light = config.clone().job();
let job_full = config.job();
let light = job_light.probe(&data).expect("probe");
let full = job_full.probe_full(&data).expect("probe_full");
assert_eq!(light.width, full.width);
assert_eq!(light.height, full.height);
assert_eq!(light.format, full.format);
assert_eq!(light.frame_count(), full.frame_count());
}
}