1use crate::av1::encode::frame::{
21 CodedStill, DEFAULT_EFFORT, Picture, encode_still_with, qindex_for_quality,
22};
23use otf_pixels_core::{
24 ChromaSubsampling, EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result,
25 Sink,
26};
27
28const CICP: (u8, u8, u8) = (1, 13, 6);
30const MAX_DIMENSION: u32 = 65_536;
32
33#[derive(Debug)]
35pub struct AvifEncoder {
36 state: Option<State>,
37 options: EncodeOptions,
38 icc: Option<Vec<u8>>,
40 exif: Option<Vec<u8>>,
42}
43
44#[derive(Debug)]
45struct State {
46 descriptor: ImageDescriptor,
47 pixels: Vec<u8>,
48 rows_written: u32,
49}
50
51impl Default for AvifEncoder {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl AvifEncoder {
58 #[must_use]
60 pub fn new() -> Self {
61 Self::from_options(&EncodeOptions::default())
62 }
63
64 #[must_use]
66 pub const fn from_options(options: &EncodeOptions) -> Self {
67 Self {
68 state: None,
69 options: *options,
70 icc: None,
71 exif: None,
72 }
73 }
74}
75
76impl Encoder for AvifEncoder {
77 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
78 if self.state.is_some() {
79 return Err(PixelsError::invalid_argument(
80 "profile",
81 "the ICC profile must be set before write_header",
82 ));
83 }
84 self.icc = profile.map(<[u8]>::to_vec);
85 Ok(())
86 }
87
88 fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
89 if self.state.is_some() {
90 return Err(PixelsError::invalid_argument(
91 "exif",
92 "the EXIF block must be set before write_header",
93 ));
94 }
95 self.exif = exif.map(<[u8]>::to_vec);
96 Ok(())
97 }
98
99 fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
100 if self.state.is_some() {
101 return Err(PixelsError::invalid_argument(
102 "descriptor",
103 "write_header called more than once",
104 ));
105 }
106 if self.options.lossless {
107 return Err(PixelsError::unsupported(
108 "lossless AVIF encoding is not implemented; encode lossy or choose PNG or WebP",
109 ));
110 }
111 match desc.pixel {
112 PixelFormat::Gray8 | PixelFormat::GrayA8 | PixelFormat::Rgb8 | PixelFormat::Rgba8 => {}
113 other => {
114 return Err(PixelsError::unsupported(format!(
115 "AVIF encoding needs an 8-bit format; got {other}. Convert first."
116 )));
117 }
118 }
119 if desc.width == 0
120 || desc.height == 0
121 || desc.width > MAX_DIMENSION
122 || desc.height > MAX_DIMENSION
123 {
124 return Err(PixelsError::unsupported(format!(
125 "AVIF dimensions must be 1 to {MAX_DIMENSION}; {}x{} is not",
126 desc.width, desc.height
127 )));
128 }
129 let capacity = desc
130 .byte_len()
131 .ok_or_else(|| PixelsError::malformed("avif", "image size overflows"))?;
132 self.state = Some(State {
133 descriptor: *desc,
134 pixels: Vec::with_capacity(capacity),
135 rows_written: 0,
136 });
137 Ok(())
138 }
139
140 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
141 let Some(state) = self.state.as_mut() else {
142 return Err(PixelsError::invalid_argument(
143 "row",
144 "write_row called before write_header",
145 ));
146 };
147 let expected = state.descriptor.row_bytes();
148 if row.len() != expected {
149 return Err(PixelsError::invalid_argument(
150 "row",
151 format!("row is {} bytes, expected {expected}", row.len()),
152 ));
153 }
154 if state.rows_written >= state.descriptor.height {
155 return Err(PixelsError::invalid_argument(
156 "row",
157 format!("more than {} rows written", state.descriptor.height),
158 ));
159 }
160 state.pixels.extend_from_slice(row);
161 state.rows_written += 1;
162 Ok(())
163 }
164
165 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
166 let Some(state) = self.state.as_ref() else {
167 return Err(PixelsError::invalid_argument(
168 "sink",
169 "finish called before write_header",
170 ));
171 };
172 if state.rows_written < state.descriptor.height {
173 return Err(PixelsError::malformed(
174 "avif",
175 format!(
176 "{} of {} rows were written",
177 state.rows_written, state.descriptor.height
178 ),
179 ));
180 }
181 let bytes = encode(
182 state,
183 &self.options,
184 self.icc.as_deref(),
185 self.exif.as_deref(),
186 )?;
187 sink.write_all(&bytes)?;
188 sink.flush()
189 }
190}
191
192fn to_planes(
195 pixels: &[u8],
196 format: PixelFormat,
197 width: usize,
198 height: usize,
199 subsampled: bool,
200) -> (Vec<Vec<u16>>, Option<Vec<u16>>) {
201 let channels = format.channels();
202 let grey = matches!(format, PixelFormat::Gray8 | PixelFormat::GrayA8);
203 let has_alpha = matches!(format, PixelFormat::GrayA8 | PixelFormat::Rgba8);
204 let pixel = |i: usize| pixels.get(i * channels..(i + 1) * channels).unwrap_or(&[]);
205 let alpha = has_alpha.then(|| {
206 (0..width * height)
207 .map(|i| u16::from(pixel(i).get(channels - 1).copied().unwrap_or(255)))
208 .collect::<Vec<u16>>()
209 });
210 if grey {
211 let y = (0..width * height)
212 .map(|i| u16::from(pixel(i).first().copied().unwrap_or(0)))
213 .collect();
214 return (vec![y], alpha);
215 }
216 let mut y = Vec::with_capacity(width * height);
218 let mut u_full = Vec::with_capacity(width * height);
219 let mut v_full = Vec::with_capacity(width * height);
220 for i in 0..width * height {
221 let p = pixel(i);
222 let [r, g, b] = [0, 1, 2].map(|k| f64::from(p.get(k).copied().unwrap_or(0)));
223 let luma = 0.299 * r + 0.587 * g + 0.114 * b;
224 y.push(luma.round().clamp(0.0, 255.0) as u16);
225 u_full.push((b - luma) / 1.772);
226 v_full.push((r - luma) / 1.402);
227 }
228 if !subsampled {
229 let full = |plane: &[f64]| -> Vec<u16> {
230 plane
231 .iter()
232 .map(|&c| (c + 128.0).round().clamp(0.0, 255.0) as u16)
233 .collect()
234 };
235 return (vec![y, full(&u_full), full(&v_full)], alpha);
236 }
237 let (cw, ch) = (width.div_ceil(2), height.div_ceil(2));
238 let average = |full: &[f64]| -> Vec<u16> {
239 let mut out = Vec::with_capacity(cw * ch);
240 for cy in 0..ch {
241 for cx in 0..cw {
242 let (mut sum, mut n) = (0.0, 0.0);
243 for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
244 let (x, yy) = (cx * 2 + dx, cy * 2 + dy);
245 if x < width && yy < height {
246 sum += full.get(yy * width + x).copied().unwrap_or(0.0);
247 n += 1.0;
248 }
249 }
250 out.push((sum / n + 128.0).round().clamp(0.0, 255.0) as u16);
251 }
252 }
253 out
254 };
255 (vec![y, average(&u_full), average(&v_full)], alpha)
256}
257
258fn encode(
259 state: &State,
260 options: &EncodeOptions,
261 icc: Option<&[u8]>,
262 exif: Option<&[u8]>,
263) -> Result<Vec<u8>> {
264 let (width, height) = (state.descriptor.width, state.descriptor.height);
265 let subsampled = options.chroma_subsampling != Some(ChromaSubsampling::Yuv444);
266 let effort = options.effort.unwrap_or(DEFAULT_EFFORT).min(9);
267 let (planes, alpha) = to_planes(
268 &state.pixels,
269 state.descriptor.pixel,
270 width as usize,
271 height as usize,
272 subsampled,
273 );
274 let qindex = qindex_for_quality(options.quality);
275 let refs: Vec<&[u16]> = planes.iter().map(Vec::as_slice).collect();
276 let colour = encode_still_with(
277 &Picture {
278 width,
279 height,
280 subsampled,
281 planes: &refs,
282 cicp: CICP,
283 full_range: true,
284 },
285 qindex,
286 effort,
287 )?;
288 let alpha = match alpha.filter(|a| a.iter().any(|&v| v != 255)) {
290 Some(alpha) => Some(encode_still_with(
291 &Picture {
292 width,
293 height,
294 subsampled: true,
295 planes: &[&alpha],
296 cicp: (2, 2, 2),
297 full_range: true,
298 },
299 (qindex / 2).max(1),
300 effort,
301 )?),
302 None => None,
303 };
304 Ok(container(
305 width,
306 height,
307 planes.len() == 1,
308 &colour,
309 alpha.as_ref(),
310 icc,
311 exif,
312 ))
313}
314
315fn bx(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
317 let mut out = Vec::with_capacity(payload.len() + 8);
318 out.extend_from_slice(&(payload.len() as u32 + 8).to_be_bytes());
319 out.extend_from_slice(kind);
320 out.extend_from_slice(payload);
321 out
322}
323
324fn full(kind: &[u8; 4], version: u8, flags: u32, payload: &[u8]) -> Vec<u8> {
326 let mut body = vec![version];
327 body.extend_from_slice(&flags.to_be_bytes()[1..]);
328 body.extend_from_slice(payload);
329 bx(kind, &body)
330}
331
332fn av1c(coded: &CodedStill, mono: bool) -> Vec<u8> {
334 let mut payload = vec![
335 0x81, (coded.profile << 5) | (coded.level & 0x1F),
337 (u8::from(mono) << 4) | if coded.subsampled { 0b1100 } else { 0 },
340 0, ];
342 payload.extend_from_slice(&coded.sequence_header_obu);
343 bx(b"av1C", &payload)
344}
345
346fn container(
348 width: u32,
349 height: u32,
350 mono: bool,
351 colour: &CodedStill,
352 alpha: Option<&CodedStill>,
353 icc: Option<&[u8]>,
354 exif: Option<&[u8]>,
355) -> Vec<u8> {
356 let ftyp = bx(b"ftyp", b"avif\0\0\0\0avifmif1miaf");
357 let ispe = {
358 let mut p = width.to_be_bytes().to_vec();
359 p.extend_from_slice(&height.to_be_bytes());
360 full(b"ispe", 0, 0, &p)
361 };
362 let pixi = |channels: u8| {
363 let mut p = vec![channels];
364 p.extend(std::iter::repeat_n(8, usize::from(channels)));
365 full(b"pixi", 0, 0, &p)
366 };
367 let colr = {
368 let mut p = b"nclx".to_vec();
369 for v in [CICP.0, CICP.1, CICP.2] {
370 p.extend_from_slice(&u16::from(v).to_be_bytes());
371 }
372 p.push(0x80); bx(b"colr", &p)
374 };
375 let mut ipco = [
378 ispe,
379 pixi(if mono { 1 } else { 3 }),
380 av1c(colour, mono),
381 colr,
382 ]
383 .concat();
384 let mut associations: Vec<(u16, Vec<u8>)> = vec![(1, vec![1, 2, 0x80 | 3, 4])];
385 if let Some(alpha) = alpha {
386 let mut urn = crate::meta::URN_ALPHA.as_bytes().to_vec();
387 urn.push(0);
388 ipco.extend_from_slice(&[pixi(1), av1c(alpha, true), full(b"auxC", 0, 0, &urn)].concat());
389 associations.push((2, vec![1, 5, 0x80 | 6, 7]));
390 }
391 if let Some(profile) = icc {
394 ipco.extend_from_slice(&bx(b"colr", &[b"prof".as_slice(), profile].concat()));
395 let index = if alpha.is_some() { 8 } else { 5 };
396 if let Some((_, props)) = associations.first_mut() {
397 props.push(index);
398 }
399 }
400 let ipma = {
401 let mut p = (associations.len() as u32).to_be_bytes().to_vec();
402 for (item, props) in &associations {
403 p.extend_from_slice(&item.to_be_bytes());
404 p.push(props.len() as u8);
405 p.extend_from_slice(props);
406 }
407 full(b"ipma", 0, 0, &p)
408 };
409 let iprp = bx(b"iprp", &[bx(b"ipco", &ipco), ipma].concat());
410
411 let hdlr = full(b"hdlr", 0, 0, b"\0\0\0\0pict\0\0\0\0\0\0\0\0\0\0\0\0\0");
412 let pitm = full(b"pitm", 0, 0, &1_u16.to_be_bytes());
413 let infe = |item: u16, kind: &[u8; 4]| {
414 let mut p = item.to_be_bytes().to_vec();
415 p.extend_from_slice(&[0, 0]);
416 p.extend_from_slice(kind);
417 p.push(0); full(b"infe", 2, 0, &p)
419 };
420 let exif_data = exif.map(|block| [&[0_u8; 4][..], block].concat());
424 let mut items: Vec<(&[u8; 4], &[u8])> = std::iter::once(colour)
425 .chain(alpha)
426 .map(|coded| (b"av01", coded.data.as_slice()))
427 .collect();
428 if let Some(data) = &exif_data {
429 items.push((b"Exif", data));
430 }
431 let iinf = {
432 let mut p = (items.len() as u16).to_be_bytes().to_vec();
433 for (i, (kind, _)) in items.iter().enumerate() {
434 p.extend_from_slice(&infe(i as u16 + 1, kind));
435 }
436 full(b"iinf", 0, 0, &p)
437 };
438 let mut references = Vec::new();
441 if alpha.is_some() {
442 references.extend(bx(b"auxl", &[0, 2, 0, 1, 0, 1]));
443 }
444 if exif.is_some() {
445 let id = items.len() as u16;
446 references.extend(bx(
447 b"cdsc",
448 &[&id.to_be_bytes()[..], &[0, 1, 0, 1]].concat(),
449 ));
450 }
451 let iref = (!references.is_empty()).then(|| full(b"iref", 0, 0, &references));
452 let iloc = |mdat_start: u32| {
454 let mut p = vec![0x44, 0x00];
455 p.extend_from_slice(&(items.len() as u16).to_be_bytes());
456 let mut offset = mdat_start;
457 for (i, (_, data)) in items.iter().enumerate() {
458 p.extend_from_slice(&(i as u16 + 1).to_be_bytes());
459 p.extend_from_slice(&[0, 0, 0, 1]); p.extend_from_slice(&offset.to_be_bytes());
461 p.extend_from_slice(&(data.len() as u32).to_be_bytes());
462 offset += data.len() as u32;
463 }
464 full(b"iloc", 0, 0, &p)
465 };
466 let meta = |mdat_start: u32| {
467 let mut p = [hdlr.clone(), pitm.clone(), iloc(mdat_start), iinf.clone()].concat();
468 if let Some(iref) = &iref {
469 p.extend_from_slice(iref);
470 }
471 p.extend_from_slice(&iprp);
472 full(b"meta", 0, 0, &p)
473 };
474 let mdat_start = (ftyp.len() + meta(0).len() + 8) as u32;
476 let mdat_payload: Vec<u8> = items
477 .iter()
478 .flat_map(|(_, data)| data.iter().copied())
479 .collect();
480 [ftyp, meta(mdat_start), bx(b"mdat", &mdat_payload)].concat()
481}
482
483#[cfg(test)]
484#[allow(
485 clippy::unwrap_used,
486 clippy::indexing_slicing,
487 reason = "tests operate on known-good values"
488)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn rgb_to_yuv_follows_bt601_full_range() {
494 let pixels = [255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 255];
495 let (planes, alpha) = to_planes(&pixels, PixelFormat::Rgb8, 2, 2, true);
496 assert!(alpha.is_none());
497 assert_eq!(planes[0], vec![255, 0, 76, 29]);
498 let u = (((-76.245_f64) + 255.0 * (1.0 - 0.114)) / 1.772 / 4.0 + 128.0).round();
500 assert!(
501 (i32::from(planes[1][0]) - u as i32).abs() <= 1,
502 "{} vs {u}",
503 planes[1][0]
504 );
505 assert_eq!(planes[1].len(), 1);
506 }
507
508 #[test]
509 fn grey_keeps_one_plane_and_alpha_is_split_out() {
510 let pixels = [10, 200, 20, 255, 30, 0];
511 let (planes, alpha) = to_planes(&pixels, PixelFormat::GrayA8, 3, 1, true);
512 assert_eq!(planes, vec![vec![10, 20, 30]]);
513 assert_eq!(alpha.unwrap(), vec![200, 255, 0]);
514 }
515
516 #[test]
517 fn odd_sizes_average_only_the_samples_present() {
518 let pixels = [0, 0, 255, 0, 0, 255, 0, 0, 255];
519 let (planes, _) = to_planes(&pixels, PixelFormat::Rgb8, 3, 1, true);
520 assert_eq!((planes[1].len(), planes[2].len()), (2, 2));
521 assert_eq!(planes[1][0], planes[1][1]);
522 }
523
524 fn encode_with(descriptor: &ImageDescriptor, pixels: &[u8], options: EncodeOptions) -> Vec<u8> {
526 let mut encoder = AvifEncoder::from_options(&options);
527 let mut out = Vec::new();
528 encoder.write_header(descriptor, &mut out).unwrap();
529 for row in pixels.chunks_exact(descriptor.row_bytes()) {
530 encoder.write_row(row, &mut out).unwrap();
531 }
532 encoder.finish(&mut out).unwrap();
533 out
534 }
535
536 fn decode(bytes: &[u8]) -> Vec<u8> {
537 use otf_pixels_core::{Decoder, Limits};
538 let mut decoder = crate::AvifDecoder::new(bytes, Limits::default()).unwrap();
539 let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
540 let mut out = Vec::new();
541 for _ in 0..decoder.descriptor().height {
542 decoder.read_row(&mut row).unwrap();
543 out.extend_from_slice(&row);
544 }
545 out
546 }
547
548 fn av1c_bytes(file: &[u8]) -> (u8, u8) {
550 let at = file.windows(4).position(|w| w == b"av1C").unwrap() + 4;
551 (file[at + 1] >> 5, file[at + 2] & 0b1100)
552 }
553
554 #[test]
555 fn full_resolution_chroma_keeps_every_chroma_sample() {
556 let pixels = [255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 255];
557 let (planes, _) = to_planes(&pixels, PixelFormat::Rgb8, 2, 2, false);
558 assert_eq!((planes[1].len(), planes[2].len()), (4, 4));
559 assert_eq!(planes[1][0], 128, "white has no chroma");
560 assert_eq!(planes[2][2], 255, "red's V is full scale");
561 }
562
563 #[test]
564 fn four_four_four_is_profile_one_and_sharper_in_colour() {
565 let descriptor = ImageDescriptor::new(32, 16, PixelFormat::Rgb8).unwrap();
567 let pixels: Vec<u8> = (0..32 * 16)
568 .flat_map(|i| if i % 2 == 0 { [255, 0, 0] } else { [0, 0, 255] })
569 .collect();
570 let options = EncodeOptions::with_quality(90).unwrap();
571 let sub = encode_with(&descriptor, &pixels, options);
572 let full = encode_with(
573 &descriptor,
574 &pixels,
575 options.with_chroma_subsampling(ChromaSubsampling::Yuv444),
576 );
577 assert_eq!(av1c_bytes(&sub), (0, 0b1100));
578 assert_eq!(av1c_bytes(&full), (1, 0));
579 assert_eq!(
580 encode_with(
581 &descriptor,
582 &pixels,
583 options.with_chroma_subsampling(ChromaSubsampling::Yuv420)
584 ),
585 sub
586 );
587 let error = |bytes: &[u8]| -> u64 {
588 decode(bytes)
589 .iter()
590 .zip(&pixels)
591 .map(|(a, b)| u64::from(a.abs_diff(*b)))
592 .sum()
593 };
594 assert!(
595 error(&full) * 2 < error(&sub),
596 "{} against {}",
597 error(&full),
598 error(&sub)
599 );
600 }
601
602 #[test]
603 fn grey_and_alpha_stay_profile_zero_under_four_four_four() {
604 let descriptor = ImageDescriptor::new(9, 7, PixelFormat::GrayA8).unwrap();
605 let pixels: Vec<u8> = (0..9 * 7)
606 .flat_map(|i| [(i * 4) as u8, (i * 3) as u8])
607 .collect();
608 let file = encode_with(
609 &descriptor,
610 &pixels,
611 EncodeOptions::default().with_chroma_subsampling(ChromaSubsampling::Yuv444),
612 );
613 assert_eq!(av1c_bytes(&file).0, 0);
614 assert_eq!(decode(&file).len(), 9 * 7 * 2);
615 }
616
617 #[test]
618 fn effort_narrows_the_search_and_four_is_the_default() {
619 let descriptor = ImageDescriptor::new(40, 24, PixelFormat::Rgb8).unwrap();
620 let pixels: Vec<u8> = (0..40 * 24 * 3)
621 .map(|i| ((i * 13 + i / 97) % 256) as u8)
622 .collect();
623 let base = EncodeOptions::default();
624 let default = encode_with(&descriptor, &pixels, base);
625 for effort in 3..=10 {
626 assert_eq!(
627 encode_with(&descriptor, &pixels, base.with_effort(effort).unwrap()),
628 default,
629 "effort {effort}"
630 );
631 }
632 let fast = encode_with(&descriptor, &pixels, base.with_effort(0).unwrap());
633 assert_ne!(fast, default);
634 assert_eq!(decode(&fast).len(), pixels.len());
635 }
636}