ff_filter/composite.rs
1//! Porter-Duff alpha-compositing operators.
2
3// ── CompositeOp ────────────────────────────────────────────────────────────────
4
5/// Porter-Duff alpha-compositing operator for combining two video layers.
6///
7/// Unlike [`BlendMode`](crate::BlendMode) (which operates on pixel values), these
8/// operators combine layers by their alpha channels. At least the top layer must
9/// carry an alpha channel (e.g. `rgba` or `yuva420p`).
10///
11/// There is no `blend all_mode` token for Porter-Duff compositing, so this type
12/// has no `FfmpegToken` impl; each operator maps to a specific `FFmpeg` construction
13/// (`overlay` or `blend` with a per-channel expression) in the filter graph.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum CompositeOp {
16 /// Top layer rendered over the bottom (standard alpha compositing).
17 ///
18 /// Built via `overlay=format=auto:shortest=1`.
19 #[default]
20 Over,
21
22 /// Bottom layer rendered over the top; `Over` with the inputs swapped.
23 ///
24 /// Built via `overlay` with swapped input order.
25 Under,
26
27 /// Top layer masked by the bottom layer's alpha (intersection).
28 ///
29 /// Built via `blend` with `c0_expr='B*A/255'`.
30 In,
31
32 /// Top layer visible only where the bottom layer is transparent.
33 ///
34 /// Built via `blend` with `c0_expr='B*(255-A)/255'`.
35 Out,
36
37 /// Top layer placed atop the bottom; visible only where the bottom is opaque.
38 ///
39 /// Built via `blend` with `c0_expr='B*A/255 + A*(255-B)/255'`.
40 Atop,
41
42 /// Pixels from exactly one layer (XOR of opaque regions).
43 ///
44 /// Built via `blend` with `c0_expr='B*(255-A)/255 + A*(255-B)/255'`.
45 Xor,
46}
47
48impl CompositeOp {
49 /// Returns the `FFmpeg` `blend` `all_expr` formula for the expression-based
50 /// operators (`In`/`Out`/`Atop`/`Xor`), or `None` for `Over`/`Under` which
51 /// are built with the `overlay` filter rather than `blend`.
52 ///
53 /// In the formula, `A` is the bottom pixel and `B` is the top pixel. This is
54 /// the single source of these formulas, shared by the `Composite` filter step
55 /// and the `MultiTrackComposer` canvas compositing.
56 #[must_use]
57 pub(crate) fn blend_all_expr(self) -> Option<&'static str> {
58 match self {
59 Self::Over | Self::Under => None,
60 Self::In => Some("B*A/255"),
61 Self::Out => Some("B*(255-A)/255"),
62 Self::Atop => Some("B*A/255 + A*(255-B)/255"),
63 Self::Xor => Some("B*(255-A)/255 + A*(255-B)/255"),
64 }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::CompositeOp;
71
72 #[test]
73 fn blend_all_expr_should_be_none_for_overlay_built_operators() {
74 assert_eq!(CompositeOp::Over.blend_all_expr(), None);
75 assert_eq!(CompositeOp::Under.blend_all_expr(), None);
76 }
77
78 #[test]
79 fn blend_all_expr_should_return_porter_duff_formula_for_expression_operators() {
80 assert_eq!(CompositeOp::In.blend_all_expr(), Some("B*A/255"));
81 assert_eq!(CompositeOp::Out.blend_all_expr(), Some("B*(255-A)/255"));
82 assert_eq!(
83 CompositeOp::Atop.blend_all_expr(),
84 Some("B*A/255 + A*(255-B)/255")
85 );
86 assert_eq!(
87 CompositeOp::Xor.blend_all_expr(),
88 Some("B*(255-A)/255 + A*(255-B)/255")
89 );
90 }
91
92 #[test]
93 fn composite_op_should_default_to_over() {
94 assert_eq!(CompositeOp::default(), CompositeOp::Over);
95 }
96}