1use crate::boxes::{FourCc, Reader};
20use otf_pixels_core::{Orientation, PixelsError, Result};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Subsampling {
26 Monochrome,
28 Yuv420,
30 Yuv422,
32 Yuv444,
34}
35
36impl Subsampling {
37 #[must_use]
39 pub const fn x_shift(self) -> u32 {
40 match self {
41 Self::Yuv420 | Self::Yuv422 => 1,
42 Self::Monochrome | Self::Yuv444 => 0,
43 }
44 }
45
46 #[must_use]
48 pub const fn y_shift(self) -> u32 {
49 match self {
50 Self::Yuv420 => 1,
51 Self::Monochrome | Self::Yuv422 | Self::Yuv444 => 0,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Av1Config {
63 pub seq_profile: u8,
65 pub seq_level_idx0: u8,
67 pub seq_tier0: u8,
69 pub bit_depth: u8,
71 pub subsampling: Subsampling,
73 pub chroma_sample_position: u8,
75 pub config_obus: Vec<u8>,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct Extents {
85 pub width: u32,
87 pub height: u32,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct PixelInfo {
94 pub bits_per_channel: Vec<u8>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum Colour {
101 Nclx {
103 primaries: u16,
105 transfer: u16,
107 matrix: u16,
109 full_range: bool,
111 },
112 Icc(Vec<u8>),
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum Property {
122 Extents(Extents),
124 Av1Config(Av1Config),
126 PixelInfo(PixelInfo),
128 Colour(Colour),
130 Rotation(u8),
132 Mirror(bool),
135 AuxiliaryType(String),
138 PixelAspect(u32, u32),
140 Unknown(FourCc),
145}
146
147impl Property {
148 #[must_use]
150 pub const fn kind(&self) -> FourCc {
151 match self {
152 Self::Extents(_) => FourCc::new(b"ispe"),
153 Self::Av1Config(_) => FourCc::new(b"av1C"),
154 Self::PixelInfo(_) => FourCc::new(b"pixi"),
155 Self::Colour(_) => FourCc::new(b"colr"),
156 Self::Rotation(_) => FourCc::new(b"irot"),
157 Self::Mirror(_) => FourCc::new(b"imir"),
158 Self::AuxiliaryType(_) => FourCc::new(b"auxC"),
159 Self::PixelAspect(_, _) => FourCc::new(b"pasp"),
160 Self::Unknown(kind) => *kind,
161 }
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct Association {
168 pub index: u16,
170 pub essential: bool,
172}
173
174#[derive(Debug, Clone, Default)]
176pub struct Properties {
177 entries: Vec<Property>,
179 associations: HashMap<u32, Vec<Association>>,
181}
182
183impl Properties {
184 pub fn parse(mut iprp: Reader<'_>) -> Result<Self> {
191 let mut entries = Vec::new();
192 let mut associations = HashMap::new();
193
194 while let Some(header) = iprp.next_box() {
195 let header = header?;
196 let payload = iprp.payload(&header);
197 match &header.kind.0 {
198 b"ipco" => entries = parse_ipco(payload)?,
199 b"ipma" => parse_ipma(payload, &mut associations)?,
202 _ => {}
203 }
204 }
205
206 Ok(Self {
207 entries,
208 associations,
209 })
210 }
211
212 #[must_use]
214 pub fn for_item(&self, item_id: u32) -> Vec<&Property> {
215 let Some(associations) = self.associations.get(&item_id) else {
216 return Vec::new();
217 };
218 associations
219 .iter()
220 .filter_map(|association| {
221 let index = usize::from(association.index).checked_sub(1)?;
223 self.entries.get(index)
224 })
225 .collect()
226 }
227
228 #[must_use]
235 pub fn essential_unknown(&self, item_id: u32) -> Option<FourCc> {
236 let associations = self.associations.get(&item_id)?;
237 associations.iter().find_map(|association| {
238 if !association.essential {
239 return None;
240 }
241 let index = usize::from(association.index).checked_sub(1)?;
242 match self.entries.get(index) {
243 Some(Property::Unknown(kind)) => Some(*kind),
244 _ => None,
245 }
246 })
247 }
248
249 #[must_use]
251 pub fn extents(&self, item_id: u32) -> Option<Extents> {
252 self.for_item(item_id)
253 .into_iter()
254 .find_map(|property| match property {
255 Property::Extents(extents) => Some(*extents),
256 _ => None,
257 })
258 }
259
260 #[must_use]
262 pub fn av1_config(&self, item_id: u32) -> Option<&Av1Config> {
263 self.for_item(item_id)
264 .into_iter()
265 .find_map(|property| match property {
266 Property::Av1Config(config) => Some(config),
267 _ => None,
268 })
269 }
270
271 #[must_use]
276 pub fn colour(&self, item_id: u32) -> Option<&Colour> {
277 let colours = || {
278 self.for_item(item_id)
279 .into_iter()
280 .filter_map(|property| match property {
281 Property::Colour(colour) => Some(colour),
282 _ => None,
283 })
284 };
285 colours()
286 .find(|colour| matches!(colour, Colour::Nclx { .. }))
287 .or_else(|| colours().next())
288 }
289
290 #[must_use]
292 pub fn icc_profile(&self, item_id: u32) -> Option<&[u8]> {
293 self.for_item(item_id)
294 .into_iter()
295 .find_map(|property| match property {
296 Property::Colour(Colour::Icc(profile)) => Some(profile.as_slice()),
297 _ => None,
298 })
299 }
300
301 #[must_use]
303 pub fn auxiliary_type(&self, item_id: u32) -> Option<&str> {
304 self.for_item(item_id)
305 .into_iter()
306 .find_map(|property| match property {
307 Property::AuxiliaryType(urn) => Some(urn.as_str()),
308 _ => None,
309 })
310 }
311
312 #[must_use]
315 pub fn orientation(&self, item_id: u32) -> Orientation {
316 let (mut turns, mut mirrored) = (0_u8, false);
319 for property in self.for_item(item_id) {
320 match property {
321 Property::Rotation(anticlockwise) => {
322 let clockwise = (4 - anticlockwise % 4) % 4;
325 turns = if mirrored {
326 (turns + 4 - clockwise) % 4
327 } else {
328 (turns + clockwise) % 4
329 };
330 }
331 Property::Mirror(left_right) => {
332 if !left_right {
336 turns = (turns + 2) % 4;
337 }
338 mirrored = !mirrored;
339 }
340 _ => {}
341 }
342 }
343 Orientation::from_parts(turns, mirrored)
344 }
345}
346
347fn parse_ipco(mut ipco: Reader<'_>) -> Result<Vec<Property>> {
349 let mut entries = Vec::new();
350 while let Some(header) = ipco.next_box() {
351 let header = header?;
352 let payload = ipco.payload(&header);
353 entries.push(parse_property(header.kind, payload)?);
354 }
355 Ok(entries)
356}
357
358fn parse_property(kind: FourCc, mut payload: Reader<'_>) -> Result<Property> {
360 match &kind.0 {
361 b"ispe" => {
362 let (_version, _flags) = payload.full_box()?;
363 let width = payload.u32()?;
364 let height = payload.u32()?;
365 Ok(Property::Extents(Extents { width, height }))
366 }
367 b"av1C" => parse_av1c(payload).map(Property::Av1Config),
368 b"pixi" => {
369 let (_version, _flags) = payload.full_box()?;
370 let count = payload.u8()?;
371 let mut bits_per_channel = Vec::with_capacity(usize::from(count));
372 for _ in 0..count {
373 bits_per_channel.push(payload.u8()?);
374 }
375 Ok(Property::PixelInfo(PixelInfo { bits_per_channel }))
376 }
377 b"colr" => parse_colr(payload).map(Property::Colour),
378 b"irot" => {
379 let turns = payload.u8()? & 0x03;
381 Ok(Property::Rotation(turns))
382 }
383 b"imir" => {
384 let mode = payload.u8()? & 0x01;
389 Ok(Property::Mirror(mode == 1))
390 }
391 b"auxC" => {
392 let (_version, _flags) = payload.full_box()?;
393 let urn = payload.cstring()?;
394 Ok(Property::AuxiliaryType(urn.to_owned()))
395 }
396 b"pasp" => {
397 let horizontal = payload.u32()?;
398 let vertical = payload.u32()?;
399 Ok(Property::PixelAspect(horizontal, vertical))
400 }
401 _ => Ok(Property::Unknown(kind)),
402 }
403}
404
405fn parse_av1c(mut payload: Reader<'_>) -> Result<Av1Config> {
407 let first = payload.u8()?;
408 if first >> 7 != 1 {
412 return Err(PixelsError::malformed(
413 "avif",
414 "the av1C marker bit is not set",
415 ));
416 }
417 let version = first & 0x7f;
418 if version != 1 {
419 return Err(PixelsError::malformed(
420 "avif",
421 format!("av1C record version {version} is not 1"),
422 ));
423 }
424
425 let second = payload.u8()?;
426 let seq_profile = second >> 5;
427 let seq_level_idx0 = second & 0x1f;
428
429 let third = payload.u8()?;
430 let seq_tier0 = (third >> 7) & 1;
431 let high_bitdepth = (third >> 6) & 1;
432 let twelve_bit = (third >> 5) & 1;
433 let monochrome = (third >> 4) & 1;
434 let subsampling_x = (third >> 3) & 1;
435 let subsampling_y = (third >> 2) & 1;
436 let chroma_sample_position = third & 0x03;
437
438 let _fourth = payload.u8()?;
441
442 let bit_depth = if seq_profile == 2 && high_bitdepth == 1 {
444 if twelve_bit == 1 { 12 } else { 10 }
445 } else if high_bitdepth == 1 {
446 10
447 } else {
448 8
449 };
450
451 let subsampling = match (monochrome, subsampling_x, subsampling_y) {
452 (1, _, _) => Subsampling::Monochrome,
453 (_, 1, 1) => Subsampling::Yuv420,
454 (_, 1, 0) => Subsampling::Yuv422,
455 (_, 0, 0) => Subsampling::Yuv444,
456 (_, x, y) => {
458 return Err(PixelsError::malformed(
459 "avif",
460 format!(
461 "av1C declares chroma subsampling ({x}, {y}), which AV1 has no such format for"
462 ),
463 ));
464 }
465 };
466
467 Ok(Av1Config {
468 seq_profile,
469 seq_level_idx0,
470 seq_tier0,
471 bit_depth,
472 subsampling,
473 chroma_sample_position,
474 config_obus: payload.rest().to_vec(),
475 })
476}
477
478fn parse_colr(mut payload: Reader<'_>) -> Result<Colour> {
480 let kind = payload.fourcc()?;
481 match &kind.0 {
482 b"nclx" => {
483 let primaries = payload.u16()?;
484 let transfer = payload.u16()?;
485 let matrix = payload.u16()?;
486 let full_range = payload.u8()? >> 7 == 1;
488 Ok(Colour::Nclx {
489 primaries,
490 transfer,
491 matrix,
492 full_range,
493 })
494 }
495 b"rICC" | b"prof" => Ok(Colour::Icc(payload.rest().to_vec())),
496 other => Err(PixelsError::malformed(
497 "avif",
498 format!(
499 "colr declares colour type '{}', which is not one this format defines",
500 FourCc(*other)
501 ),
502 )),
503 }
504}
505
506fn parse_ipma(mut payload: Reader<'_>, out: &mut HashMap<u32, Vec<Association>>) -> Result<()> {
508 let (version, flags) = payload.full_box()?;
509 let entry_count = payload.u32()?;
510 let smallest_entry = if version < 1 { 3 } else { 5 };
514 if u64::from(entry_count) * smallest_entry > payload.remaining() as u64 {
515 return Err(PixelsError::malformed(
516 "avif",
517 format!(
518 "ipma declares {entry_count} entries, more than its {} remaining bytes can hold",
519 payload.remaining()
520 ),
521 ));
522 }
523
524 let wide_indices = flags & 1 == 1;
526
527 for _ in 0..entry_count {
528 let item_id = if version < 1 {
529 u32::from(payload.u16()?)
530 } else {
531 payload.u32()?
532 };
533 let association_count = payload.u8()?;
534 let mut associations = Vec::with_capacity(usize::from(association_count));
535 for _ in 0..association_count {
536 let (essential, index) = if wide_indices {
537 let word = payload.u16()?;
538 (word >> 15 == 1, word & 0x7fff)
539 } else {
540 let byte = payload.u8()?;
541 (byte >> 7 == 1, u16::from(byte & 0x7f))
542 };
543 associations.push(Association { index, essential });
544 }
545 out.entry(item_id).or_default().extend(associations);
546 }
547 Ok(())
548}
549
550#[cfg(test)]
551#[allow(
552 clippy::unwrap_used,
553 clippy::indexing_slicing,
554 clippy::panic,
555 reason = "tests operate on known-good values and assert shapes directly"
556)]
557mod tests {
558 use super::*;
559 use otf_pixels_core::ErrorCode;
560
561 fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
562 let mut out = Vec::new();
563 let total = u32::try_from(8 + payload.len()).unwrap();
564 out.extend_from_slice(&total.to_be_bytes());
565 out.extend_from_slice(kind);
566 out.extend_from_slice(payload);
567 out
568 }
569
570 fn ispe(width: u32, height: u32) -> Vec<u8> {
571 let mut payload = vec![0, 0, 0, 0];
572 payload.extend_from_slice(&width.to_be_bytes());
573 payload.extend_from_slice(&height.to_be_bytes());
574 boxed(b"ispe", &payload)
575 }
576
577 fn av1c_420_8bit() -> Vec<u8> {
579 boxed(b"av1C", &[0x81, 0x00, 0x0C, 0x00, 0xAA])
581 }
582
583 fn parse_one(kind: &[u8; 4], payload: &[u8]) -> Result<Property> {
584 let file = boxed(kind, payload);
585 let mut reader = Reader::new(&file);
586 let header = reader.next_box().unwrap().unwrap();
587 parse_property(header.kind, reader.payload(&header))
588 }
589
590 #[test]
591 fn av1c_decodes_profile_depth_and_subsampling() {
592 let file = av1c_420_8bit();
593 let mut reader = Reader::new(&file);
594 let header = reader.next_box().unwrap().unwrap();
595 let Property::Av1Config(config) =
596 parse_property(header.kind, reader.payload(&header)).unwrap()
597 else {
598 panic!("expected an av1C");
599 };
600 assert_eq!(config.seq_profile, 0);
601 assert_eq!(config.bit_depth, 8);
602 assert_eq!(config.subsampling, Subsampling::Yuv420);
603 assert_eq!(config.config_obus, vec![0xAA]);
605 }
606
607 #[test]
608 fn av1c_twelve_bit_requires_profile_two() {
609 let file = boxed(b"av1C", &[0x81, 0x00, 0x6C, 0x00]);
612 let mut reader = Reader::new(&file);
613 let header = reader.next_box().unwrap().unwrap();
614 let Property::Av1Config(config) =
615 parse_property(header.kind, reader.payload(&header)).unwrap()
616 else {
617 panic!("expected an av1C");
618 };
619 assert_eq!(config.bit_depth, 10);
620
621 let file = boxed(b"av1C", &[0x81, 0x40, 0x6C, 0x00]);
623 let mut reader = Reader::new(&file);
624 let header = reader.next_box().unwrap().unwrap();
625 let Property::Av1Config(config) =
626 parse_property(header.kind, reader.payload(&header)).unwrap()
627 else {
628 panic!("expected an av1C");
629 };
630 assert_eq!(config.seq_profile, 2);
631 assert_eq!(config.bit_depth, 12);
632 }
633
634 #[test]
635 fn av1c_rejects_a_bad_marker_or_version() {
636 let error = parse_one(b"av1C", &[0x01, 0x00, 0x0C, 0x00]).unwrap_err();
637 assert_eq!(error.code(), ErrorCode::Malformed);
638 assert!(error.to_string().contains("marker"), "{error}");
639
640 let error = parse_one(b"av1C", &[0x82, 0x00, 0x0C, 0x00]).unwrap_err();
641 assert_eq!(error.code(), ErrorCode::Malformed);
642 assert!(error.to_string().contains("version 2"), "{error}");
643 }
644
645 #[test]
646 fn av1c_rejects_a_subsampling_av1_does_not_define() {
647 let error = parse_one(b"av1C", &[0x81, 0x00, 0x04, 0x00]).unwrap_err();
649 assert_eq!(error.code(), ErrorCode::Malformed);
650 assert!(error.to_string().contains("subsampling"), "{error}");
651 }
652
653 #[test]
654 fn monochrome_wins_over_the_subsampling_bits() {
655 let file = boxed(b"av1C", &[0x81, 0x00, 0x1C, 0x00]);
657 let mut reader = Reader::new(&file);
658 let header = reader.next_box().unwrap().unwrap();
659 let Property::Av1Config(config) =
660 parse_property(header.kind, reader.payload(&header)).unwrap()
661 else {
662 panic!("expected an av1C");
663 };
664 assert_eq!(config.subsampling, Subsampling::Monochrome);
665 assert_eq!(config.subsampling.x_shift(), 0);
666 assert_eq!(config.subsampling.y_shift(), 0);
667 }
668
669 #[test]
670 fn colr_reads_nclx_code_points_and_the_range_flag() {
671 let mut payload = Vec::from(*b"nclx");
672 payload.extend_from_slice(&1_u16.to_be_bytes());
673 payload.extend_from_slice(&13_u16.to_be_bytes());
674 payload.extend_from_slice(&6_u16.to_be_bytes());
675 payload.push(0x80);
676
677 let Property::Colour(Colour::Nclx {
678 primaries,
679 transfer,
680 matrix,
681 full_range,
682 }) = parse_one(b"colr", &payload).unwrap()
683 else {
684 panic!("expected an nclx colr");
685 };
686 assert_eq!((primaries, transfer, matrix), (1, 13, 6));
687 assert!(full_range);
688 }
689
690 #[test]
691 fn colr_carries_an_icc_profile_through() {
692 let mut payload = Vec::from(*b"prof");
693 payload.extend_from_slice(&[1, 2, 3, 4]);
694 let Property::Colour(Colour::Icc(profile)) = parse_one(b"colr", &payload).unwrap() else {
695 panic!("expected an ICC colr");
696 };
697 assert_eq!(profile, vec![1, 2, 3, 4]);
698 }
699
700 fn item_with(properties: Vec<Property>) -> Properties {
702 let associations = (1..=properties.len())
703 .map(|index| Association {
704 index: u16::try_from(index).unwrap(),
705 essential: true,
706 })
707 .collect();
708 Properties {
709 entries: properties,
710 associations: HashMap::from([(1, associations)]),
711 }
712 }
713
714 #[test]
715 fn irot_and_imir_compose_in_association_order() {
716 use Property::{Mirror, Rotation};
717 let cases = [
718 (vec![], Orientation::Normal),
719 (vec![Rotation(1)], Orientation::Rotate270),
721 (vec![Rotation(2)], Orientation::Rotate180),
722 (vec![Rotation(3)], Orientation::Rotate90),
723 (vec![Mirror(false)], Orientation::FlipVertical),
725 (vec![Mirror(true)], Orientation::FlipHorizontal),
726 (vec![Rotation(3), Mirror(true)], Orientation::Transpose),
730 (vec![Rotation(1), Mirror(true)], Orientation::Transverse),
731 (vec![Rotation(1), Mirror(false)], Orientation::Transpose),
732 (vec![Mirror(true), Rotation(1)], Orientation::Transpose),
735 ];
736 for (properties, expected) in cases {
737 assert_eq!(
738 item_with(properties.clone()).orientation(1),
739 expected,
740 "{properties:?}"
741 );
742 }
743 assert_eq!(Properties::default().orientation(7), Orientation::Normal);
744 }
745
746 #[test]
747 fn orientation_properties_are_masked_to_their_defined_bits() {
748 assert_eq!(parse_one(b"irot", &[0xFE]).unwrap(), Property::Rotation(2));
750 assert_eq!(
751 parse_one(b"imir", &[0xFE]).unwrap(),
752 Property::Mirror(false)
753 );
754 assert_eq!(parse_one(b"imir", &[0xFF]).unwrap(), Property::Mirror(true));
755 }
756
757 #[test]
758 fn pixi_reads_one_depth_per_channel() {
759 let Property::PixelInfo(info) = parse_one(b"pixi", &[0, 0, 0, 0, 3, 8, 8, 8]).unwrap()
760 else {
761 panic!("expected a pixi");
762 };
763 assert_eq!(info.bits_per_channel, vec![8, 8, 8]);
764 }
765
766 #[test]
767 fn an_uninterpreted_property_is_retained_by_type() {
768 assert_eq!(
769 parse_one(b"clap", &[0; 32]).unwrap(),
770 Property::Unknown(FourCc::new(b"clap"))
771 );
772 }
773
774 fn sample_iprp() -> Vec<u8> {
776 let mut ipco = ispe(64, 48);
777 ipco.extend_from_slice(&av1c_420_8bit());
778
779 let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2, 0x01, 0x82];
782
783 let mut iprp = boxed(b"ipco", &ipco);
784 iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
785 boxed(b"iprp", &iprp)
786 }
787
788 #[test]
789 fn properties_resolve_through_the_association_map() {
790 let file = sample_iprp();
791 let mut reader = Reader::new(&file);
792 let header = reader.next_box().unwrap().unwrap();
793 let properties = Properties::parse(reader.payload(&header)).unwrap();
794
795 assert_eq!(
796 properties.extents(1),
797 Some(Extents {
798 width: 64,
799 height: 48
800 })
801 );
802 assert_eq!(properties.av1_config(1).unwrap().bit_depth, 8);
803 assert!(properties.extents(2).is_none());
805 assert!(properties.for_item(2).is_empty());
806 }
807
808 #[test]
809 fn an_out_of_range_association_index_is_skipped_not_indexed() {
810 let mut ipco = ispe(8, 8);
811 ipco.extend_from_slice(&av1c_420_8bit());
812 let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2, 99, 0];
815 let mut iprp = boxed(b"ipco", &ipco);
816 iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
817 let file = boxed(b"iprp", &iprp);
818
819 let mut reader = Reader::new(&file);
820 let header = reader.next_box().unwrap().unwrap();
821 let properties = Properties::parse(reader.payload(&header)).unwrap();
822 assert!(properties.for_item(1).is_empty());
823 }
824
825 #[test]
826 fn an_essential_property_we_do_not_understand_is_reported() {
827 let mut ipco = ispe(8, 8);
828 ipco.extend_from_slice(&boxed(b"zzzz", &[0; 4]));
830 let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0x82];
832 let mut iprp = boxed(b"ipco", &ipco);
833 iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
834 let file = boxed(b"iprp", &iprp);
835
836 let mut reader = Reader::new(&file);
837 let header = reader.next_box().unwrap().unwrap();
838 let properties = Properties::parse(reader.payload(&header)).unwrap();
839 assert_eq!(
840 properties.essential_unknown(1),
841 Some(FourCc::new(b"zzzz")),
842 "an essential unknown property must not be silently ignored"
843 );
844
845 let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0x02];
847 let mut iprp = boxed(b"ipco", &ipco);
848 iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
849 let file = boxed(b"iprp", &iprp);
850 let mut reader = Reader::new(&file);
851 let header = reader.next_box().unwrap().unwrap();
852 let properties = Properties::parse(reader.payload(&header)).unwrap();
853 assert_eq!(properties.essential_unknown(1), None);
854 }
855
856 #[test]
857 fn ipma_supports_wide_indices_and_wide_item_ids() {
858 let mut ipco = ispe(16, 16);
859 ipco.extend_from_slice(&av1c_420_8bit());
860 let mut ipma_payload = vec![1, 0, 0, 1];
862 ipma_payload.extend_from_slice(&1_u32.to_be_bytes()); ipma_payload.extend_from_slice(&70_000_u32.to_be_bytes()); ipma_payload.push(1); ipma_payload.extend_from_slice(&0x0001_u16.to_be_bytes()); let mut iprp = boxed(b"ipco", &ipco);
868 iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
869 let file = boxed(b"iprp", &iprp);
870
871 let mut reader = Reader::new(&file);
872 let header = reader.next_box().unwrap().unwrap();
873 let properties = Properties::parse(reader.payload(&header)).unwrap();
874 assert_eq!(
875 properties.extents(70_000),
876 Some(Extents {
877 width: 16,
878 height: 16
879 })
880 );
881 }
882
883 #[test]
886 fn ipma_rejects_more_entries_than_the_box_can_hold() {
887 let ipma_payload = vec![0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0, 1, 0];
888 let mut iprp = boxed(b"ipco", &ispe(8, 8));
889 iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
890 let file = boxed(b"iprp", &iprp);
891
892 let mut reader = Reader::new(&file);
893 let header = reader.next_box().unwrap().unwrap();
894 let error = Properties::parse(reader.payload(&header)).unwrap_err();
895 assert_eq!(error.code(), ErrorCode::Malformed);
896 assert!(error.to_string().contains("more than its"), "{error}");
897 }
898
899 #[test]
900 fn auxc_reads_the_alpha_urn() {
901 let mut payload = vec![0, 0, 0, 0];
902 payload.extend_from_slice(b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0");
903 assert_eq!(
904 parse_one(b"auxC", &payload).unwrap(),
905 Property::AuxiliaryType("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha".to_owned())
906 );
907 }
908}