1use oxideav_core::frame::{VideoFrame, VideoPlane};
22use oxideav_core::Decoder;
23use oxideav_core::{
24 CodecId, CodecParameters, ColorSignal, Error, ExecutionContext, Frame, Packet, PixelFormat,
25 Result, TimeBase,
26};
27
28use crate::api::DecodeOptions;
29use crate::av1_config::{parse_av1c, Av1CodecConfig};
30
31use crate::alpha::find_alpha_item_id;
32use crate::box_parser::{b, BoxType};
33use crate::cicp::effective_cicp;
34use crate::derived::{ImageOverlay, MAX_DERIVATION_DEPTH};
35use crate::frame_bridge::to_heif;
36use crate::grid::ImageGrid;
37use crate::image::{AvifFrame, AvifPixelFormat, AvifPlane};
38use crate::inspect::AvifInfo;
39use crate::meta::{Property, ITEM_TYPE_IDEN, ITEM_TYPE_IOVL, ITEM_TYPE_TMAP};
40use crate::parser::{classify_brands, AvifHeader, ITEM_TYPE_AV01, ITEM_TYPE_GRID};
41use crate::signal::{color_signal_for, labelled_pixel_format};
42use crate::transform::{clap_extent, crop_top_left, rotation_keeps_layout};
43use oxideav_heif::compose;
44use oxideav_heif::gainmap::{
45 reconstruct_tone_map, GainMapMetadata, DEFAULT_HDR_REFERENCE_WHITE_NITS,
46};
47use oxideav_heif::props as hprops;
48use oxideav_heif::{Chroma, HeifFrame, HeifPixelFormat, Plane};
49
50pub use crate::inspect::{inspect, transforms_for};
53
54fn from_core_pix(fmt: PixelFormat) -> Result<AvifPixelFormat> {
62 AvifPixelFormat::try_from(fmt).map_err(core_err)
63}
64
65pub(crate) fn core_to_avif_frame(vf: VideoFrame) -> AvifFrame {
69 let image_planes = vf.image_plane_count();
73 AvifFrame {
74 pts: vf.pts,
75 planes: vf
76 .planes
77 .into_iter()
78 .take(image_planes)
79 .map(|p| AvifPlane {
80 stride: p.stride,
81 data: p.data,
82 })
83 .collect(),
84 }
85}
86
87pub(crate) fn infer_av1_pixmap(
95 frame: &VideoFrame,
96 cfg: &Av1CodecConfig,
97) -> Result<(PixelFormat, u32, u32)> {
98 if frame.planes.is_empty() {
99 return Err(Error::invalid("avif: AV1 frame has no planes"));
100 }
101 let depth = cfg.bit_depth();
102 let bps = if cfg.high_bitdepth { 2usize } else { 1 };
103 let y = &frame.planes[0];
104 if y.stride == 0 || y.stride % bps != 0 {
105 return Err(Error::invalid(format!(
106 "avif: AV1 frame Y plane stride {} incompatible with {depth}-bit storage",
107 y.stride
108 )));
109 }
110 let width = (y.stride / bps) as u32;
111 let height = (y.data.len() / y.stride) as u32;
112 let format = match frame.image_plane_count() {
113 1 => match depth {
114 8 => PixelFormat::Gray8,
115 10 => PixelFormat::Gray10Le,
116 _ => PixelFormat::Gray12Le,
117 },
118 3 => {
119 let u = &frame.planes[1];
120 if u.stride == 0 {
121 return Err(Error::invalid("avif: AV1 frame U plane has zero stride"));
122 }
123 let chroma_h = u.data.len() / u.stride;
128 let u_stride_doubled = u.stride.saturating_mul(2);
133 let chroma_h_doubled = chroma_h.saturating_mul(2);
134 let sub = if u_stride_doubled == y.stride && chroma_h_doubled >= height as usize {
135 if chroma_h as u32 == height.div_ceil(2) {
136 (1u8, 1u8)
137 } else {
138 (1, 0)
139 }
140 } else if u.stride == y.stride {
141 (0, 0)
142 } else {
143 return Err(Error::unsupported(format!(
144 "avif: cannot infer AV1 frame format (Y stride {}, U stride {}, U rows {})",
145 y.stride, u.stride, chroma_h
146 )));
147 };
148 match (sub, depth) {
149 ((1, 1), 8) => PixelFormat::Yuv420P,
150 ((1, 1), 10) => PixelFormat::Yuv420P10Le,
151 ((1, 1), _) => PixelFormat::Yuv420P12Le,
152 ((1, 0), 8) => PixelFormat::Yuv422P,
153 ((1, 0), 10) => PixelFormat::Yuv422P10Le,
154 ((1, 0), _) => PixelFormat::Yuv422P12Le,
155 ((_, _), 8) => PixelFormat::Yuv444P,
156 ((_, _), 10) => PixelFormat::Yuv444P10Le,
157 ((_, _), _) => PixelFormat::Yuv444P12Le,
158 }
159 }
160 n => {
161 return Err(Error::unsupported(format!(
162 "avif: AV1 frame has {n} planes, expected 1 or 3"
163 )))
164 }
165 };
166 Ok((format, width, height))
167}
168
169const AV1C: BoxType = b(b"av1C");
170const ISPE: BoxType = b(b"ispe");
171const DIMG: BoxType = b(b"dimg");
172
173pub struct AvifDecoder {
175 codec_id: CodecId,
176 pending: Vec<Frame>,
178 info: Option<AvifInfo>,
180 color_signal: Option<ColorSignal>,
183 output_format: Option<PixelFormat>,
185 options: DecodeOptions,
189 exec_threads: usize,
193}
194
195impl AvifDecoder {
196 pub fn new(codec_id: CodecId) -> Self {
197 Self {
198 codec_id,
199 pending: Vec::new(),
200 info: None,
201 color_signal: None,
202 output_format: None,
203 options: DecodeOptions::default(),
204 exec_threads: 1,
205 }
206 }
207
208 pub fn effective_options(&self) -> DecodeOptions {
213 let mut o = self.options;
214 if o.threads.is_none() && self.exec_threads > 1 {
215 o.threads = Some(self.exec_threads);
216 }
217 o
218 }
219
220 pub fn threads(&self) -> usize {
222 self.effective_options().worker_count()
223 }
224
225 pub fn with_params(params: &CodecParameters) -> Result<Self> {
237 let o = oxideav_core::parse_options::<AvifDecoderOptions>(¶ms.options)?;
238 let mut d = Self::new(params.codec_id.clone());
239 d.options = o.apply_to(d.options);
240 d.set_reference_white(o.reference_white)?;
241 Ok(d)
242 }
243
244 pub fn set_tone_mapped(&mut self, tone_mapped: bool) {
257 self.options.tone_mapped = tone_mapped;
258 }
259
260 pub fn with_tone_mapped(mut self, tone_mapped: bool) -> Self {
262 self.options.tone_mapped = tone_mapped;
263 self
264 }
265
266 pub fn tone_mapped(&self) -> bool {
269 self.options.tone_mapped
270 }
271
272 pub fn options(&self) -> &DecodeOptions {
275 &self.options
276 }
277
278 pub fn set_options(&mut self, options: DecodeOptions) {
281 self.options = options;
282 }
283
284 pub fn with_options(mut self, options: DecodeOptions) -> Self {
286 self.options = options;
287 self
288 }
289
290 pub fn set_reference_white(&mut self, nits: f64) -> Result<()> {
295 if !(nits.is_finite() && nits > 0.0) {
296 return Err(Error::invalid(format!(
297 "avif: reference white {nits} cd/m² is not a positive finite luminance"
298 )));
299 }
300 self.options.reference_white_nits = nits;
301 Ok(())
302 }
303
304 pub fn color_signal(&self) -> Option<ColorSignal> {
313 self.color_signal
314 }
315
316 pub fn output_format(&self) -> Option<PixelFormat> {
323 self.output_format
324 }
325
326 pub fn decode_file(&mut self, file: &[u8]) -> Result<AvifInfo> {
334 let decoded =
337 crate::api_codec::decode_primary(file, &self.effective_options()).map_err(core_err)?;
338 let format = decoded.image.format;
339 let signal = color_signal_for(Some(&decoded.image.color.to_colr()));
340 let out = VideoFrame::from(decoded.image);
341 self.color_signal = Some(signal);
342 self.output_format = Some(labelled_pixel_format(format, &signal));
343 self.pending.push(Frame::Video(out));
344 self.info = Some(decoded.info.clone());
345 Ok(decoded.info)
346 }
347
348 pub fn info(&self) -> Option<&AvifInfo> {
349 self.info.as_ref()
350 }
351
352 pub fn decode_avis_file(&mut self, file: &[u8]) -> Result<usize> {
375 let mut frames_queued = 0usize;
376 let opts = self.effective_options();
377 let seq =
378 crate::api_codec::decode_sequence_with(file, &opts, None, |frame, seq, _duration| {
379 self.pending.push(with_signal(frame, seq.signal));
380 frames_queued += 1;
381 Ok(())
382 })
383 .map_err(core_err)?;
384 self.color_signal = Some(seq.signal);
385 self.output_format = seq.layout.map(|l| labelled_pixel_format(l, &seq.signal));
386 Ok(frames_queued)
387 }
388}
389
390fn with_signal(frame: Frame, signal: ColorSignal) -> Frame {
392 match frame {
393 Frame::Video(v) => Frame::Video(v.with_color_signal(signal)),
394 other => other,
395 }
396}
397
398pub(crate) fn core_err(e: crate::error::AvifError) -> Error {
403 Error::from(e)
404}
405
406pub(crate) fn validate_av1_config(cfg: &Av1CodecConfig) -> Result<()> {
423 if cfg.seq_profile > 2 {
424 return Err(Error::invalid(format!(
425 "av1C: seq_profile={} > 2 (AV1 §A.4)",
426 cfg.seq_profile
427 )));
428 }
429 if (24..=30).contains(&cfg.seq_level_idx_0) {
431 return Err(Error::invalid(format!(
432 "av1C: seq_level_idx_0={} is reserved (AV1 §A.3)",
433 cfg.seq_level_idx_0
434 )));
435 }
436 let sub_x = cfg.chroma_subsampling_x;
437 let sub_y = cfg.chroma_subsampling_y;
438 if cfg.monochrome && !(sub_x && sub_y) {
439 return Err(Error::invalid(format!(
440 "av1C: monochrome=1 requires chroma_subsampling_x=chroma_subsampling_y=1, \
441 got x={sub_x} y={sub_y} (AV1 §5.5.2)"
442 )));
443 }
444 if !cfg.monochrome && sub_x && !sub_y && cfg.seq_profile != 2 {
446 return Err(Error::invalid(format!(
447 "av1C: 4:2:2 subsampling requires seq_profile=2, got profile={} (AV1 §5.5.2)",
448 cfg.seq_profile
449 )));
450 }
451 if !cfg.monochrome && !sub_x && !sub_y && cfg.seq_profile == 0 {
453 return Err(Error::invalid(
454 "av1C: 4:4:4 subsampling requires seq_profile in {1, 2}, got profile=0 (AV1 §5.5.2)",
455 ));
456 }
457 Ok(())
458}
459
460pub(crate) const MAX_AV1_ITEM_BYTES: usize = 32 * 1024 * 1024;
469
470#[derive(Clone, Copy, Debug, Default)]
478struct LayerSelect {
479 operating_point: Option<u8>,
481 layer_id: Option<u16>,
484}
485
486fn decode_av01_item(
487 obu_bytes: &[u8],
488 av1c: &[u8],
489 ispe: Option<(u32, u32)>,
490 select: LayerSelect,
491 threads: usize,
492) -> Result<(VideoFrame, PixelFormat, u32, u32)> {
493 if obu_bytes.len() > MAX_AV1_ITEM_BYTES {
494 return Err(Error::invalid(format!(
495 "avif: av01 item payload {} bytes exceeds soft cap {} bytes",
496 obu_bytes.len(),
497 MAX_AV1_ITEM_BYTES
498 )));
499 }
500 let cfg = parse_av1c(av1c)?; validate_av1_config(&cfg)?;
502 check_av1c_matches_stream(obu_bytes, &cfg)?;
503 let frames: Vec<VideoFrame> = match select.operating_point {
507 Some(op) if op > 0 => decode_at_operating_point(obu_bytes, ispe, op)?,
508 _ => {
509 let mut params = CodecParameters::video(CodecId::new("av1"));
510 if let Some((w, h)) = ispe {
511 params.width = Some(w);
512 params.height = Some(h);
513 }
514 params.extradata = av1c.to_vec();
515 let mut av1 = oxideav_av1::registry::make_decoder(¶ms)?;
516 if threads > 1 {
517 av1.set_execution_context(&ExecutionContext::with_threads(threads));
520 }
521 let pkt = Packet::new(0, TimeBase::new(1, 90_000), obu_bytes.to_vec());
522 av1.send_packet(&pkt)?;
523 let mut out = Vec::new();
524 loop {
525 match av1.receive_frame() {
526 Ok(Frame::Video(v)) => out.push(v),
527 Ok(other) => {
528 return Err(Error::unsupported(format!(
529 "avif: AV1 decoder returned non-video frame: {other:?}"
530 )))
531 }
532 Err(Error::NeedMore) => break,
533 Err(e) => {
534 if out.is_empty() {
535 return Err(e);
536 }
537 break;
538 }
539 }
540 }
541 out
542 }
543 };
544 if frames.is_empty() {
545 return Err(Error::invalid("avif: AV1 item decoded to no shown frame"));
546 }
547 let frame = match select.layer_id {
548 Some(id) if id != 0xFFFF => {
549 let idx = usize::from(id);
550 if idx >= frames.len() {
551 return Err(Error::invalid(format!(
552 "avif: lsel layer_id {id} but the item decodes {} layer frame(s) \
553 (av1-avif §2.3.2.2: the layer shall be present and produce an output frame)",
554 frames.len()
555 )));
556 }
557 frames.into_iter().nth(idx).expect("index checked")
558 }
559 _ => frames.into_iter().last().expect("non-empty"),
560 };
561 let (format, width, height) = infer_av1_pixmap(&frame, &cfg)?;
562 Ok((frame, format, width, height))
563}
564
565fn check_av1c_matches_stream(obu_bytes: &[u8], cfg: &Av1CodecConfig) -> Result<()> {
574 use oxideav_av1::{parse_obu, ObuType};
575 let mut off = 0usize;
576 while off < obu_bytes.len() {
577 let Ok((desc, consumed)) = parse_obu(&obu_bytes[off..]) else {
578 return Ok(());
579 };
580 if desc.obu_type == ObuType::SequenceHeader {
581 let Ok(seq) = oxideav_av1::sequence_header::parse_sequence_header(desc.payload) else {
582 return Ok(());
583 };
584 let cc = &seq.color_config;
585 let stream = (
586 cc.bit_depth,
587 cc.mono_chrome,
588 cc.subsampling_x,
589 cc.subsampling_y,
590 );
591 let record = (
592 cfg.bit_depth(),
593 cfg.monochrome,
594 cfg.chroma_subsampling_x,
595 cfg.chroma_subsampling_y,
596 );
597 if stream != record {
598 return Err(Error::invalid(format!(
599 "avif: av1C (bit depth {}, monochrome {}, subsampling {}/{}) disagrees with \
600 the Sequence Header OBU (bit depth {}, monochrome {}, subsampling {}/{}) — \
601 av1-avif §2.2.1",
602 record.0,
603 record.1,
604 u8::from(record.2),
605 u8::from(record.3),
606 stream.0,
607 stream.1,
608 u8::from(stream.2),
609 u8::from(stream.3)
610 )));
611 }
612 return Ok(());
613 }
614 if consumed == 0 {
615 return Ok(());
616 }
617 off += consumed;
618 }
619 Ok(())
620}
621
622fn decode_at_operating_point(
628 obu_bytes: &[u8],
629 ispe: Option<(u32, u32)>,
630 op: u8,
631) -> Result<Vec<VideoFrame>> {
632 use oxideav_av1::encoder::ivf::{build_file_header, build_frame_header, FOURCC_AV01};
633 let (w, h) = ispe.unwrap_or((0, 0));
634 let mut ivf = Vec::with_capacity(44 + obu_bytes.len());
635 ivf.extend_from_slice(&build_file_header(
636 FOURCC_AV01,
637 w.min(u32::from(u16::MAX)) as u16,
638 h.min(u32::from(u16::MAX)) as u16,
639 30,
640 1,
641 1,
642 ));
643 ivf.extend_from_slice(&build_frame_header(obu_bytes.len() as u32, 0));
644 ivf.extend_from_slice(obu_bytes);
645 let frames = oxideav_av1::decode_av1_at_operating_point(&ivf, op).map_err(|e| {
646 Error::invalid(format!(
647 "avif: AV1 decode at operating point {op} failed: {e}"
648 ))
649 })?;
650 frames
651 .into_iter()
652 .map(|f| match f {
653 oxideav_av1::decoder::Frame::Spec(sf) => {
654 let bps = if sf.bit_depth > 8 { 2 } else { 1 };
655 Ok(VideoFrame {
656 pts: None,
657 planes: sf
658 .planes
659 .iter()
660 .zip(&sf.plane_dims)
661 .map(|(data, &(pw, _))| VideoPlane {
662 stride: pw as usize * bps,
663 data: data.clone(),
664 })
665 .collect(),
666 })
667 }
668 #[allow(unreachable_patterns)]
669 other => Err(Error::unsupported(format!(
670 "avif: AV1 decoder returned an unexpected frame kind: {other:?}"
671 ))),
672 })
673 .collect()
674}
675
676pub const MAX_GRID_CANVAS_PIXELS: u64 = 1 << 28;
682
683pub const MAX_ITEM_DECODES: u32 = 4096;
690
691pub(crate) struct DecodeCtx {
695 stack: Vec<u32>,
696 cache: std::collections::HashMap<(u32, bool), ItemImage>,
697 decodes: u32,
698 tone_mapped: bool,
700 reference_white_nits: f64,
702 max_pixels: u64,
708 layer: Option<u16>,
711 threads: usize,
713}
714
715impl DecodeCtx {
716 pub(crate) fn new(opts: &crate::api::DecodeOptions) -> Self {
717 Self {
718 stack: Vec::new(),
719 cache: std::collections::HashMap::new(),
720 decodes: 0,
721 tone_mapped: opts.tone_mapped,
722 reference_white_nits: opts.reference_white_nits,
723 max_pixels: opts.max_pixels.unwrap_or(u64::MAX),
724 layer: opts.layer,
725 threads: opts.worker_count(),
726 }
727 }
728}
729
730pub(crate) type ItemImage = HeifFrame;
735
736fn heif_err(e: oxideav_heif::HeifError) -> Error {
737 core_err(crate::error::AvifError::from(e))
738}
739
740pub(crate) fn decode_item_output(
761 hdr: &AvifHeader<'_>,
762 item_id: u32,
763 depth: u32,
764 ctx: &mut DecodeCtx,
765 with_alpha: bool,
766) -> Result<ItemImage> {
767 if depth > MAX_DERIVATION_DEPTH {
768 return Err(Error::invalid(format!(
769 "avif: derivation chain deeper than {MAX_DERIVATION_DEPTH} at item {item_id}"
770 )));
771 }
772 if ctx.stack.contains(&item_id) {
773 return Err(Error::invalid(format!(
774 "avif: derivation cycle through item {item_id}"
775 )));
776 }
777 if let Some(cached) = ctx.cache.get(&(item_id, with_alpha)) {
778 return Ok(cached.clone());
779 }
780 if ctx.decodes >= MAX_ITEM_DECODES {
781 return Err(Error::invalid(format!(
782 "avif: derivation graph needs more than {MAX_ITEM_DECODES} item decodes"
783 )));
784 }
785 ctx.decodes += 1;
786 ctx.stack.push(item_id);
787 let result = decode_item_output_inner(hdr, item_id, depth, ctx, with_alpha);
788 ctx.stack.pop();
789 if let Ok(image) = &result {
790 ctx.cache.insert((item_id, with_alpha), image.clone());
791 }
792 result
793}
794
795fn decode_item_output_inner(
796 hdr: &AvifHeader<'_>,
797 item_id: u32,
798 depth: u32,
799 ctx: &mut DecodeCtx,
800 with_alpha: bool,
801) -> Result<ItemImage> {
802 let info = hdr
803 .meta
804 .item_by_id(item_id)
805 .ok_or_else(|| Error::invalid(format!("avif: item {item_id} unknown")))?;
806 let item_type = info.item_type;
807 let mut image = if item_type == ITEM_TYPE_AV01 {
808 let layer = if depth == 0 { ctx.layer } else { None };
809 decode_coded_item(hdr, item_id, layer, ctx.threads)?
810 } else if item_type == ITEM_TYPE_TMAP {
811 let (base, _gain) = tone_map_inputs(hdr, item_id)?;
812 if depth == 0 && !ctx.tone_mapped {
813 return decode_item_output(hdr, base, depth + 1, ctx, with_alpha);
818 }
819 decode_tone_map_item(hdr, item_id, depth, ctx, with_alpha)?
820 } else if item_type == ITEM_TYPE_GRID {
821 decode_grid_item(hdr, item_id, depth, ctx)?
822 } else if item_type == ITEM_TYPE_IOVL {
823 decode_overlay_item(hdr, item_id, depth, ctx)?
824 } else if item_type == ITEM_TYPE_IDEN {
825 let inputs = hdr.meta.iref_targets(&DIMG, item_id);
826 if inputs.len() != 1 {
827 return Err(Error::invalid(format!(
828 "avif: iden item {item_id} has {} dimg inputs, expected exactly 1 (HEIF §6.6.2.1)",
829 inputs.len()
830 )));
831 }
832 decode_item_output(hdr, inputs[0], depth + 1, ctx, with_alpha)?
833 } else {
834 return Err(Error::unsupported(format!(
835 "avif: item {item_id} type '{}' cannot be decoded to pixels",
836 String::from_utf8_lossy(&item_type)
837 )));
838 };
839 if with_alpha {
840 if let Some(alpha_id) = find_alpha_item_id(&hdr.meta, item_id) {
841 if image.format.has_alpha {
842 return Err(Error::unsupported(format!(
843 "avif: item {item_id} already carries alpha and has an alpha auxiliary {alpha_id}"
844 )));
845 }
846 let alpha = decode_item_output(hdr, alpha_id, depth + 1, ctx, false)?;
847 let alpha = alpha_as_gray(&alpha, image.width, image.height)?;
848 if alpha.format.bit_depth != image.format.bit_depth {
849 return Err(Error::invalid(format!(
850 "avif alpha: alpha item {alpha_id} is {}-bit but master item {item_id} is \
851 {}-bit — the auxiliary shall be encoded at the master's bit depth \
852 (av1-avif §4.1)",
853 alpha.format.bit_depth, image.format.bit_depth
854 )));
855 }
856 image = compose::attach_alpha(&image, &alpha).map_err(heif_err)?;
857 }
858 }
859 for prop in transforms_for(&hdr.meta, item_id) {
860 image = match prop {
861 Property::Clap(clap) => {
862 if clap_extent(clap, image.width, image.height).is_none() {
865 continue;
866 }
867 compose::apply_clap(&image, &hprops::Clap::from(clap))
868 }
869 Property::Irot(irot) => {
870 let turns = irot.angle & 0x03;
871 if turns != 0 {
872 let (sx, sy) = image.format.chroma.shift();
873 rotation_keeps_layout((sx as u8, sy as u8), turns).map_err(core_err)?;
874 }
875 compose::apply_irot(&image, &hprops::Irot::new(turns))
876 }
877 Property::Imir(imir) => {
878 compose::apply_imir(&image, &hprops::Imir::new(imir.axis & 0x01))
879 }
880 _ => continue,
881 }
882 .map_err(heif_err)?;
883 }
884 Ok(image)
885}
886
887fn decode_coded_item(
892 hdr: &AvifHeader<'_>,
893 item_id: u32,
894 layer: Option<u16>,
895 threads: usize,
896) -> Result<ItemImage> {
897 let bytes = hdr.item_data(item_id).map_err(core_err)?;
898 let av1c = match hdr.meta.property_for(item_id, &AV1C) {
899 Some(Property::Av1C(b)) => b.clone(),
900 _ => {
901 return Err(Error::invalid(format!(
902 "avif: item {item_id} missing av1C property"
903 )))
904 }
905 };
906 let ispe = match hdr.meta.property_for(item_id, &ISPE) {
907 Some(Property::Ispe(e)) => Some((e.width, e.height)),
908 _ => None,
909 };
910 let select = LayerSelect {
911 operating_point: match hdr.meta.property_for(item_id, b"a1op") {
912 Some(Property::A1op(a)) => Some(a.op_index),
913 _ => None,
914 },
915 layer_id: layer.or(match hdr.meta.property_for(item_id, b"lsel") {
916 Some(Property::Lsel(l)) => Some(l.layer_id),
917 _ => None,
918 }),
919 };
920 let (core_frame, fmt_core, mut w, mut h) =
921 decode_av01_item(&bytes, &av1c, ispe, select, threads)?;
922 let bit_depth = parse_av1c(&av1c)?.bit_depth();
923 let mut frame = core_to_avif_frame(core_frame);
924 let format = from_core_pix(fmt_core)?;
925 if let Some((iw, ih)) = ispe {
926 if iw > 0 && ih > 0 && iw <= w && ih <= h && (iw != w || ih != h) {
927 frame = crop_top_left(&frame, format, w, h, iw, ih).map_err(core_err)?;
928 w = iw;
929 h = ih;
930 }
931 }
932 to_heif(&frame, format, bit_depth, w, h).map_err(core_err)
933}
934
935fn decode_grid_item(
940 hdr: &AvifHeader<'_>,
941 grid_id: u32,
942 depth: u32,
943 ctx: &mut DecodeCtx,
944) -> Result<ItemImage> {
945 if hdr.meta.location_by_id(grid_id).is_none() {
946 return Err(Error::invalid("avif: grid item missing in iloc"));
947 }
948 let grid_bytes = hdr.item_data(grid_id).map_err(core_err)?;
949 let grid = ImageGrid::parse(&grid_bytes).map_err(core_err)?;
950 let tile_ids = hdr.meta.iref_targets(&DIMG, grid_id);
951 if tile_ids.is_empty() {
952 return Err(Error::invalid("avif: grid item has no dimg iref"));
953 }
954 if tile_ids.len() != grid.expected_tile_count() {
955 return Err(Error::invalid(format!(
956 "avif: grid declares {} tiles but dimg lists {}",
957 grid.expected_tile_count(),
958 tile_ids.len()
959 )));
960 }
961 if grid.output_width == 0 || grid.output_height == 0 {
962 return Err(Error::invalid("avif: grid output dimensions zero"));
963 }
964 let canvas = u64::from(grid.output_width) * u64::from(grid.output_height);
965 if canvas > MAX_GRID_CANVAS_PIXELS {
966 return Err(Error::invalid(format!(
967 "avif: grid canvas {}x{} exceeds {MAX_GRID_CANVAS_PIXELS} pixels",
968 grid.output_width, grid.output_height
969 )));
970 }
971 if canvas > ctx.max_pixels {
972 return Err(Error::ResourceExhausted(format!(
973 "avif: grid canvas {}x{} exceeds the {} pixels allowed",
974 grid.output_width, grid.output_height, ctx.max_pixels
975 )));
976 }
977 let mut tiles: Vec<HeifFrame> = Vec::with_capacity(tile_ids.len());
978 if ctx.threads > 1 && tile_ids.len() > 1 {
979 decode_tiles_parallel(hdr, &tile_ids, depth + 1, ctx)?;
980 }
981 for (i, tid) in tile_ids.iter().enumerate() {
982 let tile = decode_item_output(hdr, *tid, depth + 1, ctx, false)?;
984 if let Some(first) = tiles.first() {
985 if first.format != tile.format {
986 return Err(Error::invalid(format!(
987 "avif: grid tile {i} format {:?} differs from tile 0 {:?}",
988 tile.format, first.format
989 )));
990 }
991 if (first.width, first.height) != (tile.width, tile.height) {
992 return Err(Error::invalid(format!(
993 "avif: grid tile {i} dims {}x{} differ from tile 0 {}x{}",
994 tile.width, tile.height, first.width, first.height
995 )));
996 }
997 }
998 tiles.push(tile);
999 }
1000 crate::grid::composite_grid_frames(&grid, &tiles).map_err(core_err)
1001}
1002
1003fn decode_tiles_parallel(
1013 hdr: &AvifHeader<'_>,
1014 tile_ids: &[u32],
1015 depth: u32,
1016 ctx: &mut DecodeCtx,
1017) -> Result<()> {
1018 let mut todo: Vec<u32> = Vec::with_capacity(tile_ids.len());
1019 for &tid in tile_ids {
1020 if !todo.contains(&tid) && !ctx.cache.contains_key(&(tid, false)) {
1021 todo.push(tid);
1022 }
1023 }
1024 if todo.len() < 2 {
1025 return Ok(());
1026 }
1027 if ctx.decodes.saturating_add(todo.len() as u32) > MAX_ITEM_DECODES {
1028 return Err(Error::invalid(format!(
1029 "avif: derivation graph needs more than {MAX_ITEM_DECODES} item decodes"
1030 )));
1031 }
1032 let workers = ExecutionContext::with_threads(ctx.threads).effective_workers(todo.len());
1033 let per_tile = (ctx.threads / workers).max(1);
1034 let seed = DecodeCtx {
1035 stack: ctx.stack.clone(),
1036 cache: std::collections::HashMap::new(),
1037 decodes: 0,
1038 tone_mapped: ctx.tone_mapped,
1039 reference_white_nits: ctx.reference_white_nits,
1040 max_pixels: ctx.max_pixels,
1041 layer: None,
1042 threads: per_tile,
1043 };
1044 type TileOut = (usize, u32, Result<ItemImage>, u32);
1046 let outs: Vec<TileOut> = std::thread::scope(|scope| {
1047 let handles: Vec<_> = (0..workers)
1048 .map(|w| {
1049 let todo = &todo;
1050 let seed = &seed;
1051 scope.spawn(move || {
1052 let mut out: Vec<TileOut> = Vec::new();
1053 for (i, &tid) in todo.iter().enumerate().skip(w).step_by(workers) {
1054 let mut child = DecodeCtx {
1055 stack: seed.stack.clone(),
1056 cache: std::collections::HashMap::new(),
1057 ..*seed
1058 };
1059 let r = decode_item_output(hdr, tid, depth, &mut child, false);
1060 out.push((i, tid, r, child.decodes));
1061 }
1062 out
1063 })
1064 })
1065 .collect();
1066 let mut all = Vec::with_capacity(todo.len());
1067 for h in handles {
1068 match h.join() {
1069 Ok(v) => all.extend(v),
1070 Err(_) => all.push((
1071 usize::MAX,
1072 0,
1073 Err(Error::invalid("avif: a grid tile decode worker panicked")),
1074 0,
1075 )),
1076 }
1077 }
1078 all
1079 });
1080 let mut outs = outs;
1081 outs.sort_by_key(|o| o.0);
1082 let mut spent = 0u32;
1083 for (_, tid, r, n) in outs {
1084 spent = spent.saturating_add(n);
1085 let image = r?;
1086 ctx.cache.insert((tid, false), image);
1087 }
1088 ctx.decodes = ctx.decodes.saturating_add(spent);
1089 if ctx.decodes > MAX_ITEM_DECODES {
1090 return Err(Error::invalid(format!(
1091 "avif: derivation graph needs more than {MAX_ITEM_DECODES} item decodes"
1092 )));
1093 }
1094 Ok(())
1095}
1096
1097const COLR: BoxType = b(b"colr");
1098const PREM: BoxType = b(b"prem");
1099const PIXI: BoxType = b(b"pixi");
1100const ALTR: BoxType = b(b"altr");
1101
1102pub(crate) fn tone_map_of(hdr: &AvifHeader<'_>, primary_id: u32) -> Option<u32> {
1109 let meta = &hdr.meta;
1110 if meta.item_by_id(primary_id)?.item_type == ITEM_TYPE_TMAP {
1111 return Some(primary_id);
1112 }
1113 let candidates: Vec<u32> = meta
1114 .items
1115 .iter()
1116 .filter(|i| i.item_type == ITEM_TYPE_TMAP)
1117 .filter(|i| meta.iref_targets(&DIMG, i.id).first() == Some(&primary_id))
1118 .map(|i| i.id)
1119 .collect();
1120 if candidates.is_empty() {
1121 return None;
1122 }
1123 let groups = meta.groups().unwrap_or_default();
1124 candidates
1125 .iter()
1126 .copied()
1127 .find(|&t| {
1128 groups.iter().any(|g| {
1129 g.grouping_type == ALTR
1130 && g.entity_ids.contains(&t)
1131 && g.entity_ids.contains(&primary_id)
1132 })
1133 })
1134 .or(candidates.first().copied())
1135}
1136
1137fn tone_map_inputs(hdr: &AvifHeader<'_>, tmap_id: u32) -> Result<(u32, u32)> {
1141 let inputs = hdr.meta.iref_targets(&DIMG, tmap_id);
1142 match inputs.as_slice() {
1143 [base, gain] => Ok((*base, *gain)),
1144 other => Err(Error::invalid(format!(
1145 "avif: tmap item {tmap_id} has {} dimg inputs, expected exactly 2 (base, gain map) \
1146 — HEIF Amd 1 §6.6.2.4.1",
1147 other.len()
1148 ))),
1149 }
1150}
1151
1152fn container_colr(hdr: &AvifHeader<'_>, item_id: u32) -> Option<hprops::Colr> {
1154 match hdr.meta.property_for(item_id, &COLR) {
1155 Some(Property::Colr(c)) => Some(hprops::Colr::from(c)),
1156 _ => None,
1157 }
1158}
1159
1160fn decode_tone_map_item(
1172 hdr: &AvifHeader<'_>,
1173 tmap_id: u32,
1174 depth: u32,
1175 ctx: &mut DecodeCtx,
1176 with_alpha: bool,
1177) -> Result<ItemImage> {
1178 let (base_id, gain_id) = tone_map_inputs(hdr, tmap_id)?;
1179 if hdr.meta.location_by_id(tmap_id).is_none() {
1180 return Err(Error::invalid("avif: tmap item missing in iloc"));
1181 }
1182 let body = hdr.item_data(tmap_id).map_err(core_err)?;
1183 let metadata = GainMapMetadata::parse_tmap_body(&body).map_err(heif_err)?;
1184 let base = decode_item_output(hdr, base_id, depth + 1, ctx, with_alpha)?;
1185 let gain = decode_item_output(hdr, gain_id, depth + 1, ctx, false)?;
1186 let base_colr = container_colr(hdr, base_id);
1187 let gain_colr = container_colr(hdr, gain_id);
1188 let alternate_colr = container_colr(hdr, tmap_id);
1189 let bit_depth = match hdr.meta.property_for(tmap_id, &PIXI) {
1190 Some(Property::Pixi(p)) if p.max_bit_depth() > 0 => p.max_bit_depth(),
1191 _ => base.format.bit_depth,
1192 };
1193 let reference_white = if ctx.reference_white_nits > 0.0 {
1194 ctx.reference_white_nits
1195 } else {
1196 DEFAULT_HDR_REFERENCE_WHITE_NITS
1197 };
1198 reconstruct_tone_map(
1199 &base,
1200 base_colr.as_ref(),
1201 &gain,
1202 gain_colr.as_ref(),
1203 alternate_colr.as_ref(),
1204 &metadata,
1205 bit_depth,
1206 reference_white,
1207 )
1208 .map_err(heif_err)
1209}
1210
1211fn is_premultiplied(hdr: &AvifHeader<'_>, item_id: u32) -> bool {
1216 hdr.meta
1217 .irefs
1218 .iter()
1219 .any(|e| e.reference_type == PREM && (e.from_id == item_id || e.to_ids.contains(&item_id)))
1220}
1221
1222fn decode_overlay_item(
1227 hdr: &AvifHeader<'_>,
1228 iovl_id: u32,
1229 depth: u32,
1230 ctx: &mut DecodeCtx,
1231) -> Result<ItemImage> {
1232 if hdr.meta.location_by_id(iovl_id).is_none() {
1233 return Err(Error::invalid("avif: iovl item missing in iloc"));
1234 }
1235 let bytes = hdr.item_data(iovl_id).map_err(core_err)?;
1236 let sources = hdr.meta.iref_targets(&DIMG, iovl_id);
1237 if sources.is_empty() {
1238 return Err(Error::invalid("avif: iovl item has no dimg iref"));
1239 }
1240 let desc = ImageOverlay::parse(&bytes, sources.len()).map_err(core_err)?;
1241 let area = u64::from(desc.output_width) * u64::from(desc.output_height);
1242 if desc.output_width == 0 || desc.output_height == 0 {
1243 return Err(Error::invalid("avif: iovl canvas dimensions zero"));
1244 }
1245 if area > crate::overlay::MAX_OVERLAY_CANVAS_PIXELS {
1246 return Err(Error::invalid(format!(
1247 "avif: iovl canvas {}x{} exceeds {} pixels",
1248 desc.output_width,
1249 desc.output_height,
1250 crate::overlay::MAX_OVERLAY_CANVAS_PIXELS
1251 )));
1252 }
1253 if area > ctx.max_pixels {
1254 return Err(Error::ResourceExhausted(format!(
1255 "avif: iovl canvas {}x{} exceeds the {} pixels allowed",
1256 desc.output_width, desc.output_height, ctx.max_pixels
1257 )));
1258 }
1259 let mut inputs: Vec<(HeifFrame, bool)> = Vec::with_capacity(sources.len());
1260 for (i, &src) in sources.iter().enumerate() {
1261 let image = decode_item_output(hdr, src, depth + 1, ctx, true)?;
1262 if let Some((first, _)) = inputs.first() {
1263 if (first.format.chroma, first.format.bit_depth)
1264 != (image.format.chroma, image.format.bit_depth)
1265 {
1266 return Err(Error::unsupported(format!(
1267 "avif overlay: input {i} layout ({}-bit {:?}) differs from input 0 \
1268 ({}-bit {:?}) — mixed layouts are not composited",
1269 image.format.bit_depth,
1270 image.format.chroma,
1271 first.format.bit_depth,
1272 first.format.chroma
1273 )));
1274 }
1275 }
1276 inputs.push((image, is_premultiplied(hdr, src)));
1277 }
1278 let colr = match hdr.meta.property_for(iovl_id, &COLR) {
1279 Some(Property::Colr(c)) => Some(c.clone()),
1280 _ => match hdr.meta.property_for(sources[0], &COLR) {
1281 Some(Property::Colr(c)) => Some(c.clone()),
1282 _ => None,
1283 },
1284 };
1285 let colr = effective_cicp(colr.as_ref()).to_colr();
1286 let overlay_inputs: Vec<compose::OverlayInput<'_>> = inputs
1287 .iter()
1288 .map(|(frame, premultiplied)| compose::OverlayInput::new(frame, *premultiplied))
1289 .collect();
1290 crate::overlay::composite_overlay_frames(&desc, &overlay_inputs, &colr).map_err(core_err)
1291}
1292
1293fn alpha_as_gray(alpha: &ItemImage, master_w: u32, master_h: u32) -> Result<ItemImage> {
1300 let depth = alpha.format.bit_depth;
1301 let bps = alpha.format.bytes_per_sample();
1302 let (aw, ah) = (alpha.width, alpha.height);
1303 let luma = &alpha.planes[0];
1304 let mut data = Vec::with_capacity(master_w as usize * master_h as usize * bps);
1305 if (aw, ah) == (master_w, master_h) {
1306 for r in 0..ah as usize {
1307 let s = r * luma.stride;
1308 data.extend_from_slice(&luma.data[s..s + aw as usize * bps]);
1309 }
1310 } else {
1311 for r in 0..master_h {
1312 let sr = (u64::from(r) * u64::from(ah) / u64::from(master_h)) as usize;
1313 let row = sr * luma.stride;
1314 for c in 0..master_w {
1315 let sc = (u64::from(c) * u64::from(aw) / u64::from(master_w)) as usize;
1316 data.extend_from_slice(&luma.data[row + sc * bps..row + (sc + 1) * bps]);
1317 }
1318 }
1319 }
1320 let format = HeifPixelFormat::new(Chroma::Mono, depth, false).map_err(heif_err)?;
1321 let out = HeifFrame {
1322 width: master_w,
1323 height: master_h,
1324 format,
1325 planes: vec![Plane {
1326 stride: master_w as usize * bps,
1327 data,
1328 }],
1329 };
1330 out.validate().map_err(heif_err)?;
1331 Ok(out)
1332}
1333
1334impl Decoder for AvifDecoder {
1335 fn codec_id(&self) -> &CodecId {
1336 &self.codec_id
1337 }
1338
1339 fn send_packet(&mut self, packet: &Packet) -> Result<()> {
1340 let (ftyp_payload, _) = crate::box_parser::find_box(&packet.data, &b(b"ftyp"))
1354 .map_err(core_err)?
1355 .ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
1356 let (major_brand, _minor, compatible_brands) =
1357 crate::parser::parse_ftyp(ftyp_payload).map_err(core_err)?;
1358 let brands = classify_brands(&major_brand, &compatible_brands).map_err(core_err)?;
1359 if brands.is_sequence || brands.has_msf1 {
1360 if crate::box_parser::find_box(&packet.data, &b(b"moov"))
1363 .ok()
1364 .flatten()
1365 .is_some()
1366 {
1367 self.decode_avis_file(&packet.data).map(|_| ())
1368 } else {
1369 self.decode_file(&packet.data).map(|_| ())
1370 }
1371 } else {
1372 self.decode_file(&packet.data).map(|_| ())
1373 }
1374 }
1375
1376 fn receive_frame(&mut self) -> Result<Frame> {
1377 if self.pending.is_empty() {
1378 return Err(Error::NeedMore);
1379 }
1380 Ok(self.pending.remove(0))
1381 }
1382
1383 fn flush(&mut self) -> Result<()> {
1384 Ok(())
1385 }
1386
1387 fn reset(&mut self) -> Result<()> {
1388 self.pending.clear();
1389 self.info = None;
1390 self.color_signal = None;
1391 self.output_format = None;
1392 Ok(())
1393 }
1394
1395 fn set_execution_context(&mut self, ctx: &ExecutionContext) {
1399 self.exec_threads = ctx.threads.max(1);
1400 }
1401
1402 fn output_pixel_format(&self) -> Option<PixelFormat> {
1406 self.output_format
1407 }
1408}
1409
1410#[non_exhaustive]
1414#[derive(Clone, Copy, Debug, PartialEq)]
1415pub struct AvifDecoderOptions {
1416 pub tone_mapped: bool,
1419 pub reference_white: f64,
1421 pub threads: u32,
1424 pub max_width: u32,
1426 pub max_height: u32,
1428 pub max_pixels: u32,
1430 pub max_bytes: u32,
1432 pub strict: bool,
1434 pub layer: i32,
1436}
1437
1438impl Default for AvifDecoderOptions {
1439 fn default() -> Self {
1440 let d = DecodeOptions::default();
1441 Self {
1442 tone_mapped: d.tone_mapped,
1443 reference_white: d.reference_white_nits,
1444 threads: 0,
1445 max_width: d.max_width.unwrap_or(0),
1446 max_height: d.max_height.unwrap_or(0),
1447 max_pixels: d
1448 .max_pixels
1449 .map(|p| p.min(u64::from(u32::MAX)) as u32)
1450 .unwrap_or(0),
1451 max_bytes: d
1452 .max_bytes
1453 .map(|p| p.min(u64::from(u32::MAX)) as u32)
1454 .unwrap_or(0),
1455 strict: d.strict,
1456 layer: -1,
1457 }
1458 }
1459}
1460
1461impl AvifDecoderOptions {
1462 pub fn new() -> Self {
1465 Self::default()
1466 }
1467
1468 pub fn apply_to(&self, mut o: DecodeOptions) -> DecodeOptions {
1470 let nz32 = |v: u32| (v > 0).then_some(v);
1471 o.tone_mapped = self.tone_mapped;
1472 o.reference_white_nits = self.reference_white;
1473 o.threads = (self.threads > 0).then_some(self.threads as usize);
1474 o.max_width = nz32(self.max_width);
1475 o.max_height = nz32(self.max_height);
1476 o.max_pixels = nz32(self.max_pixels).map(u64::from);
1477 o.max_bytes = nz32(self.max_bytes).map(u64::from);
1478 o.strict = self.strict;
1479 o.layer = u16::try_from(self.layer).ok();
1480 o
1481 }
1482}
1483
1484impl oxideav_core::CodecOptionsStruct for AvifDecoderOptions {
1485 const SCHEMA: &'static [oxideav_core::OptionField] = &[
1486 oxideav_core::OptionField {
1487 name: "gain_map",
1488 kind: oxideav_core::OptionKind::Enum(&["base", "apply"]),
1489 default: oxideav_core::OptionValue::String(String::new()),
1490 help: "base (default): the primary as readers without tone-map support show it; apply: the tmap alternative with its gain map applied",
1491 },
1492 oxideav_core::OptionField {
1493 name: "reference_white",
1494 kind: oxideav_core::OptionKind::F32,
1495 default: oxideav_core::OptionValue::F32(203.0),
1496 help: "HDR reference white in cd/m2 for a PQ-coded tone-mapped reconstruction (default 203)",
1497 },
1498 oxideav_core::OptionField {
1499 name: "threads",
1500 kind: oxideav_core::OptionKind::U32,
1501 default: oxideav_core::OptionValue::U32(0),
1502 help: "AV1 decode worker budget (tile / band / wave / stripe parallel, byte-identical); 0 = the execution context's budget (serial unless granted)",
1503 },
1504 oxideav_core::OptionField {
1505 name: "max_width",
1506 kind: oxideav_core::OptionKind::U32,
1507 default: oxideav_core::OptionValue::U32(16_384),
1508 help: "Refuse a picture wider than this (0 = unlimited)",
1509 },
1510 oxideav_core::OptionField {
1511 name: "max_height",
1512 kind: oxideav_core::OptionKind::U32,
1513 default: oxideav_core::OptionValue::U32(16_384),
1514 help: "Refuse a picture taller than this (0 = unlimited)",
1515 },
1516 oxideav_core::OptionField {
1517 name: "max_pixels",
1518 kind: oxideav_core::OptionKind::U32,
1519 default: oxideav_core::OptionValue::U32(1 << 28),
1520 help: "Refuse a picture or derived canvas with more pixels than this (0 = unlimited)",
1521 },
1522 oxideav_core::OptionField {
1523 name: "max_bytes",
1524 kind: oxideav_core::OptionKind::U32,
1525 default: oxideav_core::OptionValue::U32(1 << 30),
1526 help: "Refuse an input longer than this many bytes (0 = unlimited)",
1527 },
1528 oxideav_core::OptionField {
1529 name: "strict",
1530 kind: oxideav_core::OptionKind::Bool,
1531 default: oxideav_core::OptionValue::Bool(false),
1532 help: "Strict conformance: an AVIF brand is required and a claimed mif1 must carry its mandatory boxes",
1533 },
1534 oxideav_core::OptionField {
1535 name: "layer",
1536 kind: oxideav_core::OptionKind::I32,
1537 default: oxideav_core::OptionValue::I32(-1),
1538 help: "Spatial layer of a layered primary to render (-1 = the file's lsel)",
1539 },
1540 ];
1541
1542 fn apply(&mut self, key: &str, value: &oxideav_core::OptionValue) -> Result<()> {
1543 match key {
1544 "gain_map" => self.tone_mapped = value.as_str()? == "apply",
1545 "reference_white" => self.reference_white = f64::from(value.as_f32()?),
1546 "threads" => self.threads = value.as_u32()?,
1547 "max_width" => self.max_width = value.as_u32()?,
1548 "max_height" => self.max_height = value.as_u32()?,
1549 "max_pixels" => self.max_pixels = value.as_u32()?,
1550 "max_bytes" => self.max_bytes = value.as_u32()?,
1551 "strict" => self.strict = value.as_bool()?,
1552 "layer" => {
1553 let l = value.as_i32()?;
1554 if l < -1 || l > i32::from(u16::MAX) {
1555 return Err(Error::invalid(format!(
1556 "avif: layer {l} is outside -1..=65535"
1557 )));
1558 }
1559 self.layer = l;
1560 }
1561 other => return Err(Error::invalid(format!("unknown option '{other}'"))),
1562 }
1563 Ok(())
1564 }
1565}
1566
1567pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
1568 Ok(Box::new(AvifDecoder::with_params(params)?))
1569}
1570
1571#[cfg(test)]
1572mod tests {
1573 use super::*;
1574
1575 const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/monochrome.avif");
1576
1577 #[test]
1580 fn validate_av1_config_rejects_high_profile() {
1581 let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
1582 cfg.seq_profile = 3;
1583 let err = validate_av1_config(&cfg).unwrap_err();
1584 match err {
1585 Error::InvalidData(s) => assert!(s.contains("seq_profile"), "got: {s}"),
1586 other => panic!("expected InvalidData, got {other:?}"),
1587 }
1588 }
1589
1590 #[test]
1593 fn validate_av1_config_rejects_reserved_level() {
1594 let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
1595 cfg.seq_level_idx_0 = 27;
1596 let err = validate_av1_config(&cfg).unwrap_err();
1597 match err {
1598 Error::InvalidData(s) => assert!(s.contains("seq_level_idx_0"), "got: {s}"),
1599 other => panic!("expected InvalidData, got {other:?}"),
1600 }
1601 }
1602
1603 #[test]
1607 fn validate_av1_config_rejects_monochrome_without_subsampling() {
1608 let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
1609 cfg.monochrome = true;
1610 cfg.chroma_subsampling_x = false;
1611 cfg.chroma_subsampling_y = true;
1612 let err = validate_av1_config(&cfg).unwrap_err();
1613 match err {
1614 Error::InvalidData(s) => assert!(s.contains("monochrome"), "got: {s}"),
1615 other => panic!("expected InvalidData, got {other:?}"),
1616 }
1617 }
1618
1619 #[test]
1622 fn validate_av1_config_rejects_422_outside_profile_2() {
1623 let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
1624 cfg.seq_profile = 0;
1625 cfg.chroma_subsampling_x = true;
1626 cfg.chroma_subsampling_y = false;
1627 let err = validate_av1_config(&cfg).unwrap_err();
1628 match err {
1629 Error::InvalidData(s) => assert!(s.contains("4:2:2"), "got: {s}"),
1630 other => panic!("expected InvalidData, got {other:?}"),
1631 }
1632 }
1633
1634 #[test]
1637 fn validate_av1_config_rejects_444_in_profile_0() {
1638 let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
1639 cfg.seq_profile = 0;
1640 cfg.chroma_subsampling_x = false;
1641 cfg.chroma_subsampling_y = false;
1642 let err = validate_av1_config(&cfg).unwrap_err();
1643 match err {
1644 Error::InvalidData(s) => assert!(s.contains("4:4:4"), "got: {s}"),
1645 other => panic!("expected InvalidData, got {other:?}"),
1646 }
1647 }
1648
1649 #[test]
1653 fn validate_av1_config_accepts_canonical_420_profile0() {
1654 let cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
1657 validate_av1_config(&cfg).expect("canonical 4:2:0 / profile 0 must validate");
1658 }
1659
1660 #[test]
1661 fn decoder_decodes_monochrome_fixture_end_to_end() {
1662 let mut d = AvifDecoder::new(CodecId::new(crate::CODEC_ID_STR));
1667 let pkt = Packet::new(0, TimeBase::new(1, 1), FIXTURE.to_vec());
1668 d.send_packet(&pkt).expect("send_packet");
1669 let frame = d.receive_frame().expect("receive_frame");
1670 let vf = match frame {
1671 Frame::Video(v) => v,
1672 other => panic!("expected VideoFrame, got {other:?}"),
1673 };
1674 assert_eq!(vf.image_plane_count(), 1);
1677 let y = &vf.planes[0];
1678 assert_eq!(y.stride, 1280);
1679 assert_eq!(y.data.len() / y.stride, 720);
1680 }
1681}