1pub mod animated;
12mod boxes;
13pub mod constants;
14pub mod grid;
15mod writer;
16
17use crate::boxes::*;
18use arrayvec::ArrayVec;
19use std::io;
20
21pub use crate::boxes::{Av1CBox, ClapBox, ClliBox, ColrBox, ColrIccBox, IrotBox, ImirBox, MdcvBox, PaspBox};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ChromaSubsampling {
31 pub horizontal: bool,
33 pub vertical: bool,
35}
36
37impl ChromaSubsampling {
38 pub const NONE: Self = Self { horizontal: false, vertical: false };
40 pub const YUV420: Self = Self { horizontal: true, vertical: true };
42 pub const YUV422: Self = Self { horizontal: true, vertical: false };
44}
45
46impl From<(bool, bool)> for ChromaSubsampling {
47 fn from((h, v): (bool, bool)) -> Self {
48 Self { horizontal: h, vertical: v }
49 }
50}
51
52impl From<ChromaSubsampling> for (bool, bool) {
53 fn from(cs: ChromaSubsampling) -> Self {
54 (cs.horizontal, cs.vertical)
55 }
56}
57
58pub struct Aviffy {
62 premultiplied_alpha: bool,
63 colr: ColrBox,
64 clli: Option<ClliBox>,
65 mdcv: Option<MdcvBox>,
66 irot: Option<IrotBox>,
67 imir: Option<ImirBox>,
68 clap: Option<ClapBox>,
69 pasp: Option<PaspBox>,
70 icc_profile: Option<Vec<u8>>,
71 min_seq_profile: u8,
72 chroma_subsampling: ChromaSubsampling,
73 monochrome: bool,
74 width: u32,
75 height: u32,
76 bit_depth: u8,
77 exif: Option<Vec<u8>>,
78 xmp: Option<Vec<u8>>,
79 gain_map: Option<GainMapConfig>,
80}
81
82struct GainMapConfig {
87 av1_data: Vec<u8>,
89 width: u32,
91 height: u32,
93 bit_depth: u8,
95 metadata: Vec<u8>,
97 alt_colr: Option<ColrBox>,
100 chroma_subsampling: ChromaSubsampling,
102 monochrome: bool,
104}
105
106pub fn serialize<W: io::Write>(into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<()> {
124 Aviffy::new()
125 .set_width(width)
126 .set_height(height)
127 .set_bit_depth(depth_bits)
128 .write_slice(into_output, color_av1_data, alpha_av1_data)
129}
130
131impl Default for Aviffy {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137impl Aviffy {
138 #[inline]
142 #[must_use]
143 pub fn new() -> Self {
144 Self {
145 premultiplied_alpha: false,
146 min_seq_profile: 1,
147 chroma_subsampling: ChromaSubsampling::NONE,
148 monochrome: false,
149 width: 0,
150 height: 0,
151 bit_depth: 0,
152 colr: ColrBox::default(),
153 clli: None,
154 mdcv: None,
155 irot: None,
156 imir: None,
157 clap: None,
158 pasp: None,
159 icc_profile: None,
160 exif: None,
161 xmp: None,
162 gain_map: None,
163 }
164 }
165
166 #[inline]
170 pub fn set_matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
171 self.colr.matrix_coefficients = matrix_coefficients;
172 self
173 }
174
175 #[doc(hidden)]
176 pub fn matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
177 self.set_matrix_coefficients(matrix_coefficients)
178 }
179
180 #[inline]
183 pub fn set_transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
184 self.colr.transfer_characteristics = transfer_characteristics;
185 self
186 }
187
188 #[doc(hidden)]
189 pub fn transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
190 self.set_transfer_characteristics(transfer_characteristics)
191 }
192
193 #[inline]
196 pub fn set_color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
197 self.colr.color_primaries = color_primaries;
198 self
199 }
200
201 #[doc(hidden)]
202 pub fn color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
203 self.set_color_primaries(color_primaries)
204 }
205
206 #[inline]
209 pub fn set_full_color_range(&mut self, full_range: bool) -> &mut Self {
210 self.colr.full_range_flag = full_range;
211 self
212 }
213
214 #[doc(hidden)]
215 pub fn full_color_range(&mut self, full_range: bool) -> &mut Self {
216 self.set_full_color_range(full_range)
217 }
218
219 #[inline]
226 pub fn set_content_light_level(&mut self, max_content_light_level: u16, max_pic_average_light_level: u16) -> &mut Self {
227 self.clli = Some(ClliBox {
228 max_content_light_level,
229 max_pic_average_light_level,
230 });
231 self
232 }
233
234 #[inline]
245 pub fn set_mastering_display(&mut self, primaries: [(u16, u16); 3], white_point: (u16, u16), max_luminance: u32, min_luminance: u32) -> &mut Self {
246 self.mdcv = Some(MdcvBox {
247 primaries,
248 white_point,
249 max_luminance,
250 min_luminance,
251 });
252 self
253 }
254
255 #[inline]
260 pub fn set_rotation(&mut self, angle: u8) -> &mut Self {
261 self.irot = Some(IrotBox { angle: angle & 0x03 });
262 self
263 }
264
265 #[inline]
271 pub fn set_mirror(&mut self, axis: u8) -> &mut Self {
272 self.imir = Some(ImirBox { axis: axis & 0x01 });
273 self
274 }
275
276 #[inline]
281 pub fn set_clean_aperture(&mut self, clap: ClapBox) -> &mut Self {
282 self.clap = Some(clap);
283 self
284 }
285
286 #[inline]
291 pub fn set_pixel_aspect_ratio(&mut self, h_spacing: u32, v_spacing: u32) -> &mut Self {
292 self.pasp = Some(PaspBox { h_spacing, v_spacing });
293 self
294 }
295
296 #[inline]
300 pub fn set_xmp(&mut self, xmp: Vec<u8>) -> &mut Self {
301 self.xmp = Some(xmp);
302 self
303 }
304
305 #[inline]
311 pub fn set_icc_profile(&mut self, icc_data: Vec<u8>) -> &mut Self {
312 self.icc_profile = Some(icc_data);
313 self
314 }
315
316 #[inline]
330 pub fn set_gain_map(&mut self, av1_data: Vec<u8>, width: u32, height: u32, bit_depth: u8, metadata: Vec<u8>) -> &mut Self {
331 self.gain_map = Some(GainMapConfig {
332 av1_data,
333 width,
334 height,
335 bit_depth,
336 metadata,
337 alt_colr: None,
338 chroma_subsampling: ChromaSubsampling::YUV420,
339 monochrome: false,
340 });
341 self
342 }
343
344 #[inline]
350 pub fn set_gain_map_alt_colr(&mut self, colr: ColrBox) -> &mut Self {
351 if let Some(ref mut gm) = self.gain_map {
352 gm.alt_colr = Some(colr);
353 }
354 self
355 }
356
357 #[inline]
362 pub fn set_gain_map_chroma_subsampling(&mut self, subsampling: impl Into<ChromaSubsampling>) -> &mut Self {
363 if let Some(ref mut gm) = self.gain_map {
364 gm.chroma_subsampling = subsampling.into();
365 }
366 self
367 }
368
369 #[inline]
374 pub fn set_gain_map_monochrome(&mut self, monochrome: bool) -> &mut Self {
375 if let Some(ref mut gm) = self.gain_map {
376 gm.monochrome = monochrome;
377 }
378 self
379 }
380
381 #[inline]
397 pub fn write<W: io::Write>(&self, into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<()> {
398 self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits)?.write(into_output)
399 }
400
401 #[inline]
403 pub fn write_slice<W: io::Write>(&self, into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>) -> io::Result<()> {
404 self.make_boxes(color_av1_data, alpha_av1_data, self.width, self.height, self.bit_depth)?.write(into_output)
405 }
406
407 fn make_boxes<'data>(&'data self, color_av1_data: &'data [u8], alpha_av1_data: Option<&'data [u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<AvifFile<'data>> {
408 if ![8, 10, 12].contains(&depth_bits) {
409 return Err(io::Error::new(io::ErrorKind::InvalidInput, "depth must be 8/10/12"));
410 }
411
412 let mut image_items = ArrayVec::new();
413 let mut iloc_items = ArrayVec::new();
414 let mut ipma_entries = ArrayVec::new();
415 let mut irefs = ArrayVec::new();
416 let mut multi_irefs: ArrayVec<IrefMultiEntryBox, 2> = ArrayVec::new();
417 let mut ipco = IpcoBox::new();
418 let color_image_id: u16 = 1;
419 let alpha_image_id: u16 = 2;
420 let mut next_item_id: u16 = 3;
421 const ESSENTIAL_BIT: u8 = 0x80;
422 let color_depth_bits = depth_bits;
423 let alpha_depth_bits = depth_bits; image_items.push(InfeBox {
426 id: color_image_id,
427 typ: FourCC(*b"av01"),
428 name: "",
429 content_type: "",
430 });
431
432 let ispe_prop = ipco.push(IpcoProp::Ispe(IspeBox { width, height })).ok_or(io::ErrorKind::InvalidInput)?;
433
434 let av1c_color_prop = ipco.push(IpcoProp::Av1C(Av1CBox {
436 seq_profile: self.min_seq_profile.max(if color_depth_bits >= 12 { 2 } else { 0 }),
437 seq_level_idx_0: 31,
438 seq_tier_0: false,
439 high_bitdepth: color_depth_bits >= 10,
440 twelve_bit: color_depth_bits >= 12,
441 monochrome: self.monochrome,
442 chroma_subsampling_x: self.chroma_subsampling.horizontal,
443 chroma_subsampling_y: self.chroma_subsampling.vertical,
444 chroma_sample_position: 0,
445 })).ok_or(io::ErrorKind::InvalidInput)?;
446
447 let pixi_3 = ipco.push(IpcoProp::Pixi(PixiBox {
449 channels: 3,
450 depth: color_depth_bits,
451 })).ok_or(io::ErrorKind::InvalidInput)?;
452
453 let mut ipma = IpmaEntry {
454 item_id: color_image_id,
455 prop_ids: from_array([ispe_prop, av1c_color_prop | ESSENTIAL_BIT, pixi_3]),
456 };
457
458 if let Some(ref icc_data) = self.icc_profile {
460 let colr_icc_prop = ipco.push(IpcoProp::ColrIcc(ColrIccBox {
461 icc_data: icc_data.clone(),
462 })).ok_or(io::ErrorKind::InvalidInput)?;
463 ipma.prop_ids.push(colr_icc_prop);
464 } else if self.colr != ColrBox::default() {
465 let colr_color_prop = ipco.push(IpcoProp::Colr(self.colr)).ok_or(io::ErrorKind::InvalidInput)?;
467 ipma.prop_ids.push(colr_color_prop);
468 }
469
470 if let Some(clli) = self.clli {
471 let clli_prop = ipco.push(IpcoProp::Clli(clli)).ok_or(io::ErrorKind::InvalidInput)?;
472 ipma.prop_ids.push(clli_prop);
473 }
474
475 if let Some(mdcv) = self.mdcv {
476 let mdcv_prop = ipco.push(IpcoProp::Mdcv(mdcv)).ok_or(io::ErrorKind::InvalidInput)?;
477 ipma.prop_ids.push(mdcv_prop);
478 }
479
480 if let Some(irot) = self.irot {
481 let irot_prop = ipco.push(IpcoProp::Irot(irot)).ok_or(io::ErrorKind::InvalidInput)?;
482 ipma.prop_ids.push(irot_prop | ESSENTIAL_BIT);
483 }
484
485 if let Some(imir) = self.imir {
486 let imir_prop = ipco.push(IpcoProp::Imir(imir)).ok_or(io::ErrorKind::InvalidInput)?;
487 ipma.prop_ids.push(imir_prop | ESSENTIAL_BIT);
488 }
489
490 if let Some(clap) = self.clap {
491 let clap_prop = ipco.push(IpcoProp::Clap(clap)).ok_or(io::ErrorKind::InvalidInput)?;
492 ipma.prop_ids.push(clap_prop | ESSENTIAL_BIT);
493 }
494
495 if let Some(pasp) = self.pasp {
496 let pasp_prop = ipco.push(IpcoProp::Pasp(pasp)).ok_or(io::ErrorKind::InvalidInput)?;
497 ipma.prop_ids.push(pasp_prop);
498 }
499
500 ipma_entries.push(ipma);
501
502 if let Some(exif_data) = self.exif.as_deref() {
503 let exif_id = next_item_id;
504 next_item_id += 1;
505
506 image_items.push(InfeBox {
507 id: exif_id,
508 typ: FourCC(*b"Exif"),
509 name: "",
510 content_type: "",
511 });
512
513 iloc_items.push(IlocItem {
514 id: exif_id,
515 extents: [IlocExtent { data: exif_data }],
516 });
517
518 irefs.push(IrefEntryBox {
519 from_id: exif_id,
520 to_id: color_image_id,
521 typ: FourCC(*b"cdsc"),
522 });
523 }
524
525 if let Some(xmp_data) = self.xmp.as_deref() {
526 let xmp_id = next_item_id;
527 next_item_id += 1;
528
529 image_items.push(InfeBox {
530 id: xmp_id,
531 typ: FourCC(*b"mime"),
532 name: "",
533 content_type: "application/rdf+xml",
534 });
535
536 iloc_items.push(IlocItem {
537 id: xmp_id,
538 extents: [IlocExtent { data: xmp_data }],
539 });
540
541 irefs.push(IrefEntryBox {
542 from_id: xmp_id,
543 to_id: color_image_id,
544 typ: FourCC(*b"cdsc"),
545 });
546 }
547
548 if let Some(alpha_data) = alpha_av1_data {
549 image_items.push(InfeBox {
550 id: alpha_image_id,
551 typ: FourCC(*b"av01"),
552 name: "",
553 content_type: "",
554 });
555
556 irefs.push(IrefEntryBox {
557 from_id: alpha_image_id,
558 to_id: color_image_id,
559 typ: FourCC(*b"auxl"),
560 });
561
562 if self.premultiplied_alpha {
563 irefs.push(IrefEntryBox {
564 from_id: color_image_id,
565 to_id: alpha_image_id,
566 typ: FourCC(*b"prem"),
567 });
568 }
569
570 let av1c_alpha_prop = ipco.push(boxes::IpcoProp::Av1C(Av1CBox {
571 seq_profile: if alpha_depth_bits >= 12 { 2 } else { 0 },
572 seq_level_idx_0: 31,
573 seq_tier_0: false,
574 high_bitdepth: alpha_depth_bits >= 10,
575 twelve_bit: alpha_depth_bits >= 12,
576 monochrome: true,
577 chroma_subsampling_x: true,
578 chroma_subsampling_y: true,
579 chroma_sample_position: 0,
580 })).ok_or(io::ErrorKind::InvalidInput)?;
581
582 let pixi_1 = ipco.push(IpcoProp::Pixi(PixiBox {
584 channels: 1,
585 depth: alpha_depth_bits,
586 })).ok_or(io::ErrorKind::InvalidInput)?;
587
588 let auxc_prop = ipco.push(IpcoProp::AuxC(AuxCBox {
590 urn: "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
591 })).ok_or(io::ErrorKind::InvalidInput)?;
592
593 ipma_entries.push(IpmaEntry {
594 item_id: alpha_image_id,
595 prop_ids: from_array([ispe_prop, av1c_alpha_prop | ESSENTIAL_BIT, auxc_prop, pixi_1]),
596 });
597
598 iloc_items.push(IlocItem {
601 id: alpha_image_id,
602 extents: [IlocExtent { data: alpha_data }],
603 });
604 }
605 if let Some(ref gm) = self.gain_map {
607 let gm_depth = gm.bit_depth;
608 let gain_map_id = next_item_id;
609 next_item_id += 1;
610 let tmap_id = next_item_id;
611 next_item_id += 1;
612 let _ = next_item_id;
613
614 image_items.push(InfeBox {
616 id: gain_map_id,
617 typ: FourCC(*b"av01"),
618 name: "",
619 content_type: "",
620 });
621
622 let gm_ispe = ipco.push(IpcoProp::Ispe(IspeBox {
624 width: gm.width,
625 height: gm.height,
626 })).ok_or(io::ErrorKind::InvalidInput)?;
627
628 let gm_av1c = ipco.push(IpcoProp::Av1C(Av1CBox {
630 seq_profile: if gm_depth >= 12 { 2 } else { 0 },
631 seq_level_idx_0: 31,
632 seq_tier_0: false,
633 high_bitdepth: gm_depth >= 10,
634 twelve_bit: gm_depth >= 12,
635 monochrome: gm.monochrome,
636 chroma_subsampling_x: gm.chroma_subsampling.horizontal,
637 chroma_subsampling_y: gm.chroma_subsampling.vertical,
638 chroma_sample_position: 0,
639 })).ok_or(io::ErrorKind::InvalidInput)?;
640
641 ipma_entries.push(IpmaEntry {
642 item_id: gain_map_id,
643 prop_ids: from_array([gm_ispe, gm_av1c | ESSENTIAL_BIT]),
644 });
645
646 iloc_items.push(IlocItem {
648 id: gain_map_id,
649 extents: [IlocExtent { data: &gm.av1_data }],
650 });
651
652 image_items.push(InfeBox {
654 id: tmap_id,
655 typ: FourCC(*b"tmap"),
656 name: "",
657 content_type: "",
658 });
659
660 if let Some(alt_colr) = gm.alt_colr {
662 let tmap_colr = ipco.push(IpcoProp::Colr(alt_colr)).ok_or(io::ErrorKind::InvalidInput)?;
663 ipma_entries.push(IpmaEntry {
664 item_id: tmap_id,
665 prop_ids: from_array([tmap_colr]),
666 });
667 }
668
669 iloc_items.push(IlocItem {
671 id: tmap_id,
672 extents: [IlocExtent { data: &gm.metadata }],
673 });
674
675 let mut to_ids = ArrayVec::new();
679 to_ids.push(color_image_id);
680 to_ids.push(gain_map_id);
681 multi_irefs.push(IrefMultiEntryBox {
682 from_id: tmap_id,
683 to_ids,
684 typ: FourCC(*b"dimg"),
685 });
686 }
687
688 iloc_items.push(IlocItem {
689 id: color_image_id,
690 extents: [IlocExtent { data: color_av1_data }],
691 });
692
693 Ok(AvifFile {
694 ftyp: FtypBox {
695 major_brand: FourCC(*b"avif"),
696 minor_version: 0,
697 compatible_brands: [FourCC(*b"mif1"), FourCC(*b"miaf")].into(),
698 },
699 meta: MetaBox {
700 hdlr: HdlrBox {},
701 iinf: IinfBox { items: image_items },
702 pitm: PitmBox(color_image_id),
703 iloc: IlocBox {
704 absolute_offset_start: None,
705 items: iloc_items,
706 },
707 iprp: IprpBox {
708 ipco,
709 ipma: IpmaBox { entries: ipma_entries },
712 },
713 iref: IrefBox { entries: irefs, multi_entries: multi_irefs },
714 },
715 mdat: MdatBox,
718 })
719 }
720
721 #[must_use]
723 #[track_caller]
724 pub fn to_vec(&self, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
725 let mut file = self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits).unwrap();
726 let mut out = Vec::new();
727 file.write_to_vec(&mut out).unwrap();
728 out
729 }
730
731 #[inline]
738 pub fn set_chroma_subsampling(&mut self, subsampling: impl Into<ChromaSubsampling>) -> &mut Self {
739 self.chroma_subsampling = subsampling.into();
740 self
741 }
742
743 #[inline]
746 pub fn set_monochrome(&mut self, monochrome: bool) -> &mut Self {
747 self.monochrome = monochrome;
748 self
749 }
750
751 #[inline]
753 pub fn set_exif(&mut self, exif: Vec<u8>) -> &mut Self {
754 self.exif = Some(exif);
755 self
756 }
757
758 #[inline]
762 pub fn set_seq_profile(&mut self, seq_profile: u8) -> &mut Self {
763 self.min_seq_profile = seq_profile;
764 self
765 }
766
767 #[inline]
768 pub fn set_width(&mut self, width: u32) -> &mut Self {
769 self.width = width;
770 self
771 }
772
773 #[inline]
774 pub fn set_height(&mut self, height: u32) -> &mut Self {
775 self.height = height;
776 self
777 }
778
779 #[inline]
781 pub fn set_bit_depth(&mut self, bit_depth: u8) -> &mut Self {
782 self.bit_depth = bit_depth;
783 self
784 }
785
786 #[inline]
799 pub fn set_premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
800 self.premultiplied_alpha = is_premultiplied;
801 self
802 }
803
804 #[doc(hidden)]
805 pub fn premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
806 self.set_premultiplied_alpha(is_premultiplied)
807 }
808}
809
810#[inline(always)]
811fn from_array<const L1: usize, const L2: usize, T: Copy>(array: [T; L1]) -> ArrayVec<T, L2> {
812 assert!(L1 <= L2);
813 let mut tmp = ArrayVec::new_const();
814 let _ = tmp.try_extend_from_slice(&array);
815 tmp
816}
817
818#[must_use]
820#[track_caller]
821pub fn serialize_to_vec(color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
822 Aviffy::new().to_vec(color_av1_data, alpha_av1_data, width, height, depth_bits)
823}
824
825#[test]
826fn test_roundtrip_parse_mp4() {
827 let test_img = b"av12356abc";
828 let avif = serialize_to_vec(test_img, None, 10, 20, 8);
829
830 let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
831
832 assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
833}
834
835#[test]
836fn test_roundtrip_parse_mp4_alpha() {
837 let test_img = b"av12356abc";
838 let test_a = b"alpha";
839 let avif = serialize_to_vec(test_img, Some(test_a), 10, 20, 8);
840
841 let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
842
843 assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
844 assert_eq!(&test_a[..], ctx.alpha_item_coded_data().unwrap());
845}
846
847#[test]
848fn test_roundtrip_parse_exif() {
849 let test_img = b"av12356abc";
850 let test_a = b"alpha";
851 let avif = Aviffy::new()
852 .set_exif(b"lol".to_vec())
853 .to_vec(test_img, Some(test_a), 10, 20, 8);
854
855 let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
856
857 assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
858 assert_eq!(&test_a[..], ctx.alpha_item_coded_data().unwrap());
859}
860
861#[test]
862fn test_roundtrip_parse_avif() {
863 let test_img = [1, 2, 3, 4, 5, 6];
864 let test_alpha = [77, 88, 99];
865 let avif = serialize_to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
866
867 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
868
869 assert_eq!(&test_img[..], parser.primary_data().unwrap().as_ref());
870 assert_eq!(&test_alpha[..], parser.alpha_data().unwrap().unwrap().as_ref());
871}
872
873#[test]
874fn test_roundtrip_parse_avif_colr() {
875 let test_img = [1, 2, 3, 4, 5, 6];
876 let test_alpha = [77, 88, 99];
877 let avif = Aviffy::new()
878 .matrix_coefficients(constants::MatrixCoefficients::Bt709)
879 .to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
880
881 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
882
883 assert_eq!(&test_img[..], parser.primary_data().unwrap().as_ref());
884 assert_eq!(&test_alpha[..], parser.alpha_data().unwrap().unwrap().as_ref());
885}
886
887#[test]
888fn premultiplied_flag() {
889 let test_img = [1,2,3,4];
890 let test_alpha = [55,66,77,88,99];
891 let avif = Aviffy::new().premultiplied_alpha(true).to_vec(&test_img, Some(&test_alpha), 5, 5, 8);
892
893 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
894
895 assert!(parser.premultiplied_alpha());
896 assert_eq!(&test_img[..], parser.primary_data().unwrap().as_ref());
897 assert_eq!(&test_alpha[..], parser.alpha_data().unwrap().unwrap().as_ref());
898}
899
900#[test]
901fn size_required() {
902 assert!(Aviffy::new().set_bit_depth(10).write_slice(&mut vec![], &[], None).is_err());
903}
904
905#[test]
906fn depth_required() {
907 assert!(Aviffy::new().set_width(1).set_height(1).write_slice(&mut vec![], &[], None).is_err());
908}
909
910#[test]
911fn clli_roundtrip() {
912 let test_img = [1, 2, 3, 4, 5, 6];
913 let avif = Aviffy::new()
914 .set_content_light_level(1000, 400)
915 .to_vec(&test_img, None, 10, 20, 8);
916
917 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
918 let cll = parser.content_light_level().expect("clli box should be present");
919 assert_eq!(cll.max_content_light_level, 1000);
920 assert_eq!(cll.max_pic_average_light_level, 400);
921}
922
923#[test]
924fn mdcv_roundtrip() {
925 let test_img = [1, 2, 3, 4, 5, 6];
926 let primaries = [
928 (8500, 39850), (6550, 2300), (35400, 14600), ];
932 let white_point = (15635, 16450); let max_luminance = 10_000_000; let min_luminance = 1; let avif = Aviffy::new()
937 .set_mastering_display(primaries, white_point, max_luminance, min_luminance)
938 .to_vec(&test_img, None, 10, 20, 8);
939
940 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
941 let mdcv = parser.mastering_display().expect("mdcv box should be present");
942 assert_eq!(mdcv.primaries, primaries);
943 assert_eq!(mdcv.white_point, white_point);
944 assert_eq!(mdcv.max_luminance, max_luminance);
945 assert_eq!(mdcv.min_luminance, min_luminance);
946}
947
948#[test]
949fn hdr10_full_metadata() {
950 let test_img = [1, 2, 3, 4, 5, 6];
951 let test_alpha = [77, 88, 99];
952 let primaries = [
953 (8500, 39850),
954 (6550, 2300),
955 (35400, 14600),
956 ];
957 let white_point = (15635, 16450);
958
959 let avif = Aviffy::new()
960 .set_transfer_characteristics(constants::TransferCharacteristics::Smpte2084)
961 .set_color_primaries(constants::ColorPrimaries::Bt2020)
962 .set_matrix_coefficients(constants::MatrixCoefficients::Bt2020Ncl)
963 .set_content_light_level(4000, 1000)
964 .set_mastering_display(primaries, white_point, 40_000_000, 50)
965 .to_vec(&test_img, Some(&test_alpha), 10, 20, 10);
966
967 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
968
969 let cll = parser.content_light_level().expect("clli box should be present");
971 assert_eq!(cll.max_content_light_level, 4000);
972 assert_eq!(cll.max_pic_average_light_level, 1000);
973
974 let mdcv = parser.mastering_display().expect("mdcv box should be present");
976 assert_eq!(mdcv.primaries, primaries);
977 assert_eq!(mdcv.white_point, white_point);
978 assert_eq!(mdcv.max_luminance, 40_000_000);
979 assert_eq!(mdcv.min_luminance, 50);
980
981 assert_eq!(parser.primary_data().unwrap().as_ref(), &test_img[..]);
983 assert_eq!(parser.alpha_data().unwrap().unwrap().as_ref(), &test_alpha[..]);
984}
985
986#[test]
987fn no_hdr_metadata_by_default() {
988 let test_img = [1, 2, 3, 4, 5, 6];
989 let avif = serialize_to_vec(&test_img, None, 10, 20, 8);
990
991 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
992 assert!(parser.content_light_level().is_none());
993 assert!(parser.mastering_display().is_none());
994}
995
996#[test]
997fn rotation_roundtrip() {
998 let test_img = [1, 2, 3, 4, 5, 6];
999 for angle in 0..4u8 {
1000 let avif = Aviffy::new()
1001 .set_rotation(angle)
1002 .to_vec(&test_img, None, 10, 20, 8);
1003
1004 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1005 let rot = parser.rotation().expect("irot box should be present");
1006 let expected_angle = match angle {
1007 0 => 0,
1008 1 => 90,
1009 2 => 180,
1010 3 => 270,
1011 _ => unreachable!(),
1012 };
1013 assert_eq!(rot.angle, expected_angle, "angle code {angle}");
1014
1015 assert_eq!(parser.primary_data().unwrap().as_ref(), &test_img[..]);
1017 }
1018}
1019
1020#[test]
1021fn mirror_roundtrip() {
1022 let test_img = [1, 2, 3, 4, 5, 6];
1023 for axis in 0..2u8 {
1024 let avif = Aviffy::new()
1025 .set_mirror(axis)
1026 .to_vec(&test_img, None, 10, 20, 8);
1027
1028 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1029 let mir = parser.mirror().expect("imir box should be present");
1030 assert_eq!(mir.axis, axis);
1031 }
1032}
1033
1034#[test]
1035fn clap_roundtrip() {
1036 let test_img = [1, 2, 3, 4, 5, 6];
1037 let avif = Aviffy::new()
1038 .set_clean_aperture(ClapBox {
1039 width_n: 800, width_d: 1,
1040 height_n: 600, height_d: 1,
1041 horiz_off_n: 0, horiz_off_d: 1,
1042 vert_off_n: 0, vert_off_d: 1,
1043 })
1044 .to_vec(&test_img, None, 10, 20, 8);
1045
1046 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1047 let clap = parser.clean_aperture().expect("clap box should be present");
1048 assert_eq!(clap.width_n, 800);
1049 assert_eq!(clap.width_d, 1);
1050 assert_eq!(clap.height_n, 600);
1051 assert_eq!(clap.height_d, 1);
1052 assert_eq!(clap.horiz_off_n, 0);
1053 assert_eq!(clap.horiz_off_d, 1);
1054 assert_eq!(clap.vert_off_n, 0);
1055 assert_eq!(clap.vert_off_d, 1);
1056}
1057
1058#[test]
1059fn clap_with_negative_offset() {
1060 let test_img = [1, 2, 3, 4, 5, 6];
1061 let avif = Aviffy::new()
1062 .set_clean_aperture(ClapBox {
1063 width_n: 640, width_d: 1,
1064 height_n: 480, height_d: 1,
1065 horiz_off_n: -10, horiz_off_d: 1,
1066 vert_off_n: -20, vert_off_d: 1,
1067 })
1068 .to_vec(&test_img, None, 10, 20, 8);
1069
1070 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1071 let clap = parser.clean_aperture().expect("clap box should be present");
1072 assert_eq!(clap.horiz_off_n, -10);
1073 assert_eq!(clap.vert_off_n, -20);
1074}
1075
1076#[test]
1077fn pasp_roundtrip() {
1078 let test_img = [1, 2, 3, 4, 5, 6];
1079 let avif = Aviffy::new()
1080 .set_pixel_aspect_ratio(2, 1)
1081 .to_vec(&test_img, None, 10, 20, 8);
1082
1083 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1084 let pasp = parser.pixel_aspect_ratio().expect("pasp box should be present");
1085 assert_eq!(pasp.h_spacing, 2);
1086 assert_eq!(pasp.v_spacing, 1);
1087}
1088
1089#[test]
1090fn icc_profile_roundtrip() {
1091 let test_img = [1, 2, 3, 4, 5, 6];
1092 let fake_icc = vec![0x00, 0x00, 0x00, 0x18, b'a', b'c', b's', b'p',
1094 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1095 let avif = Aviffy::new()
1096 .set_icc_profile(fake_icc.clone())
1097 .to_vec(&test_img, None, 10, 20, 8);
1098
1099 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1100 let color_info = parser.color_info().expect("colr box should be present");
1101 match color_info {
1102 zenavif_parse::ColorInformation::IccProfile(data) => {
1103 assert_eq!(data.as_slice(), &fake_icc[..]);
1104 }
1105 _ => panic!("expected ICC profile color info, got {:?}", color_info),
1106 }
1107}
1108
1109#[test]
1110fn icc_overrides_nclx() {
1111 let test_img = [1, 2, 3, 4, 5, 6];
1112 let fake_icc = vec![1, 2, 3, 4];
1113 let avif = Aviffy::new()
1115 .set_color_primaries(constants::ColorPrimaries::Bt2020)
1116 .set_icc_profile(fake_icc.clone())
1117 .to_vec(&test_img, None, 10, 20, 8);
1118
1119 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1120 match parser.color_info() {
1121 Some(zenavif_parse::ColorInformation::IccProfile(data)) => {
1122 assert_eq!(data.as_slice(), &fake_icc[..]);
1123 }
1124 other => panic!("expected ICC profile, got {:?}", other),
1125 }
1126}
1127
1128#[test]
1129fn xmp_roundtrip() {
1130 let test_img = [1, 2, 3, 4, 5, 6];
1131 let xmp_data = b"<x:xmpmeta xmlns:x='adobe:ns:meta/'><test/></x:xmpmeta>".to_vec();
1132
1133 let avif = Aviffy::new()
1134 .set_xmp(xmp_data.clone())
1135 .to_vec(&test_img, None, 10, 20, 8);
1136
1137 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1139 assert_eq!(parser.primary_data().unwrap().as_ref(), &test_img[..]);
1140
1141 let xmp_str = b"<x:xmpmeta";
1143 assert!(avif.windows(xmp_str.len()).any(|w| w == xmp_str),
1144 "XMP data should be present in AVIF file");
1145}
1146
1147#[test]
1148fn rotation_and_mirror_combined() {
1149 let test_img = [1, 2, 3, 4, 5, 6];
1150 let avif = Aviffy::new()
1151 .set_rotation(1) .set_mirror(0) .to_vec(&test_img, None, 10, 20, 8);
1154
1155 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1156 let rot = parser.rotation().expect("irot should be present");
1157 let mir = parser.mirror().expect("imir should be present");
1158 assert_eq!(rot.angle, 90);
1159 assert_eq!(mir.axis, 0);
1160}
1161
1162#[test]
1163fn all_properties_combined() {
1164 let test_img = [1, 2, 3, 4, 5, 6];
1165 let test_alpha = [77, 88, 99];
1166 let avif = Aviffy::new()
1167 .set_rotation(2)
1168 .set_mirror(1)
1169 .set_clean_aperture(ClapBox {
1170 width_n: 8, width_d: 1,
1171 height_n: 18, height_d: 1,
1172 horiz_off_n: 0, horiz_off_d: 1,
1173 vert_off_n: 0, vert_off_d: 1,
1174 })
1175 .set_pixel_aspect_ratio(1, 1)
1176 .set_content_light_level(1000, 400)
1177 .set_mastering_display(
1178 [(8500, 39850), (6550, 2300), (35400, 14600)],
1179 (15635, 16450), 10_000_000, 50,
1180 )
1181 .to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
1182
1183 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1184 assert_eq!(parser.rotation().unwrap().angle, 180);
1185 assert_eq!(parser.mirror().unwrap().axis, 1);
1186 assert!(parser.clean_aperture().is_some());
1187 assert!(parser.pixel_aspect_ratio().is_some());
1188 assert!(parser.content_light_level().is_some());
1189 assert!(parser.mastering_display().is_some());
1190
1191 assert_eq!(parser.primary_data().unwrap().as_ref(), &test_img[..]);
1192 assert_eq!(parser.alpha_data().unwrap().unwrap().as_ref(), &test_alpha[..]);
1193}
1194
1195#[cfg(test)]
1201fn parse_with_avif_parse(avif: &[u8]) -> avif_parse::AvifData {
1202 avif_parse::read_avif(&mut &*avif)
1203 .unwrap_or_else(|e| panic!("avif-parse failed to parse: {e:?}"))
1204}
1205
1206#[test]
1207fn avif_parse_basic_roundtrip() {
1208 let color = b"av1colordata";
1209 let avif = serialize_to_vec(color, None, 10, 20, 8);
1210 let parsed = parse_with_avif_parse(&avif);
1211 assert_eq!(parsed.primary_item.as_slice(), &color[..]);
1212 assert!(parsed.alpha_item.is_none());
1213}
1214
1215#[test]
1216fn avif_parse_alpha_roundtrip() {
1217 let color = b"av1colordata";
1218 let alpha = b"alphadata";
1219 let avif = serialize_to_vec(color, Some(alpha), 10, 20, 8);
1220 let parsed = parse_with_avif_parse(&avif);
1221 assert_eq!(parsed.primary_item.as_slice(), &color[..]);
1222 assert_eq!(parsed.alpha_item.unwrap().as_slice(), &alpha[..]);
1223}
1224
1225#[test]
1226fn avif_parse_premultiplied_alpha() {
1227 let color = [1, 2, 3, 4];
1228 let alpha = [55, 66, 77, 88];
1229 let avif = Aviffy::new().premultiplied_alpha(true)
1230 .to_vec(&color, Some(&alpha), 5, 5, 8);
1231 let parsed = parse_with_avif_parse(&avif);
1232 assert!(parsed.premultiplied_alpha);
1233 assert_eq!(parsed.primary_item.as_slice(), &color[..]);
1234 assert_eq!(parsed.alpha_item.unwrap().as_slice(), &alpha[..]);
1235}
1236
1237#[test]
1238fn avif_parse_clli_roundtrip() {
1239 let img = [1, 2, 3, 4, 5, 6];
1240 let avif = Aviffy::new()
1241 .set_content_light_level(1000, 400)
1242 .to_vec(&img, None, 10, 20, 8);
1243 let parsed = parse_with_avif_parse(&avif);
1244 let cll = parsed.content_light_level.expect("clli should be present");
1245 assert_eq!(cll.max_content_light_level, 1000);
1246 assert_eq!(cll.max_pic_average_light_level, 400);
1247}
1248
1249#[test]
1250fn avif_parse_mdcv_roundtrip() {
1251 let img = [1, 2, 3, 4, 5, 6];
1252 let primaries = [(8500, 39850), (6550, 2300), (35400, 14600)];
1253 let avif = Aviffy::new()
1254 .set_mastering_display(primaries, (15635, 16450), 10_000_000, 50)
1255 .to_vec(&img, None, 10, 20, 8);
1256 let parsed = parse_with_avif_parse(&avif);
1257 let mdcv = parsed.mastering_display.expect("mdcv should be present");
1258 assert_eq!(mdcv.primaries, primaries);
1259 assert_eq!(mdcv.white_point, (15635, 16450));
1260 assert_eq!(mdcv.max_luminance, 10_000_000);
1261 assert_eq!(mdcv.min_luminance, 50);
1262}
1263
1264#[test]
1265fn avif_parse_hdr10_full() {
1266 let img = [1, 2, 3, 4, 5, 6];
1267 let alpha = [77, 88, 99];
1268 let avif = Aviffy::new()
1269 .set_transfer_characteristics(constants::TransferCharacteristics::Smpte2084)
1270 .set_color_primaries(constants::ColorPrimaries::Bt2020)
1271 .set_matrix_coefficients(constants::MatrixCoefficients::Bt2020Ncl)
1272 .set_content_light_level(4000, 1000)
1273 .set_mastering_display(
1274 [(8500, 39850), (6550, 2300), (35400, 14600)],
1275 (15635, 16450), 40_000_000, 50,
1276 )
1277 .to_vec(&img, Some(&alpha), 10, 20, 10);
1278 let parsed = parse_with_avif_parse(&avif);
1279 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1280 assert_eq!(parsed.alpha_item.unwrap().as_slice(), &alpha[..]);
1281 assert!(parsed.content_light_level.is_some());
1282 assert!(parsed.mastering_display.is_some());
1283}
1284
1285#[test]
1289fn avif_parse_survives_rotation() {
1290 let img = [1, 2, 3, 4, 5, 6];
1291 for angle in 0..4u8 {
1292 let avif = Aviffy::new().set_rotation(angle).to_vec(&img, None, 10, 20, 8);
1293 let parsed = parse_with_avif_parse(&avif);
1294 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1295 }
1296}
1297
1298#[test]
1299fn avif_parse_survives_mirror() {
1300 let img = [1, 2, 3, 4, 5, 6];
1301 for axis in 0..2u8 {
1302 let avif = Aviffy::new().set_mirror(axis).to_vec(&img, None, 10, 20, 8);
1303 let parsed = parse_with_avif_parse(&avif);
1304 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1305 }
1306}
1307
1308#[test]
1309fn avif_parse_survives_clap() {
1310 let img = [1, 2, 3, 4, 5, 6];
1311 let avif = Aviffy::new()
1312 .set_clean_aperture(ClapBox {
1313 width_n: 800, width_d: 1,
1314 height_n: 600, height_d: 1,
1315 horiz_off_n: 0, horiz_off_d: 1,
1316 vert_off_n: 0, vert_off_d: 1,
1317 })
1318 .to_vec(&img, None, 10, 20, 8);
1319 let parsed = parse_with_avif_parse(&avif);
1320 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1321}
1322
1323#[test]
1324fn avif_parse_survives_pasp() {
1325 let img = [1, 2, 3, 4, 5, 6];
1326 let avif = Aviffy::new().set_pixel_aspect_ratio(2, 1).to_vec(&img, None, 10, 20, 8);
1327 let parsed = parse_with_avif_parse(&avif);
1328 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1329}
1330
1331#[test]
1332fn avif_parse_survives_icc_profile() {
1333 let img = [1, 2, 3, 4, 5, 6];
1334 let icc = vec![0, 0, 0, 24, b'a', b'c', b's', b'p', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1335 let avif = Aviffy::new().set_icc_profile(icc).to_vec(&img, None, 10, 20, 8);
1336 let parsed = parse_with_avif_parse(&avif);
1337 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1338}
1339
1340#[test]
1341fn avif_parse_survives_exif() {
1342 let img = b"av1colordata";
1343 let avif = Aviffy::new()
1344 .set_exif(b"exifdata".to_vec())
1345 .to_vec(img, None, 10, 20, 8);
1346 let parsed = parse_with_avif_parse(&avif);
1347 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1348}
1349
1350#[test]
1351fn avif_parse_survives_xmp() {
1352 let img = [1, 2, 3, 4, 5, 6];
1353 let avif = Aviffy::new()
1354 .set_xmp(b"<x:xmpmeta/>".to_vec())
1355 .to_vec(&img, None, 10, 20, 8);
1356 let parsed = parse_with_avif_parse(&avif);
1357 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1358}
1359
1360#[test]
1361fn avif_parse_survives_all_properties() {
1362 let img = [1, 2, 3, 4, 5, 6];
1363 let alpha = [77, 88, 99];
1364 let avif = Aviffy::new()
1365 .set_rotation(2)
1366 .set_mirror(1)
1367 .set_clean_aperture(ClapBox {
1368 width_n: 8, width_d: 1,
1369 height_n: 18, height_d: 1,
1370 horiz_off_n: 0, horiz_off_d: 1,
1371 vert_off_n: 0, vert_off_d: 1,
1372 })
1373 .set_pixel_aspect_ratio(1, 1)
1374 .set_content_light_level(1000, 400)
1375 .set_mastering_display(
1376 [(8500, 39850), (6550, 2300), (35400, 14600)],
1377 (15635, 16450), 10_000_000, 50,
1378 )
1379 .set_exif(b"exif".to_vec())
1380 .set_xmp(b"<xmp/>".to_vec())
1381 .to_vec(&img, Some(&alpha), 10, 20, 8);
1382 let parsed = parse_with_avif_parse(&avif);
1383 assert_eq!(parsed.primary_item.as_slice(), &img[..]);
1384 assert_eq!(parsed.alpha_item.unwrap().as_slice(), &alpha[..]);
1385 assert!(parsed.content_light_level.is_some());
1386 assert!(parsed.mastering_display.is_some());
1387}
1388
1389#[test]
1390fn avif_parse_survives_animated() {
1391 use crate::animated::{AnimatedImage, AnimFrame};
1392 let mut anim = AnimatedImage::new();
1393 anim.set_timescale(1000);
1394 anim.set_color_config(boxes::Av1CBox {
1395 seq_profile: 0, seq_level_idx_0: 4, seq_tier_0: false,
1396 high_bitdepth: false, twelve_bit: false, monochrome: false,
1397 chroma_subsampling_x: true, chroma_subsampling_y: true,
1398 chroma_sample_position: 0,
1399 });
1400 let frames = [
1401 AnimFrame::new(b"frame0data", 33).with_sync(true),
1402 AnimFrame::new(b"frame1data", 33),
1403 ];
1404 let avif = anim.serialize(320, 240, &frames, b"seqhdr", None);
1405 let _ = avif_parse::read_avif(&mut avif.as_slice());
1407}
1408
1409#[test]
1410fn avif_parse_survives_grid() {
1411 use crate::grid::GridImage;
1412 let tiles: Vec<Vec<u8>> = (0..4).map(|i| vec![i as u8; 100]).collect();
1413 let tile_refs: Vec<&[u8]> = tiles.iter().map(|t| t.as_slice()).collect();
1414
1415 let mut grid = GridImage::new();
1416 grid.set_color_config(boxes::Av1CBox {
1417 seq_profile: 0, seq_level_idx_0: 4, seq_tier_0: false,
1418 high_bitdepth: false, twelve_bit: false, monochrome: false,
1419 chroma_subsampling_x: true, chroma_subsampling_y: true,
1420 chroma_sample_position: 0,
1421 });
1422 let avif = grid.serialize(2, 2, 200, 200, 100, 100, &tile_refs, None).unwrap();
1423 let _ = avif_parse::read_avif(&mut avif.as_slice());
1425}
1426
1427#[cfg(test)]
1436fn make_test_tmap_metadata(
1437 is_multichannel: bool,
1438 use_base_colour_space: bool,
1439 base_headroom_n: u32,
1440 base_headroom_d: u32,
1441 alt_headroom_n: u32,
1442 alt_headroom_d: u32,
1443) -> Vec<u8> {
1444 let mut buf = Vec::new();
1445 buf.push(0); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); let flags = (u8::from(is_multichannel) << 7) | (u8::from(use_base_colour_space) << 6);
1449 buf.push(flags);
1450
1451 buf.extend_from_slice(&base_headroom_n.to_be_bytes());
1452 buf.extend_from_slice(&base_headroom_d.to_be_bytes());
1453 buf.extend_from_slice(&alt_headroom_n.to_be_bytes());
1454 buf.extend_from_slice(&alt_headroom_d.to_be_bytes());
1455
1456 let channel_count = if is_multichannel { 3 } else { 1 };
1457 for _ in 0..channel_count {
1458 buf.extend_from_slice(&0i32.to_be_bytes());
1460 buf.extend_from_slice(&1u32.to_be_bytes());
1461 buf.extend_from_slice(&1i32.to_be_bytes());
1463 buf.extend_from_slice(&1u32.to_be_bytes());
1464 buf.extend_from_slice(&1u32.to_be_bytes());
1466 buf.extend_from_slice(&1u32.to_be_bytes());
1467 buf.extend_from_slice(&0i32.to_be_bytes());
1469 buf.extend_from_slice(&1u32.to_be_bytes());
1470 buf.extend_from_slice(&0i32.to_be_bytes());
1472 buf.extend_from_slice(&1u32.to_be_bytes());
1473 }
1474 buf
1475}
1476
1477#[test]
1478fn gain_map_roundtrip() {
1479 let primary_data = b"primary_av1_data";
1480 let gain_map_data = b"gain_map_av1_data";
1481 let metadata = make_test_tmap_metadata(false, true, 0, 1, 1, 1);
1482
1483 let avif = Aviffy::new()
1484 .set_gain_map(gain_map_data.to_vec(), 4, 4, 8, metadata.clone())
1485 .to_vec(primary_data, None, 10, 20, 8);
1486
1487 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1488
1489 assert_eq!(parser.primary_data().unwrap().as_ref(), &primary_data[..]);
1491
1492 let gm_meta = parser.gain_map_metadata().expect("gain map metadata should be present");
1494 assert!(!gm_meta.is_multichannel);
1495 assert!(gm_meta.use_base_colour_space);
1496 assert_eq!(gm_meta.base_hdr_headroom_n, 0);
1497 assert_eq!(gm_meta.base_hdr_headroom_d, 1);
1498 assert_eq!(gm_meta.alternate_hdr_headroom_n, 1);
1499 assert_eq!(gm_meta.alternate_hdr_headroom_d, 1);
1500
1501 let gm_data = parser.gain_map_data().expect("gain map data should be present").unwrap();
1503 assert_eq!(gm_data.as_ref(), &gain_map_data[..]);
1504}
1505
1506#[test]
1507fn gain_map_with_alpha() {
1508 let primary_data = b"primary_av1";
1509 let alpha_data = b"alpha_av1";
1510 let gain_map_data = b"gm_av1_data";
1511 let metadata = make_test_tmap_metadata(false, true, 0, 1, 1, 1);
1512
1513 let avif = Aviffy::new()
1514 .set_gain_map(gain_map_data.to_vec(), 4, 4, 8, metadata)
1515 .to_vec(primary_data, Some(alpha_data), 10, 20, 8);
1516
1517 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1518
1519 assert_eq!(parser.primary_data().unwrap().as_ref(), &primary_data[..]);
1521 assert_eq!(parser.alpha_data().unwrap().unwrap().as_ref(), &alpha_data[..]);
1522
1523 let gm_data = parser.gain_map_data().expect("gain map data present").unwrap();
1525 assert_eq!(gm_data.as_ref(), &gain_map_data[..]);
1526 assert!(parser.gain_map_metadata().is_some());
1527}
1528
1529#[test]
1530fn gain_map_multichannel_metadata() {
1531 let primary_data = [1, 2, 3, 4, 5, 6];
1532 let gain_map_data = [10, 20, 30, 40];
1533 let metadata = make_test_tmap_metadata(true, false, 0, 1, 3, 1);
1534
1535 let avif = Aviffy::new()
1536 .set_gain_map(gain_map_data.to_vec(), 2, 2, 8, metadata.clone())
1537 .to_vec(&primary_data, None, 10, 20, 8);
1538
1539 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1540 let gm_meta = parser.gain_map_metadata().expect("gain map metadata present");
1541 assert!(gm_meta.is_multichannel);
1542 assert!(!gm_meta.use_base_colour_space);
1543 assert_eq!(gm_meta.alternate_hdr_headroom_n, 3);
1544}
1545
1546#[test]
1547fn gain_map_metadata_field_exact() {
1548 let primary_data = [1, 2, 3, 4];
1550 let gain_map_data = [99, 88, 77];
1551 let metadata = make_test_tmap_metadata(false, true, 0, 1, 6, 1);
1552
1553 let avif = Aviffy::new()
1554 .set_gain_map(gain_map_data.to_vec(), 1, 1, 8, metadata.clone())
1555 .to_vec(&primary_data, None, 10, 20, 8);
1556
1557 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1558 let gm_meta = parser.gain_map_metadata().expect("gain map metadata present");
1559
1560 assert_eq!(gm_meta.alternate_hdr_headroom_n, 6);
1561 assert_eq!(gm_meta.alternate_hdr_headroom_d, 1);
1562 assert_eq!(gm_meta.channels[0].gain_map_min_n, 0);
1563 assert_eq!(gm_meta.channels[0].gain_map_min_d, 1);
1564 assert_eq!(gm_meta.channels[0].gain_map_max_n, 1);
1565 assert_eq!(gm_meta.channels[0].gain_map_max_d, 1);
1566 assert_eq!(gm_meta.channels[0].gamma_n, 1);
1567 assert_eq!(gm_meta.channels[0].gamma_d, 1);
1568 assert_eq!(gm_meta.channels[0].base_offset_n, 0);
1569 assert_eq!(gm_meta.channels[0].base_offset_d, 1);
1570 assert_eq!(gm_meta.channels[0].alternate_offset_n, 0);
1571 assert_eq!(gm_meta.channels[0].alternate_offset_d, 1);
1572}
1573
1574#[test]
1575fn gain_map_alt_colr_roundtrip() {
1576 let primary_data = [1, 2, 3, 4, 5, 6];
1577 let gain_map_data = [10, 20, 30];
1578 let metadata = make_test_tmap_metadata(false, true, 0, 1, 1, 1);
1579
1580 let alt_colr = ColrBox {
1581 color_primaries: constants::ColorPrimaries::Bt2020,
1582 transfer_characteristics: constants::TransferCharacteristics::Smpte2084,
1583 matrix_coefficients: constants::MatrixCoefficients::Bt2020Ncl,
1584 full_range_flag: false,
1585 };
1586
1587 let avif = Aviffy::new()
1588 .set_gain_map(gain_map_data.to_vec(), 2, 2, 8, metadata)
1589 .set_gain_map_alt_colr(alt_colr)
1590 .to_vec(&primary_data, None, 10, 20, 8);
1591
1592 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1593
1594 let gm_data = parser.gain_map_data().expect("gain map data present").unwrap();
1596 assert_eq!(gm_data.as_ref(), &gain_map_data[..]);
1597
1598 let alt = parser.gain_map_color_info().expect("alt color info should be present");
1600 match alt {
1601 zenavif_parse::ColorInformation::Nclx {
1602 color_primaries,
1603 transfer_characteristics,
1604 matrix_coefficients,
1605 full_range,
1606 } => {
1607 assert_eq!(*color_primaries, 9); assert_eq!(*transfer_characteristics, 16); assert_eq!(*matrix_coefficients, 9); assert!(!full_range);
1611 }
1612 other => panic!("expected NCLX color info, got: {:?}", other),
1613 }
1614}
1615
1616#[test]
1617fn no_gain_map_by_default() {
1618 let test_img = [1, 2, 3, 4, 5, 6];
1619 let avif = serialize_to_vec(&test_img, None, 10, 20, 8);
1620 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1621 assert!(parser.gain_map_metadata().is_none(), "no gain map metadata by default");
1622 assert!(parser.gain_map_data().is_none(), "no gain map data by default");
1623}
1624
1625#[test]
1626fn avif_parse_survives_gain_map() {
1627 let primary_data = b"primary_color";
1628 let gain_map_data = b"gain_map_pixels";
1629 let metadata = make_test_tmap_metadata(false, true, 0, 1, 1, 1);
1630
1631 let avif = Aviffy::new()
1632 .set_gain_map(gain_map_data.to_vec(), 4, 4, 8, metadata)
1633 .to_vec(primary_data, None, 10, 20, 8);
1634
1635 let _ = avif_parse::read_avif(&mut avif.as_slice());
1637}
1638
1639#[test]
1640fn gain_map_metadata_bytes_exact_roundtrip() {
1641 let primary_data = [1u8, 2, 3, 4];
1644 let gain_map_data = [10u8, 20, 30];
1645 let original_meta = make_test_tmap_metadata(false, true, 0, 1, 13, 10);
1646
1647 let avif = Aviffy::new()
1648 .set_gain_map(gain_map_data.to_vec(), 1, 1, 8, original_meta.clone())
1649 .to_vec(&primary_data, None, 10, 20, 8);
1650
1651 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1652 let parsed = parser.gain_map_metadata().expect("gain map metadata present");
1653 let roundtripped = parsed.to_bytes();
1654
1655 assert_eq!(original_meta, roundtripped,
1656 "tmap payload bytes must survive serialize → parse → to_bytes unchanged");
1657}
1658
1659#[test]
1660fn gain_map_backward_direction_flag_roundtrip() {
1661 let primary_data = [1u8, 2, 3, 4];
1664 let gain_map_data = [55u8, 66];
1665
1666 let mut meta_bytes = make_test_tmap_metadata(false, false, 0, 1, 4, 1);
1667 meta_bytes[5] |= 0x04;
1669
1670 let avif = Aviffy::new()
1671 .set_gain_map(gain_map_data.to_vec(), 1, 1, 8, meta_bytes.clone())
1672 .to_vec(&primary_data, None, 10, 20, 8);
1673
1674 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1675 let parsed = parser.gain_map_metadata().expect("gain map metadata");
1676
1677 assert!(parsed.backward_direction, "backward_direction must be parsed as true");
1678 assert!(!parsed.use_base_colour_space);
1679 assert!(!parsed.is_multichannel);
1680
1681 let roundtripped = parsed.to_bytes();
1682 assert_eq!(meta_bytes, roundtripped, "backward_direction flag must survive full roundtrip");
1683 assert_eq!(roundtripped[5] & 0x04, 0x04, "bit 2 must be set in roundtripped flags byte");
1684}
1685
1686#[test]
1687fn gain_map_multichannel_metadata_bytes_roundtrip() {
1688 let primary_data = [1u8, 2, 3, 4, 5, 6];
1689 let gain_map_data = [10u8, 20, 30, 40];
1690 let original_meta = make_test_tmap_metadata(true, true, 0, 1, 3, 1);
1691
1692 let avif = Aviffy::new()
1693 .set_gain_map(gain_map_data.to_vec(), 2, 2, 8, original_meta.clone())
1694 .to_vec(&primary_data, None, 10, 20, 8);
1695
1696 let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
1697 let parsed = parser.gain_map_metadata().expect("gain map metadata");
1698 assert!(parsed.is_multichannel);
1699
1700 let roundtripped = parsed.to_bytes();
1701 assert_eq!(original_meta, roundtripped,
1702 "multichannel tmap payload bytes must survive roundtrip");
1703}