1use 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 pub raten: usize,
52 pub rated: usize,
54 pub aspectn: usize,
56 pub aspectd: usize,
58}
59
60enum Writer {
61 NotStarted(Box<dyn Write>),
62 Started(y4m::Encoder<Box<dyn Write>>),
63 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
76pub 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 pub fn flush(&mut self) -> Result<()> {
158 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
176fn downsample_plane(arr: &[u8], h: usize, w: usize) -> Vec<u8> {
179 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
221pub 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 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 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 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 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 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 let frame_yuv444 = convert_ref::<_, pixel_format::YUV444>(frame)?;
490
491 let width: usize = frame.width().try_into().unwrap();
492
493 let fullstride: usize = next_multiple(frame.width(), block_size).try_into().unwrap();
495
496 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 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 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 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 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 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 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 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 frame = convert_ref::<_, pixel_format::YUV444>(frame)?;
632
633 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 let yuv_vec: Vec<YUV444> = yuv_iter.collect();
647
648 let y_plane: Vec<u8> = yuv_vec.iter().map(|yuv| yuv.Y).collect();
650 let y_size = y_plane.len();
651
652 let full_u_plane: Vec<u8> = yuv_vec.iter().map(|yuv| yuv.U).collect();
654 let full_v_plane: Vec<u8> = yuv_vec.iter().map(|yuv| yuv.V).collect();
656
657 let u_plane = downsample_plane(&full_u_plane, h, width);
659 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 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
687pub 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
702fn 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 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 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 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 let expected_size =
796 luma_stride * num_luma_alloc_rows + chroma_stride * num_chroma_alloc_rows * 2;
797 let mut data = vec![128u8; expected_size];
799 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}