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