Skip to main content

oximedia_graph/filters/video/
overlay.rs

1//! Video overlay/compositing filter.
2//!
3//! This filter composites one video stream over another, supporting various
4//! blend modes, positioning, and alpha blending.
5
6#![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/// Blend mode for compositing.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
37pub enum BlendMode {
38    /// Normal blend (overlay on top with alpha).
39    #[default]
40    Normal,
41    /// Additive blend (lighten).
42    Add,
43    /// Multiplicative blend (darken).
44    Multiply,
45    /// Screen blend (lighten, opposite of multiply).
46    Screen,
47    /// Overlay blend (combination of multiply and screen).
48    Overlay,
49    /// Darken (take minimum).
50    Darken,
51    /// Lighten (take maximum).
52    Lighten,
53    /// Difference blend.
54    Difference,
55    /// Exclusion blend.
56    Exclusion,
57}
58
59impl BlendMode {
60    /// Apply this blend mode to two pixel values.
61    ///
62    /// Both values should be in the range 0.0-1.0.
63    #[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    /// Apply blend with alpha.
85    #[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/// Position alignment for overlay.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
94pub enum Alignment {
95    /// Top-left corner.
96    #[default]
97    TopLeft,
98    /// Top-center.
99    TopCenter,
100    /// Top-right corner.
101    TopRight,
102    /// Center-left.
103    CenterLeft,
104    /// Center.
105    Center,
106    /// Center-right.
107    CenterRight,
108    /// Bottom-left corner.
109    BottomLeft,
110    /// Bottom-center.
111    BottomCenter,
112    /// Bottom-right corner.
113    BottomRight,
114}
115
116impl Alignment {
117    /// Calculate position offset for alignment.
118    #[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/// Configuration for the overlay filter.
151#[derive(Clone, Debug)]
152pub struct OverlayConfig {
153    /// X position of overlay (relative to base).
154    pub x: i32,
155    /// Y position of overlay (relative to base).
156    pub y: i32,
157    /// Alignment mode.
158    pub alignment: Alignment,
159    /// Blend mode.
160    pub blend_mode: BlendMode,
161    /// Global alpha (0.0 = transparent, 1.0 = opaque).
162    pub alpha: f64,
163    /// Use overlay's alpha channel if available.
164    pub use_alpha_channel: bool,
165    /// Enable alpha premultiplication.
166    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    /// Create a new overlay configuration.
185    #[must_use]
186    pub fn new(x: i32, y: i32) -> Self {
187        Self {
188            x,
189            y,
190            ..Default::default()
191        }
192    }
193
194    /// Create a centered overlay configuration.
195    #[must_use]
196    pub fn centered() -> Self {
197        Self {
198            alignment: Alignment::Center,
199            ..Default::default()
200        }
201    }
202
203    /// Set the position.
204    #[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    /// Set the alignment.
212    #[must_use]
213    pub fn with_alignment(mut self, alignment: Alignment) -> Self {
214        self.alignment = alignment;
215        self
216    }
217
218    /// Set the blend mode.
219    #[must_use]
220    pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
221        self.blend_mode = mode;
222        self
223    }
224
225    /// Set the global alpha.
226    #[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    /// Enable or disable alpha channel usage.
233    #[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    /// Calculate the actual position for overlay.
240    #[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
253/// Video overlay filter.
254///
255/// Composites an overlay video onto a base video with various blend modes
256/// and positioning options.
257///
258/// # Example
259///
260/// ```ignore
261/// use oximedia_graph::filters::video::{OverlayFilter, OverlayConfig, BlendMode, Alignment};
262/// use oximedia_graph::node::NodeId;
263///
264/// // Create a centered overlay with 50% opacity
265/// let config = OverlayConfig::centered()
266///     .with_blend_mode(BlendMode::Normal)
267///     .with_alpha(0.5);
268///
269/// let filter = OverlayFilter::new(NodeId(0), "overlay", config);
270/// ```
271pub struct OverlayFilter {
272    id: NodeId,
273    name: String,
274    state: NodeState,
275    inputs: Vec<InputPort>,
276    outputs: Vec<OutputPort>,
277    config: OverlayConfig,
278    /// Buffered base frame.
279    base_frame: Option<VideoFrame>,
280    /// Buffered overlay frame.
281    overlay_frame: Option<VideoFrame>,
282}
283
284impl OverlayFilter {
285    /// Create a new overlay filter.
286    #[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    /// Get the current configuration.
307    #[must_use]
308    pub fn config(&self) -> &OverlayConfig {
309        &self.config
310    }
311
312    /// Update the configuration.
313    pub fn set_config(&mut self, config: OverlayConfig) {
314        self.config = config;
315    }
316
317    /// Set the base frame.
318    pub fn set_base_frame(&mut self, frame: VideoFrame) {
319        self.base_frame = Some(frame);
320    }
321
322    /// Set the overlay frame.
323    pub fn set_overlay_frame(&mut self, frame: VideoFrame) {
324        self.overlay_frame = Some(frame);
325    }
326
327    /// Composite the overlay onto the base frame.
328    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        // Handle YUV formats
336        if base.format.is_yuv() && overlay.format.is_yuv() {
337            self.composite_yuv(&mut output, overlay, pos_x, pos_y);
338        } else {
339            // For simplicity, handle RGB/RGBA compositing
340            self.composite_rgb(&mut output, overlay, pos_x, pos_y);
341        }
342
343        output
344    }
345
346    /// Composite YUV frames.
347    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        // Pre-calculate dimensions for each plane
352        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            // Clone and modify data
385            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    /// Composite RGB/RGBA frames.
421    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                // Get alpha
459                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                // Blend each channel
472                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                // Handle output alpha
490                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    /// Process when both frames are available.
503    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), // No overlay, pass through base
507            (None, Some(_)) => None,          // No base, cannot output
508            (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        // Note: This filter has 2 inputs. In a real implementation,
552        // the graph runtime would manage multiple inputs.
553        // For simplicity, we treat the input as the base frame.
554        match input {
555            Some(FilterFrame::Video(frame)) => {
556                self.base_frame = Some(frame);
557
558                // If we have both frames, composite
559                if self.overlay_frame.is_some() {
560                    Ok(self.try_composite().map(FilterFrame::Video))
561                } else {
562                    // Pass through base if no overlay
563                    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/// Helper function to create a simple color overlay.
582#[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/// Helper function to create a gradient overlay.
601#[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        // Normal blend
673        assert!((BlendMode::Normal.blend(0.5, 0.8) - 0.8).abs() < 0.001);
674
675        // Add blend
676        assert!((BlendMode::Add.blend(0.6, 0.5) - 1.0).abs() < 0.001); // Clamped
677
678        // Multiply blend
679        assert!((BlendMode::Multiply.blend(0.5, 0.5) - 0.25).abs() < 0.001);
680
681        // Screen blend
682        assert!((BlendMode::Screen.blend(0.5, 0.5) - 0.75).abs() < 0.001);
683
684        // Darken blend
685        assert!((BlendMode::Darken.blend(0.3, 0.7) - 0.3).abs() < 0.001);
686
687        // Lighten blend
688        assert!((BlendMode::Lighten.blend(0.3, 0.7) - 0.7).abs() < 0.001);
689
690        // Difference blend
691        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        // 0.2 * 0.5 + 0.8 * 0.5 = 0.1 + 0.4 = 0.5
698        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        // Center offset (40, 40) + position (10, 20) = (50, 60)
744        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); // Base and overlay inputs
757        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        // Check first pixel
798        let data = &overlay.planes[0].data;
799        assert_eq!(data[0], 255); // R
800        assert_eq!(data[1], 0); // G
801        assert_eq!(data[2], 0); // B
802        assert_eq!(data[3], 128); // A
803    }
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        // Check gradient: first pixel should be red-ish, last should be blue-ish
814        let data = &overlay.planes[0].data;
815        assert!(data[0] > 200); // R high at start
816        let last_idx = ((64 * 48 - 1) * 4) as usize;
817        assert!(data[last_idx + 2] > 200); // B high at end
818    }
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        // Should pass through base when no overlay
832        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        // Set overlay first
841        let overlay = create_test_yuv_frame(32, 24, 200);
842        filter.set_overlay_frame(overlay);
843
844        // Then process base
845        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        // Test overlay blend mode (combination)
892        let result = BlendMode::Overlay.blend(0.25, 0.5);
893        // For base < 0.5: 2 * base * overlay = 2 * 0.25 * 0.5 = 0.25
894        assert!((result - 0.25).abs() < 0.001);
895
896        let result = BlendMode::Overlay.blend(0.75, 0.5);
897        // For base >= 0.5: 1 - 2 * (1-base) * (1-overlay) = 1 - 2 * 0.25 * 0.5 = 0.75
898        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        // base + overlay - 2 * base * overlay = 0.5 + 0.5 - 2 * 0.25 = 0.5
905        assert!((result - 0.5).abs() < 0.001);
906    }
907}