Coverage Report

Created: 2026-08-13 08:17

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