1use crate::av1::encode::frame::{CodedStill, Picture, encode_still, qindex_for_quality};
17use otf_pixels_core::{
18 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
19};
20
21const CICP: (u8, u8, u8) = (1, 13, 6);
23const MAX_DIMENSION: u32 = 65_536;
25
26#[derive(Debug)]
28pub struct AvifEncoder {
29 state: Option<State>,
30 options: EncodeOptions,
31 icc: Option<Vec<u8>>,
33}
34
35#[derive(Debug)]
36struct State {
37 descriptor: ImageDescriptor,
38 pixels: Vec<u8>,
39 rows_written: u32,
40}
41
42impl Default for AvifEncoder {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl AvifEncoder {
49 #[must_use]
51 pub fn new() -> Self {
52 Self::from_options(&EncodeOptions::default())
53 }
54
55 #[must_use]
57 pub const fn from_options(options: &EncodeOptions) -> Self {
58 Self {
59 state: None,
60 options: *options,
61 icc: None,
62 }
63 }
64}
65
66impl Encoder for AvifEncoder {
67 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
68 if self.state.is_some() {
69 return Err(PixelsError::invalid_argument(
70 "profile",
71 "the ICC profile must be set before write_header",
72 ));
73 }
74 self.icc = profile.map(<[u8]>::to_vec);
75 Ok(())
76 }
77
78 fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
79 if self.state.is_some() {
80 return Err(PixelsError::invalid_argument(
81 "descriptor",
82 "write_header called more than once",
83 ));
84 }
85 if self.options.lossless {
86 return Err(PixelsError::unsupported(
87 "lossless AVIF encoding is not implemented; encode lossy or choose PNG or WebP",
88 ));
89 }
90 match desc.pixel {
91 PixelFormat::Gray8 | PixelFormat::GrayA8 | PixelFormat::Rgb8 | PixelFormat::Rgba8 => {}
92 other => {
93 return Err(PixelsError::unsupported(format!(
94 "AVIF encoding needs an 8-bit format; got {other}. Convert first."
95 )));
96 }
97 }
98 if desc.width == 0
99 || desc.height == 0
100 || desc.width > MAX_DIMENSION
101 || desc.height > MAX_DIMENSION
102 {
103 return Err(PixelsError::unsupported(format!(
104 "AVIF dimensions must be 1 to {MAX_DIMENSION}; {}x{} is not",
105 desc.width, desc.height
106 )));
107 }
108 let capacity = desc
109 .byte_len()
110 .ok_or_else(|| PixelsError::malformed("avif", "image size overflows"))?;
111 self.state = Some(State {
112 descriptor: *desc,
113 pixels: Vec::with_capacity(capacity),
114 rows_written: 0,
115 });
116 Ok(())
117 }
118
119 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
120 let Some(state) = self.state.as_mut() else {
121 return Err(PixelsError::invalid_argument(
122 "row",
123 "write_row called before write_header",
124 ));
125 };
126 let expected = state.descriptor.row_bytes();
127 if row.len() != expected {
128 return Err(PixelsError::invalid_argument(
129 "row",
130 format!("row is {} bytes, expected {expected}", row.len()),
131 ));
132 }
133 if state.rows_written >= state.descriptor.height {
134 return Err(PixelsError::invalid_argument(
135 "row",
136 format!("more than {} rows written", state.descriptor.height),
137 ));
138 }
139 state.pixels.extend_from_slice(row);
140 state.rows_written += 1;
141 Ok(())
142 }
143
144 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
145 let Some(state) = self.state.as_ref() else {
146 return Err(PixelsError::invalid_argument(
147 "sink",
148 "finish called before write_header",
149 ));
150 };
151 if state.rows_written < state.descriptor.height {
152 return Err(PixelsError::malformed(
153 "avif",
154 format!(
155 "{} of {} rows were written",
156 state.rows_written, state.descriptor.height
157 ),
158 ));
159 }
160 let bytes = encode(state, self.options.quality, self.icc.as_deref())?;
161 sink.write_all(&bytes)?;
162 sink.flush()
163 }
164}
165
166fn to_planes(
169 pixels: &[u8],
170 format: PixelFormat,
171 width: usize,
172 height: usize,
173) -> (Vec<Vec<u16>>, Option<Vec<u16>>) {
174 let channels = format.channels();
175 let grey = matches!(format, PixelFormat::Gray8 | PixelFormat::GrayA8);
176 let has_alpha = matches!(format, PixelFormat::GrayA8 | PixelFormat::Rgba8);
177 let pixel = |i: usize| pixels.get(i * channels..(i + 1) * channels).unwrap_or(&[]);
178 let alpha = has_alpha.then(|| {
179 (0..width * height)
180 .map(|i| u16::from(pixel(i).get(channels - 1).copied().unwrap_or(255)))
181 .collect::<Vec<u16>>()
182 });
183 if grey {
184 let y = (0..width * height)
185 .map(|i| u16::from(pixel(i).first().copied().unwrap_or(0)))
186 .collect();
187 return (vec![y], alpha);
188 }
189 let mut y = Vec::with_capacity(width * height);
191 let mut u_full = Vec::with_capacity(width * height);
192 let mut v_full = Vec::with_capacity(width * height);
193 for i in 0..width * height {
194 let p = pixel(i);
195 let [r, g, b] = [0, 1, 2].map(|k| f64::from(p.get(k).copied().unwrap_or(0)));
196 let luma = 0.299 * r + 0.587 * g + 0.114 * b;
197 y.push(luma.round().clamp(0.0, 255.0) as u16);
198 u_full.push((b - luma) / 1.772);
199 v_full.push((r - luma) / 1.402);
200 }
201 let (cw, ch) = (width.div_ceil(2), height.div_ceil(2));
202 let average = |full: &[f64]| -> Vec<u16> {
203 let mut out = Vec::with_capacity(cw * ch);
204 for cy in 0..ch {
205 for cx in 0..cw {
206 let (mut sum, mut n) = (0.0, 0.0);
207 for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
208 let (x, yy) = (cx * 2 + dx, cy * 2 + dy);
209 if x < width && yy < height {
210 sum += full.get(yy * width + x).copied().unwrap_or(0.0);
211 n += 1.0;
212 }
213 }
214 out.push((sum / n + 128.0).round().clamp(0.0, 255.0) as u16);
215 }
216 }
217 out
218 };
219 (vec![y, average(&u_full), average(&v_full)], alpha)
220}
221
222fn encode(state: &State, quality: u8, icc: Option<&[u8]>) -> Result<Vec<u8>> {
223 let (width, height) = (state.descriptor.width, state.descriptor.height);
224 let (planes, alpha) = to_planes(
225 &state.pixels,
226 state.descriptor.pixel,
227 width as usize,
228 height as usize,
229 );
230 let qindex = qindex_for_quality(quality);
231 let refs: Vec<&[u16]> = planes.iter().map(Vec::as_slice).collect();
232 let colour = encode_still(
233 &Picture {
234 width,
235 height,
236 planes: &refs,
237 cicp: CICP,
238 full_range: true,
239 },
240 qindex,
241 )?;
242 let alpha = match alpha.filter(|a| a.iter().any(|&v| v != 255)) {
244 Some(alpha) => Some(encode_still(
245 &Picture {
246 width,
247 height,
248 planes: &[&alpha],
249 cicp: (2, 2, 2),
250 full_range: true,
251 },
252 (qindex / 2).max(1),
253 )?),
254 None => None,
255 };
256 Ok(container(
257 width,
258 height,
259 planes.len() == 1,
260 &colour,
261 alpha.as_ref(),
262 icc,
263 ))
264}
265
266fn bx(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
268 let mut out = Vec::with_capacity(payload.len() + 8);
269 out.extend_from_slice(&(payload.len() as u32 + 8).to_be_bytes());
270 out.extend_from_slice(kind);
271 out.extend_from_slice(payload);
272 out
273}
274
275fn full(kind: &[u8; 4], version: u8, flags: u32, payload: &[u8]) -> Vec<u8> {
277 let mut body = vec![version];
278 body.extend_from_slice(&flags.to_be_bytes()[1..]);
279 body.extend_from_slice(payload);
280 bx(kind, &body)
281}
282
283fn av1c(coded: &CodedStill, mono: bool) -> Vec<u8> {
285 let mut payload = vec![
286 0x81, coded.level & 0x1F, (u8::from(mono) << 4) | 0b1100,
290 0, ];
292 payload.extend_from_slice(&coded.sequence_header_obu);
293 bx(b"av1C", &payload)
294}
295
296fn container(
298 width: u32,
299 height: u32,
300 mono: bool,
301 colour: &CodedStill,
302 alpha: Option<&CodedStill>,
303 icc: Option<&[u8]>,
304) -> Vec<u8> {
305 let ftyp = bx(b"ftyp", b"avif\0\0\0\0avifmif1miaf");
306 let ispe = {
307 let mut p = width.to_be_bytes().to_vec();
308 p.extend_from_slice(&height.to_be_bytes());
309 full(b"ispe", 0, 0, &p)
310 };
311 let pixi = |channels: u8| {
312 let mut p = vec![channels];
313 p.extend(std::iter::repeat_n(8, usize::from(channels)));
314 full(b"pixi", 0, 0, &p)
315 };
316 let colr = {
317 let mut p = b"nclx".to_vec();
318 for v in [CICP.0, CICP.1, CICP.2] {
319 p.extend_from_slice(&u16::from(v).to_be_bytes());
320 }
321 p.push(0x80); bx(b"colr", &p)
323 };
324 let mut ipco = [
327 ispe,
328 pixi(if mono { 1 } else { 3 }),
329 av1c(colour, mono),
330 colr,
331 ]
332 .concat();
333 let mut associations: Vec<(u16, Vec<u8>)> = vec![(1, vec![1, 2, 0x80 | 3, 4])];
334 if let Some(alpha) = alpha {
335 let mut urn = crate::meta::URN_ALPHA.as_bytes().to_vec();
336 urn.push(0);
337 ipco.extend_from_slice(&[pixi(1), av1c(alpha, true), full(b"auxC", 0, 0, &urn)].concat());
338 associations.push((2, vec![1, 5, 0x80 | 6, 7]));
339 }
340 if let Some(profile) = icc {
343 ipco.extend_from_slice(&bx(b"colr", &[b"prof".as_slice(), profile].concat()));
344 let index = if alpha.is_some() { 8 } else { 5 };
345 if let Some((_, props)) = associations.first_mut() {
346 props.push(index);
347 }
348 }
349 let ipma = {
350 let mut p = (associations.len() as u32).to_be_bytes().to_vec();
351 for (item, props) in &associations {
352 p.extend_from_slice(&item.to_be_bytes());
353 p.push(props.len() as u8);
354 p.extend_from_slice(props);
355 }
356 full(b"ipma", 0, 0, &p)
357 };
358 let iprp = bx(b"iprp", &[bx(b"ipco", &ipco), ipma].concat());
359
360 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");
361 let pitm = full(b"pitm", 0, 0, &1_u16.to_be_bytes());
362 let infe = |item: u16| {
363 let mut p = item.to_be_bytes().to_vec();
364 p.extend_from_slice(&[0, 0]);
365 p.extend_from_slice(b"av01");
366 p.push(0); full(b"infe", 2, 0, &p)
368 };
369 let items: Vec<&CodedStill> = std::iter::once(colour).chain(alpha).collect();
370 let iinf = {
371 let mut p = (items.len() as u16).to_be_bytes().to_vec();
372 for i in 0..items.len() {
373 p.extend_from_slice(&infe(i as u16 + 1));
374 }
375 full(b"iinf", 0, 0, &p)
376 };
377 let iref = alpha.map(|_| full(b"iref", 0, 0, &bx(b"auxl", &[0, 2, 0, 1, 0, 1])));
379 let iloc = |mdat_start: u32| {
381 let mut p = vec![0x44, 0x00];
382 p.extend_from_slice(&(items.len() as u16).to_be_bytes());
383 let mut offset = mdat_start;
384 for (i, item) in items.iter().enumerate() {
385 p.extend_from_slice(&(i as u16 + 1).to_be_bytes());
386 p.extend_from_slice(&[0, 0, 0, 1]); p.extend_from_slice(&offset.to_be_bytes());
388 p.extend_from_slice(&(item.data.len() as u32).to_be_bytes());
389 offset += item.data.len() as u32;
390 }
391 full(b"iloc", 0, 0, &p)
392 };
393 let meta = |mdat_start: u32| {
394 let mut p = [hdlr.clone(), pitm.clone(), iloc(mdat_start), iinf.clone()].concat();
395 if let Some(iref) = &iref {
396 p.extend_from_slice(iref);
397 }
398 p.extend_from_slice(&iprp);
399 full(b"meta", 0, 0, &p)
400 };
401 let mdat_start = (ftyp.len() + meta(0).len() + 8) as u32;
403 let mdat_payload: Vec<u8> = items
404 .iter()
405 .flat_map(|item| item.data.iter().copied())
406 .collect();
407 [ftyp, meta(mdat_start), bx(b"mdat", &mdat_payload)].concat()
408}
409
410#[cfg(test)]
411#[allow(
412 clippy::unwrap_used,
413 clippy::indexing_slicing,
414 reason = "tests operate on known-good values"
415)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn rgb_to_yuv_follows_bt601_full_range() {
421 let pixels = [255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 255];
422 let (planes, alpha) = to_planes(&pixels, PixelFormat::Rgb8, 2, 2);
423 assert!(alpha.is_none());
424 assert_eq!(planes[0], vec![255, 0, 76, 29]);
425 let u = (((-76.245_f64) + 255.0 * (1.0 - 0.114)) / 1.772 / 4.0 + 128.0).round();
427 assert!(
428 (i32::from(planes[1][0]) - u as i32).abs() <= 1,
429 "{} vs {u}",
430 planes[1][0]
431 );
432 assert_eq!(planes[1].len(), 1);
433 }
434
435 #[test]
436 fn grey_keeps_one_plane_and_alpha_is_split_out() {
437 let pixels = [10, 200, 20, 255, 30, 0];
438 let (planes, alpha) = to_planes(&pixels, PixelFormat::GrayA8, 3, 1);
439 assert_eq!(planes, vec![vec![10, 20, 30]]);
440 assert_eq!(alpha.unwrap(), vec![200, 255, 0]);
441 }
442
443 #[test]
444 fn odd_sizes_average_only_the_samples_present() {
445 let pixels = [0, 0, 255, 0, 0, 255, 0, 0, 255];
446 let (planes, _) = to_planes(&pixels, PixelFormat::Rgb8, 3, 1);
447 assert_eq!((planes[1].len(), planes[2].len()), (2, 2));
448 assert_eq!(planes[1][0], planes[1][1]);
449 }
450}