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 | /// Config for the serialization (allows setting advanced image properties). |
20 | /// |
21 | /// See [`Aviffy::new`]. |
22 | pub 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`. |
44 | 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<()> { |
45 | Aviffy::new().write(into_output, color_av1_data, alpha_av1_data, width, height, depth_bits) |
46 | } |
47 | |
48 | impl 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 ] |
304 | fn 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 ] |
314 | fn 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 ] |
326 | fn 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 ] |
338 | fn 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 ] |
352 | fn 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 | |