Skip to main content

maroontree/
encoder.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 6/2026. All rights reserved.
3 * //
4 * // Redistribution and use in source and binary forms, with or without modification,
5 * // are permitted provided that the following conditions are met:
6 * //
7 * // 1.  Redistributions of source code must retain the above copyright notice, this
8 * // list of conditions and the following disclaimer.
9 * //
10 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * // this list of conditions and the following disclaimer in the documentation
12 * // and/or other materials provided with the distribution.
13 * //
14 * // 3.  Neither the name of the copyright holder nor the names of its
15 * // contributors may be used to endorse or promote products derived from
16 * // this software without specific prior written permission.
17 * //
18 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29use 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
114// Q0.13 coefficients  (value = round(f * 8192))
115const Q: i32 = 13;
116const HALF: i32 = 1 << (Q - 1); // 0.5 rounding bias
117
118const Y_R: i32 = 2449; // round( 0.299    * 8192)
119const Y_G: i32 = 4809; // round( 0.587    * 8192)
120const Y_B: i32 = 934; // round( 0.114    * 8192)
121
122const CB_R: i32 = -1382; // round(-0.168736 * 8192)
123const CB_G: i32 = -2714; // round(-0.331264 * 8192)
124const CB_B: i32 = 4096; // round( 0.5      * 8192)
125
126const CR_R: i32 = 4096; // round( 0.5      * 8192)
127const CR_G: i32 = -3430; // round(-0.418688 * 8192)
128const CR_B: i32 = -666; // round(-0.081312 * 8192)
129
130/// Convert RGB rows to three output planes in parallel row bands. `f` maps one
131/// (r, g, b) pixel to the three outputs; planes are zipped per band.
132fn 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; // rows per work item
140    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    /// Build from interleaved RGB samples (`r,g,b,r,g,b,...`).
166    /// AV1 identity matrix mapping: plane0=G, plane1=B, plane2=R. No alpha.
167    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    /// Build from interleaved RGBA samples (`r,g,b,a,r,g,b,a,...`) in a single
204    /// pass: plane0=G, plane1=B, plane2=R, plane3=A. This is the deinterleave
205    /// the `*_with_alpha` paths use — the color planes and the alpha plane are
206    /// split once, with no intermediate RGB buffer.
207    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    /// Build a monochrome image from a single luma plane. No alpha.
247    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    /// Build a monochrome-plus-alpha image from interleaved gray/alpha samples
268    /// (`l,a,l,a,...`) in a single pass: plane0=luma, plane3=alpha (planes 1/2
269    /// stay empty). Mirrors [`Self::from_interleaved_rgba`] for the 2-channel
270    /// gray+alpha case.
271    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    /// Reconstruct interleaved RGB from the GBR planes (alpha is dropped).
346    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    // Horizontal 2:1 averaging, parallel over disjoint output row bands.
472    {
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); // rounding bias for >> (Q + 2)
558
559    let (mut cb, mut cr) = (vec![0i32; cw * ch], vec![0i32; cw * ch]);
560
561    // 2x2 averaging, parallel over disjoint output row bands.
562    {
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
673/// Encode a lossless grayscale (monochrome) AVIF still.
674pub 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
726/// Use AV1's GBR identity matrix only when the caller supplied no CICP.
727fn lossless_rgb_cicp(color: Option<&Cicp>) -> Cicp {
728    color.copied().unwrap_or_else(Cicp::identity_rgb)
729}
730
731/// Encode a lossless 4:4:4 still with color signaling.
732pub 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
775/// Encode a lossless 4:4:4 AVIF still with color signaling.
776pub 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
806/// Encode a lossless 4:4:4 AVIF still with color signaling.
807pub 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/// Encode a pre-converted 4:4:4 YCbCr still.
850#[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/// Encode a pre-subsampled 4:2:2 YCbCr still.
892#[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/// Encode a pre-subsampled 4:2:0 YCbCr still.
934#[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    /// Non-multiple-of-8 sizes must not panic and must produce a non-empty
1042    /// stream for both very small and odd dimensions.
1043    #[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}