1mod audio;
13
14pub use audio::{trim_audio_frame, AudioFilterChain};
15
16use rff_core::{ColorRange, Error, PixelFormat, Result, VideoFrame};
17
18pub trait Filter: Send {
20 fn name(&self) -> &'static str;
22
23 fn filter(&mut self, frame: VideoFrame) -> Result<VideoFrame>;
25
26 fn output_dims(&self, in_w: u32, in_h: u32) -> (u32, u32) {
30 (in_w, in_h)
31 }
32
33 fn output_color_range(&self) -> Option<ColorRange> {
40 None
41 }
42
43 fn set_source_color_range(&mut self, _range: ColorRange) {}
46}
47
48#[derive(Default)]
50pub struct FilterChain {
51 filters: Vec<Box<dyn Filter>>,
52}
53
54impl FilterChain {
55 pub fn parse(spec: &str) -> Result<FilterChain> {
58 let mut filters: Vec<Box<dyn Filter>> = Vec::new();
59 for token in spec.split(',') {
60 let token = token.trim();
61 if token.is_empty() {
62 continue;
63 }
64 let (name, args) = token.split_once('=').unwrap_or((token, ""));
65 let parts: Vec<&str> = if args.is_empty() {
66 Vec::new()
67 } else {
68 args.split(':').collect()
69 };
70 match name {
71 "scale" => filters.push(Box::new(Scale::parse(&parts)?)),
72 "crop" => filters.push(Box::new(Crop::parse(&parts)?)),
73 "hflip" => filters.push(Box::new(HFlip)),
74 "vflip" => filters.push(Box::new(VFlip)),
75 "transpose" => filters.push(Box::new(Transpose::parse(&parts)?)),
76 "pad" => filters.push(Box::new(Pad::parse(&parts)?)),
77 "format" => filters.push(Box::new(FormatConv::parse(&parts)?)),
78 "setrange" => filters.push(Box::new(SetRange::parse(&parts)?)),
79 "negate" => filters.push(Box::new(Negate)),
80 "gray" | "grayscale" => filters.push(Box::new(Grayscale)),
81 other => return Err(Error::unsupported(format!("unknown filter `{other}`"))),
82 }
83 }
84 Ok(FilterChain { filters })
85 }
86
87 pub fn is_empty(&self) -> bool {
88 self.filters.is_empty()
89 }
90
91 pub fn prepend_range_conversion(&mut self, to_full: bool) {
94 self.filters.insert(0, Box::new(SetRange { to_full }));
95 }
96
97 pub fn set_source_color_range(&mut self, range: ColorRange) {
100 for f in &mut self.filters {
101 f.set_source_color_range(range);
102 }
103 }
104
105 pub fn output_color_range(&self) -> Option<ColorRange> {
108 self.filters
109 .iter()
110 .rev()
111 .find_map(|f| f.output_color_range())
112 }
113
114 pub fn apply(&mut self, mut frame: VideoFrame) -> Result<VideoFrame> {
116 for filter in &mut self.filters {
117 frame = filter.filter(frame)?;
118 }
119 Ok(frame)
120 }
121
122 pub fn output_dims(&self, mut w: u32, mut h: u32) -> (u32, u32) {
124 for filter in &self.filters {
125 (w, h) = filter.output_dims(w, h);
126 }
127 (w, h)
128 }
129}
130
131pub struct FilterComplex {
140 pub overlay: Option<(u32, u32)>,
143}
144
145impl FilterComplex {
146 pub fn parse(spec: &str) -> Result<FilterComplex> {
150 if let Some(pos) = spec.find("overlay") {
151 let after = spec[pos + "overlay".len()..].trim_start_matches('=');
152 let args = after.split([',', '[', ';']).next().unwrap_or("");
154 let mut parts = args.split(':');
155 let x = parts
156 .next()
157 .and_then(|s| s.trim().parse().ok())
158 .unwrap_or(0);
159 let y = parts
160 .next()
161 .and_then(|s| s.trim().parse().ok())
162 .unwrap_or(0);
163 return Ok(FilterComplex {
164 overlay: Some((x, y)),
165 });
166 }
167 Err(Error::unsupported(format!(
168 "filter_complex: unsupported graph `{spec}` (only `overlay` so far)"
169 )))
170 }
171}
172
173pub fn overlay(base: VideoFrame, over: &VideoFrame, x: u32, y: u32) -> Result<VideoFrame> {
178 ensure_planar_yuv(&base, "overlay")?;
179 ensure_planar_yuv(over, "overlay")?;
180 if base.format != over.format {
181 return Err(Error::unsupported(format!(
182 "overlay: base {} vs overlay {} — convert with the `format` filter first",
183 base.format.name(),
184 over.format.name()
185 )));
186 }
187 let (sx, sy) = subsampling(base.format)?;
188 let (x, y) = (x - x % sx, y - y % sy);
189 let mut base = base;
190 for i in 0..3 {
191 let (div_x, div_y) = if i == 0 { (1, 1) } else { (sx, sy) };
192 let (ox, oy) = ((x / div_x) as usize, (y / div_y) as usize);
193 let (ow, oh) = plane_dims(over.format, over.width, over.height, i)?;
194 let (bw, bh) = plane_dims(base.format, base.width, base.height, i)?;
195 let (bw, bh) = (bw as usize, bh as usize);
196 let (bstride, ostride) = (base.strides[i], over.strides[i]);
197 for row in 0..oh as usize {
198 let by = oy + row;
199 if by >= bh {
200 break;
201 }
202 let copy_w = (ow as usize).min(bw.saturating_sub(ox));
203 if copy_w == 0 {
204 break;
205 }
206 let bstart = by * bstride + ox;
207 let ostart = row * ostride;
208 base.planes[i][bstart..bstart + copy_w]
209 .copy_from_slice(&over.planes[i][ostart..ostart + copy_w]);
210 }
211 }
212 Ok(base)
213}
214
215fn subsampling(format: PixelFormat) -> Result<(u32, u32)> {
221 match format {
222 PixelFormat::Yuv420p => Ok((2, 2)),
223 PixelFormat::Yuv422p => Ok((2, 1)),
224 PixelFormat::Yuv444p => Ok((1, 1)),
225 other => Err(Error::unsupported(format!(
226 "filter: pixel format `{}` (only 8-bit planar YUV is supported)",
227 other.name()
228 ))),
229 }
230}
231
232fn plane_dims(format: PixelFormat, w: u32, h: u32, index: usize) -> Result<(u32, u32)> {
234 if index == 0 {
235 Ok((w, h))
236 } else {
237 let (sx, sy) = subsampling(format)?;
238 Ok((w.div_ceil(sx), h.div_ceil(sy)))
239 }
240}
241
242fn ensure_planar_yuv(frame: &VideoFrame, op: &str) -> Result<()> {
243 subsampling(frame.format)?; if frame.planes.len() < 3 || frame.strides.len() < 3 {
245 return Err(Error::invalid(format!(
246 "{op}: expected 3 planes, got {}",
247 frame.planes.len()
248 )));
249 }
250 Ok(())
251}
252
253struct Scale {
261 width: i32,
262 height: i32,
263}
264
265impl Scale {
266 fn parse(args: &[&str]) -> Result<Scale> {
267 let [w, h] = args else {
268 return Err(Error::Option("scale: expected scale=W:H".into()));
269 };
270 let parse = |s: &str| {
271 s.trim()
272 .parse::<i32>()
273 .ok()
274 .filter(|&v| v == -1 || v > 0)
275 .ok_or_else(|| Error::Option(format!("scale: bad dimension `{s}`")))
276 };
277 let (width, height) = (parse(w)?, parse(h)?);
278 if width == -1 && height == -1 {
279 return Err(Error::Option(
280 "scale: at least one of W/H must be a fixed size".into(),
281 ));
282 }
283 Ok(Scale { width, height })
284 }
285
286 fn resolve(&self, in_w: u32, in_h: u32) -> (u32, u32) {
288 let even = |v: u32| {
289 let v = v.max(2);
290 if v % 2 == 0 {
291 v
292 } else {
293 v + 1
294 }
295 };
296 match (self.width, self.height) {
297 (w, h) if w > 0 && h > 0 => (w as u32, h as u32),
298 (-1, h) => {
299 let h = h as u32;
300 let w = (in_w as u64 * h as u64 / in_h.max(1) as u64) as u32;
301 (even(w), h)
302 }
303 (w, _) => {
304 let w = w as u32;
305 let h = (in_h as u64 * w as u64 / in_w.max(1) as u64) as u32;
306 (w, even(h))
307 }
308 }
309 }
310}
311
312impl Filter for Scale {
313 fn name(&self) -> &'static str {
314 "scale"
315 }
316
317 fn output_dims(&self, in_w: u32, in_h: u32) -> (u32, u32) {
318 self.resolve(in_w, in_h)
319 }
320
321 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
322 ensure_planar_yuv(&src, "scale")?;
323 let (out_w, out_h) = self.resolve(src.width, src.height);
324 let mut planes = Vec::with_capacity(src.planes.len());
325 let mut strides = Vec::with_capacity(src.planes.len());
326 for (i, plane) in src.planes.iter().enumerate() {
327 let (sw, sh) = plane_dims(src.format, src.width, src.height, i)?;
328 let (dw, dh) = plane_dims(src.format, out_w, out_h, i)?;
329 planes.push(bilinear(plane, src.strides[i], sw, sh, dw, dh));
330 strides.push(dw as usize);
331 }
332 Ok(VideoFrame {
333 width: out_w,
334 height: out_h,
335 format: src.format,
336 planes,
337 strides,
338 pts: src.pts,
339 })
340 }
341}
342
343fn bilinear(src: &[u8], src_stride: usize, sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
346 let mut out = vec![0u8; (dw * dh) as usize];
347 if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
348 return out;
349 }
350 let x_ratio = sw as f32 / dw as f32;
351 let y_ratio = sh as f32 / dh as f32;
352 let at = |x: u32, y: u32| src[y as usize * src_stride + x as usize] as f32;
353
354 for dy in 0..dh {
355 let fy = (((dy as f32 + 0.5) * y_ratio) - 0.5).max(0.0);
356 let y0 = fy.floor() as u32;
357 let y1 = (y0 + 1).min(sh - 1);
358 let wy = fy - y0 as f32;
359 for dx in 0..dw {
360 let fx = (((dx as f32 + 0.5) * x_ratio) - 0.5).max(0.0);
361 let x0 = fx.floor() as u32;
362 let x1 = (x0 + 1).min(sw - 1);
363 let wx = fx - x0 as f32;
364
365 let top = at(x0, y0) * (1.0 - wx) + at(x1, y0) * wx;
366 let bottom = at(x0, y1) * (1.0 - wx) + at(x1, y1) * wx;
367 let value = top * (1.0 - wy) + bottom * wy;
368 out[(dy * dw + dx) as usize] = value.round().clamp(0.0, 255.0) as u8;
369 }
370 }
371 out
372}
373
374struct Crop {
381 w: u32,
382 h: u32,
383 x: u32,
384 y: u32,
385}
386
387impl Crop {
388 fn parse(args: &[&str]) -> Result<Crop> {
389 let [w, h, x, y] = args else {
390 return Err(Error::Option("crop: expected crop=W:H:X:Y".into()));
391 };
392 let num = |s: &str| {
393 s.trim()
394 .parse::<u32>()
395 .map_err(|_| Error::Option(format!("crop: bad value `{s}`")))
396 };
397 Ok(Crop {
398 w: num(w)?,
399 h: num(h)?,
400 x: num(x)?,
401 y: num(y)?,
402 })
403 }
404}
405
406impl Filter for Crop {
407 fn name(&self) -> &'static str {
408 "crop"
409 }
410
411 fn output_dims(&self, _in_w: u32, _in_h: u32) -> (u32, u32) {
412 (self.w, self.h)
413 }
414
415 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
416 ensure_planar_yuv(&src, "crop")?;
417 if self.x + self.w > src.width || self.y + self.h > src.height {
418 return Err(Error::Option(format!(
419 "crop: rectangle {}x{}+{}+{} exceeds frame {}x{}",
420 self.w, self.h, self.x, self.y, src.width, src.height
421 )));
422 }
423 let (sx, sy) = subsampling(src.format)?;
424 if self.x % sx != 0 || self.w % sx != 0 || self.y % sy != 0 || self.h % sy != 0 {
425 return Err(Error::Option(
426 "crop: offset/size must align to the chroma grid".into(),
427 ));
428 }
429
430 let mut planes = Vec::with_capacity(src.planes.len());
431 let mut strides = Vec::with_capacity(src.planes.len());
432 for (i, plane) in src.planes.iter().enumerate() {
433 let (div_x, div_y) = if i == 0 { (1, 1) } else { (sx, sy) };
435 let (px, py) = (self.x / div_x, self.y / div_y);
436 let (pw, ph) = (self.w / div_x, self.h / div_y);
437
438 let mut out = Vec::with_capacity((pw * ph) as usize);
439 for row in 0..ph {
440 let start = (py + row) as usize * src.strides[i] + px as usize;
441 out.extend_from_slice(&plane[start..start + pw as usize]);
442 }
443 planes.push(out);
444 strides.push(pw as usize);
445 }
446 Ok(VideoFrame {
447 width: self.w,
448 height: self.h,
449 format: src.format,
450 planes,
451 strides,
452 pts: src.pts,
453 })
454 }
455}
456
457struct HFlip;
463
464impl Filter for HFlip {
465 fn name(&self) -> &'static str {
466 "hflip"
467 }
468
469 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
470 ensure_planar_yuv(&src, "hflip")?;
471 let mut planes = Vec::with_capacity(src.planes.len());
472 let mut strides = Vec::with_capacity(src.planes.len());
473 for (i, plane) in src.planes.iter().enumerate() {
474 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
475 let (pw, ph, stride) = (pw as usize, ph as usize, src.strides[i]);
476 let mut out = vec![0u8; pw * ph];
477 for y in 0..ph {
478 let row = &plane[y * stride..y * stride + pw];
479 let dst = &mut out[y * pw..(y + 1) * pw];
480 for x in 0..pw {
481 dst[x] = row[pw - 1 - x];
482 }
483 }
484 planes.push(out);
485 strides.push(pw);
486 }
487 Ok(VideoFrame {
488 width: src.width,
489 height: src.height,
490 format: src.format,
491 planes,
492 strides,
493 pts: src.pts,
494 })
495 }
496}
497
498struct VFlip;
500
501impl Filter for VFlip {
502 fn name(&self) -> &'static str {
503 "vflip"
504 }
505
506 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
507 ensure_planar_yuv(&src, "vflip")?;
508 let mut planes = Vec::with_capacity(src.planes.len());
509 let mut strides = Vec::with_capacity(src.planes.len());
510 for (i, plane) in src.planes.iter().enumerate() {
511 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
512 let (pw, ph, stride) = (pw as usize, ph as usize, src.strides[i]);
513 let mut out = vec![0u8; pw * ph];
514 for y in 0..ph {
515 let src_row = &plane[(ph - 1 - y) * stride..(ph - 1 - y) * stride + pw];
516 out[y * pw..(y + 1) * pw].copy_from_slice(src_row);
517 }
518 planes.push(out);
519 strides.push(pw);
520 }
521 Ok(VideoFrame {
522 width: src.width,
523 height: src.height,
524 format: src.format,
525 planes,
526 strides,
527 pts: src.pts,
528 })
529 }
530}
531
532struct Transpose {
540 dir: u8,
541}
542
543impl Transpose {
544 fn parse(args: &[&str]) -> Result<Transpose> {
545 let dir = match args.first().map(|s| s.trim()) {
546 None | Some("") => 1, Some("cclock_flip") | Some("0") => 0,
548 Some("clock") | Some("1") => 1,
549 Some("cclock") | Some("2") => 2,
550 Some("clock_flip") | Some("3") => 3,
551 Some(other) => {
552 return Err(Error::Option(format!(
553 "transpose: unknown direction `{other}`"
554 )))
555 }
556 };
557 Ok(Transpose { dir })
558 }
559
560 fn source(&self, dx: u32, dy: u32, w: u32, h: u32) -> (u32, u32) {
563 match self.dir {
564 0 => (dy, dx), 1 => (dy, h - 1 - dx), 2 => (w - 1 - dy, dx), _ => (w - 1 - dy, h - 1 - dx), }
569 }
570}
571
572impl Filter for Transpose {
573 fn name(&self) -> &'static str {
574 "transpose"
575 }
576
577 fn output_dims(&self, in_w: u32, in_h: u32) -> (u32, u32) {
578 (in_h, in_w)
579 }
580
581 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
582 ensure_planar_yuv(&src, "transpose")?;
583 let (sx, sy) = subsampling(src.format)?;
584 if sx != sy {
585 return Err(Error::unsupported(
586 "transpose: needs symmetric chroma (4:2:0 or 4:4:4)",
587 ));
588 }
589 let mut planes = Vec::with_capacity(src.planes.len());
590 let mut strides = Vec::with_capacity(src.planes.len());
591 for (i, plane) in src.planes.iter().enumerate() {
592 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
593 let (dw, dh) = (ph, pw); let stride = src.strides[i];
595 let mut out = vec![0u8; (dw * dh) as usize];
596 for dy in 0..dh {
597 for dx in 0..dw {
598 let (mx, my) = self.source(dx, dy, pw, ph);
599 out[(dy * dw + dx) as usize] = plane[my as usize * stride + mx as usize];
600 }
601 }
602 planes.push(out);
603 strides.push(dw as usize);
604 }
605 Ok(VideoFrame {
606 width: src.height,
607 height: src.width,
608 format: src.format,
609 planes,
610 strides,
611 pts: src.pts,
612 })
613 }
614}
615
616struct Pad {
624 w: u32,
625 h: u32,
626 x: u32,
627 y: u32,
628}
629
630impl Pad {
631 fn parse(args: &[&str]) -> Result<Pad> {
632 let [w, h, x, y] = args else {
633 return Err(Error::Option("pad: expected pad=W:H:X:Y".into()));
634 };
635 let num = |s: &str| {
636 s.trim()
637 .parse::<u32>()
638 .map_err(|_| Error::Option(format!("pad: bad value `{s}`")))
639 };
640 Ok(Pad {
641 w: num(w)?,
642 h: num(h)?,
643 x: num(x)?,
644 y: num(y)?,
645 })
646 }
647}
648
649impl Filter for Pad {
650 fn name(&self) -> &'static str {
651 "pad"
652 }
653
654 fn output_dims(&self, _in_w: u32, _in_h: u32) -> (u32, u32) {
655 (self.w, self.h)
656 }
657
658 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
659 ensure_planar_yuv(&src, "pad")?;
660 if self.x + src.width > self.w || self.y + src.height > self.h {
661 return Err(Error::Option(format!(
662 "pad: {}x{} input at +{}+{} doesn't fit a {}x{} canvas",
663 src.width, src.height, self.x, self.y, self.w, self.h
664 )));
665 }
666 let (sx, sy) = subsampling(src.format)?;
667 if self.x % sx != 0 || self.w % sx != 0 || self.y % sy != 0 || self.h % sy != 0 {
668 return Err(Error::Option(
669 "pad: offset/size must align to the chroma grid".into(),
670 ));
671 }
672
673 let mut planes = Vec::with_capacity(src.planes.len());
674 let mut strides = Vec::with_capacity(src.planes.len());
675 for (i, plane) in src.planes.iter().enumerate() {
676 let (cw, ch) = plane_dims(src.format, self.w, self.h, i)?; let (sw, sh) = plane_dims(src.format, src.width, src.height, i)?; let (div_x, div_y) = if i == 0 { (1, 1) } else { (sx, sy) };
679 let (px, py) = (self.x / div_x, self.y / div_y);
680 let fill = if i == 0 { 0u8 } else { 128u8 }; let (cw, ch) = (cw as usize, ch as usize);
683 let mut out = vec![fill; cw * ch];
684 for row in 0..sh as usize {
685 let dst_start = (py as usize + row) * cw + px as usize;
686 let src_start = row * src.strides[i];
687 out[dst_start..dst_start + sw as usize]
688 .copy_from_slice(&plane[src_start..src_start + sw as usize]);
689 }
690 planes.push(out);
691 strides.push(cw);
692 }
693 Ok(VideoFrame {
694 width: self.w,
695 height: self.h,
696 format: src.format,
697 planes,
698 strides,
699 pts: src.pts,
700 })
701 }
702}
703
704#[derive(Clone, Copy)]
715struct ColorSpec {
716 kr: f32,
717 kb: f32,
718 limited: bool,
719}
720impl Default for ColorSpec {
721 fn default() -> Self {
722 ColorSpec {
723 kr: 0.299,
724 kb: 0.114,
725 limited: false,
726 } }
728}
729impl ColorSpec {
730 #[inline]
731 fn kg(&self) -> f32 {
732 1.0 - self.kr - self.kb
733 }
734}
735
736struct FormatConv {
740 target: PixelFormat,
741 spec: ColorSpec,
742 range_explicit: bool,
746}
747
748impl FormatConv {
749 fn parse(args: &[&str]) -> Result<FormatConv> {
750 let name = args.first().copied().unwrap_or("").trim();
751 let target = match name {
752 "yuv420p" => PixelFormat::Yuv420p,
753 "rgb24" => PixelFormat::Rgb24,
754 other => {
755 return Err(Error::unsupported(format!(
756 "format: target `{other}` (only yuv420p and rgb24)"
757 )))
758 }
759 };
760 let mut spec = ColorSpec::default();
761 let mut range_explicit = false;
762 for opt in args.iter().skip(1).map(|s| s.trim()) {
763 match opt {
764 "" => {}
765 "bt601" | "601" | "smpte170m" => {
766 spec.kr = 0.299;
767 spec.kb = 0.114;
768 }
769 "bt709" | "709" => {
770 spec.kr = 0.2126;
771 spec.kb = 0.0722;
772 }
773 "full" | "pc" | "jpeg" => {
774 spec.limited = false;
775 range_explicit = true;
776 }
777 "limited" | "tv" | "mpeg" => {
778 spec.limited = true;
779 range_explicit = true;
780 }
781 other => {
782 return Err(Error::unsupported(format!(
783 "format: option `{other}` (matrix bt601/bt709, range full/limited)"
784 )))
785 }
786 }
787 }
788 Ok(FormatConv {
789 target,
790 spec,
791 range_explicit,
792 })
793 }
794}
795
796impl Filter for FormatConv {
797 fn name(&self) -> &'static str {
798 "format"
799 }
800
801 fn set_source_color_range(&mut self, range: ColorRange) {
802 if !self.range_explicit {
805 match range {
806 ColorRange::Limited => self.spec.limited = true,
807 ColorRange::Full => self.spec.limited = false,
808 ColorRange::Unspecified => {}
809 }
810 }
811 }
812
813 fn output_color_range(&self) -> Option<ColorRange> {
817 match self.target {
818 PixelFormat::Yuv420p | PixelFormat::Yuv422p | PixelFormat::Yuv444p => {
819 Some(if self.spec.limited {
820 ColorRange::Limited
821 } else {
822 ColorRange::Full
823 })
824 }
825 _ => None,
826 }
827 }
828
829 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
830 if src.format == self.target {
831 return Ok(src);
832 }
833 match (src.format, self.target) {
834 (PixelFormat::Rgb24, PixelFormat::Yuv420p) => rgb_to_yuv420(&src, 3, self.spec),
835 (PixelFormat::Rgba, PixelFormat::Yuv420p) => rgb_to_yuv420(&src, 4, self.spec),
836 (PixelFormat::Rgba, PixelFormat::Rgb24) => rgba_to_rgb(&src),
837 (PixelFormat::Yuv420p, PixelFormat::Rgb24) => yuv420_to_rgb(&src, self.spec),
838 (PixelFormat::Yuv422p, PixelFormat::Rgb24)
839 | (PixelFormat::Yuv444p, PixelFormat::Rgb24) => yuv_planar_to_rgb(&src, self.spec),
840 (PixelFormat::Yuv422p, PixelFormat::Yuv420p)
841 | (PixelFormat::Yuv444p, PixelFormat::Yuv420p) => yuv_planar_to_420(&src),
842 (from, to) if from.bit_depth() > 8 && to.bit_depth() == 8 => {
845 let narrowed = narrow_to_8(&src)?;
846 if narrowed.format == to {
847 Ok(narrowed)
848 } else {
849 self.filter(narrowed)
850 }
851 }
852 (from, to) => Err(Error::unsupported(format!(
853 "format: {} → {} not supported",
854 from.name(),
855 to.name()
856 ))),
857 }
858 }
859}
860
861fn clamp_u8(v: f32) -> u8 {
862 v.round().clamp(0.0, 255.0) as u8
863}
864
865fn rgb_to_yuv420(src: &VideoFrame, bpp: usize, spec: ColorSpec) -> Result<VideoFrame> {
867 let (w, h) = (src.width as usize, src.height as usize);
868 let stride = src.strides[0];
869 let rgb = &src.planes[0];
870 let px = |i: usize, j: usize| {
871 let p = j * stride + i * bpp;
872 (rgb[p] as f32, rgb[p + 1] as f32, rgb[p + 2] as f32)
873 };
874
875 let (kr, kb, kg) = (spec.kr, spec.kb, spec.kg());
878 let (yoff, yscale, cscale) = if spec.limited {
879 (16.0, 219.0 / 255.0, 224.0 / 255.0)
880 } else {
881 (0.0, 1.0, 1.0)
882 };
883 let (ur, ug) = (-0.5 * kr / (1.0 - kb), -0.5 * kg / (1.0 - kb));
884 let (vg, vb) = (-0.5 * kg / (1.0 - kr), -0.5 * kb / (1.0 - kr));
885
886 let mut y = vec![0u8; w * h];
887 for j in 0..h {
888 for i in 0..w {
889 let (r, g, b) = px(i, j);
890 y[j * w + i] = clamp_u8(yoff + yscale * (kr * r + kg * g + kb * b));
891 }
892 }
893
894 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
895 let (mut u, mut v) = (vec![0u8; cw * ch], vec![0u8; cw * ch]);
896 for cj in 0..ch {
897 for ci in 0..cw {
898 let (mut sr, mut sg, mut sb, mut n) = (0.0, 0.0, 0.0, 0.0);
900 for dy in 0..2 {
901 for dx in 0..2 {
902 let (i, j) = (ci * 2 + dx, cj * 2 + dy);
903 if i < w && j < h {
904 let (r, g, b) = px(i, j);
905 sr += r;
906 sg += g;
907 sb += b;
908 n += 1.0;
909 }
910 }
911 }
912 let (r, g, b) = (sr / n, sg / n, sb / n);
913 u[cj * cw + ci] = clamp_u8(128.0 + cscale * (ur * r + ug * g + 0.5 * b));
914 v[cj * cw + ci] = clamp_u8(128.0 + cscale * (0.5 * r + vg * g + vb * b));
915 }
916 }
917
918 Ok(VideoFrame {
919 width: src.width,
920 height: src.height,
921 format: PixelFormat::Yuv420p,
922 planes: vec![y, u, v],
923 strides: vec![w, cw, cw],
924 pts: src.pts,
925 })
926}
927
928fn planar_subsampling(format: PixelFormat) -> Option<(usize, usize)> {
931 match format {
932 PixelFormat::Yuv444p | PixelFormat::Yuv444p10 | PixelFormat::Yuv444p12 => Some((1, 1)),
933 PixelFormat::Yuv422p | PixelFormat::Yuv422p10 | PixelFormat::Yuv422p12 => Some((2, 1)),
934 PixelFormat::Yuv420p | PixelFormat::Yuv420p10 | PixelFormat::Yuv420p12 => Some((2, 2)),
935 _ => None,
936 }
937}
938
939fn yuv_planar_to_rgb(src: &VideoFrame, spec: ColorSpec) -> Result<VideoFrame> {
945 let (sh, sv) = planar_subsampling(src.format).ok_or_else(|| {
946 Error::unsupported(format!(
947 "format: {} is not planar Y'CbCr",
948 src.format.name()
949 ))
950 })?;
951 let (w, h) = (src.width as usize, src.height as usize);
952 let (ys, us, vs) = (src.strides[0], src.strides[1], src.strides[2]);
953 let (yp, up, vp) = (&src.planes[0], &src.planes[1], &src.planes[2]);
954
955 let kg = spec.kg();
956 let (yoff, yscale, cscale) = if spec.limited {
957 (16.0, 255.0 / 219.0, 255.0 / 224.0)
958 } else {
959 (0.0, 1.0, 1.0)
960 };
961 let kr_c = 2.0 * (1.0 - spec.kr) * cscale;
962 let kb_c = 2.0 * (1.0 - spec.kb) * cscale;
963 let kgv = -2.0 * spec.kr * (1.0 - spec.kr) / kg * cscale;
964 let kgu = -2.0 * spec.kb * (1.0 - spec.kb) / kg * cscale;
965
966 let mut out = vec![0u8; w * h * 3];
967 for j in 0..h {
968 for i in 0..w {
969 let ya = yscale * (yp[j * ys + i] as f32 - yoff);
970 let cu = up[(j / sv) * us + i / sh] as f32 - 128.0;
971 let cv = vp[(j / sv) * vs + i / sh] as f32 - 128.0;
972 let o = (j * w + i) * 3;
973 out[o] = clamp_u8(ya + kr_c * cv);
974 out[o + 1] = clamp_u8(ya + kgu * cu + kgv * cv);
975 out[o + 2] = clamp_u8(ya + kb_c * cu);
976 }
977 }
978 Ok(VideoFrame {
979 width: src.width,
980 height: src.height,
981 format: PixelFormat::Rgb24,
982 planes: vec![out],
983 strides: vec![w * 3],
984 pts: src.pts,
985 })
986}
987
988fn narrow_to_8(src: &VideoFrame) -> Result<VideoFrame> {
991 let (target, shift) = match src.format {
992 PixelFormat::Yuv420p10 => (PixelFormat::Yuv420p, 2),
993 PixelFormat::Yuv422p10 => (PixelFormat::Yuv422p, 2),
994 PixelFormat::Yuv444p10 => (PixelFormat::Yuv444p, 2),
995 PixelFormat::Yuv420p12 => (PixelFormat::Yuv420p, 4),
996 PixelFormat::Yuv422p12 => (PixelFormat::Yuv422p, 4),
997 PixelFormat::Yuv444p12 => (PixelFormat::Yuv444p, 4),
998 other => {
999 return Err(Error::unsupported(format!(
1000 "format: {} cannot be narrowed to 8 bits",
1001 other.name()
1002 )))
1003 }
1004 };
1005 let (sh, sv) = planar_subsampling(src.format).unwrap_or((1, 1));
1006 let (w, h) = (src.width as usize, src.height as usize);
1007 let round = 1u32 << (shift - 1);
1008 let mut planes = Vec::with_capacity(3);
1009 let mut strides = Vec::with_capacity(3);
1010 for (i, plane) in src.planes.iter().enumerate().take(3) {
1011 let (pw, ph) = if i == 0 {
1012 (w, h)
1013 } else {
1014 (w.div_ceil(sh), h.div_ceil(sv))
1015 };
1016 let stride = src.strides.get(i).copied().unwrap_or(pw * 2);
1017 let mut out = Vec::with_capacity(pw * ph);
1018 for y in 0..ph {
1019 let row = plane.get(y * stride..y * stride + pw * 2).ok_or_else(|| {
1020 Error::invalid("narrow_to_8: plane shorter than its declared geometry")
1021 })?;
1022 out.extend(row.chunks_exact(2).map(|c| {
1023 let v = u16::from_le_bytes([c[0], c[1]]) as u32;
1024 ((v + round) >> shift).min(255) as u8
1025 }));
1026 }
1027 strides.push(pw);
1028 planes.push(out);
1029 }
1030 Ok(VideoFrame {
1031 width: src.width,
1032 height: src.height,
1033 format: target,
1034 planes,
1035 strides,
1036 pts: src.pts,
1037 })
1038}
1039
1040fn yuv_planar_to_420(src: &VideoFrame) -> Result<VideoFrame> {
1046 if src.format.bit_depth() > 8 {
1047 return Err(Error::unsupported(format!(
1048 "format: {} must be narrowed to 8 bits first",
1049 src.format.name()
1050 )));
1051 }
1052 let (sh, sv) = planar_subsampling(src.format).ok_or_else(|| {
1053 Error::unsupported(format!(
1054 "format: {} is not planar Y'CbCr",
1055 src.format.name()
1056 ))
1057 })?;
1058 if (sh, sv) == (2, 2) {
1059 return Ok(src.clone());
1060 }
1061 let (w, h) = (src.width as usize, src.height as usize);
1062 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
1063 let (scw, sch) = (w.div_ceil(sh), h.div_ceil(sv));
1065
1066 let ys = src.strides[0];
1068 let mut luma = Vec::with_capacity(w * h);
1069 for j in 0..h {
1070 luma.extend_from_slice(&src.planes[0][j * ys..j * ys + w]);
1071 }
1072
1073 let mut out_planes = vec![luma];
1074 for plane_idx in 1..3 {
1075 let stride = src.strides[plane_idx];
1076 let plane = &src.planes[plane_idx];
1077 let mut dst = vec![0u8; cw * ch];
1078 let (fx, fy) = (2 / sh, 2 / sv);
1080 for j in 0..ch {
1081 for i in 0..cw {
1082 let mut sum = 0u32;
1083 let mut n = 0u32;
1084 for dy in 0..fy.max(1) {
1085 let sy = (j * fy + dy).min(sch - 1);
1086 for dx in 0..fx.max(1) {
1087 let sx = (i * fx + dx).min(scw - 1);
1088 sum += plane[sy * stride + sx] as u32;
1089 n += 1;
1090 }
1091 }
1092 dst[j * cw + i] = ((sum + n / 2) / n) as u8;
1093 }
1094 }
1095 out_planes.push(dst);
1096 }
1097 Ok(VideoFrame {
1098 width: src.width,
1099 height: src.height,
1100 format: PixelFormat::Yuv420p,
1101 planes: out_planes,
1102 strides: vec![w, cw, cw],
1103 pts: src.pts,
1104 })
1105}
1106
1107fn yuv420_to_rgb(src: &VideoFrame, spec: ColorSpec) -> Result<VideoFrame> {
1108 let (w, h) = (src.width as usize, src.height as usize);
1109 let (ys, us, vs) = (src.strides[0], src.strides[1], src.strides[2]);
1110 let (yp, up, vp) = (&src.planes[0], &src.planes[1], &src.planes[2]);
1111
1112 let kg = spec.kg();
1115 let (yoff, yscale, cscale) = if spec.limited {
1116 (16.0, 255.0 / 219.0, 255.0 / 224.0)
1117 } else {
1118 (0.0, 1.0, 1.0)
1119 };
1120 let kr_c = 2.0 * (1.0 - spec.kr) * cscale;
1121 let kb_c = 2.0 * (1.0 - spec.kb) * cscale;
1122 let kgv = -2.0 * spec.kr * (1.0 - spec.kr) / kg * cscale;
1123 let kgu = -2.0 * spec.kb * (1.0 - spec.kb) / kg * cscale;
1124
1125 let mut out = vec![0u8; w * h * 3];
1126 for j in 0..h {
1127 for i in 0..w {
1128 let ya = yscale * (yp[j * ys + i] as f32 - yoff);
1129 let cu = up[(j / 2) * us + i / 2] as f32 - 128.0;
1130 let cv = vp[(j / 2) * vs + i / 2] as f32 - 128.0;
1131 let o = (j * w + i) * 3;
1132 out[o] = clamp_u8(ya + kr_c * cv);
1133 out[o + 1] = clamp_u8(ya + kgu * cu + kgv * cv);
1134 out[o + 2] = clamp_u8(ya + kb_c * cu);
1135 }
1136 }
1137 Ok(VideoFrame {
1138 width: src.width,
1139 height: src.height,
1140 format: PixelFormat::Rgb24,
1141 planes: vec![out],
1142 strides: vec![w * 3],
1143 pts: src.pts,
1144 })
1145}
1146
1147fn rgba_to_rgb(src: &VideoFrame) -> Result<VideoFrame> {
1149 let (w, h) = (src.width as usize, src.height as usize);
1150 let stride = src.strides[0];
1151 let rgba = &src.planes[0];
1152 let mut out = vec![0u8; w * h * 3];
1153 for j in 0..h {
1154 for i in 0..w {
1155 let s = j * stride + i * 4;
1156 let d = (j * w + i) * 3;
1157 out[d..d + 3].copy_from_slice(&rgba[s..s + 3]);
1158 }
1159 }
1160 Ok(VideoFrame {
1161 width: src.width,
1162 height: src.height,
1163 format: PixelFormat::Rgb24,
1164 planes: vec![out],
1165 strides: vec![w * 3],
1166 pts: src.pts,
1167 })
1168}
1169
1170fn map_plane(plane: &[u8], stride: usize, pw: usize, ph: usize, f: impl Fn(u8) -> u8) -> Vec<u8> {
1176 let mut out = vec![0u8; pw * ph];
1177 for y in 0..ph {
1178 let row = &plane[y * stride..y * stride + pw];
1179 let dst = &mut out[y * pw..(y + 1) * pw];
1180 for x in 0..pw {
1181 dst[x] = f(row[x]);
1182 }
1183 }
1184 out
1185}
1186
1187struct Negate;
1189
1190impl Filter for Negate {
1191 fn name(&self) -> &'static str {
1192 "negate"
1193 }
1194
1195 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
1196 ensure_planar_yuv(&src, "negate")?;
1197 let mut planes = Vec::with_capacity(src.planes.len());
1198 let mut strides = Vec::with_capacity(src.planes.len());
1199 for (i, plane) in src.planes.iter().enumerate() {
1200 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
1201 planes.push(map_plane(
1202 plane,
1203 src.strides[i],
1204 pw as usize,
1205 ph as usize,
1206 |v| 255 - v,
1207 ));
1208 strides.push(pw as usize);
1209 }
1210 Ok(VideoFrame {
1211 planes,
1212 strides,
1213 ..src
1214 })
1215 }
1216}
1217
1218struct SetRange {
1231 to_full: bool,
1232}
1233
1234impl SetRange {
1235 fn parse(args: &[&str]) -> Result<SetRange> {
1236 match args.first().copied().unwrap_or("").trim() {
1237 "full" | "pc" | "jpeg" => Ok(SetRange { to_full: true }),
1238 "limited" | "tv" | "mpeg" => Ok(SetRange { to_full: false }),
1239 other => Err(Error::unsupported(format!(
1240 "setrange: `{other}` (want full/pc/jpeg or limited/tv/mpeg)"
1241 ))),
1242 }
1243 }
1244
1245 fn lut(&self, chroma: bool) -> [u8; 256] {
1248 let (lo, span) = if chroma { (16.0, 224.0) } else { (16.0, 219.0) };
1249 let mut lut = [0u8; 256];
1250 for (v, out) in lut.iter_mut().enumerate() {
1251 let v = v as f32;
1252 let mapped = if self.to_full {
1253 (v - lo) * 255.0 / span
1254 } else {
1255 v * span / 255.0 + lo
1256 };
1257 *out = mapped.round().clamp(0.0, 255.0) as u8;
1258 }
1259 lut
1260 }
1261}
1262
1263impl Filter for SetRange {
1264 fn name(&self) -> &'static str {
1265 "setrange"
1266 }
1267
1268 fn output_color_range(&self) -> Option<ColorRange> {
1269 Some(if self.to_full {
1270 ColorRange::Full
1271 } else {
1272 ColorRange::Limited
1273 })
1274 }
1275
1276 fn filter(&mut self, mut frame: VideoFrame) -> Result<VideoFrame> {
1277 if ensure_planar_yuv(&frame, "setrange").is_err() {
1279 return Ok(frame);
1280 }
1281 let luma = self.lut(false);
1282 let chroma = self.lut(true);
1283 for (i, plane) in frame.planes.iter_mut().enumerate() {
1284 let lut = if i == 0 { &luma } else { &chroma };
1285 for b in plane.iter_mut() {
1286 *b = lut[*b as usize];
1287 }
1288 }
1289 Ok(frame)
1290 }
1291}
1292
1293struct Grayscale;
1294
1295impl Filter for Grayscale {
1296 fn name(&self) -> &'static str {
1297 "grayscale"
1298 }
1299
1300 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
1301 ensure_planar_yuv(&src, "grayscale")?;
1302 let mut planes = Vec::with_capacity(src.planes.len());
1303 let mut strides = Vec::with_capacity(src.planes.len());
1304 for (i, plane) in src.planes.iter().enumerate() {
1305 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
1306 let (pw, ph) = (pw as usize, ph as usize);
1307 if i == 0 {
1308 planes.push(map_plane(plane, src.strides[i], pw, ph, |v| v));
1309 } else {
1310 planes.push(vec![128u8; pw * ph]);
1311 }
1312 strides.push(pw);
1313 }
1314 Ok(VideoFrame {
1315 planes,
1316 strides,
1317 ..src
1318 })
1319 }
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324 use super::*;
1325
1326 fn frame(w: u32, h: u32, luma: impl Fn(u32, u32) -> u8) -> VideoFrame {
1328 let (wi, hi) = (w as usize, h as usize);
1329 let mut y = vec![0u8; wi * hi];
1330 for row in 0..h {
1331 for col in 0..w {
1332 y[row as usize * wi + col as usize] = luma(col, row);
1333 }
1334 }
1335 let chroma = vec![128u8; (wi / 2) * (hi / 2)];
1336 VideoFrame {
1337 width: w,
1338 height: h,
1339 format: PixelFormat::Yuv420p,
1340 planes: vec![y, chroma.clone(), chroma],
1341 strides: vec![wi, wi / 2, wi / 2],
1342 pts: Some(0),
1343 }
1344 }
1345
1346 #[test]
1347 fn scale_changes_dimensions_and_keeps_planes_consistent() {
1348 let mut chain = FilterChain::parse("scale=32:16").unwrap();
1349 assert_eq!(chain.output_dims(64, 64), (32, 16));
1350 let out = chain.apply(frame(64, 64, |x, _| x as u8)).unwrap();
1351 assert_eq!((out.width, out.height), (32, 16));
1352 assert_eq!(out.planes[0].len(), 32 * 16);
1354 assert_eq!(out.planes[1].len(), 16 * 8);
1355 assert_eq!(out.strides, vec![32, 16, 16]);
1356 }
1357
1358 #[test]
1359 fn scale_preserves_a_flat_field_exactly() {
1360 let mut chain = FilterChain::parse("scale=20:30").unwrap();
1362 let out = chain.apply(frame(64, 48, |_, _| 200)).unwrap();
1363 assert!(out.planes[0].iter().all(|&v| v == 200));
1364 }
1365
1366 #[test]
1367 fn crop_extracts_the_right_rectangle() {
1368 let mut chain = FilterChain::parse("crop=16:16:8:4").unwrap();
1370 assert_eq!(chain.output_dims(64, 64), (16, 16));
1371 let out = chain.apply(frame(64, 64, |x, _| x as u8)).unwrap();
1372 assert_eq!((out.width, out.height), (16, 16));
1373 assert_eq!(out.planes[0][0], 8); assert_eq!(out.planes[0][15], 23); }
1376
1377 #[test]
1378 fn crop_rejects_out_of_bounds_and_misalignment() {
1379 let mut oob = FilterChain::parse("crop=64:64:8:8").unwrap();
1380 assert!(oob.apply(frame(64, 64, |_, _| 0)).is_err());
1381 let mut odd = FilterChain::parse("crop=15:15:1:1").unwrap();
1382 assert!(odd.apply(frame(64, 64, |_, _| 0)).is_err());
1383 }
1384
1385 #[test]
1386 fn rejects_unknown_filter() {
1387 assert!(FilterChain::parse("frobnicate=1").is_err());
1388 }
1389
1390 #[test]
1391 fn negate_inverts_samples() {
1392 let mut chain = FilterChain::parse("negate").unwrap();
1393 let out = chain.apply(frame(4, 4, |x, _| x as u8)).unwrap();
1394 assert_eq!(out.planes[0][0], 255); assert_eq!(out.planes[0][3], 252); assert_eq!(out.planes[1][0], 127); }
1398
1399 #[test]
1400 fn grayscale_neutralizes_chroma() {
1401 let mut f = frame(8, 8, |x, _| x as u8);
1403 for c in [1, 2] {
1404 f.planes[c].iter_mut().for_each(|p| *p = 200);
1405 }
1406 let luma_before = f.planes[0].clone();
1407 let mut chain = FilterChain::parse("grayscale").unwrap();
1408 let out = chain.apply(f).unwrap();
1409 assert_eq!(out.planes[0], luma_before); assert!(out.planes[1].iter().all(|&v| v == 128));
1411 assert!(out.planes[2].iter().all(|&v| v == 128));
1412 }
1413
1414 #[test]
1415 fn scale_minus_one_preserves_aspect() {
1416 let chain = FilterChain::parse("scale=-1:32").unwrap();
1417 assert_eq!(chain.output_dims(64, 48), (42, 32));
1419 assert!(FilterChain::parse("scale=-1:-1").is_err());
1420 }
1421
1422 #[test]
1423 fn hflip_mirrors_columns() {
1424 let mut chain = FilterChain::parse("hflip").unwrap();
1425 let out = chain.apply(frame(8, 4, |x, _| x as u8)).unwrap();
1426 assert_eq!((out.width, out.height), (8, 4));
1427 assert_eq!(out.planes[0][0], 7); assert_eq!(out.planes[0][7], 0);
1429 }
1430
1431 #[test]
1432 fn vflip_mirrors_rows() {
1433 let mut chain = FilterChain::parse("vflip").unwrap();
1434 let out = chain.apply(frame(4, 8, |_, y| y as u8)).unwrap();
1435 assert_eq!(out.planes[0][0], 7); assert_eq!(out.planes[0][7 * 4], 0);
1437 }
1438
1439 #[test]
1440 fn transpose_clock_then_cclock_is_identity() {
1441 let original = frame(8, 4, |x, y| (x * 4 + y) as u8);
1444 let expected = original.planes[0].clone();
1445 let mut chain = FilterChain::parse("transpose=clock,transpose=cclock").unwrap();
1446 let out = chain.apply(original).unwrap();
1447 assert_eq!((out.width, out.height), (8, 4));
1448 assert_eq!(out.planes[0], expected);
1449 }
1450
1451 #[test]
1452 fn transpose_swaps_dimensions() {
1453 let chain = FilterChain::parse("transpose=1").unwrap();
1454 assert_eq!(chain.output_dims(96, 64), (64, 96));
1455 }
1456
1457 #[test]
1458 fn format_rgb_yuv_roundtrip_is_close() {
1459 let (w, h) = (32usize, 32usize);
1461 let mut rgb = vec![0u8; w * h * 3];
1462 for j in 0..h {
1463 for i in 0..w {
1464 let o = (j * w + i) * 3;
1465 rgb[o] = (i * 255 / (w - 1)) as u8; rgb[o + 1] = (j * 255 / (h - 1)) as u8; rgb[o + 2] = 96;
1468 }
1469 }
1470 let src = VideoFrame {
1471 width: w as u32,
1472 height: h as u32,
1473 format: PixelFormat::Rgb24,
1474 planes: vec![rgb.clone()],
1475 strides: vec![w * 3],
1476 pts: Some(0),
1477 };
1478
1479 let mut to_yuv = FilterChain::parse("format=yuv420p").unwrap();
1480 let yuv = to_yuv.apply(src).unwrap();
1481 assert_eq!(yuv.format, PixelFormat::Yuv420p);
1482 assert_eq!(yuv.planes.len(), 3);
1483
1484 let mut to_rgb = FilterChain::parse("format=rgb24").unwrap();
1485 let back = to_rgb.apply(yuv).unwrap();
1486 assert_eq!(back.format, PixelFormat::Rgb24);
1487
1488 let total: u64 = rgb
1489 .iter()
1490 .zip(&back.planes[0])
1491 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
1492 .sum();
1493 let mean = total as f64 / (w * h * 3) as f64;
1494 assert!(mean < 8.0, "rgb↔yuv round-trip drifted too far: {mean:.2}");
1495 }
1496
1497 #[test]
1498 fn colorspace_matrices_and_range() {
1499 let solid = |r: u8, g: u8, b: u8| VideoFrame {
1501 width: 2,
1502 height: 2,
1503 format: PixelFormat::Rgb24,
1504 planes: vec![[r, g, b].repeat(4)],
1505 strides: vec![6],
1506 pts: Some(0),
1507 };
1508 let to_yuv = |opts: &str, f: VideoFrame| {
1509 let mut c = FilterChain::parse(&format!("format=yuv420p{opts}")).unwrap();
1510 let y = c.apply(f).unwrap();
1511 (y.planes[0][0], y.planes[1][0], y.planes[2][0])
1512 };
1513 assert_eq!(to_yuv(":limited", solid(255, 255, 255)).0, 235);
1515 assert_eq!(to_yuv(":limited", solid(0, 0, 0)).0, 16);
1516 assert_eq!(to_yuv(":full", solid(255, 255, 255)).0, 255);
1517 assert_eq!(to_yuv(":full", solid(0, 0, 0)).0, 0);
1518 let y601 = to_yuv(":bt601:full", solid(255, 0, 0)).0;
1520 let y709 = to_yuv(":bt709:full", solid(255, 0, 0)).0;
1521 assert!(y601 > y709, "601 red-luma {y601} should exceed 709 {y709}");
1522 for m in [":full", ":limited", ":bt709:limited"] {
1524 let (_, u, v) = to_yuv(m, solid(128, 128, 128));
1525 assert_eq!((u, v), (128, 128), "grey chroma ({m})");
1526 }
1527 for m in [":bt709:limited", ":bt601:limited", ":bt709:full"] {
1529 let mut to_y = FilterChain::parse(&format!("format=yuv420p{m}")).unwrap();
1530 let mut to_r = FilterChain::parse(&format!("format=rgb24{m}")).unwrap();
1531 let src = solid(200, 100, 50);
1532 let back = to_r.apply(to_y.apply(src.clone()).unwrap()).unwrap();
1533 let drift: u32 = src.planes[0]
1534 .iter()
1535 .zip(&back.planes[0])
1536 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u32)
1537 .sum::<u32>()
1538 / 12;
1539 assert!(drift < 6, "round-trip drift for {m}: {drift}");
1540 }
1541 }
1542
1543 #[test]
1544 fn pad_centers_input_on_black_canvas() {
1545 let mut chain = FilterChain::parse("pad=16:16:4:4").unwrap();
1546 assert_eq!(chain.output_dims(8, 8), (16, 16));
1547 let out = chain.apply(frame(8, 8, |_, _| 200)).unwrap();
1548 assert_eq!((out.width, out.height), (16, 16));
1549 assert_eq!(out.planes[0][0], 0); assert_eq!(out.planes[0][4 * 16 + 4], 200); assert_eq!(out.planes[1][0], 128); assert!(chain.apply(frame(64, 64, |_, _| 0)).is_err()); }
1554
1555 #[test]
1556 fn filter_complex_parses_overlay_offset() {
1557 let fc = FilterComplex::parse("[0:v][1:v]overlay=16:8[out]").unwrap();
1558 assert_eq!(fc.overlay, Some((16, 8)));
1559 assert_eq!(
1561 FilterComplex::parse("overlay").unwrap().overlay,
1562 Some((0, 0))
1563 );
1564 assert!(FilterComplex::parse("[0][1]hstack").is_err());
1565 }
1566
1567 #[test]
1568 fn overlay_composites_at_offset() {
1569 let base = frame(32, 32, |_, _| 10);
1572 let over = frame(8, 8, |_, _| 200);
1573 let out = overlay(base, &over, 8, 4).unwrap();
1574 assert_eq!((out.width, out.height), (32, 32));
1575 assert_eq!(out.planes[0][4 * 32 + 8], 200); assert_eq!(out.planes[0][11 * 32 + 15], 200); assert_eq!(out.planes[0][0], 10); assert_eq!(out.planes[0][12 * 32 + 8], 10); }
1580
1581 #[test]
1582 fn overlay_clips_to_base_bounds() {
1583 let base = frame(16, 16, |_, _| 0);
1586 let over = frame(8, 8, |_, _| 99);
1587 let out = overlay(base, &over, 12, 12).unwrap();
1588 assert_eq!(out.planes[0][12 * 16 + 12], 99); assert_eq!(out.planes[0][15 * 16 + 15], 99);
1590 }
1591}
1592
1593#[cfg(test)]
1594mod setrange_tests {
1595 use super::*;
1596
1597 fn planar(y: Vec<u8>, u: Vec<u8>, v: Vec<u8>) -> VideoFrame {
1598 VideoFrame {
1599 width: 2,
1600 height: 2,
1601 format: PixelFormat::Yuv420p,
1602 planes: vec![y, u, v],
1603 strides: vec![2, 1, 1],
1604 pts: None,
1605 }
1606 }
1607
1608 #[test]
1609 fn limited_to_full_hits_the_anchors() {
1610 let mut f = SetRange { to_full: true };
1611 let out = f
1613 .filter(planar(vec![16, 235, 16, 235], vec![16], vec![240]))
1614 .unwrap();
1615 assert_eq!(out.planes[0], vec![0, 255, 0, 255]);
1616 assert_eq!(out.planes[1], vec![0]);
1618 assert_eq!(out.planes[2], vec![255]);
1619 }
1620
1621 #[test]
1622 fn full_to_limited_hits_the_anchors() {
1623 let mut f = SetRange { to_full: false };
1624 let out = f
1625 .filter(planar(vec![0, 255, 0, 255], vec![0], vec![255]))
1626 .unwrap();
1627 assert_eq!(out.planes[0], vec![16, 235, 16, 235]);
1628 assert_eq!(out.planes[1], vec![16]);
1629 assert_eq!(out.planes[2], vec![240]);
1630 }
1631
1632 #[test]
1633 fn out_of_range_values_clamp_not_wrap() {
1634 let mut f = SetRange { to_full: true };
1636 let out = f
1637 .filter(planar(vec![0, 255, 8, 240], vec![0], vec![255]))
1638 .unwrap();
1639 assert_eq!(out.planes[0][0], 0);
1640 assert_eq!(out.planes[0][1], 255);
1641 }
1642
1643 #[test]
1644 fn rgb_frames_pass_through_untouched() {
1645 let mut f = SetRange { to_full: true };
1646 let rgb = VideoFrame {
1647 width: 1,
1648 height: 1,
1649 format: PixelFormat::Rgb24,
1650 planes: vec![vec![10, 20, 30]],
1651 strides: vec![3],
1652 pts: None,
1653 };
1654 let out = f.filter(rgb).unwrap();
1655 assert_eq!(out.planes[0], vec![10, 20, 30]);
1656 }
1657
1658 #[test]
1659 fn reports_the_range_it_produces() {
1660 assert_eq!(
1661 SetRange { to_full: true }.output_color_range(),
1662 Some(ColorRange::Full)
1663 );
1664 assert_eq!(
1665 SetRange { to_full: false }.output_color_range(),
1666 Some(ColorRange::Limited)
1667 );
1668 }
1669
1670 #[test]
1671 fn format_conv_takes_the_source_range_unless_pinned() {
1672 let mut chain = FilterChain::parse("format=rgb24").unwrap();
1674 chain.set_source_color_range(ColorRange::Limited);
1675 let mut pinned = FilterChain::parse("format=rgb24:full").unwrap();
1677 pinned.set_source_color_range(ColorRange::Limited);
1678
1679 let frame = || VideoFrame {
1681 width: 2,
1682 height: 2,
1683 format: PixelFormat::Yuv420p,
1684 planes: vec![vec![16; 4], vec![128], vec![128]],
1685 strides: vec![2, 1, 1],
1686 pts: None,
1687 };
1688 let a = chain.apply(frame()).unwrap();
1689 let b = pinned.apply(frame()).unwrap();
1690 assert_eq!(a.planes[0][0], 0, "limited reading maps 16 -> 0");
1691 assert!(b.planes[0][0] > 0, "full reading leaves 16 above black");
1692 }
1693}