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) => Err(Error::unsupported(format!(
843 "format: {} → {} not supported",
844 from.name(),
845 to.name()
846 ))),
847 }
848 }
849}
850
851fn clamp_u8(v: f32) -> u8 {
852 v.round().clamp(0.0, 255.0) as u8
853}
854
855fn rgb_to_yuv420(src: &VideoFrame, bpp: usize, spec: ColorSpec) -> Result<VideoFrame> {
857 let (w, h) = (src.width as usize, src.height as usize);
858 let stride = src.strides[0];
859 let rgb = &src.planes[0];
860 let px = |i: usize, j: usize| {
861 let p = j * stride + i * bpp;
862 (rgb[p] as f32, rgb[p + 1] as f32, rgb[p + 2] as f32)
863 };
864
865 let (kr, kb, kg) = (spec.kr, spec.kb, spec.kg());
868 let (yoff, yscale, cscale) = if spec.limited {
869 (16.0, 219.0 / 255.0, 224.0 / 255.0)
870 } else {
871 (0.0, 1.0, 1.0)
872 };
873 let (ur, ug) = (-0.5 * kr / (1.0 - kb), -0.5 * kg / (1.0 - kb));
874 let (vg, vb) = (-0.5 * kg / (1.0 - kr), -0.5 * kb / (1.0 - kr));
875
876 let mut y = vec![0u8; w * h];
877 for j in 0..h {
878 for i in 0..w {
879 let (r, g, b) = px(i, j);
880 y[j * w + i] = clamp_u8(yoff + yscale * (kr * r + kg * g + kb * b));
881 }
882 }
883
884 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
885 let (mut u, mut v) = (vec![0u8; cw * ch], vec![0u8; cw * ch]);
886 for cj in 0..ch {
887 for ci in 0..cw {
888 let (mut sr, mut sg, mut sb, mut n) = (0.0, 0.0, 0.0, 0.0);
890 for dy in 0..2 {
891 for dx in 0..2 {
892 let (i, j) = (ci * 2 + dx, cj * 2 + dy);
893 if i < w && j < h {
894 let (r, g, b) = px(i, j);
895 sr += r;
896 sg += g;
897 sb += b;
898 n += 1.0;
899 }
900 }
901 }
902 let (r, g, b) = (sr / n, sg / n, sb / n);
903 u[cj * cw + ci] = clamp_u8(128.0 + cscale * (ur * r + ug * g + 0.5 * b));
904 v[cj * cw + ci] = clamp_u8(128.0 + cscale * (0.5 * r + vg * g + vb * b));
905 }
906 }
907
908 Ok(VideoFrame {
909 width: src.width,
910 height: src.height,
911 format: PixelFormat::Yuv420p,
912 planes: vec![y, u, v],
913 strides: vec![w, cw, cw],
914 pts: src.pts,
915 })
916}
917
918fn planar_subsampling(format: PixelFormat) -> Option<(usize, usize)> {
921 match format {
922 PixelFormat::Yuv444p => Some((1, 1)),
923 PixelFormat::Yuv422p => Some((2, 1)),
924 PixelFormat::Yuv420p => Some((2, 2)),
925 _ => None,
926 }
927}
928
929fn yuv_planar_to_rgb(src: &VideoFrame, spec: ColorSpec) -> Result<VideoFrame> {
935 let (sh, sv) = planar_subsampling(src.format).ok_or_else(|| {
936 Error::unsupported(format!(
937 "format: {} is not planar Y'CbCr",
938 src.format.name()
939 ))
940 })?;
941 let (w, h) = (src.width as usize, src.height as usize);
942 let (ys, us, vs) = (src.strides[0], src.strides[1], src.strides[2]);
943 let (yp, up, vp) = (&src.planes[0], &src.planes[1], &src.planes[2]);
944
945 let kg = spec.kg();
946 let (yoff, yscale, cscale) = if spec.limited {
947 (16.0, 255.0 / 219.0, 255.0 / 224.0)
948 } else {
949 (0.0, 1.0, 1.0)
950 };
951 let kr_c = 2.0 * (1.0 - spec.kr) * cscale;
952 let kb_c = 2.0 * (1.0 - spec.kb) * cscale;
953 let kgv = -2.0 * spec.kr * (1.0 - spec.kr) / kg * cscale;
954 let kgu = -2.0 * spec.kb * (1.0 - spec.kb) / kg * cscale;
955
956 let mut out = vec![0u8; w * h * 3];
957 for j in 0..h {
958 for i in 0..w {
959 let ya = yscale * (yp[j * ys + i] as f32 - yoff);
960 let cu = up[(j / sv) * us + i / sh] as f32 - 128.0;
961 let cv = vp[(j / sv) * vs + i / sh] as f32 - 128.0;
962 let o = (j * w + i) * 3;
963 out[o] = clamp_u8(ya + kr_c * cv);
964 out[o + 1] = clamp_u8(ya + kgu * cu + kgv * cv);
965 out[o + 2] = clamp_u8(ya + kb_c * cu);
966 }
967 }
968 Ok(VideoFrame {
969 width: src.width,
970 height: src.height,
971 format: PixelFormat::Rgb24,
972 planes: vec![out],
973 strides: vec![w * 3],
974 pts: src.pts,
975 })
976}
977
978fn yuv_planar_to_420(src: &VideoFrame) -> Result<VideoFrame> {
984 let (sh, sv) = planar_subsampling(src.format).ok_or_else(|| {
985 Error::unsupported(format!(
986 "format: {} is not planar Y'CbCr",
987 src.format.name()
988 ))
989 })?;
990 if (sh, sv) == (2, 2) {
991 return Ok(src.clone());
992 }
993 let (w, h) = (src.width as usize, src.height as usize);
994 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
995 let (scw, sch) = (w.div_ceil(sh), h.div_ceil(sv));
997
998 let ys = src.strides[0];
1000 let mut luma = Vec::with_capacity(w * h);
1001 for j in 0..h {
1002 luma.extend_from_slice(&src.planes[0][j * ys..j * ys + w]);
1003 }
1004
1005 let mut out_planes = vec![luma];
1006 for plane_idx in 1..3 {
1007 let stride = src.strides[plane_idx];
1008 let plane = &src.planes[plane_idx];
1009 let mut dst = vec![0u8; cw * ch];
1010 let (fx, fy) = (2 / sh, 2 / sv);
1012 for j in 0..ch {
1013 for i in 0..cw {
1014 let mut sum = 0u32;
1015 let mut n = 0u32;
1016 for dy in 0..fy.max(1) {
1017 let sy = (j * fy + dy).min(sch - 1);
1018 for dx in 0..fx.max(1) {
1019 let sx = (i * fx + dx).min(scw - 1);
1020 sum += plane[sy * stride + sx] as u32;
1021 n += 1;
1022 }
1023 }
1024 dst[j * cw + i] = ((sum + n / 2) / n) as u8;
1025 }
1026 }
1027 out_planes.push(dst);
1028 }
1029 Ok(VideoFrame {
1030 width: src.width,
1031 height: src.height,
1032 format: PixelFormat::Yuv420p,
1033 planes: out_planes,
1034 strides: vec![w, cw, cw],
1035 pts: src.pts,
1036 })
1037}
1038
1039fn yuv420_to_rgb(src: &VideoFrame, spec: ColorSpec) -> Result<VideoFrame> {
1040 let (w, h) = (src.width as usize, src.height as usize);
1041 let (ys, us, vs) = (src.strides[0], src.strides[1], src.strides[2]);
1042 let (yp, up, vp) = (&src.planes[0], &src.planes[1], &src.planes[2]);
1043
1044 let kg = spec.kg();
1047 let (yoff, yscale, cscale) = if spec.limited {
1048 (16.0, 255.0 / 219.0, 255.0 / 224.0)
1049 } else {
1050 (0.0, 1.0, 1.0)
1051 };
1052 let kr_c = 2.0 * (1.0 - spec.kr) * cscale;
1053 let kb_c = 2.0 * (1.0 - spec.kb) * cscale;
1054 let kgv = -2.0 * spec.kr * (1.0 - spec.kr) / kg * cscale;
1055 let kgu = -2.0 * spec.kb * (1.0 - spec.kb) / kg * cscale;
1056
1057 let mut out = vec![0u8; w * h * 3];
1058 for j in 0..h {
1059 for i in 0..w {
1060 let ya = yscale * (yp[j * ys + i] as f32 - yoff);
1061 let cu = up[(j / 2) * us + i / 2] as f32 - 128.0;
1062 let cv = vp[(j / 2) * vs + i / 2] as f32 - 128.0;
1063 let o = (j * w + i) * 3;
1064 out[o] = clamp_u8(ya + kr_c * cv);
1065 out[o + 1] = clamp_u8(ya + kgu * cu + kgv * cv);
1066 out[o + 2] = clamp_u8(ya + kb_c * cu);
1067 }
1068 }
1069 Ok(VideoFrame {
1070 width: src.width,
1071 height: src.height,
1072 format: PixelFormat::Rgb24,
1073 planes: vec![out],
1074 strides: vec![w * 3],
1075 pts: src.pts,
1076 })
1077}
1078
1079fn rgba_to_rgb(src: &VideoFrame) -> Result<VideoFrame> {
1081 let (w, h) = (src.width as usize, src.height as usize);
1082 let stride = src.strides[0];
1083 let rgba = &src.planes[0];
1084 let mut out = vec![0u8; w * h * 3];
1085 for j in 0..h {
1086 for i in 0..w {
1087 let s = j * stride + i * 4;
1088 let d = (j * w + i) * 3;
1089 out[d..d + 3].copy_from_slice(&rgba[s..s + 3]);
1090 }
1091 }
1092 Ok(VideoFrame {
1093 width: src.width,
1094 height: src.height,
1095 format: PixelFormat::Rgb24,
1096 planes: vec![out],
1097 strides: vec![w * 3],
1098 pts: src.pts,
1099 })
1100}
1101
1102fn map_plane(plane: &[u8], stride: usize, pw: usize, ph: usize, f: impl Fn(u8) -> u8) -> Vec<u8> {
1108 let mut out = vec![0u8; pw * ph];
1109 for y in 0..ph {
1110 let row = &plane[y * stride..y * stride + pw];
1111 let dst = &mut out[y * pw..(y + 1) * pw];
1112 for x in 0..pw {
1113 dst[x] = f(row[x]);
1114 }
1115 }
1116 out
1117}
1118
1119struct Negate;
1121
1122impl Filter for Negate {
1123 fn name(&self) -> &'static str {
1124 "negate"
1125 }
1126
1127 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
1128 ensure_planar_yuv(&src, "negate")?;
1129 let mut planes = Vec::with_capacity(src.planes.len());
1130 let mut strides = Vec::with_capacity(src.planes.len());
1131 for (i, plane) in src.planes.iter().enumerate() {
1132 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
1133 planes.push(map_plane(
1134 plane,
1135 src.strides[i],
1136 pw as usize,
1137 ph as usize,
1138 |v| 255 - v,
1139 ));
1140 strides.push(pw as usize);
1141 }
1142 Ok(VideoFrame {
1143 planes,
1144 strides,
1145 ..src
1146 })
1147 }
1148}
1149
1150struct SetRange {
1163 to_full: bool,
1164}
1165
1166impl SetRange {
1167 fn parse(args: &[&str]) -> Result<SetRange> {
1168 match args.first().copied().unwrap_or("").trim() {
1169 "full" | "pc" | "jpeg" => Ok(SetRange { to_full: true }),
1170 "limited" | "tv" | "mpeg" => Ok(SetRange { to_full: false }),
1171 other => Err(Error::unsupported(format!(
1172 "setrange: `{other}` (want full/pc/jpeg or limited/tv/mpeg)"
1173 ))),
1174 }
1175 }
1176
1177 fn lut(&self, chroma: bool) -> [u8; 256] {
1180 let (lo, span) = if chroma { (16.0, 224.0) } else { (16.0, 219.0) };
1181 let mut lut = [0u8; 256];
1182 for (v, out) in lut.iter_mut().enumerate() {
1183 let v = v as f32;
1184 let mapped = if self.to_full {
1185 (v - lo) * 255.0 / span
1186 } else {
1187 v * span / 255.0 + lo
1188 };
1189 *out = mapped.round().clamp(0.0, 255.0) as u8;
1190 }
1191 lut
1192 }
1193}
1194
1195impl Filter for SetRange {
1196 fn name(&self) -> &'static str {
1197 "setrange"
1198 }
1199
1200 fn output_color_range(&self) -> Option<ColorRange> {
1201 Some(if self.to_full {
1202 ColorRange::Full
1203 } else {
1204 ColorRange::Limited
1205 })
1206 }
1207
1208 fn filter(&mut self, mut frame: VideoFrame) -> Result<VideoFrame> {
1209 if ensure_planar_yuv(&frame, "setrange").is_err() {
1211 return Ok(frame);
1212 }
1213 let luma = self.lut(false);
1214 let chroma = self.lut(true);
1215 for (i, plane) in frame.planes.iter_mut().enumerate() {
1216 let lut = if i == 0 { &luma } else { &chroma };
1217 for b in plane.iter_mut() {
1218 *b = lut[*b as usize];
1219 }
1220 }
1221 Ok(frame)
1222 }
1223}
1224
1225struct Grayscale;
1226
1227impl Filter for Grayscale {
1228 fn name(&self) -> &'static str {
1229 "grayscale"
1230 }
1231
1232 fn filter(&mut self, src: VideoFrame) -> Result<VideoFrame> {
1233 ensure_planar_yuv(&src, "grayscale")?;
1234 let mut planes = Vec::with_capacity(src.planes.len());
1235 let mut strides = Vec::with_capacity(src.planes.len());
1236 for (i, plane) in src.planes.iter().enumerate() {
1237 let (pw, ph) = plane_dims(src.format, src.width, src.height, i)?;
1238 let (pw, ph) = (pw as usize, ph as usize);
1239 if i == 0 {
1240 planes.push(map_plane(plane, src.strides[i], pw, ph, |v| v));
1241 } else {
1242 planes.push(vec![128u8; pw * ph]);
1243 }
1244 strides.push(pw);
1245 }
1246 Ok(VideoFrame {
1247 planes,
1248 strides,
1249 ..src
1250 })
1251 }
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256 use super::*;
1257
1258 fn frame(w: u32, h: u32, luma: impl Fn(u32, u32) -> u8) -> VideoFrame {
1260 let (wi, hi) = (w as usize, h as usize);
1261 let mut y = vec![0u8; wi * hi];
1262 for row in 0..h {
1263 for col in 0..w {
1264 y[row as usize * wi + col as usize] = luma(col, row);
1265 }
1266 }
1267 let chroma = vec![128u8; (wi / 2) * (hi / 2)];
1268 VideoFrame {
1269 width: w,
1270 height: h,
1271 format: PixelFormat::Yuv420p,
1272 planes: vec![y, chroma.clone(), chroma],
1273 strides: vec![wi, wi / 2, wi / 2],
1274 pts: Some(0),
1275 }
1276 }
1277
1278 #[test]
1279 fn scale_changes_dimensions_and_keeps_planes_consistent() {
1280 let mut chain = FilterChain::parse("scale=32:16").unwrap();
1281 assert_eq!(chain.output_dims(64, 64), (32, 16));
1282 let out = chain.apply(frame(64, 64, |x, _| x as u8)).unwrap();
1283 assert_eq!((out.width, out.height), (32, 16));
1284 assert_eq!(out.planes[0].len(), 32 * 16);
1286 assert_eq!(out.planes[1].len(), 16 * 8);
1287 assert_eq!(out.strides, vec![32, 16, 16]);
1288 }
1289
1290 #[test]
1291 fn scale_preserves_a_flat_field_exactly() {
1292 let mut chain = FilterChain::parse("scale=20:30").unwrap();
1294 let out = chain.apply(frame(64, 48, |_, _| 200)).unwrap();
1295 assert!(out.planes[0].iter().all(|&v| v == 200));
1296 }
1297
1298 #[test]
1299 fn crop_extracts_the_right_rectangle() {
1300 let mut chain = FilterChain::parse("crop=16:16:8:4").unwrap();
1302 assert_eq!(chain.output_dims(64, 64), (16, 16));
1303 let out = chain.apply(frame(64, 64, |x, _| x as u8)).unwrap();
1304 assert_eq!((out.width, out.height), (16, 16));
1305 assert_eq!(out.planes[0][0], 8); assert_eq!(out.planes[0][15], 23); }
1308
1309 #[test]
1310 fn crop_rejects_out_of_bounds_and_misalignment() {
1311 let mut oob = FilterChain::parse("crop=64:64:8:8").unwrap();
1312 assert!(oob.apply(frame(64, 64, |_, _| 0)).is_err());
1313 let mut odd = FilterChain::parse("crop=15:15:1:1").unwrap();
1314 assert!(odd.apply(frame(64, 64, |_, _| 0)).is_err());
1315 }
1316
1317 #[test]
1318 fn rejects_unknown_filter() {
1319 assert!(FilterChain::parse("frobnicate=1").is_err());
1320 }
1321
1322 #[test]
1323 fn negate_inverts_samples() {
1324 let mut chain = FilterChain::parse("negate").unwrap();
1325 let out = chain.apply(frame(4, 4, |x, _| x as u8)).unwrap();
1326 assert_eq!(out.planes[0][0], 255); assert_eq!(out.planes[0][3], 252); assert_eq!(out.planes[1][0], 127); }
1330
1331 #[test]
1332 fn grayscale_neutralizes_chroma() {
1333 let mut f = frame(8, 8, |x, _| x as u8);
1335 for c in [1, 2] {
1336 f.planes[c].iter_mut().for_each(|p| *p = 200);
1337 }
1338 let luma_before = f.planes[0].clone();
1339 let mut chain = FilterChain::parse("grayscale").unwrap();
1340 let out = chain.apply(f).unwrap();
1341 assert_eq!(out.planes[0], luma_before); assert!(out.planes[1].iter().all(|&v| v == 128));
1343 assert!(out.planes[2].iter().all(|&v| v == 128));
1344 }
1345
1346 #[test]
1347 fn scale_minus_one_preserves_aspect() {
1348 let chain = FilterChain::parse("scale=-1:32").unwrap();
1349 assert_eq!(chain.output_dims(64, 48), (42, 32));
1351 assert!(FilterChain::parse("scale=-1:-1").is_err());
1352 }
1353
1354 #[test]
1355 fn hflip_mirrors_columns() {
1356 let mut chain = FilterChain::parse("hflip").unwrap();
1357 let out = chain.apply(frame(8, 4, |x, _| x as u8)).unwrap();
1358 assert_eq!((out.width, out.height), (8, 4));
1359 assert_eq!(out.planes[0][0], 7); assert_eq!(out.planes[0][7], 0);
1361 }
1362
1363 #[test]
1364 fn vflip_mirrors_rows() {
1365 let mut chain = FilterChain::parse("vflip").unwrap();
1366 let out = chain.apply(frame(4, 8, |_, y| y as u8)).unwrap();
1367 assert_eq!(out.planes[0][0], 7); assert_eq!(out.planes[0][7 * 4], 0);
1369 }
1370
1371 #[test]
1372 fn transpose_clock_then_cclock_is_identity() {
1373 let original = frame(8, 4, |x, y| (x * 4 + y) as u8);
1376 let expected = original.planes[0].clone();
1377 let mut chain = FilterChain::parse("transpose=clock,transpose=cclock").unwrap();
1378 let out = chain.apply(original).unwrap();
1379 assert_eq!((out.width, out.height), (8, 4));
1380 assert_eq!(out.planes[0], expected);
1381 }
1382
1383 #[test]
1384 fn transpose_swaps_dimensions() {
1385 let chain = FilterChain::parse("transpose=1").unwrap();
1386 assert_eq!(chain.output_dims(96, 64), (64, 96));
1387 }
1388
1389 #[test]
1390 fn format_rgb_yuv_roundtrip_is_close() {
1391 let (w, h) = (32usize, 32usize);
1393 let mut rgb = vec![0u8; w * h * 3];
1394 for j in 0..h {
1395 for i in 0..w {
1396 let o = (j * w + i) * 3;
1397 rgb[o] = (i * 255 / (w - 1)) as u8; rgb[o + 1] = (j * 255 / (h - 1)) as u8; rgb[o + 2] = 96;
1400 }
1401 }
1402 let src = VideoFrame {
1403 width: w as u32,
1404 height: h as u32,
1405 format: PixelFormat::Rgb24,
1406 planes: vec![rgb.clone()],
1407 strides: vec![w * 3],
1408 pts: Some(0),
1409 };
1410
1411 let mut to_yuv = FilterChain::parse("format=yuv420p").unwrap();
1412 let yuv = to_yuv.apply(src).unwrap();
1413 assert_eq!(yuv.format, PixelFormat::Yuv420p);
1414 assert_eq!(yuv.planes.len(), 3);
1415
1416 let mut to_rgb = FilterChain::parse("format=rgb24").unwrap();
1417 let back = to_rgb.apply(yuv).unwrap();
1418 assert_eq!(back.format, PixelFormat::Rgb24);
1419
1420 let total: u64 = rgb
1421 .iter()
1422 .zip(&back.planes[0])
1423 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
1424 .sum();
1425 let mean = total as f64 / (w * h * 3) as f64;
1426 assert!(mean < 8.0, "rgb↔yuv round-trip drifted too far: {mean:.2}");
1427 }
1428
1429 #[test]
1430 fn colorspace_matrices_and_range() {
1431 let solid = |r: u8, g: u8, b: u8| VideoFrame {
1433 width: 2,
1434 height: 2,
1435 format: PixelFormat::Rgb24,
1436 planes: vec![[r, g, b].repeat(4)],
1437 strides: vec![6],
1438 pts: Some(0),
1439 };
1440 let to_yuv = |opts: &str, f: VideoFrame| {
1441 let mut c = FilterChain::parse(&format!("format=yuv420p{opts}")).unwrap();
1442 let y = c.apply(f).unwrap();
1443 (y.planes[0][0], y.planes[1][0], y.planes[2][0])
1444 };
1445 assert_eq!(to_yuv(":limited", solid(255, 255, 255)).0, 235);
1447 assert_eq!(to_yuv(":limited", solid(0, 0, 0)).0, 16);
1448 assert_eq!(to_yuv(":full", solid(255, 255, 255)).0, 255);
1449 assert_eq!(to_yuv(":full", solid(0, 0, 0)).0, 0);
1450 let y601 = to_yuv(":bt601:full", solid(255, 0, 0)).0;
1452 let y709 = to_yuv(":bt709:full", solid(255, 0, 0)).0;
1453 assert!(y601 > y709, "601 red-luma {y601} should exceed 709 {y709}");
1454 for m in [":full", ":limited", ":bt709:limited"] {
1456 let (_, u, v) = to_yuv(m, solid(128, 128, 128));
1457 assert_eq!((u, v), (128, 128), "grey chroma ({m})");
1458 }
1459 for m in [":bt709:limited", ":bt601:limited", ":bt709:full"] {
1461 let mut to_y = FilterChain::parse(&format!("format=yuv420p{m}")).unwrap();
1462 let mut to_r = FilterChain::parse(&format!("format=rgb24{m}")).unwrap();
1463 let src = solid(200, 100, 50);
1464 let back = to_r.apply(to_y.apply(src.clone()).unwrap()).unwrap();
1465 let drift: u32 = src.planes[0]
1466 .iter()
1467 .zip(&back.planes[0])
1468 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u32)
1469 .sum::<u32>()
1470 / 12;
1471 assert!(drift < 6, "round-trip drift for {m}: {drift}");
1472 }
1473 }
1474
1475 #[test]
1476 fn pad_centers_input_on_black_canvas() {
1477 let mut chain = FilterChain::parse("pad=16:16:4:4").unwrap();
1478 assert_eq!(chain.output_dims(8, 8), (16, 16));
1479 let out = chain.apply(frame(8, 8, |_, _| 200)).unwrap();
1480 assert_eq!((out.width, out.height), (16, 16));
1481 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()); }
1486
1487 #[test]
1488 fn filter_complex_parses_overlay_offset() {
1489 let fc = FilterComplex::parse("[0:v][1:v]overlay=16:8[out]").unwrap();
1490 assert_eq!(fc.overlay, Some((16, 8)));
1491 assert_eq!(
1493 FilterComplex::parse("overlay").unwrap().overlay,
1494 Some((0, 0))
1495 );
1496 assert!(FilterComplex::parse("[0][1]hstack").is_err());
1497 }
1498
1499 #[test]
1500 fn overlay_composites_at_offset() {
1501 let base = frame(32, 32, |_, _| 10);
1504 let over = frame(8, 8, |_, _| 200);
1505 let out = overlay(base, &over, 8, 4).unwrap();
1506 assert_eq!((out.width, out.height), (32, 32));
1507 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); }
1512
1513 #[test]
1514 fn overlay_clips_to_base_bounds() {
1515 let base = frame(16, 16, |_, _| 0);
1518 let over = frame(8, 8, |_, _| 99);
1519 let out = overlay(base, &over, 12, 12).unwrap();
1520 assert_eq!(out.planes[0][12 * 16 + 12], 99); assert_eq!(out.planes[0][15 * 16 + 15], 99);
1522 }
1523}
1524
1525#[cfg(test)]
1526mod setrange_tests {
1527 use super::*;
1528
1529 fn planar(y: Vec<u8>, u: Vec<u8>, v: Vec<u8>) -> VideoFrame {
1530 VideoFrame {
1531 width: 2,
1532 height: 2,
1533 format: PixelFormat::Yuv420p,
1534 planes: vec![y, u, v],
1535 strides: vec![2, 1, 1],
1536 pts: None,
1537 }
1538 }
1539
1540 #[test]
1541 fn limited_to_full_hits_the_anchors() {
1542 let mut f = SetRange { to_full: true };
1543 let out = f
1545 .filter(planar(vec![16, 235, 16, 235], vec![16], vec![240]))
1546 .unwrap();
1547 assert_eq!(out.planes[0], vec![0, 255, 0, 255]);
1548 assert_eq!(out.planes[1], vec![0]);
1550 assert_eq!(out.planes[2], vec![255]);
1551 }
1552
1553 #[test]
1554 fn full_to_limited_hits_the_anchors() {
1555 let mut f = SetRange { to_full: false };
1556 let out = f
1557 .filter(planar(vec![0, 255, 0, 255], vec![0], vec![255]))
1558 .unwrap();
1559 assert_eq!(out.planes[0], vec![16, 235, 16, 235]);
1560 assert_eq!(out.planes[1], vec![16]);
1561 assert_eq!(out.planes[2], vec![240]);
1562 }
1563
1564 #[test]
1565 fn out_of_range_values_clamp_not_wrap() {
1566 let mut f = SetRange { to_full: true };
1568 let out = f
1569 .filter(planar(vec![0, 255, 8, 240], vec![0], vec![255]))
1570 .unwrap();
1571 assert_eq!(out.planes[0][0], 0);
1572 assert_eq!(out.planes[0][1], 255);
1573 }
1574
1575 #[test]
1576 fn rgb_frames_pass_through_untouched() {
1577 let mut f = SetRange { to_full: true };
1578 let rgb = VideoFrame {
1579 width: 1,
1580 height: 1,
1581 format: PixelFormat::Rgb24,
1582 planes: vec![vec![10, 20, 30]],
1583 strides: vec![3],
1584 pts: None,
1585 };
1586 let out = f.filter(rgb).unwrap();
1587 assert_eq!(out.planes[0], vec![10, 20, 30]);
1588 }
1589
1590 #[test]
1591 fn reports_the_range_it_produces() {
1592 assert_eq!(
1593 SetRange { to_full: true }.output_color_range(),
1594 Some(ColorRange::Full)
1595 );
1596 assert_eq!(
1597 SetRange { to_full: false }.output_color_range(),
1598 Some(ColorRange::Limited)
1599 );
1600 }
1601
1602 #[test]
1603 fn format_conv_takes_the_source_range_unless_pinned() {
1604 let mut chain = FilterChain::parse("format=rgb24").unwrap();
1606 chain.set_source_color_range(ColorRange::Limited);
1607 let mut pinned = FilterChain::parse("format=rgb24:full").unwrap();
1609 pinned.set_source_color_range(ColorRange::Limited);
1610
1611 let frame = || VideoFrame {
1613 width: 2,
1614 height: 2,
1615 format: PixelFormat::Yuv420p,
1616 planes: vec![vec![16; 4], vec![128], vec![128]],
1617 strides: vec![2, 1, 1],
1618 pts: None,
1619 };
1620 let a = chain.apply(frame()).unwrap();
1621 let b = pinned.apply(frame()).unwrap();
1622 assert_eq!(a.planes[0][0], 0, "limited reading maps 16 -> 0");
1623 assert!(b.planes[0][0] > 0, "full reading leaves 16 above black");
1624 }
1625}