Skip to main content

y4m_writer/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::io::Write;
5
6use machine_vision_formats as formats;
7use strand_dynamic_frame::DynamicFrame;
8
9use formats::{
10    ImageData, PixelFormat, Stride,
11    iter::HasRowChunksExact,
12    owned::OImage,
13    pixel_format::{self, Mono8, PixFmt},
14};
15
16use convert_image::{convert_owned, convert_ref};
17
18const EMPTY_BYTE: u8 = 128;
19
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22    #[error("convert-image error: {0}")]
23    ConvertImageError(#[from] convert_image::Error),
24    #[error("format or size changed")]
25    FormatOrSizeChanged,
26    #[error("unknown pixel format: {0}")]
27    UnknownPixelFormat(String),
28    #[error("unsupported pixel format: {0}")]
29    UnsupportedPixelFormat(formats::pixel_format::PixFmt),
30    #[error("unsupported colorspace: {0:?}")]
31    UnsupportedColorspace(y4m::Colorspace),
32    #[error("invalid allocated buffer size")]
33    InvalidAllocatedBufferSize,
34    #[error("{0}")]
35    Y4mError(#[from] y4m::Error),
36}
37
38pub type Result<T> = std::result::Result<T, Error>;
39
40#[expect(non_snake_case)]
41#[derive(PartialEq, Eq, Debug)]
42struct YUV444 {
43    Y: u8,
44    U: u8,
45    V: u8,
46}
47
48#[derive(Debug)]
49pub struct Y4MOptions {
50    /// Frame rate (numerator)
51    pub raten: usize,
52    /// Frame rate (denominator)
53    pub rated: usize,
54    /// Aspect ratio (numerator)
55    pub aspectn: usize,
56    /// Aspect ratio (denominator)
57    pub aspectd: usize,
58}
59
60enum Writer {
61    NotStarted(Box<dyn Write>),
62    Started(y4m::Encoder<Box<dyn Write>>),
63    /// Placeholder value for internal use
64    Undefined,
65}
66
67impl Writer {
68    fn encoder(&mut self) -> Option<&mut y4m::Encoder<Box<dyn Write>>> {
69        match self {
70            Self::Started(e) => Some(e),
71            _ => None,
72        }
73    }
74}
75
76/// An opinionated Y4M writer.
77///
78/// Saves only progressive video with full color range.
79pub struct Y4MWriter {
80    wtr: Writer,
81    opts: Y4MOptions,
82    info: Option<Y4MInfo>,
83}
84
85struct Y4MInfo {
86    width: usize,
87    height: usize,
88    fmt: formats::pixel_format::PixFmt,
89}
90
91impl Y4MWriter {
92    pub fn from_writer(wtr: Box<dyn Write>, opts: Y4MOptions) -> Self {
93        Self {
94            wtr: Writer::NotStarted(wtr),
95            opts,
96            info: None,
97        }
98    }
99    pub fn write_dynamic_frame(&mut self, frame: &DynamicFrame) -> Result<()> {
100        let this_fmt: formats::pixel_format::PixFmt = frame.pixel_format();
101        let this_width: usize = frame.width().try_into().unwrap();
102        let this_height: usize = frame.height().try_into().unwrap();
103
104        let info = self.info.get_or_insert(Y4MInfo {
105            width: this_width,
106            height: this_height,
107            fmt: this_fmt,
108        });
109        if this_width != info.width || this_height != info.height || this_fmt != info.fmt {
110            return Err(Error::FormatOrSizeChanged);
111        }
112
113        let colorspace = match this_fmt {
114            formats::pixel_format::PixFmt::Mono8 => y4m::Colorspace::Cmono,
115            formats::pixel_format::PixFmt::RGB8 => y4m::Colorspace::C420paldv,
116            formats::pixel_format::PixFmt::YUV422 => y4m::Colorspace::C420paldv,
117            _ => {
118                return Err(Error::UnsupportedPixelFormat(this_fmt));
119            }
120        };
121
122        let wtr = std::mem::replace(&mut self.wtr, Writer::Undefined);
123
124        match wtr {
125            Writer::NotStarted(wtr) => {
126                let builder = y4m::EncoderBuilder::new(
127                    info.width,
128                    info.height,
129                    y4m::Ratio::new(self.opts.raten, self.opts.rated),
130                )
131                .with_pixel_aspect(y4m::Ratio::new(self.opts.aspectn, self.opts.aspectd))
132                .with_colorspace(colorspace)
133                .append_vendor_extension(y4m::VendorExtensionString::new(
134                    b"COLORRANGE=FULL".into(),
135                )?);
136                let encoder = builder.write_header(wtr)?;
137                self.wtr = Writer::Started(encoder);
138            }
139            Writer::Started(encoder) => {
140                self.wtr = Writer::Started(encoder);
141            }
142            Writer::Undefined => {
143                unreachable!();
144            }
145        };
146
147        let encoder = self.wtr.encoder().unwrap();
148
149        let encoded = encode_y4m_dynamic_frame(frame, colorspace, None)?;
150        let frame = (&encoded).into();
151        encoder.write_frame(&frame)?;
152
153        Ok(())
154    }
155
156    // flush to disk.
157    pub fn flush(&mut self) -> Result<()> {
158        // Only if we started the writer is there anything to flush.
159        if let Writer::Started(encoder) = &mut self.wtr {
160            encoder.flush()?;
161        }
162        Ok(())
163    }
164
165    pub fn into_inner(self) -> Box<dyn Write> {
166        match self.wtr {
167            Writer::NotStarted(w) => w,
168            Writer::Started(e) => e.into_inner(),
169            _ => {
170                unreachable!();
171            }
172        }
173    }
174}
175
176// -----------
177
178fn downsample_plane(arr: &[u8], h: usize, w: usize) -> Vec<u8> {
179    // This could be optimized for speed.
180    let mut result = Vec::with_capacity((h / 2) * (w / 2));
181    for i in 0..(h / 2) {
182        for j in 0..(w / 2) {
183            let tmp: u8 = ((arr[2 * i * w + 2 * j] as u16
184                + arr[2 * i * w + 2 * j + 1] as u16
185                + arr[(2 * i + 1) * w + 2 * j] as u16
186                + arr[(2 * i + 1) * w + 2 * j + 1] as u16)
187                / 4) as u8;
188            result.push(tmp);
189        }
190    }
191    result
192}
193
194fn next_multiple(a: u32, b: u32) -> u32 {
195    div_ceil(a, b) * b
196}
197
198#[test]
199fn test_next_multiple() {
200    assert_eq!(next_multiple(10, 2), 10);
201    assert_eq!(next_multiple(11, 2), 12);
202    assert_eq!(next_multiple(15, 3), 15);
203    assert_eq!(next_multiple(16, 3), 18);
204    assert_eq!(next_multiple(18, 3), 18);
205}
206
207#[inline]
208fn div_ceil(a: u32, b: u32) -> u32 {
209    a.div_ceil(b)
210}
211
212#[test]
213fn test_div_ceil() {
214    assert_eq!(div_ceil(10, 2), 5);
215    assert_eq!(div_ceil(11, 2), 6);
216    assert_eq!(div_ceil(15, 3), 5);
217    assert_eq!(div_ceil(16, 3), 6);
218    assert_eq!(div_ceil(18, 3), 6);
219}
220
221/// Contains information to output y4m data.
222///
223/// The y4m format is described at
224/// <http://wiki.multimedia.cx/index.php?title=YUV4MPEG2>
225pub struct Y4MFrame {
226    pub data: Vec<u8>,
227    pub width: i32,
228    pub height: i32,
229    pub y_stride: i32,
230    colorspace: y4m::Colorspace,
231    chroma_stride: usize,
232    alloc_rows: i32,
233    alloc_chroma_rows: i32,
234    /// True if the U and V planes are known to contain no data.
235    is_known_mono_only: bool,
236    forced_block_size: Option<u32>,
237}
238
239impl<'a> From<&'a Y4MFrame> for y4m::Frame<'a> {
240    fn from(val: &'a Y4MFrame) -> Self {
241        Self::new(
242            [val.y_plane_data(), val.u_plane_data(), val.v_plane_data()],
243            None,
244        )
245    }
246}
247
248impl std::fmt::Debug for Y4MFrame {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        write!(
251            f,
252            "Y4MFrame{{width: {}, height: {}, y_stride: {}, chroma_stride: {}, data.len(): {}, alloc_rows: {}, alloc_chroma_rows: {}, is_known_mono_only: {}, forced_block_size: {:?}}}",
253            self.width,
254            self.height,
255            self.y_stride,
256            self.chroma_stride,
257            self.data.len(),
258            self.alloc_rows,
259            self.alloc_chroma_rows,
260            self.is_known_mono_only,
261            self.forced_block_size
262        )
263    }
264}
265
266impl Y4MFrame {
267    #[expect(clippy::too_many_arguments)]
268    fn new(
269        data: Vec<u8>,
270        width: u32,
271        height: u32,
272        stride: i32,
273        chroma_stride: usize,
274        alloc_rows: i32,
275        alloc_chroma_rows: i32,
276        is_known_mono_only: bool,
277        forced_block_size: Option<u32>,
278        colorspace: y4m::Colorspace,
279    ) -> Self {
280        let width: i32 = width.try_into().unwrap();
281        let height: i32 = height.try_into().unwrap();
282        let y_stride = stride;
283
284        if let Some(sz) = forced_block_size {
285            debug_assert_eq!(y_stride % sz as i32, 0);
286            debug_assert_eq!(chroma_stride % sz as usize, 0);
287        }
288
289        Self {
290            data,
291            width,
292            height,
293            y_stride,
294            colorspace,
295            chroma_stride,
296            alloc_rows,
297            alloc_chroma_rows,
298            is_known_mono_only,
299            forced_block_size,
300        }
301    }
302
303    pub fn convert<DEST>(&self) -> Result<impl HasRowChunksExact<DEST> + use<DEST>>
304    where
305        DEST: PixelFormat,
306    {
307        let y_data = self.y_plane_data();
308
309        match &self.colorspace {
310            y4m::Colorspace::C420paldv => {
311                // // Convert from color data RGB8.
312
313                // // TODO: implement shortcut when DEST is Mono8.
314
315                // // Instead of iterating smartly, just fill to 444.
316                // fn expand_plane(small: &[u8], small_stride: usize) -> Vec<u8> {
317                //     let small_rows = small.len() / small_stride;
318                //     let full_rows = small_rows * 2;
319                //     let full_stride = small_stride * 2;
320                //     let full_len = full_rows * full_stride;
321                //     let mut result = vec![0u8; full_len];
322
323                //     for (small_row_num, small_row) in small.chunks_exact(small_stride).enumerate() {
324                //         let big_row_num = small_row_num * 2;
325
326                //         for (small_col, val) in small_row.iter().enumerate() {
327                //             let big_col = small_col * 2;
328                //             result[big_row_num * full_stride + big_col] = *val;
329                //             result[big_row_num * full_stride + big_col + 1] = *val;
330
331                //             result[(big_row_num + 1) * full_stride + big_col] = *val;
332                //             result[(big_row_num + 1) * full_stride + big_col + 1] = *val;
333                //         }
334                //     }
335
336                //     result
337                // }
338                // let ufull_data = expand_plane(self.u_plane_data(), self.u_stride());
339                // let vfull_data = expand_plane(self.v_plane_data(), self.v_stride());
340
341                // let mut image_data = vec![0u8; vfull_data.len() * 3];
342                // let y_stride = self.y_stride();
343
344                // for (dest_row, (y_row, (u_row, v_row))) in
345                //     image_data.chunks_exact_mut(y_stride * 3).zip(
346                //         y_data.chunks_exact(y_stride).zip(
347                //             ufull_data
348                //                 .chunks_exact(y_stride)
349                //                 .zip(vfull_data.chunks_exact(y_stride)),
350                //         ),
351                //     )
352                // {
353                //     for (col, (y, (u, v))) in
354                //         y_row.iter().zip(u_row.iter().zip(v_row.iter())).enumerate()
355                //     {
356                //         let rgb = YUV444_bt601_toRGB(*y, *u, *v);
357                //         dest_row[col * 3] = rgb.R;
358                //         dest_row[col * 3 + 1] = rgb.G;
359                //         dest_row[col * 3 + 2] = rgb.B;
360                //     }
361                // }
362
363                // let rgb8 = Image::<RGB8>::new(
364                //     self.width.try_into().unwrap(),
365                //     self.height.try_into().unwrap(),
366                //     (self.width * 3).try_into().unwrap(),
367                //     image_data,
368                // )
369                // .unwrap();
370
371                // // Then convert to final target output
372                // let out = convert_owned::<_, RGB8, DEST>(rgb8)?;
373                // Ok(out)
374                todo!();
375            }
376            y4m::Colorspace::Cmono => {
377                let mono8 = OImage::<Mono8>::new(
378                    self.width.try_into().unwrap(),
379                    self.height.try_into().unwrap(),
380                    self.width.try_into().unwrap(),
381                    y_data.to_vec(),
382                )
383                .unwrap();
384
385                // Then convert to final target output
386                let out = convert_owned::<_, Mono8, DEST>(mono8)?;
387                Ok(out)
388            }
389            cs => Err(Error::UnsupportedColorspace(*cs)),
390        }
391    }
392
393    pub fn forced_block_size(&self) -> Option<u32> {
394        self.forced_block_size
395    }
396    /// get the size of the luminance plane
397    fn y_size(&self) -> usize {
398        if self.forced_block_size.is_some() {
399            self.y_stride as usize * self.alloc_rows as usize
400        } else {
401            self.y_stride as usize * self.height as usize
402        }
403    }
404    /// get the size of each chrominance plane
405    ///
406    /// The U plane will have this size of data. The V plane will also. If
407    /// requested with `forced_block_size`, this includes potentially invalid
408    /// rows allocated for macroblocks.
409    fn uv_size(&self) -> usize {
410        self.u_stride() * TryInto::<usize>::try_into(self.alloc_chroma_rows).unwrap()
411    }
412    pub fn new_mono8(data: Vec<u8>, width: u32, height: u32) -> Result<Self> {
413        let width: i32 = width.try_into().unwrap();
414        let height: i32 = height.try_into().unwrap();
415        let y_stride = width;
416        let chroma_stride = 0;
417        let expected_size = width as usize * height as usize;
418        if data.len() != expected_size {
419            return Err(Error::InvalidAllocatedBufferSize);
420        }
421        let alloc_chroma_rows = 0;
422
423        Ok(Self {
424            data,
425            width,
426            height,
427            y_stride,
428            colorspace: y4m::Colorspace::Cmono,
429            chroma_stride,
430            alloc_rows: height,
431            alloc_chroma_rows,
432            is_known_mono_only: true,
433            forced_block_size: None,
434        })
435    }
436    pub fn is_known_mono_only(&self) -> bool {
437        self.is_known_mono_only
438    }
439    pub fn data(&self) -> &[u8] {
440        &self.data[..]
441    }
442    pub fn into_data(self) -> Vec<u8> {
443        self.data
444    }
445    pub fn y_plane_data(&self) -> &[u8] {
446        let ysize = self.y_size();
447        &self.data[..ysize]
448    }
449    pub fn u_plane_data(&self) -> &[u8] {
450        let ysize = self.y_size();
451        &self.data[ysize..ysize + self.uv_size()]
452    }
453    pub fn v_plane_data(&self) -> &[u8] {
454        let ysize = self.y_size();
455        &self.data[(ysize + self.uv_size())..]
456    }
457
458    pub fn width(&self) -> u32 {
459        self.width.try_into().unwrap()
460    }
461    pub fn height(&self) -> u32 {
462        self.height.try_into().unwrap()
463    }
464    pub fn y_stride(&self) -> usize {
465        self.y_stride.try_into().unwrap()
466    }
467    pub fn u_stride(&self) -> usize {
468        self.chroma_stride
469    }
470    pub fn v_stride(&self) -> usize {
471        self.chroma_stride
472    }
473    pub fn colorspace(&self) -> y4m::Colorspace {
474        self.colorspace
475    }
476}
477
478fn generic_to_c420paldv_macroblocks<FMT>(
479    frame: &dyn HasRowChunksExact<FMT>,
480    block_size: u32,
481) -> Result<Y4MFrame>
482where
483    FMT: PixelFormat,
484{
485    // Convert to planar data with macroblock size
486
487    // TODO: convert directly to YUV420 instead of YUV444 for efficiency.
488    // Currently we convert to YUV444 first and then downsample later.
489    let frame_yuv444 = convert_ref::<_, pixel_format::YUV444>(frame)?;
490
491    let width: usize = frame.width().try_into().unwrap();
492
493    // full width (i.e. Y plane)
494    let fullstride: usize = next_multiple(frame.width(), block_size).try_into().unwrap();
495
496    // full height (i.e. Y plane)
497    let num_dest_alloc_rows_luma: usize = next_multiple(frame.height(), block_size)
498        .try_into()
499        .unwrap();
500
501    let half_width = div_ceil(frame.width(), 2);
502
503    // Calculate stride for downsampled chroma planes. It may not really be
504    // "half" size because it needs to be a multiple of the block_size
505    let halfstride: usize = next_multiple(half_width, block_size).try_into().unwrap();
506    let half_height = div_ceil(frame.height(), 2);
507    let valid_chroma_size: usize = halfstride * TryInto::<usize>::try_into(half_height).unwrap();
508    let num_dest_allow_rows_chroma: usize =
509        next_multiple(half_height, block_size).try_into().unwrap();
510
511    // Allocate space for Y U and V planes. We already allocate one big
512    // contiguous chunk with the fullsize Y and quarter size U and V planes.
513    let y_size = fullstride * num_dest_alloc_rows_luma;
514    let full_chroma_size = halfstride * num_dest_allow_rows_chroma;
515    let mut data = vec![EMPTY_BYTE; y_size + 2 * full_chroma_size];
516
517    // OK to use .chunks_exact() and .chunks_exact_mut() on `data` because we
518    // know even the last row has a full stride worth of bytes. However, this is
519    // not true for the source image.
520
521    let (y_plane_dest, uv_data) = data.split_at_mut(y_size);
522    debug_assert_eq!(2 * full_chroma_size, uv_data.len());
523
524    let (u_plane_dest, v_plane_dest) = uv_data.split_at_mut(full_chroma_size);
525
526    // Here we allocate separate buffers for the fullsize U and V plane.
527    let mut fullsize_u_plane = vec![EMPTY_BYTE; fullstride * num_dest_alloc_rows_luma];
528    let mut fullsize_v_plane = vec![EMPTY_BYTE; fullstride * num_dest_alloc_rows_luma];
529
530    // First, fill fullsize Y, U, and V planes. This would be full YUV444 resolution.
531    for (
532        y_plane_dest_row,
533        (fullsize_u_plane_dest_row, (fullsize_v_plane_dest_row, src_yuv444_row)),
534    ) in y_plane_dest.chunks_exact_mut(fullstride).zip(
535        fullsize_u_plane.chunks_exact_mut(fullstride).zip(
536            fullsize_v_plane
537                .chunks_exact_mut(fullstride)
538                .zip(frame_yuv444.rowchunks_exact()),
539        ),
540    ) {
541        for (y_dest_pix, (fullsize_u_dest_pix, (fullsize_v_dest_pix, yuv444_pix))) in
542            y_plane_dest_row[..width].iter_mut().zip(
543                fullsize_u_plane_dest_row[..width].iter_mut().zip(
544                    fullsize_v_plane_dest_row[..width]
545                        .iter_mut()
546                        .zip(src_yuv444_row.chunks_exact(3)),
547                ),
548            )
549        {
550            *y_dest_pix = yuv444_pix[0];
551            *fullsize_u_dest_pix = yuv444_pix[1];
552            *fullsize_v_dest_pix = yuv444_pix[2];
553        }
554    }
555
556    let y_data_ptr = y_plane_dest.as_ptr();
557    let u_data_ptr = u_plane_dest.as_ptr();
558    let v_data_ptr = v_plane_dest.as_ptr();
559
560    fn u16(v: u8) -> u16 {
561        v as u16
562    }
563
564    fn u8(v: u16) -> u8 {
565        v as u8
566    }
567
568    let valid_chroma_width: usize = half_width.try_into().unwrap();
569
570    // Now, downsample U and V planes into 420 scaling.
571    for (dest_plane, src_plane_fullsize) in [
572        (u_plane_dest, fullsize_u_plane),
573        (v_plane_dest, fullsize_v_plane),
574    ]
575    .into_iter()
576    {
577        for (dest_row, dest_data) in dest_plane[0..valid_chroma_size]
578            .chunks_exact_mut(halfstride)
579            .enumerate()
580        {
581            let src_row = dest_row * 2;
582            for (dest_col, dest_pix) in dest_data[..valid_chroma_width].iter_mut().enumerate() {
583                let src_col = dest_col * 2;
584
585                let a = u16(src_plane_fullsize[src_row * fullstride + src_col]);
586                let b = u16(src_plane_fullsize[src_row * fullstride + src_col + 1]);
587                let c = u16(src_plane_fullsize[(src_row + 1) * fullstride + src_col]);
588                let d = u16(src_plane_fullsize[(src_row + 1) * fullstride + src_col + 1]);
589                *dest_pix = u8((a + b + c + d) / 4);
590            }
591        }
592    }
593    let result = Y4MFrame::new(
594        data,
595        frame_yuv444.width(),
596        frame_yuv444.height(),
597        fullstride.try_into().unwrap(),
598        halfstride,
599        num_dest_alloc_rows_luma.try_into().unwrap(),
600        num_dest_allow_rows_chroma.try_into().unwrap(),
601        false,
602        Some(block_size),
603        y4m::Colorspace::C420paldv,
604    );
605
606    debug_assert_eq!(result.y_stride(), fullstride);
607    debug_assert_eq!(result.u_stride(), halfstride);
608    debug_assert_eq!(result.v_stride(), halfstride);
609
610    debug_assert_eq!(result.y_size(), y_size);
611    debug_assert_eq!(result.uv_size(), full_chroma_size);
612
613    // ---
614
615    debug_assert_eq!(result.y_plane_data().as_ptr(), y_data_ptr);
616    debug_assert_eq!(result.u_plane_data().as_ptr(), u_data_ptr);
617    debug_assert_eq!(result.v_plane_data().as_ptr(), v_data_ptr);
618
619    Ok(result)
620}
621
622fn generic_to_c420paldv<FMT>(frame: &dyn HasRowChunksExact<FMT>) -> Result<Y4MFrame>
623where
624    FMT: PixelFormat,
625{
626    // let colorspace = Y4MColorspace::C420paldv;
627    // Convert to YUV444 first, then convert and downsample to YUV420
628    // planar.
629
630    // TODO: convert to YUV422 instead of YUV444 for efficiency.
631    let frame = convert_ref::<_, pixel_format::YUV444>(frame)?;
632
633    // Convert to planar data.
634
635    // TODO: allocate final buffer first and write directly into that. Here we make
636    // intermediate copies.
637    let h = frame.height() as usize;
638    let width = frame.width() as usize;
639
640    let yuv_iter = frame.image_data().chunks_exact(3).map(|yuv| YUV444 {
641        Y: yuv[0],
642        U: yuv[1],
643        V: yuv[2],
644    });
645    // intermediate copy 1
646    let yuv_vec: Vec<YUV444> = yuv_iter.collect();
647
648    // intermediate copy 2a
649    let y_plane: Vec<u8> = yuv_vec.iter().map(|yuv| yuv.Y).collect();
650    let y_size = y_plane.len();
651
652    // intermediate copy 2b
653    let full_u_plane: Vec<u8> = yuv_vec.iter().map(|yuv| yuv.U).collect();
654    // intermediate copy 2c
655    let full_v_plane: Vec<u8> = yuv_vec.iter().map(|yuv| yuv.V).collect();
656
657    // intermediate copy 3a
658    let u_plane = downsample_plane(&full_u_plane, h, width);
659    // intermediate copy 3b
660    let v_plane = downsample_plane(&full_v_plane, h, width);
661
662    let u_size = u_plane.len();
663    let v_size = v_plane.len();
664    debug_assert!(y_size == 4 * u_size);
665    debug_assert!(u_size == v_size);
666
667    // final copy
668    let mut final_buf = vec![EMPTY_BYTE; y_size + u_size + v_size];
669    final_buf[..y_size].copy_from_slice(&y_plane);
670    final_buf[y_size..(y_size + u_size)].copy_from_slice(&u_plane);
671    final_buf[(y_size + u_size)..].copy_from_slice(&v_plane);
672
673    Ok(Y4MFrame::new(
674        final_buf,
675        frame.width(),
676        frame.height(),
677        width.try_into().unwrap(),
678        width / 2,
679        h.try_into().unwrap(),
680        (h / 2).try_into().unwrap(),
681        false,
682        None,
683        y4m::Colorspace::C420paldv,
684    ))
685}
686
687/// Converts input frame into a [Y4MFrame].
688pub fn encode_y4m_dynamic_frame(
689    frame: &DynamicFrame,
690    out_colorspace: y4m::Colorspace,
691    forced_block_size: Option<u32>,
692) -> Result<Y4MFrame> {
693    let pixfmt = frame.pixel_format();
694    strand_dynamic_frame::match_all_dynamic_fmts!(
695        frame,
696        x,
697        encode_y4m_frame(&x, out_colorspace, forced_block_size),
698        Error::ConvertImageError(convert_image::Error::UnimplementedPixelFormat(pixfmt))
699    )
700}
701
702/// Converts input, a reference to a trait object implementing
703/// [`HasRowChunksExact<FMT>`], into a [Y4MFrame].
704fn encode_y4m_frame<FMT>(
705    frame: &dyn HasRowChunksExact<FMT>,
706    out_colorspace: y4m::Colorspace,
707    forced_block_size: Option<u32>,
708) -> Result<Y4MFrame>
709where
710    FMT: PixelFormat,
711{
712    match out_colorspace {
713        y4m::Colorspace::Cmono => {
714            if let Some(block_size) = forced_block_size
715                && !((frame.width() % block_size == 0) && (frame.height() % block_size == 0))
716            {
717                unimplemented!("conversion to mono with forced block size");
718            }
719            let frame = convert_ref::<_, Mono8>(frame)?;
720            if frame.width() as usize != frame.stride() {
721                // Copy into new buffer with no padding.
722                let mut buf = vec![EMPTY_BYTE; frame.height() as usize * frame.width() as usize];
723                for (dest_row, src_row) in buf
724                    .chunks_exact_mut(frame.width() as usize)
725                    .zip(frame.image_data().chunks_exact(frame.stride()))
726                {
727                    dest_row.copy_from_slice(&src_row[..frame.width() as usize]);
728                }
729                Ok(Y4MFrame::new_mono8(buf, frame.width(), frame.height())?)
730            } else {
731                Ok(Y4MFrame::new_mono8(
732                    frame.image_data().to_vec(),
733                    frame.width(),
734                    frame.height(),
735                )?)
736            }
737        }
738        y4m::Colorspace::C420paldv => {
739            let input_pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
740            match input_pixfmt {
741                PixFmt::Mono8 => {
742                    // Special case for mono8.
743                    Ok(mono8_into_yuv420_planar(frame, forced_block_size))
744                }
745                _ => {
746                    if let Some(block_size) = forced_block_size {
747                        generic_to_c420paldv_macroblocks(frame, block_size)
748                    } else {
749                        generic_to_c420paldv(frame)
750                    }
751                }
752            }
753        }
754        cs => Err(Error::UnsupportedColorspace(cs)),
755    }
756}
757
758fn mono8_into_yuv420_planar<FMT>(
759    frame: &dyn HasRowChunksExact<FMT>,
760    forced_block_size: Option<u32>,
761) -> Y4MFrame
762where
763    FMT: PixelFormat,
764{
765    // Copy intensity data, other planes
766    // at 128.
767    let width: usize = frame.width().try_into().unwrap();
768    let height: usize = frame.height().try_into().unwrap();
769
770    let (luma_stride, chroma_stride): (usize, usize) = if let Some(block_size) = forced_block_size {
771        let w_mbs = div_ceil(frame.width(), block_size);
772        let dest_stride = (w_mbs * block_size).try_into().unwrap();
773
774        let chroma_w_mbs = div_ceil(frame.width() / 2, block_size);
775        let chroma_stride = (chroma_w_mbs * block_size).try_into().unwrap();
776        (dest_stride, chroma_stride)
777    } else {
778        (width, width / 2)
779    };
780
781    let (num_luma_alloc_rows, num_chroma_alloc_rows): (usize, usize) =
782        if let Some(block_size) = forced_block_size {
783            let h_mbs = div_ceil(frame.height(), block_size);
784            let num_dest_alloc_rows = (h_mbs * block_size).try_into().unwrap();
785
786            let chroma_h_mbs = div_ceil(frame.height() / 2, block_size);
787            let num_chroma_alloc_rows = (chroma_h_mbs * block_size).try_into().unwrap();
788
789            (num_dest_alloc_rows, num_chroma_alloc_rows)
790        } else {
791            (height, height / 2)
792        };
793
794    // allocate space for Y U and V planes
795    let expected_size =
796        luma_stride * num_luma_alloc_rows + chroma_stride * num_chroma_alloc_rows * 2;
797    // Fill with value 128, which is neutral chrominance
798    let mut data = vec![128u8; expected_size];
799    // We fill the Y plane (and only the Y plane, leaving the
800    // chrominance planes at 128).
801
802    let luma_fill_size = luma_stride * height;
803
804    for (dest_luma_row_slice, src) in data[..luma_fill_size]
805        .chunks_exact_mut(luma_stride)
806        .zip(frame.rowchunks_exact())
807    {
808        debug_assert_eq!(width, src.len());
809        dest_luma_row_slice[..width].copy_from_slice(src);
810    }
811
812    let stride = luma_stride.try_into().unwrap();
813
814    Y4MFrame::new(
815        data,
816        frame.width(),
817        frame.height(),
818        stride,
819        chroma_stride,
820        num_luma_alloc_rows.try_into().unwrap(),
821        num_chroma_alloc_rows.try_into().unwrap(),
822        true,
823        forced_block_size,
824        y4m::Colorspace::C420paldv,
825    )
826}