1use crate::Speed;
30use crate::avif::{
31 checked_buffer_size, finalize_color, finalize_with_alpha, make_av1c, validate_dims,
32};
33use crate::color::Cicp;
34use crate::err::EncodeError;
35use crate::obu::temporal_delimiter;
36use crate::pixel::Pixel;
37use crate::{BitDepth, ChromaFormat, EncodeConfig, isobmff};
38
39pub struct PlanarImage<T: Pixel> {
40 pub width: usize,
41 pub height: usize,
42 pub bit_depth: BitDepth,
43 pub planes: [Vec<T>; 4],
44}
45
46fn validate_buf<T>(buf: &[T], w: usize, h: usize, ch: usize) -> Result<(), EncodeError> {
47 let needed = checked_buffer_size::<T>(w, h, ch)?;
48 if buf.len() != needed {
49 return Err(EncodeError::InvalidInput);
50 }
51 Ok(())
52}
53
54impl<T: Pixel> PlanarImage<T> {
55 pub(crate) fn validate_400(&self) -> Result<(), EncodeError> {
56 validate_dims(self.width as u32, self.height as u32)?;
57 validate_buf(&self.planes[0], self.width, self.height, 1)?;
58 Ok(())
59 }
60
61 pub(crate) fn validate_444(&self) -> Result<(), EncodeError> {
62 validate_dims(self.width as u32, self.height as u32)?;
63 validate_buf(&self.planes[0], self.width, self.height, 1)?;
64 validate_buf(&self.planes[1], self.width, self.height, 1)?;
65 validate_buf(&self.planes[2], self.width, self.height, 1)?;
66 Ok(())
67 }
68
69 pub(crate) fn validate_422(&self) -> Result<(), EncodeError> {
70 validate_dims(self.width as u32, self.height as u32)?;
71 validate_buf(&self.planes[0], self.width, self.height, 1)?;
72 validate_buf(&self.planes[1], self.width.div_ceil(2), self.height, 1)?;
73 validate_buf(&self.planes[2], self.width.div_ceil(2), self.height, 1)?;
74 Ok(())
75 }
76
77 pub(crate) fn validate_420(&self) -> Result<(), EncodeError> {
78 validate_dims(self.width as u32, self.height as u32)?;
79 validate_buf(&self.planes[0], self.width, self.height, 1)?;
80 validate_buf(
81 &self.planes[1],
82 self.width.div_ceil(2),
83 self.height.div_ceil(2),
84 1,
85 )?;
86 validate_buf(
87 &self.planes[2],
88 self.width.div_ceil(2),
89 self.height.div_ceil(2),
90 1,
91 )?;
92 Ok(())
93 }
94
95 pub(crate) fn validate_with(&self, chroma_format: ChromaFormat) -> Result<(), EncodeError> {
96 match chroma_format {
97 ChromaFormat::Yuv420 => {
98 self.validate_420()?;
99 }
100 ChromaFormat::Yuv422 => {
101 self.validate_422()?;
102 }
103 ChromaFormat::Yuv444 => {
104 self.validate_444()?;
105 }
106 ChromaFormat::Monochrome => {
107 self.validate_400()?;
108 }
109 }
110 Ok(())
111 }
112}
113
114const Q: i32 = 13;
116const HALF: i32 = 1 << (Q - 1); const Y_R: i32 = 2449; const Y_G: i32 = 4809; const Y_B: i32 = 934; const CB_R: i32 = -1382; const CB_G: i32 = -2714; const CB_B: i32 = 4096; const CR_R: i32 = 4096; const CR_G: i32 = -3430; const CR_B: i32 = -666; fn csc_rows_par<T: Pixel + Sync>(
133 pool: &crate::par::Pool,
134 w: usize,
135 rgb: [&[T]; 3],
136 out: [&mut [i32]; 3],
137 f: impl Fn(i32, i32, i32) -> (i32, i32, i32) + Sync,
138) {
139 const BAND: usize = 64; let [o0, o1, o2] = out;
141 #[allow(clippy::type_complexity)]
142 let items: Vec<(usize, (&mut [i32], (&mut [i32], &mut [i32])))> = o0
143 .chunks_mut(BAND * w)
144 .zip(o1.chunks_mut(BAND * w).zip(o2.chunks_mut(BAND * w)))
145 .enumerate()
146 .collect();
147 pool.for_each(pool.width(), items, |(bi, (b0, (b1, b2)))| {
148 let base = bi * BAND * w;
149 for (i, ((v0, v1), v2)) in b0
150 .iter_mut()
151 .zip(b1.iter_mut())
152 .zip(b2.iter_mut())
153 .enumerate()
154 {
155 let j = base + i;
156 let (a, b, c) = f(rgb[0][j].to_i32(), rgb[1][j].to_i32(), rgb[2][j].to_i32());
157 *v0 = a;
158 *v1 = b;
159 *v2 = c;
160 }
161 });
162}
163
164impl<T: Pixel> PlanarImage<T> {
165 pub fn from_interleaved_rgb(
168 width: usize,
169 height: usize,
170 bit_depth: BitDepth,
171 rgb: &[T],
172 ) -> Result<Self, EncodeError> {
173 if rgb.len() != width * height * 3 {
174 return Err(EncodeError::InvalidDimensions {
175 width: width as u32,
176 height: height as u32,
177 });
178 }
179 let n = width * height;
180 let mut g = vec![T::default(); n];
181 let mut b = vec![T::default(); n];
182 let mut r = vec![T::default(); n];
183 for (((px, g), b), r) in rgb
184 .as_chunks::<3>()
185 .0
186 .iter()
187 .zip(g.iter_mut())
188 .zip(b.iter_mut())
189 .zip(r.iter_mut())
190 {
191 *r = px[0];
192 *g = px[1];
193 *b = px[2];
194 }
195 Ok(PlanarImage {
196 width,
197 height,
198 bit_depth,
199 planes: [g, b, r, Vec::new()],
200 })
201 }
202
203 pub fn from_interleaved_rgba(
208 width: usize,
209 height: usize,
210 bit_depth: BitDepth,
211 rgba: &[T],
212 ) -> Result<Self, EncodeError> {
213 if rgba.len() != width * height * 4 {
214 return Err(EncodeError::InvalidDimensions {
215 width: width as u32,
216 height: height as u32,
217 });
218 }
219 let n = width * height;
220 let mut g = vec![T::default(); n];
221 let mut b = vec![T::default(); n];
222 let mut r = vec![T::default(); n];
223 let mut a = vec![T::default(); n];
224 for ((((px, g), b), r), a) in rgba
225 .as_chunks::<4>()
226 .0
227 .iter()
228 .zip(g.iter_mut())
229 .zip(b.iter_mut())
230 .zip(r.iter_mut())
231 .zip(a.iter_mut())
232 {
233 *r = px[0];
234 *g = px[1];
235 *b = px[2];
236 *a = px[3];
237 }
238 Ok(PlanarImage {
239 width,
240 height,
241 bit_depth,
242 planes: [g, b, r, a],
243 })
244 }
245
246 pub fn from_luma(
248 width: usize,
249 height: usize,
250 bit_depth: BitDepth,
251 luma: &[T],
252 ) -> Result<Self, EncodeError> {
253 if luma.len() != width * height {
254 return Err(EncodeError::InvalidDimensions {
255 width: width as u32,
256 height: height as u32,
257 });
258 }
259 Ok(PlanarImage {
260 width,
261 height,
262 bit_depth,
263 planes: [luma.to_vec(), Vec::new(), Vec::new(), Vec::new()],
264 })
265 }
266
267 pub fn from_interleaved_gray_alpha(
272 width: usize,
273 height: usize,
274 bit_depth: BitDepth,
275 gray_alpha: &[T],
276 ) -> Result<Self, EncodeError> {
277 if gray_alpha.len() != width * height * 2 {
278 return Err(EncodeError::InvalidDimensions {
279 width: width as u32,
280 height: height as u32,
281 });
282 }
283 let n = width * height;
284 let mut luma = vec![T::default(); n];
285 let mut a = vec![T::default(); n];
286 for ((px, luma), a) in gray_alpha
287 .as_chunks::<2>()
288 .0
289 .iter()
290 .zip(luma.iter_mut())
291 .zip(a.iter_mut())
292 {
293 *luma = px[0];
294 *a = px[1];
295 }
296 Ok(PlanarImage {
297 width,
298 height,
299 bit_depth,
300 planes: [luma, a, Vec::new(), Vec::new()],
301 })
302 }
303
304 pub(crate) fn packed_3(&self) -> PlanarImage<T> {
305 PlanarImage {
306 width: self.width,
307 height: self.height,
308 bit_depth: self.bit_depth,
309 planes: [
310 self.planes[0].to_vec(),
311 self.planes[1].to_vec(),
312 self.planes[2].to_vec(),
313 vec![],
314 ],
315 }
316 }
317
318 pub(crate) fn packed_alpha_4(&self) -> PlanarImage<T> {
319 PlanarImage {
320 width: self.width,
321 height: self.height,
322 bit_depth: self.bit_depth,
323 planes: [self.planes[3].to_vec(), vec![], vec![], vec![]],
324 }
325 }
326
327 pub(crate) fn packed_alpha_2(&self) -> PlanarImage<T> {
328 PlanarImage {
329 width: self.width,
330 height: self.height,
331 bit_depth: self.bit_depth,
332 planes: [self.planes[1].to_vec(), vec![], vec![], vec![]],
333 }
334 }
335
336 pub(crate) fn packed_1(&self) -> PlanarImage<T> {
337 PlanarImage {
338 width: self.width,
339 height: self.height,
340 bit_depth: self.bit_depth,
341 planes: [self.planes[0].to_vec(), vec![], vec![], vec![]],
342 }
343 }
344
345 pub fn to_interleaved_rgb(&self) -> Vec<T> {
347 let n = self.width * self.height;
348 let mut out = vec![T::default(); n * 3];
349 for (((dst, &r), &g), &b) in out
350 .as_chunks_mut::<3>()
351 .0
352 .iter_mut()
353 .zip(self.planes[2].iter())
354 .zip(self.planes[0].iter())
355 .zip(self.planes[1].iter())
356 {
357 dst[0] = r;
358 dst[1] = g;
359 dst[2] = b;
360 }
361 out
362 }
363}
364
365#[allow(clippy::too_many_arguments)]
366pub fn encode_still_lossy<T: Pixel>(
367 img: &PlanarImage<T>,
368 base_q_idx: u8,
369 color: Option<&Cicp>,
370 threads: usize,
371 speed: Speed,
372 aq: bool,
373 vb: crate::coder::VarianceBoost,
374 cdef: bool,
375 wiener: bool,
376) -> Vec<u8> {
377 assert!(
378 img.width > 0 && img.height > 0,
379 "width/height must be non-zero"
380 );
381 assert!(base_q_idx != 0, "use encode_still for lossless (q=0)");
382 let bd = img.bit_depth;
383 let maxv = (1i32 << bd.bits()) - 1;
384 let off = (1i32 << (bd.bits() - 1)) as f32;
385 let mx = maxv as f32;
386 let n = img.planes[0].len();
387 let off_q = (off as i32) << Q;
388 let mx_i = mx as i32;
389 let pool = crate::par::Pool::new(threads);
390 let (mut y, mut cb, mut cr) = (vec![0i32; n], vec![0i32; n], vec![0i32; n]);
391 csc_rows_par(
392 &pool,
393 img.width,
394 [&img.planes[2], &img.planes[0], &img.planes[1]],
395 [&mut y, &mut cb, &mut cr],
396 |ri, gi, bi| {
397 (
398 ((Y_R * ri + Y_G * gi + Y_B * bi + HALF) >> Q).clamp(0, mx_i),
399 ((CB_R * ri + CB_G * gi + CB_B * bi + off_q + HALF) >> Q).clamp(0, mx_i),
400 ((CR_R * ri + CR_G * gi + CR_B * bi + off_q + HALF) >> Q).clamp(0, mx_i),
401 )
402 },
403 );
404 crate::dispatch::encode_lossy_444(
405 base_q_idx,
406 bd.bits(),
407 img.width,
408 img.height,
409 &y,
410 &cb,
411 &cr,
412 color,
413 &pool,
414 speed,
415 aq,
416 vb,
417 cdef,
418 wiener,
419 )
420}
421
422#[allow(clippy::too_many_arguments)]
423pub fn encode_still_lossy_422<T: Pixel>(
424 img: &PlanarImage<T>,
425 base_q_idx: u8,
426 color: Option<&Cicp>,
427 threads: usize,
428 speed: Speed,
429 aq: bool,
430 vb: crate::coder::VarianceBoost,
431 cdef: bool,
432 wiener: bool,
433) -> Vec<u8> {
434 assert!(
435 img.width > 0 && img.height > 0,
436 "width/height must be non-zero"
437 );
438 assert!(base_q_idx != 0, "use encode_still for lossless (q=0)");
439 let (w, h) = (img.width, img.height);
440 let bd = img.bit_depth;
441 let maxv = (1i32 << bd.bits()) - 1;
442 let off = (1i32 << (bd.bits() - 1)) as f32;
443 let mx = maxv as f32;
444 let cw = w.div_ceil(2);
445 let mut y = vec![0i32; w * h];
446
447 let off_q = (off as i32) << Q;
448 let mx_i = mx as i32;
449
450 let mut fcb_q = vec![0i32; w * h];
451 let mut fcr_q = vec![0i32; w * h];
452
453 let pool = crate::par::Pool::new(threads);
454 csc_rows_par(
455 &pool,
456 w,
457 [&img.planes[2], &img.planes[0], &img.planes[1]],
458 [&mut y, &mut fcb_q, &mut fcr_q],
459 |ri, gi, bi| {
460 (
461 ((Y_R * ri + Y_G * gi + Y_B * bi + HALF) >> Q).clamp(0, mx_i),
462 CB_R * ri + CB_G * gi + CB_B * bi + off_q,
463 CR_R * ri + CR_G * gi + CR_B * bi + off_q,
464 )
465 },
466 );
467 const HALF_AVG: i32 = 1 << Q;
468
469 let (mut cb, mut cr) = (vec![0i32; cw * h], vec![0i32; cw * h]);
470
471 {
473 #[allow(clippy::type_complexity)]
474 let items: Vec<(usize, (&mut [i32], &mut [i32]))> = cb
475 .chunks_mut(cw)
476 .zip(cr.chunks_mut(cw))
477 .enumerate()
478 .collect();
479 let (fcb, fcr) = (&fcb_q, &fcr_q);
480 pool.for_each(pool.width(), items, |(row, (cbr, crr))| {
481 for (c, (cb, cr)) in cbr.iter_mut().zip(crr.iter_mut()).enumerate() {
482 let x0 = 2 * c;
483 let x1 = (2 * c + 1).min(w - 1);
484 let cb0 = fcb[row * w + x0];
485 let cb1 = fcb[row * w + x1];
486 let cr0 = fcr[row * w + x0];
487 let cr1 = fcr[row * w + x1];
488 *cb = ((cb0 + cb1 + HALF_AVG) >> (Q + 1)).clamp(0, mx_i);
489 *cr = ((cr0 + cr1 + HALF_AVG) >> (Q + 1)).clamp(0, mx_i);
490 }
491 });
492 }
493 crate::dispatch::encode_lossy_422(
494 base_q_idx,
495 bd.bits(),
496 w,
497 h,
498 &y,
499 &cb,
500 &cr,
501 color,
502 &pool,
503 speed,
504 aq,
505 vb,
506 cdef,
507 wiener,
508 )
509}
510
511#[allow(clippy::too_many_arguments)]
512pub fn encode_still_lossy_420<T: Pixel>(
513 img: &PlanarImage<T>,
514 base_q_idx: u8,
515 color: Option<&Cicp>,
516 threads: usize,
517 speed: Speed,
518 aq: bool,
519 vb: crate::coder::VarianceBoost,
520 cdef: bool,
521 wiener: bool,
522) -> Vec<u8> {
523 assert!(
524 img.width > 0 && img.height > 0,
525 "width/height must be non-zero"
526 );
527 assert!(base_q_idx != 0, "use encode_still for lossless (q=0)");
528 let (w, h) = (img.width, img.height);
529 let bd = img.bit_depth;
530 let maxv = (1i32 << bd.bits()) - 1;
531 let off = (1i32 << (bd.bits() - 1)) as f32;
532 let mx = maxv as f32;
533 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
534
535 let off_q = (off as i32) << Q;
536 let mx_i = mx as i32;
537
538 let mut y = vec![0i32; w * h];
539 let mut fcb_q = vec![0i32; w * h];
540 let mut fcr_q = vec![0i32; w * h];
541
542 let pool = crate::par::Pool::new(threads);
543 csc_rows_par(
544 &pool,
545 w,
546 [&img.planes[2], &img.planes[0], &img.planes[1]],
547 [&mut y, &mut fcb_q, &mut fcr_q],
548 |ri, gi, bi| {
549 (
550 ((Y_R * ri + Y_G * gi + Y_B * bi + HALF) >> Q).clamp(0, mx_i),
551 CB_R * ri + CB_G * gi + CB_B * bi + off_q,
552 CR_R * ri + CR_G * gi + CR_B * bi + off_q,
553 )
554 },
555 );
556
557 const HALF_AVG: i32 = 1 << (Q + 1); let (mut cb, mut cr) = (vec![0i32; cw * ch], vec![0i32; cw * ch]);
560
561 {
563 #[allow(clippy::type_complexity)]
564 let items: Vec<(usize, (&mut [i32], &mut [i32]))> = cb
565 .chunks_mut(cw)
566 .zip(cr.chunks_mut(cw))
567 .enumerate()
568 .collect();
569 let (fcb, fcr) = (&fcb_q, &fcr_q);
570 pool.for_each(pool.width(), items, |(row, (cbr, crr))| {
571 for (c, (cb, cr)) in cbr.iter_mut().zip(crr.iter_mut()).enumerate() {
572 let (x0, x1) = (2 * c, (2 * c + 1).min(w - 1));
573 let (y0, y1) = (2 * row, (2 * row + 1).min(h - 1));
574
575 let avg_q =
576 |f: &[i32]| f[y0 * w + x0] + f[y0 * w + x1] + f[y1 * w + x0] + f[y1 * w + x1];
577
578 *cb = ((avg_q(fcb) + HALF_AVG) >> (Q + 2)).clamp(0, mx_i);
579 *cr = ((avg_q(fcr) + HALF_AVG) >> (Q + 2)).clamp(0, mx_i);
580 }
581 });
582 }
583 crate::dispatch::encode_lossy_420(
584 base_q_idx,
585 bd.bits(),
586 w,
587 h,
588 &y,
589 &cb,
590 &cr,
591 color,
592 &pool,
593 speed,
594 aq,
595 vb,
596 cdef,
597 wiener,
598 )
599}
600
601#[allow(clippy::too_many_arguments)]
602pub(crate) fn encode_lossy_gray_obu<T: Pixel>(
603 img: &PlanarImage<T>,
604 bit_depth: BitDepth,
605 base_q_idx: u8,
606 full_range: bool,
607 threads: usize,
608 speed: Speed,
609 aq: bool,
610 vb: crate::coder::VarianceBoost,
611 cdef: bool,
612 wiener: bool,
613) -> Result<Vec<u8>, EncodeError> {
614 validate_dims(img.width as u32, img.height as u32)?;
615 img.validate_400()?;
616 let maxv = (1i32 << bit_depth.bits()) - 1;
617 if base_q_idx == 0 {
618 let luma: Vec<i16> = img.planes[0]
619 .iter()
620 .map(|v| v.to_i32().clamp(0, maxv) as i16)
621 .collect();
622 return Ok(crate::dispatch::encode_lossless_monochrome(
623 bit_depth.bits(),
624 img.width,
625 img.height,
626 &luma,
627 full_range,
628 threads,
629 ));
630 }
631 let luma: Vec<i32> = img.planes[0]
632 .iter()
633 .map(|v| v.to_i32().clamp(0, maxv))
634 .collect();
635 let bytes = crate::dispatch::encode_lossy_monochrome(
636 base_q_idx,
637 bit_depth.bits(),
638 img.width,
639 img.height,
640 &luma,
641 full_range,
642 threads,
643 speed,
644 aq,
645 vb,
646 cdef,
647 wiener,
648 );
649 Ok(bytes)
650}
651
652pub fn encode_lossless_gray_obu<T: Pixel>(
653 img: &PlanarImage<T>,
654 full_range: bool,
655 threads: usize,
656) -> Result<Vec<u8>, EncodeError> {
657 validate_dims(img.width as u32, img.height as u32)?;
658 img.validate_400()?;
659 encode_lossy_gray_obu(
660 img,
661 img.bit_depth,
662 0,
663 full_range,
664 threads,
665 Speed::Slow,
666 false,
667 crate::coder::VarianceBoost::off(),
668 false,
669 false,
670 )
671}
672
673pub fn encode_lossless_gray<T: Pixel>(
675 img: &PlanarImage<T>,
676 cfg: &EncodeConfig,
677) -> Result<Vec<u8>, EncodeError> {
678 validate_dims(img.width as u32, img.height as u32)?;
679 img.validate_400()?;
680 let obu = encode_lossy_gray_obu(
681 img,
682 img.bit_depth,
683 0,
684 true,
685 cfg.threads,
686 Speed::Slow,
687 false,
688 crate::coder::VarianceBoost::off(),
689 false,
690 false,
691 )?;
692 finalize_color(
693 obu,
694 img.width as u32,
695 img.height as u32,
696 img.bit_depth.bits(),
697 ChromaFormat::Monochrome,
698 cfg,
699 )
700}
701
702pub fn encode_lossless_gray_alpha<T: Pixel>(
703 img: &PlanarImage<T>,
704 cfg: &EncodeConfig,
705) -> Result<Vec<u8>, EncodeError> {
706 validate_dims(img.width as u32, img.height as u32)?;
707 cfg.validate()?;
708 crate::avif::validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
709 crate::avif::validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
710 if cfg.chroma != ChromaFormat::Monochrome {
711 return Err(EncodeError::UnsupportedChromaFormat(cfg.chroma));
712 }
713 let luma_obu = encode_lossless_gray(&img.packed_1(), cfg)?;
714 let alpha_obu = encode_lossless_gray(&img.packed_alpha_2(), cfg)?;
715 finalize_with_alpha(
716 luma_obu,
717 alpha_obu,
718 img.width as u32,
719 img.height as u32,
720 img.bit_depth.bits(),
721 ChromaFormat::Monochrome,
722 cfg,
723 )
724}
725
726fn lossless_rgb_cicp(color: Option<&Cicp>) -> Cicp {
728 color.copied().unwrap_or_else(Cicp::identity_rgb)
729}
730
731pub fn encode_lossless_obu<T: Pixel>(
733 img: &PlanarImage<T>,
734 color: Option<&Cicp>,
735 threads: usize,
736) -> Result<Vec<u8>, EncodeError> {
737 img.validate_444()?;
738 let effective_color = lossless_rgb_cicp(color);
739 let profile: u32 = if img.bit_depth == BitDepth::Twelve {
740 2
741 } else {
742 1
743 };
744 let (w, h) = (img.width, img.height);
745 let (w8, h8) = (crate::coder::align8(w), crate::coder::align8(h));
746 let to_i16 = |p: &[T]| p.iter().map(|p| p.to_i32() as i16).collect::<Vec<i16>>();
747 let planes_i16: [Vec<i16>; 3] = [
748 crate::coder::pad_to_mult8(&to_i16(&img.planes[0]), w, h, w8, h8),
749 crate::coder::pad_to_mult8(&to_i16(&img.planes[1]), w, h, w8, h8),
750 crate::coder::pad_to_mult8(&to_i16(&img.planes[2]), w, h, w8, h8),
751 ];
752 let mut bytes = Vec::new();
753 bytes.extend_from_slice(&temporal_delimiter());
754 bytes.extend_from_slice(&crate::obu::sequence_header_cicp(
755 w as u32,
756 h as u32,
757 profile,
758 img.bit_depth.bits(),
759 Some(&effective_color),
760 false,
761 false,
762 ));
763 bytes.extend_from_slice(&crate::coder::encode_lossless_frame_obus(
764 img.bit_depth.bits(),
765 w8,
766 h8,
767 w,
768 h,
769 &planes_i16,
770 threads,
771 ));
772 Ok(bytes)
773}
774
775pub fn encode_lossless<T: Pixel>(
777 img: &PlanarImage<T>,
778 cfg: &EncodeConfig,
779) -> Result<Vec<u8>, EncodeError> {
780 img.validate_444()?;
781 if cfg.chroma != ChromaFormat::Yuv444 {
782 return Err(EncodeError::UnsupportedChromaFormat(cfg.chroma));
783 }
784 let effective_color = lossless_rgb_cicp(cfg.color_encoding.as_ref());
785 let obu = encode_lossless_obu(img, Some(&effective_color), cfg.threads)?;
786 let av1c = make_av1c(
787 &obu,
788 img.bit_depth.bits(),
789 img.width as u32,
790 img.height as u32,
791 ChromaFormat::Yuv444,
792 );
793 isobmff::wrap_av1_image(
794 &obu,
795 img.width as u32,
796 img.height as u32,
797 img.bit_depth.bits(),
798 3,
799 &av1c,
800 Some(&effective_color),
801 cfg.icc.as_deref(),
802 &cfg.metadata,
803 )
804}
805
806pub fn encode_lossless_with_alpha<T: Pixel + Copy>(
808 img: &PlanarImage<T>,
809 cfg: &EncodeConfig,
810) -> Result<Vec<u8>, EncodeError> {
811 validate_dims(img.width as u32, img.height as u32)?;
812 cfg.validate()?;
813 crate::avif::validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
814 crate::avif::validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
815 crate::avif::validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
816 crate::avif::validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
817 if cfg.chroma != ChromaFormat::Yuv444 {
818 return Err(EncodeError::UnsupportedChromaFormat(cfg.chroma));
819 }
820
821 let effective_color = lossless_rgb_cicp(cfg.color_encoding.as_ref());
822 let obu = encode_lossless_obu(&img.packed_3(), Some(&effective_color), cfg.threads)?;
823
824 let alpha_obu = encode_lossy_gray_obu(
825 &img.packed_alpha_4(),
826 img.bit_depth,
827 0,
828 true,
829 cfg.threads,
830 Speed::Slow,
831 false,
832 crate::coder::VarianceBoost::off(),
833 false,
834 false,
835 )?;
836 let mut effective_cfg = cfg.clone();
837 effective_cfg.color_encoding = Some(effective_color);
838 finalize_with_alpha(
839 obu,
840 alpha_obu,
841 img.width as u32,
842 img.height as u32,
843 img.bit_depth.bits(),
844 cfg.chroma,
845 &effective_cfg,
846 )
847}
848
849#[allow(clippy::too_many_arguments)]
851pub(crate) fn encode_yuv444_obu<T: Pixel>(
852 planar_image: &PlanarImage<T>,
853 bit_depth: BitDepth,
854 base_q_idx: u8,
855 color: Option<&Cicp>,
856 threads: usize,
857 speed: Speed,
858 aq: bool,
859 vb: crate::coder::VarianceBoost,
860 cdef: bool,
861 wiener: bool,
862) -> Result<Vec<u8>, EncodeError> {
863 planar_image.validate_444()?;
864 assert_ne!(base_q_idx, 0, "use encode_still for lossless");
865 let maxv = (1i32 << bit_depth.bits()) - 1;
866 let to_i = |p: &[T]| {
867 p.iter()
868 .map(|v| v.to_i32().clamp(0, maxv))
869 .collect::<Vec<i32>>()
870 };
871 let pool = crate::par::Pool::new(threads);
872 let bytes = crate::dispatch::encode_lossy_444(
873 base_q_idx,
874 bit_depth.bits(),
875 planar_image.width,
876 planar_image.height,
877 &to_i(&planar_image.planes[0]),
878 &to_i(&planar_image.planes[1]),
879 &to_i(&planar_image.planes[2]),
880 color,
881 &pool,
882 speed,
883 aq,
884 vb,
885 cdef,
886 wiener,
887 );
888 Ok(bytes)
889}
890
891#[allow(clippy::too_many_arguments)]
893pub(crate) fn encode_yuv422_obu<T: Pixel>(
894 planar_image: &PlanarImage<T>,
895 bit_depth: BitDepth,
896 base_q_idx: u8,
897 color: Option<&Cicp>,
898 threads: usize,
899 speed: Speed,
900 aq: bool,
901 vb: crate::coder::VarianceBoost,
902 cdef: bool,
903 wiener: bool,
904) -> Result<Vec<u8>, EncodeError> {
905 planar_image.validate_422()?;
906 assert!(base_q_idx != 0, "4:2:2 doesn't support lossless encoding");
907 let maxv = (1i32 << bit_depth.bits()) - 1;
908 let to_i = |p: &[T]| {
909 p.iter()
910 .map(|v| v.to_i32().clamp(0, maxv))
911 .collect::<Vec<i32>>()
912 };
913 let pool = crate::par::Pool::new(threads);
914 let bytes = crate::dispatch::encode_lossy_422(
915 base_q_idx,
916 bit_depth.bits(),
917 planar_image.width,
918 planar_image.height,
919 &to_i(&planar_image.planes[0]),
920 &to_i(&planar_image.planes[1]),
921 &to_i(&planar_image.planes[2]),
922 color,
923 &pool,
924 speed,
925 aq,
926 vb,
927 cdef,
928 wiener,
929 );
930 Ok(bytes)
931}
932
933#[allow(clippy::too_many_arguments)]
935pub(crate) fn encode_yuv420_obu<T: Pixel>(
936 planar_image: &PlanarImage<T>,
937 bit_depth: BitDepth,
938 base_q_idx: u8,
939 color: Option<&Cicp>,
940 threads: usize,
941 speed: Speed,
942 aq: bool,
943 vb: crate::coder::VarianceBoost,
944 cdef: bool,
945 wiener: bool,
946) -> Result<Vec<u8>, EncodeError> {
947 planar_image.validate_420()?;
948 assert!(base_q_idx != 0, "use encode_still for lossless");
949 let maxv = (1i32 << bit_depth.bits()) - 1;
950 let to_i = |p: &[T]| {
951 p.iter()
952 .map(|v| v.to_i32().clamp(0, maxv))
953 .collect::<Vec<i32>>()
954 };
955 let pool = crate::par::Pool::new(threads);
956 let bytes = crate::dispatch::encode_lossy_420(
957 base_q_idx,
958 bit_depth.bits(),
959 planar_image.width,
960 planar_image.height,
961 &to_i(&planar_image.planes[0]),
962 &to_i(&planar_image.planes[1]),
963 &to_i(&planar_image.planes[2]),
964 color,
965 &pool,
966 speed,
967 aq,
968 vb,
969 cdef,
970 wiener,
971 );
972 Ok(bytes)
973}
974
975#[cfg(test)]
976mod tests {
977 use super::*;
978 use crate::coder::VarianceBoost;
979 use std::path::PathBuf;
980 use std::process::Command;
981
982 #[test]
983 fn lossless_rgb_preserves_explicit_cicp_and_defaults_to_identity() {
984 let explicit = Cicp::srgb_ycbcr();
985 assert_eq!(lossless_rgb_cicp(Some(&explicit)), explicit);
986 assert_eq!(
987 lossless_rgb_cicp(None).matrix,
988 crate::color::MatrixCoefficients::Identity
989 );
990 }
991
992 #[test]
993 fn lossless_rgb_avif_preserves_explicit_or_defaults_identity() {
994 let decoder = std::env::var_os("AVIFDEC").map(PathBuf::from).or_else(|| {
995 let path = PathBuf::from("/opt/homebrew/bin/avifdec");
996 path.is_file().then_some(path)
997 });
998 let Some(decoder) = decoder else {
999 return;
1000 };
1001 let (w, h) = (8usize, 8usize);
1002 let rgb: Vec<u8> = (0..w * h)
1003 .flat_map(|i| {
1004 [
1005 ((i * 37) & 255) as u8,
1006 ((i * 71) & 255) as u8,
1007 ((i * 13) & 255) as u8,
1008 ]
1009 })
1010 .collect();
1011 let image = PlanarImage::from_interleaved_rgb(w, h, BitDepth::Eight, &rgb).unwrap();
1012 for (color, expected_matrix) in [(None, 0), (Some(Cicp::srgb_ycbcr()), 6)] {
1013 let mut cfg = EncodeConfig::new()
1014 .with_chroma(ChromaFormat::Yuv444)
1015 .with_threads(1);
1016 cfg = match color {
1017 Some(cicp) => cfg.with_cicp(cicp),
1018 None => cfg.without_cicp(),
1019 };
1020 let avif = encode_lossless(&image, &cfg).unwrap();
1021 let input = std::env::temp_dir().join(format!(
1022 "maroontree-lossless-rgb-{}-{expected_matrix}.avif",
1023 std::process::id()
1024 ));
1025 std::fs::write(&input, avif).unwrap();
1026 let output = Command::new(&decoder)
1027 .arg("--info")
1028 .arg(&input)
1029 .output()
1030 .unwrap();
1031 let _ = std::fs::remove_file(input);
1032 assert!(output.status.success());
1033 let info = String::from_utf8_lossy(&output.stdout);
1034 assert!(
1035 info.contains(&format!("Matrix Coeffs. : {expected_matrix}")),
1036 "lossless RGB signaled the wrong matrix:\n{info}"
1037 );
1038 }
1039 }
1040
1041 #[test]
1044 fn arbitrary_sizes_do_not_panic() {
1045 for &(w, h) in &[(1usize, 1usize), (17, 17), (65, 33), (127, 129), (33, 7)] {
1046 let rgb = vec![100u8; w * h * 3];
1047 let img = PlanarImage::from_interleaved_rgb(w, h, BitDepth::Twelve, &rgb).unwrap();
1048 assert!(!encode_lossless_obu(&img, None, 9).unwrap().is_empty());
1049 assert!(
1050 !encode_still_lossy(
1051 &img,
1052 16,
1053 Some(&Cicp::srgb_ycbcr()),
1054 0,
1055 Speed::Slow,
1056 false,
1057 VarianceBoost::off(),
1058 true,
1059 false
1060 )
1061 .is_empty()
1062 );
1063 }
1064 }
1065}