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