1#![forbid(unsafe_code)]
7#![allow(clippy::cast_lossless)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::cast_possible_truncation)]
10#![allow(clippy::cast_sign_loss)]
11#![allow(clippy::cast_possible_wrap)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::many_single_char_names)]
14#![allow(clippy::missing_errors_doc)]
15#![allow(clippy::match_same_arms)]
16#![allow(clippy::doc_markdown)]
17#![allow(clippy::unused_self)]
18#![allow(clippy::unnecessary_cast)]
19#![allow(clippy::bool_to_int_with_if)]
20#![allow(clippy::needless_range_loop)]
21#![allow(clippy::too_many_lines)]
22#![allow(clippy::unnecessary_wraps)]
23#![allow(clippy::map_unwrap_or)]
24#![allow(clippy::no_effect_underscore_binding)]
25#![allow(clippy::unreadable_literal)]
26#![allow(dead_code)]
27
28use crate::error::{GraphError, GraphResult};
29use crate::frame::FilterFrame;
30use crate::node::{Node, NodeId, NodeState, NodeType};
31use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
32use oximedia_codec::{Plane, VideoFrame};
33use oximedia_core::PixelFormat;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
37pub enum BlendMode {
38 #[default]
40 Normal,
41 Add,
43 Multiply,
45 Screen,
47 Overlay,
49 Darken,
51 Lighten,
53 Difference,
55 Exclusion,
57}
58
59impl BlendMode {
60 #[must_use]
64 pub fn blend(&self, base: f64, overlay: f64) -> f64 {
65 match self {
66 Self::Normal => overlay,
67 Self::Add => (base + overlay).min(1.0),
68 Self::Multiply => base * overlay,
69 Self::Screen => 1.0 - (1.0 - base) * (1.0 - overlay),
70 Self::Overlay => {
71 if base < 0.5 {
72 2.0 * base * overlay
73 } else {
74 1.0 - 2.0 * (1.0 - base) * (1.0 - overlay)
75 }
76 }
77 Self::Darken => base.min(overlay),
78 Self::Lighten => base.max(overlay),
79 Self::Difference => (base - overlay).abs(),
80 Self::Exclusion => base + overlay - 2.0 * base * overlay,
81 }
82 }
83
84 #[must_use]
86 pub fn blend_with_alpha(&self, base: f64, overlay: f64, alpha: f64) -> f64 {
87 let blended = self.blend(base, overlay);
88 base * (1.0 - alpha) + blended * alpha
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
94pub enum Alignment {
95 #[default]
97 TopLeft,
98 TopCenter,
100 TopRight,
102 CenterLeft,
104 Center,
106 CenterRight,
108 BottomLeft,
110 BottomCenter,
112 BottomRight,
114}
115
116impl Alignment {
117 #[must_use]
119 pub fn offset(
120 &self,
121 container_w: u32,
122 container_h: u32,
123 content_w: u32,
124 content_h: u32,
125 ) -> (i32, i32) {
126 let h_offset = match self {
127 Self::TopLeft | Self::CenterLeft | Self::BottomLeft => 0,
128 Self::TopCenter | Self::Center | Self::BottomCenter => {
129 (container_w.saturating_sub(content_w) / 2) as i32
130 }
131 Self::TopRight | Self::CenterRight | Self::BottomRight => {
132 container_w.saturating_sub(content_w) as i32
133 }
134 };
135
136 let v_offset = match self {
137 Self::TopLeft | Self::TopCenter | Self::TopRight => 0,
138 Self::CenterLeft | Self::Center | Self::CenterRight => {
139 (container_h.saturating_sub(content_h) / 2) as i32
140 }
141 Self::BottomLeft | Self::BottomCenter | Self::BottomRight => {
142 container_h.saturating_sub(content_h) as i32
143 }
144 };
145
146 (h_offset, v_offset)
147 }
148}
149
150#[derive(Clone, Debug)]
152pub struct OverlayConfig {
153 pub x: i32,
155 pub y: i32,
157 pub alignment: Alignment,
159 pub blend_mode: BlendMode,
161 pub alpha: f64,
163 pub use_alpha_channel: bool,
165 pub premultiplied_alpha: bool,
167}
168
169impl Default for OverlayConfig {
170 fn default() -> Self {
171 Self {
172 x: 0,
173 y: 0,
174 alignment: Alignment::default(),
175 blend_mode: BlendMode::default(),
176 alpha: 1.0,
177 use_alpha_channel: true,
178 premultiplied_alpha: false,
179 }
180 }
181}
182
183impl OverlayConfig {
184 #[must_use]
186 pub fn new(x: i32, y: i32) -> Self {
187 Self {
188 x,
189 y,
190 ..Default::default()
191 }
192 }
193
194 #[must_use]
196 pub fn centered() -> Self {
197 Self {
198 alignment: Alignment::Center,
199 ..Default::default()
200 }
201 }
202
203 #[must_use]
205 pub fn with_position(mut self, x: i32, y: i32) -> Self {
206 self.x = x;
207 self.y = y;
208 self
209 }
210
211 #[must_use]
213 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
214 self.alignment = alignment;
215 self
216 }
217
218 #[must_use]
220 pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
221 self.blend_mode = mode;
222 self
223 }
224
225 #[must_use]
227 pub fn with_alpha(mut self, alpha: f64) -> Self {
228 self.alpha = alpha.clamp(0.0, 1.0);
229 self
230 }
231
232 #[must_use]
234 pub fn with_use_alpha_channel(mut self, use_alpha: bool) -> Self {
235 self.use_alpha_channel = use_alpha;
236 self
237 }
238
239 #[must_use]
241 pub fn calculate_position(
242 &self,
243 base_w: u32,
244 base_h: u32,
245 overlay_w: u32,
246 overlay_h: u32,
247 ) -> (i32, i32) {
248 let (align_x, align_y) = self.alignment.offset(base_w, base_h, overlay_w, overlay_h);
249 (align_x + self.x, align_y + self.y)
250 }
251}
252
253pub struct OverlayFilter {
272 id: NodeId,
273 name: String,
274 state: NodeState,
275 inputs: Vec<InputPort>,
276 outputs: Vec<OutputPort>,
277 config: OverlayConfig,
278 base_frame: Option<VideoFrame>,
280 overlay_frame: Option<VideoFrame>,
282}
283
284impl OverlayFilter {
285 #[must_use]
287 pub fn new(id: NodeId, name: impl Into<String>, config: OverlayConfig) -> Self {
288 Self {
289 id,
290 name: name.into(),
291 state: NodeState::Idle,
292 inputs: vec![
293 InputPort::new(PortId(0), "base", PortType::Video)
294 .with_format(PortFormat::Video(VideoPortFormat::any())),
295 InputPort::new(PortId(1), "overlay", PortType::Video)
296 .with_format(PortFormat::Video(VideoPortFormat::any())),
297 ],
298 outputs: vec![OutputPort::new(PortId(0), "output", PortType::Video)
299 .with_format(PortFormat::Video(VideoPortFormat::any()))],
300 config,
301 base_frame: None,
302 overlay_frame: None,
303 }
304 }
305
306 #[must_use]
308 pub fn config(&self) -> &OverlayConfig {
309 &self.config
310 }
311
312 pub fn set_config(&mut self, config: OverlayConfig) {
314 self.config = config;
315 }
316
317 pub fn set_base_frame(&mut self, frame: VideoFrame) {
319 self.base_frame = Some(frame);
320 }
321
322 pub fn set_overlay_frame(&mut self, frame: VideoFrame) {
324 self.overlay_frame = Some(frame);
325 }
326
327 fn composite(&self, base: &VideoFrame, overlay: &VideoFrame) -> VideoFrame {
329 let mut output = base.clone();
330
331 let (pos_x, pos_y) =
332 self.config
333 .calculate_position(base.width, base.height, overlay.width, overlay.height);
334
335 if base.format.is_yuv() && overlay.format.is_yuv() {
337 self.composite_yuv(&mut output, overlay, pos_x, pos_y);
338 } else {
339 self.composite_rgb(&mut output, overlay, pos_x, pos_y);
341 }
342
343 output
344 }
345
346 fn composite_yuv(&self, output: &mut VideoFrame, overlay: &VideoFrame, pos_x: i32, pos_y: i32) {
348 let format = output.format;
349 let (h_sub, v_sub) = format.chroma_subsampling();
350
351 let plane_infos: Vec<_> = (0..output.planes.len().min(overlay.planes.len()))
353 .map(|plane_idx| {
354 let (base_w, base_h) = if plane_idx == 0 {
355 (output.width, output.height)
356 } else {
357 (output.width / h_sub, output.height / v_sub)
358 };
359 let (overlay_w, overlay_h) = if plane_idx == 0 {
360 (overlay.width, overlay.height)
361 } else {
362 (overlay.width / h_sub, overlay.height / v_sub)
363 };
364 let (scale_x, scale_y) = if plane_idx > 0 {
365 (h_sub as i32, v_sub as i32)
366 } else {
367 (1, 1)
368 };
369 (base_w, base_h, overlay_w, overlay_h, scale_x, scale_y)
370 })
371 .collect();
372
373 for (plane_idx, (base_plane, overlay_plane)) in output
374 .planes
375 .iter_mut()
376 .zip(overlay.planes.iter())
377 .enumerate()
378 {
379 let (base_w, base_h, overlay_w, overlay_h, scale_x, scale_y) = plane_infos[plane_idx];
380
381 let plane_pos_x = pos_x / scale_x;
382 let plane_pos_y = pos_y / scale_y;
383
384 let mut new_data = base_plane.data.to_vec();
386
387 for oy in 0..overlay_h as i32 {
388 let by = plane_pos_y + oy;
389 if by < 0 || by >= base_h as i32 {
390 continue;
391 }
392
393 for ox in 0..overlay_w as i32 {
394 let bx = plane_pos_x + ox;
395 if bx < 0 || bx >= base_w as i32 {
396 continue;
397 }
398
399 let base_idx = (by as usize) * base_w as usize + bx as usize;
400 let overlay_idx = (oy as usize) * overlay_plane.stride + ox as usize;
401
402 let base_val = new_data.get(base_idx).copied().unwrap_or(128) as f64 / 255.0;
403 let overlay_val =
404 overlay_plane.data.get(overlay_idx).copied().unwrap_or(128) as f64 / 255.0;
405
406 let blended = self.config.blend_mode.blend_with_alpha(
407 base_val,
408 overlay_val,
409 self.config.alpha,
410 );
411
412 new_data[base_idx] = (blended * 255.0).round().clamp(0.0, 255.0) as u8;
413 }
414 }
415
416 *base_plane = Plane::new(new_data, base_plane.stride);
417 }
418 }
419
420 fn composite_rgb(&self, output: &mut VideoFrame, overlay: &VideoFrame, pos_x: i32, pos_y: i32) {
422 if output.planes.is_empty() || overlay.planes.is_empty() {
423 return;
424 }
425
426 let base_plane = &output.planes[0];
427 let overlay_plane = &overlay.planes[0];
428
429 let base_bpp = if output.format == PixelFormat::Rgba32 {
430 4
431 } else {
432 3
433 };
434 let overlay_bpp = if overlay.format == PixelFormat::Rgba32 {
435 4
436 } else {
437 3
438 };
439
440 let mut new_data = base_plane.data.to_vec();
441
442 for oy in 0..overlay.height as i32 {
443 let by = pos_y + oy;
444 if by < 0 || by >= output.height as i32 {
445 continue;
446 }
447
448 for ox in 0..overlay.width as i32 {
449 let bx = pos_x + ox;
450 if bx < 0 || bx >= output.width as i32 {
451 continue;
452 }
453
454 let base_idx = (by as usize * output.width as usize + bx as usize) * base_bpp;
455 let overlay_idx =
456 (oy as usize * overlay.width as usize + ox as usize) * overlay_bpp;
457
458 let alpha = if self.config.use_alpha_channel && overlay_bpp == 4 {
460 let overlay_alpha = overlay_plane
461 .data
462 .get(overlay_idx + 3)
463 .copied()
464 .unwrap_or(255) as f64
465 / 255.0;
466 overlay_alpha * self.config.alpha
467 } else {
468 self.config.alpha
469 };
470
471 for c in 0..3 {
473 let base_val = new_data.get(base_idx + c).copied().unwrap_or(0) as f64 / 255.0;
474 let overlay_val = overlay_plane
475 .data
476 .get(overlay_idx + c)
477 .copied()
478 .unwrap_or(0) as f64
479 / 255.0;
480
481 let blended =
482 self.config
483 .blend_mode
484 .blend_with_alpha(base_val, overlay_val, alpha);
485
486 new_data[base_idx + c] = (blended * 255.0).round().clamp(0.0, 255.0) as u8;
487 }
488
489 if base_bpp == 4 {
491 let base_alpha =
492 new_data.get(base_idx + 3).copied().unwrap_or(255) as f64 / 255.0;
493 let out_alpha = base_alpha + alpha * (1.0 - base_alpha);
494 new_data[base_idx + 3] = (out_alpha * 255.0).round().clamp(0.0, 255.0) as u8;
495 }
496 }
497 }
498
499 output.planes[0] = Plane::new(new_data, base_plane.stride);
500 }
501
502 fn try_composite(&mut self) -> Option<VideoFrame> {
504 match (self.base_frame.take(), self.overlay_frame.take()) {
505 (Some(base), Some(overlay)) => Some(self.composite(&base, &overlay)),
506 (Some(base), None) => Some(base), (None, Some(_)) => None, (None, None) => None,
509 }
510 }
511}
512
513impl Node for OverlayFilter {
514 fn id(&self) -> NodeId {
515 self.id
516 }
517
518 fn name(&self) -> &str {
519 &self.name
520 }
521
522 fn node_type(&self) -> NodeType {
523 NodeType::Filter
524 }
525
526 fn state(&self) -> NodeState {
527 self.state
528 }
529
530 fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
531 if !self.state.can_transition_to(state) {
532 return Err(GraphError::InvalidStateTransition {
533 node: self.id,
534 from: self.state.to_string(),
535 to: state.to_string(),
536 });
537 }
538 self.state = state;
539 Ok(())
540 }
541
542 fn inputs(&self) -> &[InputPort] {
543 &self.inputs
544 }
545
546 fn outputs(&self) -> &[OutputPort] {
547 &self.outputs
548 }
549
550 fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
551 match input {
555 Some(FilterFrame::Video(frame)) => {
556 self.base_frame = Some(frame);
557
558 if self.overlay_frame.is_some() {
560 Ok(self.try_composite().map(FilterFrame::Video))
561 } else {
562 Ok(self.base_frame.take().map(FilterFrame::Video))
564 }
565 }
566 Some(_) => Err(GraphError::PortTypeMismatch {
567 expected: "Video".to_string(),
568 actual: "Audio".to_string(),
569 }),
570 None => Ok(None),
571 }
572 }
573
574 fn reset(&mut self) -> GraphResult<()> {
575 self.base_frame = None;
576 self.overlay_frame = None;
577 self.set_state(NodeState::Idle)
578 }
579}
580
581#[must_use]
583pub fn create_color_overlay(width: u32, height: u32, r: u8, g: u8, b: u8, alpha: u8) -> VideoFrame {
584 let mut frame = VideoFrame::new(PixelFormat::Rgba32, width, height);
585
586 let size = (width * height * 4) as usize;
587 let mut data = vec![0u8; size];
588
589 for i in (0..size).step_by(4) {
590 data[i] = r;
591 data[i + 1] = g;
592 data[i + 2] = b;
593 data[i + 3] = alpha;
594 }
595
596 frame.planes.push(Plane::new(data, (width * 4) as usize));
597 frame
598}
599
600#[must_use]
602pub fn create_gradient_overlay(
603 width: u32,
604 height: u32,
605 start_color: (u8, u8, u8),
606 end_color: (u8, u8, u8),
607 horizontal: bool,
608) -> VideoFrame {
609 let mut frame = VideoFrame::new(PixelFormat::Rgba32, width, height);
610
611 let size = (width * height * 4) as usize;
612 let mut data = vec![0u8; size];
613
614 for y in 0..height as usize {
615 for x in 0..width as usize {
616 let t = if horizontal {
617 x as f64 / (width as f64 - 1.0).max(1.0)
618 } else {
619 y as f64 / (height as f64 - 1.0).max(1.0)
620 };
621
622 let r = (start_color.0 as f64 * (1.0 - t) + end_color.0 as f64 * t).round() as u8;
623 let g = (start_color.1 as f64 * (1.0 - t) + end_color.1 as f64 * t).round() as u8;
624 let b = (start_color.2 as f64 * (1.0 - t) + end_color.2 as f64 * t).round() as u8;
625
626 let idx = (y * width as usize + x) * 4;
627 data[idx] = r;
628 data[idx + 1] = g;
629 data[idx + 2] = b;
630 data[idx + 3] = 255;
631 }
632 }
633
634 frame.planes.push(Plane::new(data, (width * 4) as usize));
635 frame
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn create_test_yuv_frame(width: u32, height: u32, fill_y: u8) -> VideoFrame {
643 let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
644 frame.allocate();
645
646 if let Some(plane) = frame.planes.get_mut(0) {
647 let data = vec![fill_y; (width * height) as usize];
648 *plane = Plane::new(data, width as usize);
649 }
650
651 frame
652 }
653
654 fn create_test_rgb_frame(width: u32, height: u32, r: u8, g: u8, b: u8) -> VideoFrame {
655 let mut frame = VideoFrame::new(PixelFormat::Rgb24, width, height);
656
657 let size = (width * height * 3) as usize;
658 let mut data = vec![0u8; size];
659
660 for i in (0..size).step_by(3) {
661 data[i] = r;
662 data[i + 1] = g;
663 data[i + 2] = b;
664 }
665
666 frame.planes.push(Plane::new(data, (width * 3) as usize));
667 frame
668 }
669
670 #[test]
671 fn test_blend_modes() {
672 assert!((BlendMode::Normal.blend(0.5, 0.8) - 0.8).abs() < 0.001);
674
675 assert!((BlendMode::Add.blend(0.6, 0.5) - 1.0).abs() < 0.001); assert!((BlendMode::Multiply.blend(0.5, 0.5) - 0.25).abs() < 0.001);
680
681 assert!((BlendMode::Screen.blend(0.5, 0.5) - 0.75).abs() < 0.001);
683
684 assert!((BlendMode::Darken.blend(0.3, 0.7) - 0.3).abs() < 0.001);
686
687 assert!((BlendMode::Lighten.blend(0.3, 0.7) - 0.7).abs() < 0.001);
689
690 assert!((BlendMode::Difference.blend(0.8, 0.3) - 0.5).abs() < 0.001);
692 }
693
694 #[test]
695 fn test_blend_with_alpha() {
696 let result = BlendMode::Normal.blend_with_alpha(0.2, 0.8, 0.5);
697 assert!((result - 0.5).abs() < 0.001);
699 }
700
701 #[test]
702 fn test_alignment_offset() {
703 let (x, y) = Alignment::Center.offset(100, 100, 20, 20);
704 assert_eq!(x, 40);
705 assert_eq!(y, 40);
706
707 let (x, y) = Alignment::TopLeft.offset(100, 100, 20, 20);
708 assert_eq!(x, 0);
709 assert_eq!(y, 0);
710
711 let (x, y) = Alignment::BottomRight.offset(100, 100, 20, 20);
712 assert_eq!(x, 80);
713 assert_eq!(y, 80);
714 }
715
716 #[test]
717 fn test_overlay_config() {
718 let config = OverlayConfig::new(10, 20)
719 .with_alignment(Alignment::Center)
720 .with_blend_mode(BlendMode::Multiply)
721 .with_alpha(0.75)
722 .with_use_alpha_channel(false);
723
724 assert_eq!(config.x, 10);
725 assert_eq!(config.y, 20);
726 assert_eq!(config.alignment, Alignment::Center);
727 assert_eq!(config.blend_mode, BlendMode::Multiply);
728 assert!((config.alpha - 0.75).abs() < 0.001);
729 assert!(!config.use_alpha_channel);
730 }
731
732 #[test]
733 fn test_overlay_config_centered() {
734 let config = OverlayConfig::centered();
735 assert_eq!(config.alignment, Alignment::Center);
736 }
737
738 #[test]
739 fn test_calculate_position() {
740 let config = OverlayConfig::new(10, 20).with_alignment(Alignment::Center);
741 let (x, y) = config.calculate_position(100, 100, 20, 20);
742
743 assert_eq!(x, 50);
745 assert_eq!(y, 60);
746 }
747
748 #[test]
749 fn test_overlay_filter_creation() {
750 let config = OverlayConfig::default();
751 let filter = OverlayFilter::new(NodeId(0), "overlay", config);
752
753 assert_eq!(filter.id(), NodeId(0));
754 assert_eq!(filter.name(), "overlay");
755 assert_eq!(filter.node_type(), NodeType::Filter);
756 assert_eq!(filter.inputs().len(), 2); assert_eq!(filter.outputs().len(), 1);
758 }
759
760 #[test]
761 fn test_composite_yuv() {
762 let config = OverlayConfig::new(0, 0).with_alpha(0.5);
763 let filter = OverlayFilter::new(NodeId(0), "overlay", config);
764
765 let base = create_test_yuv_frame(64, 48, 100);
766 let overlay = create_test_yuv_frame(32, 24, 200);
767
768 let result = filter.composite(&base, &overlay);
769
770 assert_eq!(result.width, 64);
771 assert_eq!(result.height, 48);
772 }
773
774 #[test]
775 fn test_composite_rgb() {
776 let config = OverlayConfig::new(0, 0).with_alpha(1.0);
777 let filter = OverlayFilter::new(NodeId(0), "overlay", config);
778
779 let base = create_test_rgb_frame(64, 48, 0, 0, 0);
780 let overlay = create_test_rgb_frame(32, 24, 255, 255, 255);
781
782 let result = filter.composite(&base, &overlay);
783
784 assert_eq!(result.width, 64);
785 assert_eq!(result.height, 48);
786 }
787
788 #[test]
789 fn test_create_color_overlay() {
790 let overlay = create_color_overlay(64, 48, 255, 0, 0, 128);
791
792 assert_eq!(overlay.width, 64);
793 assert_eq!(overlay.height, 48);
794 assert_eq!(overlay.format, PixelFormat::Rgba32);
795 assert!(!overlay.planes.is_empty());
796
797 let data = &overlay.planes[0].data;
799 assert_eq!(data[0], 255); assert_eq!(data[1], 0); assert_eq!(data[2], 0); assert_eq!(data[3], 128); }
804
805 #[test]
806 fn test_create_gradient_overlay() {
807 let overlay = create_gradient_overlay(64, 48, (255, 0, 0), (0, 0, 255), true);
808
809 assert_eq!(overlay.width, 64);
810 assert_eq!(overlay.height, 48);
811 assert_eq!(overlay.format, PixelFormat::Rgba32);
812
813 let data = &overlay.planes[0].data;
815 assert!(data[0] > 200); let last_idx = ((64 * 48 - 1) * 4) as usize;
817 assert!(data[last_idx + 2] > 200); }
819
820 #[test]
821 fn test_process_with_base_only() {
822 let config = OverlayConfig::default();
823 let mut filter = OverlayFilter::new(NodeId(0), "overlay", config);
824
825 let base = create_test_yuv_frame(64, 48, 100);
826 let result = filter
827 .process(Some(FilterFrame::Video(base)))
828 .expect("operation should succeed")
829 .expect("operation should succeed");
830
831 assert!(matches!(result, FilterFrame::Video(_)));
833 }
834
835 #[test]
836 fn test_process_with_both_frames() {
837 let config = OverlayConfig::new(0, 0);
838 let mut filter = OverlayFilter::new(NodeId(0), "overlay", config);
839
840 let overlay = create_test_yuv_frame(32, 24, 200);
842 filter.set_overlay_frame(overlay);
843
844 let base = create_test_yuv_frame(64, 48, 100);
846 let result = filter
847 .process(Some(FilterFrame::Video(base)))
848 .expect("operation should succeed")
849 .expect("operation should succeed");
850
851 assert!(matches!(result, FilterFrame::Video(_)));
852 }
853
854 #[test]
855 fn test_node_state_transitions() {
856 let config = OverlayConfig::default();
857 let mut filter = OverlayFilter::new(NodeId(0), "overlay", config);
858
859 assert_eq!(filter.state(), NodeState::Idle);
860 filter
861 .set_state(NodeState::Processing)
862 .expect("set_state should succeed");
863 assert_eq!(filter.state(), NodeState::Processing);
864 }
865
866 #[test]
867 fn test_process_none_input() {
868 let config = OverlayConfig::default();
869 let mut filter = OverlayFilter::new(NodeId(0), "overlay", config);
870
871 let result = filter.process(None).expect("process should succeed");
872 assert!(result.is_none());
873 }
874
875 #[test]
876 fn test_reset() {
877 let config = OverlayConfig::default();
878 let mut filter = OverlayFilter::new(NodeId(0), "overlay", config);
879
880 filter.set_base_frame(create_test_yuv_frame(64, 48, 100));
881 filter.set_overlay_frame(create_test_yuv_frame(32, 24, 200));
882
883 filter.reset().expect("reset should succeed");
884
885 assert!(filter.base_frame.is_none());
886 assert!(filter.overlay_frame.is_none());
887 }
888
889 #[test]
890 fn test_overlay_blend_mode() {
891 let result = BlendMode::Overlay.blend(0.25, 0.5);
893 assert!((result - 0.25).abs() < 0.001);
895
896 let result = BlendMode::Overlay.blend(0.75, 0.5);
897 assert!((result - 0.75).abs() < 0.001);
899 }
900
901 #[test]
902 fn test_exclusion_blend() {
903 let result = BlendMode::Exclusion.blend(0.5, 0.5);
904 assert!((result - 0.5).abs() < 0.001);
906 }
907}