avif_serialize/
lib.rs

1//! # AVIF image serializer (muxer)
2//!
3//! ## Usage
4//!
5//! 1. Compress pixels using an AV1 encoder, such as [rav1e](https://lib.rs/rav1e). [libaom](https://lib.rs/libaom-sys) works too.
6//!
7//! 2. Call `avif_serialize::serialize_to_vec(av1_data, None, width, height, 8)`
8//!
9//! See [cavif](https://github.com/kornelski/cavif-rs) for a complete implementation.
10
11mod boxes;
12pub mod constants;
13mod writer;
14
15use crate::boxes::*;
16use arrayvec::ArrayVec;
17use std::io;
18
19/// Config for the serialization (allows setting advanced image properties).
20///
21/// See [`Aviffy::new`].
22pub struct Aviffy {
23    premultiplied_alpha: bool,
24    colr: ColrBox,
25    clli: Option<ClliBox>,
26    mdcv: Option<MdcvBox>,
27    min_seq_profile: u8,
28    chroma_subsampling: (bool, bool),
29    monochrome: bool,
30    width: u32,
31    height: u32,
32    bit_depth: u8,
33    exif: Option<Vec<u8>>,
34}
35
36/// Makes an AVIF file given encoded AV1 data (create the data with [`rav1e`](https://lib.rs/rav1e))
37///
38/// `color_av1_data` is already-encoded AV1 image data for the color channels (YUV, RGB, etc.).
39/// [You can parse this information out of AV1 payload with `avif-parse`](https://docs.rs/avif-parse/latest/avif_parse/struct.AV1Metadata.html).
40///
41/// The color image should have been encoded without chroma subsampling AKA YUV444 (`Cs444` in `rav1e`)
42/// AV1 handles full-res color so effortlessly, you should never need chroma subsampling ever again.
43///
44/// Optional `alpha_av1_data` is a monochrome image (`rav1e` calls it "YUV400"/`Cs400`) representing transparency.
45/// Alpha adds a lot of header bloat, so don't specify it unless it's necessary.
46///
47/// `width`/`height` is image size in pixels. It must of course match the size of encoded image data.
48/// `depth_bits` should be 8, 10 or 12, depending on how the image was encoded.
49///
50/// Color and alpha must have the same dimensions and depth.
51///
52/// Data is written (streamed) to `into_output`.
53pub 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<()> {
54    Aviffy::new()
55        .set_width(width)
56        .set_height(height)
57        .set_bit_depth(depth_bits)
58        .write_slice(into_output, color_av1_data, alpha_av1_data)
59}
60
61impl Aviffy {
62    /// You will have to set image properties to match the AV1 bitstream.
63    ///
64    /// [You can get this information out of the AV1 payload with `avif-parse`](https://docs.rs/avif-parse/latest/avif_parse/struct.AV1Metadata.html).
65    #[inline]
66    #[must_use]
67    pub fn new() -> Self {
68        Self {
69            premultiplied_alpha: false,
70            min_seq_profile: 1,
71            chroma_subsampling: (false, false),
72            monochrome: false,
73            width: 0,
74            height: 0,
75            bit_depth: 0,
76            colr: ColrBox::default(),
77            clli: None,
78            mdcv: None,
79            exif: None,
80        }
81    }
82
83    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
84    /// Defaults to BT.601, because that's what Safari assumes when `colr` is missing.
85    /// Other browsers are smart enough to read this from the AV1 payload instead.
86    #[inline]
87    pub fn set_matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
88        self.colr.matrix_coefficients = matrix_coefficients;
89        self
90    }
91
92    #[doc(hidden)]
93    pub fn matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
94        self.set_matrix_coefficients(matrix_coefficients)
95    }
96
97    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
98    /// Defaults to sRGB.
99    #[inline]
100    pub fn set_transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
101        self.colr.transfer_characteristics = transfer_characteristics;
102        self
103    }
104
105    #[doc(hidden)]
106    pub fn transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
107        self.set_transfer_characteristics(transfer_characteristics)
108    }
109
110    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
111    /// Defaults to sRGB/Rec.709.
112    #[inline]
113    pub fn set_color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
114        self.colr.color_primaries = color_primaries;
115        self
116    }
117
118    #[doc(hidden)]
119    pub fn color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
120        self.set_color_primaries(color_primaries)
121    }
122
123    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
124    /// Defaults to full.
125    #[inline]
126    pub fn set_full_color_range(&mut self, full_range: bool) -> &mut Self {
127        self.colr.full_range_flag = full_range;
128        self
129    }
130
131    #[doc(hidden)]
132    pub fn full_color_range(&mut self, full_range: bool) -> &mut Self {
133        self.set_full_color_range(full_range)
134    }
135
136    /// Set Content Light Level Information for HDR (CEA-861.3).
137    ///
138    /// `max_content_light_level` (MaxCLL) is the maximum light level of any single pixel in cd/m².
139    /// `max_pic_average_light_level` (MaxFALL) is the maximum frame-average light level in cd/m².
140    ///
141    /// Adds a `clli` property box to the AVIF container.
142    #[inline]
143    pub fn set_content_light_level(&mut self, max_content_light_level: u16, max_pic_average_light_level: u16) -> &mut Self {
144        self.clli = Some(ClliBox {
145            max_content_light_level,
146            max_pic_average_light_level,
147        });
148        self
149    }
150
151    /// Set Mastering Display Colour Volume for HDR (SMPTE ST 2086).
152    ///
153    /// `primaries` are the display primaries in CIE 1931 xy × 50000.
154    /// Order: \[green, blue, red\] per SMPTE ST 2086.
155    /// `white_point` uses the same encoding (e.g. D65 = (15635, 16450)).
156    ///
157    /// `max_luminance` and `min_luminance` are in cd/m² × 10000
158    /// (e.g. 1000 cd/m² = 10_000_000, 0.005 cd/m² = 50).
159    ///
160    /// Adds an `mdcv` property box to the AVIF container.
161    #[inline]
162    pub fn set_mastering_display(&mut self, primaries: [(u16, u16); 3], white_point: (u16, u16), max_luminance: u32, min_luminance: u32) -> &mut Self {
163        self.mdcv = Some(MdcvBox {
164            primaries,
165            white_point,
166            max_luminance,
167            min_luminance,
168        });
169        self
170    }
171
172    /// Makes an AVIF file given encoded AV1 data (create the data with [`rav1e`](https://lib.rs/rav1e))
173    ///
174    /// `color_av1_data` is already-encoded AV1 image data for the color channels (YUV, RGB, etc.).
175    /// The color image should have been encoded without chroma subsampling AKA YUV444 (`Cs444` in `rav1e`)
176    /// AV1 handles full-res color so effortlessly, you should never need chroma subsampling ever again.
177    ///
178    /// Optional `alpha_av1_data` is a monochrome image (`rav1e` calls it "YUV400"/`Cs400`) representing transparency.
179    /// Alpha adds a lot of header bloat, so don't specify it unless it's necessary.
180    ///
181    /// `width`/`height` is image size in pixels. It must of course match the size of encoded image data.
182    /// `depth_bits` should be 8, 10 or 12, depending on how the image has been encoded in AV1.
183    ///
184    /// Color and alpha must have the same dimensions and depth.
185    ///
186    /// Data is written (streamed) to `into_output`.
187    #[inline]
188    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<()> {
189        self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits)?.write(into_output)
190    }
191
192    /// See [`Self::write`]
193    #[inline]
194    pub fn write_slice<W: io::Write>(&self, into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>) -> io::Result<()> {
195        self.make_boxes(color_av1_data, alpha_av1_data, self.width, self.height, self.bit_depth)?.write(into_output)
196    }
197
198    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>> {
199        if ![8, 10, 12].contains(&depth_bits) {
200            return Err(io::Error::new(io::ErrorKind::InvalidInput, "depth must be 8/10/12"));
201        }
202
203        let mut image_items = ArrayVec::new();
204        let mut iloc_items = ArrayVec::new();
205        let mut ipma_entries = ArrayVec::new();
206        let mut irefs = ArrayVec::new();
207        let mut ipco = IpcoBox::new();
208        let color_image_id = 1;
209        let alpha_image_id = 2;
210        let exif_id = 3;
211        const ESSENTIAL_BIT: u8 = 0x80;
212        let color_depth_bits = depth_bits;
213        let alpha_depth_bits = depth_bits; // Sadly, the spec requires these to match.
214
215        image_items.push(InfeBox {
216            id: color_image_id,
217            typ: FourCC(*b"av01"),
218            name: "",
219        });
220
221        let ispe_prop = ipco.push(IpcoProp::Ispe(IspeBox { width, height })).ok_or(io::ErrorKind::InvalidInput)?;
222
223        // This is redundant, but Chrome wants it, and checks that it matches :(
224        let av1c_color_prop = ipco.push(IpcoProp::Av1C(Av1CBox {
225            seq_profile: self.min_seq_profile.max(if color_depth_bits >= 12 { 2 } else { 0 }),
226            seq_level_idx_0: 31,
227            seq_tier_0: false,
228            high_bitdepth: color_depth_bits >= 10,
229            twelve_bit: color_depth_bits >= 12,
230            monochrome: self.monochrome,
231            chroma_subsampling_x: self.chroma_subsampling.0,
232            chroma_subsampling_y: self.chroma_subsampling.1,
233            chroma_sample_position: 0,
234        })).ok_or(io::ErrorKind::InvalidInput)?;
235
236        // Useless bloat
237        let pixi_3 = ipco.push(IpcoProp::Pixi(PixiBox {
238            channels: 3,
239            depth: color_depth_bits,
240        })).ok_or(io::ErrorKind::InvalidInput)?;
241
242        let mut ipma = IpmaEntry {
243            item_id: color_image_id,
244            prop_ids: from_array([ispe_prop, av1c_color_prop | ESSENTIAL_BIT, pixi_3]),
245        };
246
247        // Redundant info, already in AV1
248        if self.colr != ColrBox::default() {
249            let colr_color_prop = ipco.push(IpcoProp::Colr(self.colr)).ok_or(io::ErrorKind::InvalidInput)?;
250            ipma.prop_ids.push(colr_color_prop);
251        }
252
253        if let Some(clli) = self.clli {
254            let clli_prop = ipco.push(IpcoProp::Clli(clli)).ok_or(io::ErrorKind::InvalidInput)?;
255            ipma.prop_ids.push(clli_prop);
256        }
257
258        if let Some(mdcv) = self.mdcv {
259            let mdcv_prop = ipco.push(IpcoProp::Mdcv(mdcv)).ok_or(io::ErrorKind::InvalidInput)?;
260            ipma.prop_ids.push(mdcv_prop);
261        }
262
263        ipma_entries.push(ipma);
264
265        if let Some(exif_data) = self.exif.as_deref() {
266            image_items.push(InfeBox {
267                id: exif_id,
268                typ: FourCC(*b"Exif"),
269                name: "",
270            });
271
272            iloc_items.push(IlocItem {
273                id: exif_id,
274                extents: [IlocExtent { data: exif_data }],
275            });
276
277            irefs.push(IrefEntryBox {
278                from_id: exif_id,
279                to_id: color_image_id,
280                typ: FourCC(*b"cdsc"),
281            });
282        }
283
284        if let Some(alpha_data) = alpha_av1_data {
285            image_items.push(InfeBox {
286                id: alpha_image_id,
287                typ: FourCC(*b"av01"),
288                name: "",
289            });
290
291            irefs.push(IrefEntryBox {
292                from_id: alpha_image_id,
293                to_id: color_image_id,
294                typ: FourCC(*b"auxl"),
295            });
296
297            if self.premultiplied_alpha {
298                irefs.push(IrefEntryBox {
299                    from_id: color_image_id,
300                    to_id: alpha_image_id,
301                    typ: FourCC(*b"prem"),
302                });
303            }
304
305            let av1c_alpha_prop = ipco.push(boxes::IpcoProp::Av1C(Av1CBox {
306                seq_profile: if alpha_depth_bits >= 12 { 2 } else { 0 },
307                seq_level_idx_0: 31,
308                seq_tier_0: false,
309                high_bitdepth: alpha_depth_bits >= 10,
310                twelve_bit: alpha_depth_bits >= 12,
311                monochrome: true,
312                chroma_subsampling_x: true,
313                chroma_subsampling_y: true,
314                chroma_sample_position: 0,
315            })).ok_or(io::ErrorKind::InvalidInput)?;
316
317            // So pointless
318            let pixi_1 = ipco.push(IpcoProp::Pixi(PixiBox {
319                channels: 1,
320                depth: alpha_depth_bits,
321            })).ok_or(io::ErrorKind::InvalidInput)?;
322
323            // that's a silly way to add 1 bit of information, isn't it?
324            let auxc_prop = ipco.push(IpcoProp::AuxC(AuxCBox {
325                urn: "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
326            })).ok_or(io::ErrorKind::InvalidInput)?;
327
328            ipma_entries.push(IpmaEntry {
329                item_id: alpha_image_id,
330                prop_ids: from_array([ispe_prop, av1c_alpha_prop | ESSENTIAL_BIT, auxc_prop, pixi_1]),
331            });
332
333            // Use interleaved color and alpha, with alpha first.
334            // Makes it possible to display partial image.
335            iloc_items.push(IlocItem {
336                id: alpha_image_id,
337                extents: [IlocExtent { data: alpha_data }],
338            });
339        }
340        iloc_items.push(IlocItem {
341            id: color_image_id,
342            extents: [IlocExtent { data: color_av1_data }],
343        });
344
345        Ok(AvifFile {
346            ftyp: FtypBox {
347                major_brand: FourCC(*b"avif"),
348                minor_version: 0,
349                compatible_brands: [FourCC(*b"mif1"), FourCC(*b"miaf")].into(),
350            },
351            meta: MetaBox {
352                hdlr: HdlrBox {},
353                iinf: IinfBox { items: image_items },
354                pitm: PitmBox(color_image_id),
355                iloc: IlocBox {
356                    absolute_offset_start: None,
357                    items: iloc_items,
358                },
359                iprp: IprpBox {
360                    ipco,
361                    // It's not enough to define these properties,
362                    // they must be assigned to the image
363                    ipma: IpmaBox { entries: ipma_entries },
364                },
365                iref: IrefBox { entries: irefs },
366            },
367            // Here's the actual data. If HEIF wasn't such a kitchen sink, this
368            // would have been the only data this file needs.
369            mdat: MdatBox,
370        })
371    }
372
373    /// Panics if the input arguments were invalid. Use [`Self::write`] to handle the errors.
374    #[must_use]
375    #[track_caller]
376    pub fn to_vec(&self, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
377        let mut file = self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits).unwrap();
378        let mut out = Vec::new();
379        file.write_to_vec(&mut out).unwrap();
380        out
381    }
382
383    /// `(false, false)` is 4:4:4
384    /// `(true, true)` is 4:2:0
385    ///
386    /// `chroma_sample_position` is always 0. Don't use chroma subsampling with AVIF.
387    #[inline]
388    pub fn set_chroma_subsampling(&mut self, subsampled_xy: (bool, bool)) -> &mut Self {
389        self.chroma_subsampling = subsampled_xy;
390        self
391    }
392
393    /// Set whether the image is monochrome (grayscale).
394    /// This is used to set the `monochrome` flag in the AV1 sequence header.
395    #[inline]
396    pub fn set_monochrome(&mut self, monochrome: bool) -> &mut Self {
397        self.monochrome = monochrome;
398        self
399    }
400
401    /// Set exif metadata to be included in the AVIF file as a separate item.
402    #[inline]
403    pub fn set_exif(&mut self, exif: Vec<u8>) -> &mut Self {
404        self.exif = Some(exif);
405        self
406    }
407
408    /// Sets minimum required
409    ///
410    /// Higher bit depth may increase this
411    #[inline]
412    pub fn set_seq_profile(&mut self, seq_profile: u8) -> &mut Self {
413        self.min_seq_profile = seq_profile;
414        self
415    }
416
417    #[inline]
418    pub fn set_width(&mut self, width: u32) -> &mut Self {
419        self.width = width;
420        self
421    }
422
423    #[inline]
424    pub fn set_height(&mut self, height: u32) -> &mut Self {
425        self.height = height;
426        self
427    }
428
429    /// 8, 10 or 12.
430    #[inline]
431    pub fn set_bit_depth(&mut self, bit_depth: u8) -> &mut Self {
432        self.bit_depth = bit_depth;
433        self
434    }
435
436    /// Set whether image's colorspace uses premultiplied alpha, i.e. RGB channels were multiplied by their alpha value,
437    /// so that transparent areas are all black. Image decoders will be instructed to undo the premultiplication.
438    ///
439    /// Premultiplied alpha images usually compress better and tolerate heavier compression, but
440    /// may not be supported correctly by less capable AVIF decoders.
441    ///
442    /// This just sets the configuration property. The pixel data must have already been processed before compression.
443    /// If a decoder displays semitransparent colors too dark, it doesn't support premultiplied alpha.
444    /// If a decoder displays semitransparent colors too bright, you didn't premultiply the colors before encoding.
445    ///
446    /// If you're not using premultiplied alpha, consider bleeding RGB colors into transparent areas,
447    /// otherwise there may be unwanted outlines around edges of transparency.
448    #[inline]
449    pub fn set_premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
450        self.premultiplied_alpha = is_premultiplied;
451        self
452    }
453
454    #[doc(hidden)]
455    pub fn premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
456        self.set_premultiplied_alpha(is_premultiplied)
457    }
458}
459
460#[inline(always)]
461fn from_array<const L1: usize, const L2: usize, T: Copy>(array: [T; L1]) -> ArrayVec<T, L2> {
462    assert!(L1 <= L2);
463    let mut tmp = ArrayVec::new_const();
464    let _ = tmp.try_extend_from_slice(&array);
465    tmp
466}
467
468/// See [`serialize`] for description. This one makes a `Vec` instead of using `io::Write`.
469#[must_use]
470#[track_caller]
471pub fn serialize_to_vec(color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
472    Aviffy::new().to_vec(color_av1_data, alpha_av1_data, width, height, depth_bits)
473}
474
475#[test]
476fn test_roundtrip_parse_mp4() {
477    let test_img = b"av12356abc";
478    let avif = serialize_to_vec(test_img, None, 10, 20, 8);
479
480    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
481
482    assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
483}
484
485#[test]
486fn test_roundtrip_parse_mp4_alpha() {
487    let test_img = b"av12356abc";
488    let test_a = b"alpha";
489    let avif = serialize_to_vec(test_img, Some(test_a), 10, 20, 8);
490
491    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
492
493    assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
494    assert_eq!(&test_a[..], ctx.alpha_item_coded_data().unwrap());
495}
496
497#[test]
498fn test_roundtrip_parse_exif() {
499    let test_img = b"av12356abc";
500    let test_a = b"alpha";
501    let avif = Aviffy::new()
502        .set_exif(b"lol".to_vec())
503        .to_vec(test_img, Some(test_a), 10, 20, 8);
504
505    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
506
507    assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
508    assert_eq!(&test_a[..], ctx.alpha_item_coded_data().unwrap());
509}
510
511#[test]
512fn test_roundtrip_parse_avif() {
513    let test_img = [1, 2, 3, 4, 5, 6];
514    let test_alpha = [77, 88, 99];
515    let avif = serialize_to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
516
517    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
518
519    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
520    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
521}
522
523#[test]
524fn test_roundtrip_parse_avif_colr() {
525    let test_img = [1, 2, 3, 4, 5, 6];
526    let test_alpha = [77, 88, 99];
527    let avif = Aviffy::new()
528        .matrix_coefficients(constants::MatrixCoefficients::Bt709)
529        .to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
530
531    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
532
533    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
534    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
535}
536
537#[test]
538fn premultiplied_flag() {
539    let test_img = [1,2,3,4];
540    let test_alpha = [55,66,77,88,99];
541    let avif = Aviffy::new().premultiplied_alpha(true).to_vec(&test_img, Some(&test_alpha), 5, 5, 8);
542
543    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
544
545    assert!(ctx.premultiplied_alpha);
546    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
547    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
548}
549
550#[test]
551fn size_required() {
552    assert!(Aviffy::new().set_bit_depth(10).write_slice(&mut vec![], &[], None).is_err());
553}
554
555#[test]
556fn depth_required() {
557    assert!(Aviffy::new().set_width(1).set_height(1).write_slice(&mut vec![], &[], None).is_err());
558}
559
560#[test]
561fn clli_roundtrip() {
562    let test_img = [1, 2, 3, 4, 5, 6];
563    let avif = Aviffy::new()
564        .set_content_light_level(1000, 400)
565        .to_vec(&test_img, None, 10, 20, 8);
566
567    let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
568    let cll = parser.content_light_level.expect("clli box should be present");
569    assert_eq!(cll.max_content_light_level, 1000);
570    assert_eq!(cll.max_pic_average_light_level, 400);
571}
572
573#[test]
574fn mdcv_roundtrip() {
575    let test_img = [1, 2, 3, 4, 5, 6];
576    // BT.2020 primaries (standard encoding: CIE xy × 50000)
577    let primaries = [
578        (8500, 39850),   // green
579        (6550, 2300),    // blue
580        (35400, 14600),  // red
581    ];
582    let white_point = (15635, 16450); // D65
583    let max_luminance = 10_000_000; // 1000 cd/m²
584    let min_luminance = 1;          // 0.0001 cd/m²
585
586    let avif = Aviffy::new()
587        .set_mastering_display(primaries, white_point, max_luminance, min_luminance)
588        .to_vec(&test_img, None, 10, 20, 8);
589
590    let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
591    let mdcv = parser.mastering_display.expect("mdcv box should be present");
592    assert_eq!(mdcv.primaries, primaries);
593    assert_eq!(mdcv.white_point, white_point);
594    assert_eq!(mdcv.max_luminance, max_luminance);
595    assert_eq!(mdcv.min_luminance, min_luminance);
596}
597
598#[test]
599fn hdr10_full_metadata() {
600    let test_img = [1, 2, 3, 4, 5, 6];
601    let test_alpha = [77, 88, 99];
602    let primaries = [
603        (8500, 39850),
604        (6550, 2300),
605        (35400, 14600),
606    ];
607    let white_point = (15635, 16450);
608
609    let avif = Aviffy::new()
610        .set_transfer_characteristics(constants::TransferCharacteristics::Smpte2084)
611        .set_color_primaries(constants::ColorPrimaries::Bt2020)
612        .set_matrix_coefficients(constants::MatrixCoefficients::Bt2020Ncl)
613        .set_content_light_level(4000, 1000)
614        .set_mastering_display(primaries, white_point, 40_000_000, 50)
615        .to_vec(&test_img, Some(&test_alpha), 10, 20, 10);
616
617    let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
618
619    // Verify CLLI
620    let cll = parser.content_light_level.expect("clli box should be present");
621    assert_eq!(cll.max_content_light_level, 4000);
622    assert_eq!(cll.max_pic_average_light_level, 1000);
623
624    // Verify MDCV
625    let mdcv = parser.mastering_display.expect("mdcv box should be present");
626    assert_eq!(mdcv.primaries, primaries);
627    assert_eq!(mdcv.white_point, white_point);
628    assert_eq!(mdcv.max_luminance, 40_000_000);
629    assert_eq!(mdcv.min_luminance, 50);
630
631    // Verify data integrity
632    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
633    assert_eq!(ctx.primary_item.as_slice(), &test_img[..]);
634    assert_eq!(ctx.alpha_item.as_deref().unwrap(), &test_alpha[..]);
635}
636
637#[test]
638fn no_hdr_metadata_by_default() {
639    let test_img = [1, 2, 3, 4, 5, 6];
640    let avif = serialize_to_vec(&test_img, None, 10, 20, 8);
641
642    let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
643    assert!(parser.content_light_level.is_none());
644    assert!(parser.mastering_display.is_none());
645}