| 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 | |