avif_serialize/
lib.rs

1//! # AVIF image serializer (muxer)
2//!
3//! ## Usage
4//!
5//! 1. Compress pixels using an AV1 encoder, such as [rav1e](//lib.rs/rav1e). [libaom](//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}
26
27/// Makes an AVIF file given encoded AV1 data (create the data with [`rav1e`](//lib.rs/rav1e))
28///
29/// `color_av1_data` is already-encoded AV1 image data for the color channels (YUV, RGB, etc.).
30/// The color image MUST have been encoded without chroma subsampling AKA YUV444 (`Cs444` in `rav1e`)
31/// AV1 handles full-res color so effortlessly, you should never need chroma subsampling ever again.
32///
33/// Optional `alpha_av1_data` is a monochrome image (`rav1e` calls it "YUV400"/`Cs400`) representing transparency.
34/// Alpha adds a lot of header bloat, so don't specify it unless it's necessary.
35///
36/// `width`/`height` is image size in pixels. It must of course match the size of encoded image data.
37/// `depth_bits` should be 8, 10 or 12, depending on how the image was encoded (typically 8).
38///
39/// Color and alpha must have the same dimensions and depth.
40///
41/// Data is written (streamed) to `into_output`.
42pub 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<()> {
43    Aviffy::new().write(into_output, color_av1_data, alpha_av1_data, width, height, depth_bits)
44}
45
46impl Aviffy {
47    #[must_use]
48    pub fn new() -> Self {
49        Self {
50            premultiplied_alpha: false,
51            colr: Default::default(),
52        }
53    }
54
55    /// Set whether image's colorspace uses premultiplied alpha, i.e. RGB channels were multiplied by their alpha value,
56    /// so that transparent areas are all black. Image decoders will be instructed to undo the premultiplication.
57    ///
58    /// Premultiplied alpha images usually compress better and tolerate heavier compression, but
59    /// may not be supported correctly by less capable AVIF decoders.
60    ///
61    /// This just sets the configuration property. The pixel data must have already been processed before compression.
62    pub fn premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
63        self.premultiplied_alpha = is_premultiplied;
64        self
65    }
66
67    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
68    /// Defaults to BT.601, because that's what Safari assumes when `colr` is missing.
69    /// Other browsers are smart enough to read this from the AV1 payload instead.
70    pub fn matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
71        self.colr.matrix_coefficients = matrix_coefficients;
72        self
73    }
74
75    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
76    /// Defaults to sRGB.
77    pub fn transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
78        self.colr.transfer_characteristics = transfer_characteristics;
79        self
80    }
81
82    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
83    /// Defaults to sRGB/Rec.709.
84    pub fn color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
85        self.colr.color_primaries = color_primaries;
86        self
87    }
88
89    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
90    /// Defaults to full.
91    pub fn full_color_range(&mut self, full_range: bool) -> &mut Self {
92        self.colr.full_range_flag = full_range;
93        self
94    }
95
96    /// Makes an AVIF file given encoded AV1 data (create the data with [`rav1e`](//lib.rs/rav1e))
97    ///
98    /// `color_av1_data` is already-encoded AV1 image data for the color channels (YUV, RGB, etc.).
99    /// The color image MUST have been encoded without chroma subsampling AKA YUV444 (`Cs444` in `rav1e`)
100    /// AV1 handles full-res color so effortlessly, you should never need chroma subsampling ever again.
101    ///
102    /// Optional `alpha_av1_data` is a monochrome image (`rav1e` calls it "YUV400"/`Cs400`) representing transparency.
103    /// Alpha adds a lot of header bloat, so don't specify it unless it's necessary.
104    ///
105    /// `width`/`height` is image size in pixels. It must of course match the size of encoded image data.
106    /// `depth_bits` should be 8, 10 or 12, depending on how the image has been encoded in AV1.
107    ///
108    /// Color and alpha must have the same dimensions and depth.
109    ///
110    /// Data is written (streamed) to `into_output`.
111    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<()> {
112        self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits).write(into_output)
113    }
114
115    fn make_boxes<'data>(&self, color_av1_data: &'data [u8], alpha_av1_data: Option<&'data [u8]>, width: u32, height: u32, depth_bits: u8) -> AvifFile<'data> {
116        let mut image_items = ArrayVec::new();
117        let mut iloc_items = ArrayVec::new();
118        let mut compatible_brands = ArrayVec::new();
119        let mut ipma_entries = ArrayVec::new();
120        let mut data_chunks = ArrayVec::new();
121        let mut irefs = ArrayVec::new();
122        let mut ipco = IpcoBox::new();
123        let color_image_id = 1;
124        let alpha_image_id = 2;
125        const ESSENTIAL_BIT: u8 = 0x80;
126        let color_depth_bits = depth_bits;
127        let alpha_depth_bits = depth_bits; // Sadly, the spec requires these to match.
128
129        image_items.push(InfeBox {
130            id: color_image_id,
131            typ: FourCC(*b"av01"),
132            name: "",
133        });
134        let ispe_prop = ipco.push(IpcoProp::Ispe(IspeBox { width, height }));
135        // This is redundant, but Chrome wants it, and checks that it matches :(
136        let av1c_color_prop = ipco.push(IpcoProp::Av1C(Av1CBox {
137            seq_profile: if color_depth_bits >= 12 { 2 } else { 1 },
138            seq_level_idx_0: 31,
139            seq_tier_0: false,
140            high_bitdepth: color_depth_bits >= 10,
141            twelve_bit: color_depth_bits >= 12,
142            monochrome: false,
143            chroma_subsampling_x: false,
144            chroma_subsampling_y: false,
145            chroma_sample_position: 0,
146        }));
147        // Useless bloat
148        let pixi_3 = ipco.push(IpcoProp::Pixi(PixiBox {
149            channels: 3,
150            depth: color_depth_bits,
151        }));
152        let mut prop_ids: ArrayVec<u8, 5> = [ispe_prop, av1c_color_prop | ESSENTIAL_BIT, pixi_3].into_iter().collect();
153        // Redundant info, already in AV1
154        if self.colr != Default::default() {
155            let colr_color_prop = ipco.push(IpcoProp::Colr(self.colr));
156            prop_ids.push(colr_color_prop);
157        }
158        ipma_entries.push(IpmaEntry {
159            item_id: color_image_id,
160            prop_ids,
161        });
162
163        if let Some(alpha_data) = alpha_av1_data {
164            image_items.push(InfeBox {
165                id: alpha_image_id,
166                typ: FourCC(*b"av01"),
167                name: "",
168            });
169            let av1c_alpha_prop = ipco.push(boxes::IpcoProp::Av1C(Av1CBox {
170                seq_profile: if alpha_depth_bits >= 12 { 2 } else { 0 },
171                seq_level_idx_0: 31,
172                seq_tier_0: false,
173                high_bitdepth: alpha_depth_bits >= 10,
174                twelve_bit: alpha_depth_bits >= 12,
175                monochrome: true,
176                chroma_subsampling_x: true,
177                chroma_subsampling_y: true,
178                chroma_sample_position: 0,
179            }));
180            // So pointless
181            let pixi_1 = ipco.push(IpcoProp::Pixi(PixiBox {
182                channels: 1,
183                depth: alpha_depth_bits,
184            }));
185
186            // that's a silly way to add 1 bit of information, isn't it?
187            let auxc_prop = ipco.push(IpcoProp::AuxC(AuxCBox {
188                urn: "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
189            }));
190            irefs.push(IrefBox {
191                entry: IrefEntryBox {
192                    from_id: alpha_image_id,
193                    to_id: color_image_id,
194                    typ: FourCC(*b"auxl"),
195                },
196            });
197            if self.premultiplied_alpha {
198                irefs.push(IrefBox {
199                    entry: IrefEntryBox {
200                        from_id: color_image_id,
201                        to_id: alpha_image_id,
202                        typ: FourCC(*b"prem"),
203                    },
204                });
205            }
206            ipma_entries.push(IpmaEntry {
207                item_id: alpha_image_id,
208                prop_ids: [ispe_prop, av1c_alpha_prop | ESSENTIAL_BIT, auxc_prop, pixi_1].into_iter().collect(),
209            });
210
211            // Use interleaved color and alpha, with alpha first.
212            // Makes it possible to display partial image.
213            iloc_items.push(IlocItem {
214                id: color_image_id,
215                extents: [
216                    IlocExtent {
217                        offset: IlocOffset::Relative(alpha_data.len()),
218                        len: color_av1_data.len(),
219                    },
220                ].into(),
221            });
222            iloc_items.push(IlocItem {
223                id: alpha_image_id,
224                extents: [
225                    IlocExtent {
226                        offset: IlocOffset::Relative(0),
227                        len: alpha_data.len(),
228                    },
229                ].into(),
230            });
231            data_chunks.push(alpha_data);
232            data_chunks.push(color_av1_data);
233        } else {
234            iloc_items.push(IlocItem {
235                id: color_image_id,
236                extents: [
237                    IlocExtent {
238                        offset: IlocOffset::Relative(0),
239                        len: color_av1_data.len(),
240                    },
241                ].into(),
242            });
243            data_chunks.push(color_av1_data);
244        };
245
246        compatible_brands.push(FourCC(*b"mif1"));
247        compatible_brands.push(FourCC(*b"miaf"));
248        AvifFile {
249            ftyp: FtypBox {
250                major_brand: FourCC(*b"avif"),
251                minor_version: 0,
252                compatible_brands,
253            },
254            meta: MetaBox {
255                hdlr: HdlrBox {},
256                iinf: IinfBox { items: image_items },
257                pitm: PitmBox(color_image_id),
258                iloc: IlocBox { items: iloc_items },
259                iprp: IprpBox {
260                    ipco,
261                    // It's not enough to define these properties,
262                    // they must be assigned to the image
263                    ipma: IpmaBox {
264                        entries: ipma_entries,
265                    },
266                },
267                iref: irefs,
268            },
269            // Here's the actual data. If HEIF wasn't such a kitchen sink, this
270            // would have been the only data this file needs.
271            mdat: MdatBox {
272                data_chunks,
273            },
274        }
275    }
276
277    #[must_use] pub fn to_vec(&self, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
278        let mut out = Vec::with_capacity(color_av1_data.len() + alpha_av1_data.map_or(0, |a| a.len()) + 410);
279        self.write(&mut out, color_av1_data, alpha_av1_data, width, height, depth_bits).unwrap(); // Vec can't fail
280        out
281    }
282}
283
284/// See [`serialize`] for description. This one makes a `Vec` instead of using `io::Write`.
285#[must_use] pub fn serialize_to_vec(color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
286    Aviffy::new().to_vec(color_av1_data, alpha_av1_data, width, height, depth_bits)
287}
288
289#[test]
290fn test_roundtrip_parse_mp4() {
291    let test_img = b"av12356abc";
292    let avif = serialize_to_vec(test_img, None, 10, 20, 8);
293
294    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
295
296    assert_eq!(&test_img[..], ctx.primary_item_coded_data());
297}
298
299#[test]
300fn test_roundtrip_parse_mp4_alpha() {
301    let test_img = b"av12356abc";
302    let test_a = b"alpha";
303    let avif = serialize_to_vec(test_img, Some(test_a), 10, 20, 8);
304
305    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
306
307    assert_eq!(&test_img[..], ctx.primary_item_coded_data());
308    assert_eq!(&test_a[..], ctx.alpha_item_coded_data());
309}
310
311#[test]
312fn test_roundtrip_parse_avif() {
313    let test_img = [1,2,3,4,5,6];
314    let test_alpha = [77,88,99];
315    let avif = serialize_to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
316
317    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
318
319    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
320    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
321}
322
323#[test]
324fn test_roundtrip_parse_avif_colr() {
325    let test_img = [1,2,3,4,5,6];
326    let test_alpha = [77,88,99];
327    let avif = Aviffy::new()
328        .matrix_coefficients(constants::MatrixCoefficients::Bt709)
329        .to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
330
331    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
332
333    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
334    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
335}
336
337#[test]
338fn premultiplied_flag() {
339    let test_img = [1,2,3,4];
340    let test_alpha = [55,66,77,88,99];
341    let avif = Aviffy::new().premultiplied_alpha(true).to_vec(&test_img, Some(&test_alpha), 5, 5, 8);
342
343    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
344
345    assert!(ctx.premultiplied_alpha);
346    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
347    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
348}