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 { (w, h) } else { (w.div_ceil(sh), h.div_ceil(sv)) };
1012 let stride = src.strides.get(i).copied().unwrap_or(pw * 2);
1013 let mut out = Vec::with_capacity(pw * ph);
1014 for y in 0..ph {
1015 let row = plane.get(y * stride..y * stride + pw * 2).ok_or_else(|| {
1016 Error::invalid("narrow_to_8: plane shorter than its declared geometry")
1017 })?;
1018 out.extend(row.chunks_exact(2).map(|c| {
1019 let v = u16::from_le_bytes([c[0], c[1]]) as u32;
1020 ((v + round) >> shift).min(255) as u8
1021 }));
1022 }
1023 strides.push(pw);
1024 planes.push(out);
1025 }
1026 Ok(VideoFrame {
1027 width: src.width,
1028 height: src.height,
1029 format: target,
1030 planes,
1031 strides,
1032 pts: src.pts,
1033 })
1034}
1035
1036
1037fn yuv_planar_to_420(src: &VideoFrame) -> Result<VideoFrame> {
1043 if src.format.bit_depth() > 8 {
1044 return Err(Error::unsupported(format!(
1045 "format: {} must be narrowed to 8 bits first",
1046 src.format.name()
1047 )));
1048 }
1049 let (sh, sv) = planar_subsampling(src.format).ok_or_else(|| {
1050 Error::unsupported(format!(
1051 "format: {} is not planar Y'CbCr",
1052 src.format.name()
1053 ))
1054 })?;
1055 if (sh, sv) == (2, 2) {
1056 return Ok(src.clone());
1057 }
1058 let (w, h) = (src.width as usize, src.height as usize);
1059 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
1060 let (scw, sch) = (w.div_ceil(sh), h.div_ceil(sv));
1062
1063 let ys = src.strides[0];
1065 let mut luma = Vec::with_capacity(w * h);
1066 for j in 0..h {
1067 luma.extend_from_slice(&src.planes[0][j * ys..j * ys + w]);
1068 }
1069
1070 let mut out_planes = vec![luma];
1071 for plane_idx in 1..3 {
1072 let stride = src.strides[plane_idx];
1073 let plane = &src.planes[plane_idx];
1074 let mut dst = vec![0u8; cw * ch];
1075 let (fx, fy) = (2 / sh, 2 / sv);
1077 for j in 0..ch {
1078 for i in 0..cw {
1079 let mut sum = 0u32;
1080 let mut n = 0u32;
1081 for dy in 0..fy.max(1) {
1082 let sy = (j * fy + dy).min(sch - 1);
1083 for dx in 0..fx.max(1) {
1084 let sx = (i * fx + dx).min(scw - 1);
1085 sum += plane[sy * stride + sx] as u32;
1086 n += 1;
1087 }
1088 }
1089 dst[j * cw + i] = ((sum + n / 2) / n) as u8;
1090 }
1091 }
1092 out_planes.push(dst);
1093 }
1094 Ok(VideoFrame {
1095 width: src.width,
1096 height: src.height,
1097 format: PixelFormat::Yuv420p,
1098 planes: out_planes,
1099 strides: vec![w, cw, cw],
1100 pts: src.pts,
1101 })
1102}
1103
1104fn yuv420_to_rgb(src: &VideoFrame, spec: ColorSpec) -> Result<VideoFrame> {
1105 let (w, h) = (src.width as usize, src.height as usize);
1106 let (ys, us, vs) = (src.strides[0], src.strides[1], src.strides[2]);
1107 let (yp, up, vp) = (&src.planes[0], &src.planes[1], &src.planes[2]);
1108
1109 let kg = spec.kg();
1112 let (yoff, yscale, cscale) = if spec.limited {
1113 (16.0, 255.0 / 219.0, 255.0 / 224.0)
1114 } else {
1115 (0.0, 1.0, 1.0)
1116 };
1117 let kr_c = 2.0 * (1.0 - spec.kr) * cscale;
1118 let kb_c = 2.0 * (1.0 - spec.kb) * cscale;
1119 let kgv = -2.0 * spec.kr * (1.0 - spec.kr) / kg * cscale;
1120 let kgu = -2.0 * spec.kb * (1.0 - spec.kb) / kg * cscale;
1121
1122 let mut out = vec![0u8; w * h * 3];
1123 for j in 0..h {
1124 for i in 0..w {
1125 let ya = yscale * (yp[j * ys + i] as f32 - yoff);
1126 let cu = up[(j / 2) * us + i / 2] as f32 - 128.0;
1127 let cv = vp[(j / 2) * vs + i / 2] as f32 - 128.0;
1128 let o = (j * w + i) * 3;
1129 out[o] = clamp_u8(ya + kr_c * cv);
1130 out[o + 1] = clamp_u8(ya + kgu * cu + kgv * cv);
1131 out[o + 2] = clamp_u8(ya + kb_c * cu);
1132 }
1133 }
1134 Ok(VideoFrame {
1135 width: src.width,
1136 height: src.height,
1137 format: PixelFormat::Rgb24,
1138 planes: vec![out],
1139 strides: vec![w * 3],
1140 pts: src.pts,
1141 })
1142}
1143
1144fn rgba_to_rgb(src: &VideoFrame) -> Result<VideoFrame> {
1146 let (w, h) = (src.width as usize, src.height as usize);
1147 let stride = src.strides[0];
1148 let rgba = &src.planes[0];
1149 let mut out = vec![0u8; w * h * 3];
1150 for j in 0..h {
1151 for i in 0..w {
1152 let s = j * stride + i * 4;
1153 let d = (j * w + i) * 3;
1154 out[d..d + 3].copy_from_slice(&rgba[s..s + 3]);
1155 }
1156 }
1157 Ok(VideoFrame {
1158 width: src.width,
1159 height: src.height,
1160 format: PixelFormat::Rgb24,
1161 planes: vec![out],
1162 strides: vec![w * 3],
1163 pts: src.pts,
1164 })
1165}
1166
1167fn map_plane(plane: &[u8], stride: usize, pw: usize, ph: usize, f: impl Fn(u8) -> u8) -> Vec<u8> {
1173 let mut out = vec![0u8; pw * ph];
1174 for y in 0..ph {
1175 let row = &plane[y * stride..y * stride + pw];
1176 let dst = &mut out[y * pw..(y + 1) * pw];
1177 for x in 0..pw {
1178 dst[x] = f(row[x]);
1179 }
1180 }
1181 out
1182}
1183
1184struct Negate;
1186
1187impl Filter for Negate {
1188 fn name(&self) -> &'static str {
1189 "negate"
1190 }
1191
1192 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
1193 ensure_planar_yuv(&src, "negate")?;
1194 let mut planes = Vec::with_capacity(src.planes.len());
1195 let mut strides = Vec::with_capacity(src.planes.len());
1196 for (i, plane) in src.planes.iter().enumerate() {
1197 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
1198 planes.push(map_plane(
1199 plane,
1200 src.strides[i],
1201 pw as usize,
1202 ph as usize,
1203 |v| 255 - v,
1204 ));
1205 strides.push(pw as usize);
1206 }
1207 Ok(VideoFrame {
1208 planes,
1209 strides,
1210 ..src
1211 })
1212 }
1213}
1214
1215struct SetRange {
1228 to_full: bool,
1229}
1230
1231impl SetRange {
1232 fn parse(args: &[&str]) -> Result<SetRange> {
1233 match args.first().copied().unwrap_or("").trim() {
1234 "full" | "pc" | "jpeg" => Ok(SetRange { to_full: true }),
1235 "limited" | "tv" | "mpeg" => Ok(SetRange { to_full: false }),
1236 other => Err(Error::unsupported(format!(
1237 "setrange: `{other}` (want full/pc/jpeg or limited/tv/mpeg)"
1238 ))),
1239 }
1240 }
1241
1242 fn lut(&self, chroma: bool) -> [u8; 256] {
1245 let (lo, span) = if chroma { (16.0, 224.0) } else { (16.0, 219.0) };
1246 let mut lut = [0u8; 256];
1247 for (v, out) in lut.iter_mut().enumerate() {
1248 let v = v as f32;
1249 let mapped = if self.to_full {
1250 (v - lo) * 255.0 / span
1251 } else {
1252 v * span / 255.0 + lo
1253 };
1254 *out = mapped.round().clamp(0.0, 255.0) as u8;
1255 }
1256 lut
1257 }
1258}
1259
1260impl Filter for SetRange {
1261 fn name(&self) -> &'static str {
1262 "setrange"
1263 }
1264
1265 fn output_color_range(&self) -> Option<ColorRange> {
1266 Some(if self.to_full {
1267 ColorRange::Full
1268 } else {
1269 ColorRange::Limited
1270 })
1271 }
1272
1273 fn filter(&mut self, mut frame: VideoFrame) -> Result<VideoFrame> {
1274 if ensure_planar_yuv(&frame, "setrange").is_err() {
1276 return Ok(frame);
1277 }
1278 let luma = self.lut(false);
1279 let chroma = self.lut(true);
1280 for (i, plane) in frame.planes.iter_mut().enumerate() {
1281 let lut = if i == 0 { &luma } else { &chroma };
1282 for b in plane.iter_mut() {
1283 *b = lut[*b as usize];
1284 }
1285 }
1286 Ok(frame)
1287 }
1288}
1289
1290struct Grayscale;
1291
1292impl Filter for Grayscale {
1293 fn name(&self) -> &'static str {
1294 "grayscale"
1295 }
1296
1297 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
1298 ensure_planar_yuv(&src, "grayscale")?;
1299 let mut planes = Vec::with_capacity(src.planes.len());
1300 let mut strides = Vec::with_capacity(src.planes.len());
1301 for (i, plane) in src.planes.iter().enumerate() {
1302 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
1303 let (pw, ph) = (pw as usize, ph as usize);
1304 if i == 0 {
1305 planes.push(map_plane(plane, src.strides[i], pw, ph, |v| v));
1306 } else {
1307 planes.push(vec![128u8; pw * ph]);
1308 }
1309 strides.push(pw);
1310 }
1311 Ok(VideoFrame {
1312 planes,
1313 strides,
1314 ..src
1315 })
1316 }
1317}
1318
1319#[cfg(test)]
1320mod tests {
1321 use super::*;
1322
1323 fn frame(w: u32, h: u32, luma: impl Fn(u32, u32) -> u8) -> VideoFrame {
1325 let (wi, hi) = (w as usize, h as usize);
1326 let mut y = vec![0u8; wi * hi];
1327 for row in 0..h {
1328 for col in 0..w {
1329 y[row as usize * wi + col as usize] = luma(col, row);
1330 }
1331 }
1332 let chroma = vec![128u8; (wi / 2) * (hi / 2)];
1333 VideoFrame {
1334 width: w,
1335 height: h,
1336 format: PixelFormat::Yuv420p,
1337 planes: vec![y, chroma.clone(), chroma],
1338 strides: vec![wi, wi / 2, wi / 2],
1339 pts: Some(0),
1340 }
1341 }
1342
1343 #[test]
1344 fn scale_changes_dimensions_and_keeps_planes_consistent() {
1345 let mut chain = FilterChain::parse("scale=32:16").unwrap();
1346 assert_eq!(chain.output_dims(64, 64), (32, 16));
1347 let out = chain.apply(frame(64, 64, |x, _| x as u8)).unwrap();
1348 assert_eq!((out.width, out.height), (32, 16));
1349 assert_eq!(out.planes[0].len(), 32 * 16);
1351 assert_eq!(out.planes[1].len(), 16 * 8);
1352 assert_eq!(out.strides, vec![32, 16, 16]);
1353 }
1354
1355 #[test]
1356 fn scale_preserves_a_flat_field_exactly() {
1357 let mut chain = FilterChain::parse("scale=20:30").unwrap();
1359 let out = chain.apply(frame(64, 48, |_, _| 200)).unwrap();
1360 assert!(out.planes[0].iter().all(|&v| v == 200));
1361 }
1362
1363 #[test]
1364 fn crop_extracts_the_right_rectangle() {
1365 let mut chain = FilterChain::parse("crop=16:16:8:4").unwrap();
1367 assert_eq!(chain.output_dims(64, 64), (16, 16));
1368 let out = chain.apply(frame(64, 64, |x, _| x as u8)).unwrap();
1369 assert_eq!((out.width, out.height), (16, 16));
1370 assert_eq!(out.planes[0][0], 8); assert_eq!(out.planes[0][15], 23); }
1373
1374 #[test]
1375 fn crop_rejects_out_of_bounds_and_misalignment() {
1376 let mut oob = FilterChain::parse("crop=64:64:8:8").unwrap();
1377 assert!(oob.apply(frame(64, 64, |_, _| 0)).is_err());
1378 let mut odd = FilterChain::parse("crop=15:15:1:1").unwrap();
1379 assert!(odd.apply(frame(64, 64, |_, _| 0)).is_err());
1380 }
1381
1382 #[test]
1383 fn rejects_unknown_filter() {
1384 assert!(FilterChain::parse("frobnicate=1").is_err());
1385 }
1386
1387 #[test]
1388 fn negate_inverts_samples() {
1389 let mut chain = FilterChain::parse("negate").unwrap();
1390 let out = chain.apply(frame(4, 4, |x, _| x as u8)).unwrap();
1391 assert_eq!(out.planes[0][0], 255); assert_eq!(out.planes[0][3], 252); assert_eq!(out.planes[1][0], 127); }
1395
1396 #[test]
1397 fn grayscale_neutralizes_chroma() {
1398 let mut f = frame(8, 8, |x, _| x as u8);
1400 for c in [1, 2] {
1401 f.planes[c].iter_mut().for_each(|p| *p = 200);
1402 }
1403 let luma_before = f.planes[0].clone();
1404 let mut chain = FilterChain::parse("grayscale").unwrap();
1405 let out = chain.apply(f).unwrap();
1406 assert_eq!(out.planes[0], luma_before); assert!(out.planes[1].iter().all(|&v| v == 128));
1408 assert!(out.planes[2].iter().all(|&v| v == 128));
1409 }
1410
1411 #[test]
1412 fn scale_minus_one_preserves_aspect() {
1413 let chain = FilterChain::parse("scale=-1:32").unwrap();
1414 assert_eq!(chain.output_dims(64, 48), (42, 32));
1416 assert!(FilterChain::parse("scale=-1:-1").is_err());
1417 }
1418
1419 #[test]
1420 fn hflip_mirrors_columns() {
1421 let mut chain = FilterChain::parse("hflip").unwrap();
1422 let out = chain.apply(frame(8, 4, |x, _| x as u8)).unwrap();
1423 assert_eq!((out.width, out.height), (8, 4));
1424 assert_eq!(out.planes[0][0], 7); assert_eq!(out.planes[0][7], 0);
1426 }
1427
1428 #[test]
1429 fn vflip_mirrors_rows() {
1430 let mut chain = FilterChain::parse("vflip").unwrap();
1431 let out = chain.apply(frame(4, 8, |_, y| y as u8)).unwrap();
1432 assert_eq!(out.planes[0][0], 7); assert_eq!(out.planes[0][7 * 4], 0);
1434 }
1435
1436 #[test]
1437 fn transpose_clock_then_cclock_is_identity() {
1438 let original = frame(8, 4, |x, y| (x * 4 + y) as u8);
1441 let expected = original.planes[0].clone();
1442 let mut chain = FilterChain::parse("transpose=clock,transpose=cclock").unwrap();
1443 let out = chain.apply(original).unwrap();
1444 assert_eq!((out.width, out.height), (8, 4));
1445 assert_eq!(out.planes[0], expected);
1446 }
1447
1448 #[test]
1449 fn transpose_swaps_dimensions() {
1450 let chain = FilterChain::parse("transpose=1").unwrap();
1451 assert_eq!(chain.output_dims(96, 64), (64, 96));
1452 }
1453
1454 #[test]
1455 fn format_rgb_yuv_roundtrip_is_close() {
1456 let (w, h) = (32usize, 32usize);
1458 let mut rgb = vec![0u8; w * h * 3];
1459 for j in 0..h {
1460 for i in 0..w {
1461 let o = (j * w + i) * 3;
1462 rgb[o] = (i * 255 / (w - 1)) as u8; rgb[o + 1] = (j * 255 / (h - 1)) as u8; rgb[o + 2] = 96;
1465 }
1466 }
1467 let src = VideoFrame {
1468 width: w as u32,
1469 height: h as u32,
1470 format: PixelFormat::Rgb24,
1471 planes: vec![rgb.clone()],
1472 strides: vec![w * 3],
1473 pts: Some(0),
1474 };
1475
1476 let mut to_yuv = FilterChain::parse("format=yuv420p").unwrap();
1477 let yuv = to_yuv.apply(src).unwrap();
1478 assert_eq!(yuv.format, PixelFormat::Yuv420p);
1479 assert_eq!(yuv.planes.len(), 3);
1480
1481 let mut to_rgb = FilterChain::parse("format=rgb24").unwrap();
1482 let back = to_rgb.apply(yuv).unwrap();
1483 assert_eq!(back.format, PixelFormat::Rgb24);
1484
1485 let total: u64 = rgb
1486 .iter()
1487 .zip(&back.planes[0])
1488 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
1489 .sum();
1490 let mean = total as f64 / (w * h * 3) as f64;
1491 assert!(mean < 8.0, "rgb↔yuv round-trip drifted too far: {mean:.2}");
1492 }
1493
1494 #[test]
1495 fn colorspace_matrices_and_range() {
1496 let solid = |r: u8, g: u8, b: u8| VideoFrame {
1498 width: 2,
1499 height: 2,
1500 format: PixelFormat::Rgb24,
1501 planes: vec![[r, g, b].repeat(4)],
1502 strides: vec![6],
1503 pts: Some(0),
1504 };
1505 let to_yuv = |opts: &str, f: VideoFrame| {
1506 let mut c = FilterChain::parse(&format!("format=yuv420p{opts}")).unwrap();
1507 let y = c.apply(f).unwrap();
1508 (y.planes[0][0], y.planes[1][0], y.planes[2][0])
1509 };
1510 assert_eq!(to_yuv(":limited", solid(255, 255, 255)).0, 235);
1512 assert_eq!(to_yuv(":limited", solid(0, 0, 0)).0, 16);
1513 assert_eq!(to_yuv(":full", solid(255, 255, 255)).0, 255);
1514 assert_eq!(to_yuv(":full", solid(0, 0, 0)).0, 0);
1515 let y601 = to_yuv(":bt601:full", solid(255, 0, 0)).0;
1517 let y709 = to_yuv(":bt709:full", solid(255, 0, 0)).0;
1518 assert!(y601 > y709, "601 red-luma {y601} should exceed 709 {y709}");
1519 for m in [":full", ":limited", ":bt709:limited"] {
1521 let (_, u, v) = to_yuv(m, solid(128, 128, 128));
1522 assert_eq!((u, v), (128, 128), "grey chroma ({m})");
1523 }
1524 for m in [":bt709:limited", ":bt601:limited", ":bt709:full"] {
1526 let mut to_y = FilterChain::parse(&format!("format=yuv420p{m}")).unwrap();
1527 let mut to_r = FilterChain::parse(&format!("format=rgb24{m}")).unwrap();
1528 let src = solid(200, 100, 50);
1529 let back = to_r.apply(to_y.apply(src.clone()).unwrap()).unwrap();
1530 let drift: u32 = src.planes[0]
1531 .iter()
1532 .zip(&back.planes[0])
1533 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u32)
1534 .sum::<u32>()
1535 / 12;
1536 assert!(drift < 6, "round-trip drift for {m}: {drift}");
1537 }
1538 }
1539
1540 #[test]
1541 fn pad_centers_input_on_black_canvas() {
1542 let mut chain = FilterChain::parse("pad=16:16:4:4").unwrap();
1543 assert_eq!(chain.output_dims(8, 8), (16, 16));
1544 let out = chain.apply(frame(8, 8, |_, _| 200)).unwrap();
1545 assert_eq!((out.width, out.height), (16, 16));
1546 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()); }
1551
1552 #[test]
1553 fn filter_complex_parses_overlay_offset() {
1554 let fc = FilterComplex::parse("[0:v][1:v]overlay=16:8[out]").unwrap();
1555 assert_eq!(fc.overlay, Some((16, 8)));
1556 assert_eq!(
1558 FilterComplex::parse("overlay").unwrap().overlay,
1559 Some((0, 0))
1560 );
1561 assert!(FilterComplex::parse("[0][1]hstack").is_err());
1562 }
1563
1564 #[test]
1565 fn overlay_composites_at_offset() {
1566 let base = frame(32, 32, |_, _| 10);
1569 let over = frame(8, 8, |_, _| 200);
1570 let out = overlay(base, &over, 8, 4).unwrap();
1571 assert_eq!((out.width, out.height), (32, 32));
1572 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); }
1577
1578 #[test]
1579 fn overlay_clips_to_base_bounds() {
1580 let base = frame(16, 16, |_, _| 0);
1583 let over = frame(8, 8, |_, _| 99);
1584 let out = overlay(base, &over, 12, 12).unwrap();
1585 assert_eq!(out.planes[0][12 * 16 + 12], 99); assert_eq!(out.planes[0][15 * 16 + 15], 99);
1587 }
1588}
1589
1590#[cfg(test)]
1591mod setrange_tests {
1592 use super::*;
1593
1594 fn planar(y: Vec<u8>, u: Vec<u8>, v: Vec<u8>) -> VideoFrame {
1595 VideoFrame {
1596 width: 2,
1597 height: 2,
1598 format: PixelFormat::Yuv420p,
1599 planes: vec![y, u, v],
1600 strides: vec![2, 1, 1],
1601 pts: None,
1602 }
1603 }
1604
1605 #[test]
1606 fn limited_to_full_hits_the_anchors() {
1607 let mut f = SetRange { to_full: true };
1608 let out = f
1610 .filter(planar(vec![16, 235, 16, 235], vec![16], vec![240]))
1611 .unwrap();
1612 assert_eq!(out.planes[0], vec![0, 255, 0, 255]);
1613 assert_eq!(out.planes[1], vec![0]);
1615 assert_eq!(out.planes[2], vec![255]);
1616 }
1617
1618 #[test]
1619 fn full_to_limited_hits_the_anchors() {
1620 let mut f = SetRange { to_full: false };
1621 let out = f
1622 .filter(planar(vec![0, 255, 0, 255], vec![0], vec![255]))
1623 .unwrap();
1624 assert_eq!(out.planes[0], vec![16, 235, 16, 235]);
1625 assert_eq!(out.planes[1], vec![16]);
1626 assert_eq!(out.planes[2], vec![240]);
1627 }
1628
1629 #[test]
1630 fn out_of_range_values_clamp_not_wrap() {
1631 let mut f = SetRange { to_full: true };
1633 let out = f
1634 .filter(planar(vec![0, 255, 8, 240], vec![0], vec![255]))
1635 .unwrap();
1636 assert_eq!(out.planes[0][0], 0);
1637 assert_eq!(out.planes[0][1], 255);
1638 }
1639
1640 #[test]
1641 fn rgb_frames_pass_through_untouched() {
1642 let mut f = SetRange { to_full: true };
1643 let rgb = VideoFrame {
1644 width: 1,
1645 height: 1,
1646 format: PixelFormat::Rgb24,
1647 planes: vec![vec![10, 20, 30]],
1648 strides: vec![3],
1649 pts: None,
1650 };
1651 let out = f.filter(rgb).unwrap();
1652 assert_eq!(out.planes[0], vec![10, 20, 30]);
1653 }
1654
1655 #[test]
1656 fn reports_the_range_it_produces() {
1657 assert_eq!(
1658 SetRange { to_full: true }.output_color_range(),
1659 Some(ColorRange::Full)
1660 );
1661 assert_eq!(
1662 SetRange { to_full: false }.output_color_range(),
1663 Some(ColorRange::Limited)
1664 );
1665 }
1666
1667 #[test]
1668 fn format_conv_takes_the_source_range_unless_pinned() {
1669 let mut chain = FilterChain::parse("format=rgb24").unwrap();
1671 chain.set_source_color_range(ColorRange::Limited);
1672 let mut pinned = FilterChain::parse("format=rgb24:full").unwrap();
1674 pinned.set_source_color_range(ColorRange::Limited);
1675
1676 let frame = || VideoFrame {
1678 width: 2,
1679 height: 2,
1680 format: PixelFormat::Yuv420p,
1681 planes: vec![vec![16; 4], vec![128], vec![128]],
1682 strides: vec![2, 1, 1],
1683 pts: None,
1684 };
1685 let a = chain.apply(frame()).unwrap();
1686 let b = pinned.apply(frame()).unwrap();
1687 assert_eq!(a.planes[0][0], 0, "limited reading maps 16 -> 0");
1688 assert!(b.planes[0][0] > 0, "full reading leaves 16 above black");
1689 }
1690}