1#![allow(dead_code)]
2use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum OverlayPosition {
12 TopLeft,
14 TopRight,
16 BottomLeft,
18 BottomRight,
20 Center,
22 Custom {
24 x: u32,
26 y: u32,
28 },
29}
30
31impl fmt::Display for OverlayPosition {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::TopLeft => write!(f, "top-left"),
35 Self::TopRight => write!(f, "top-right"),
36 Self::BottomLeft => write!(f, "bottom-left"),
37 Self::BottomRight => write!(f, "bottom-right"),
38 Self::Center => write!(f, "center"),
39 Self::Custom { x, y } => write!(f, "custom({x},{y})"),
40 }
41 }
42}
43
44impl OverlayPosition {
45 #[allow(clippy::cast_precision_loss)]
48 #[must_use]
49 pub fn resolve(self, frame_w: u32, frame_h: u32, overlay_w: u32, overlay_h: u32) -> (u32, u32) {
50 match self {
51 Self::TopLeft => (0, 0),
52 Self::TopRight => (frame_w.saturating_sub(overlay_w), 0),
53 Self::BottomLeft => (0, frame_h.saturating_sub(overlay_h)),
54 Self::BottomRight => (
55 frame_w.saturating_sub(overlay_w),
56 frame_h.saturating_sub(overlay_h),
57 ),
58 Self::Center => (
59 frame_w.saturating_sub(overlay_w) / 2,
60 frame_h.saturating_sub(overlay_h) / 2,
61 ),
62 Self::Custom { x, y } => (x, y),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub enum WatermarkContent {
70 Text(String),
72 ImageFile(String),
74 RawRgba {
76 width: u32,
78 height: u32,
80 data: Vec<u8>,
82 },
83}
84
85#[derive(Debug, Clone)]
87pub struct WatermarkConfig {
88 pub content: WatermarkContent,
90 pub position: OverlayPosition,
92 pub opacity: f32,
94 pub scale: f32,
96 pub margin: u32,
98 pub start_time: Option<f64>,
100 pub end_time: Option<f64>,
102}
103
104impl WatermarkConfig {
105 pub fn text(text: impl Into<String>) -> Self {
107 Self {
108 content: WatermarkContent::Text(text.into()),
109 position: OverlayPosition::BottomRight,
110 opacity: 0.5,
111 scale: 1.0,
112 margin: 10,
113 start_time: None,
114 end_time: None,
115 }
116 }
117
118 pub fn image(path: impl Into<String>) -> Self {
120 Self {
121 content: WatermarkContent::ImageFile(path.into()),
122 position: OverlayPosition::BottomRight,
123 opacity: 0.8,
124 scale: 1.0,
125 margin: 10,
126 start_time: None,
127 end_time: None,
128 }
129 }
130
131 #[must_use]
133 pub fn with_position(mut self, pos: OverlayPosition) -> Self {
134 self.position = pos;
135 self
136 }
137
138 #[must_use]
140 pub fn with_opacity(mut self, opacity: f32) -> Self {
141 self.opacity = opacity.clamp(0.0, 1.0);
142 self
143 }
144
145 #[must_use]
147 pub fn with_scale(mut self, scale: f32) -> Self {
148 self.scale = scale.max(0.01);
149 self
150 }
151
152 #[must_use]
154 pub fn with_margin(mut self, margin: u32) -> Self {
155 self.margin = margin;
156 self
157 }
158
159 #[must_use]
161 pub fn with_time_range(mut self, start: f64, end: f64) -> Self {
162 self.start_time = Some(start);
163 self.end_time = Some(end);
164 self
165 }
166
167 #[must_use]
169 pub fn is_visible_at(&self, time_seconds: f64) -> bool {
170 if let Some(start) = self.start_time {
171 if time_seconds < start {
172 return false;
173 }
174 }
175 if let Some(end) = self.end_time {
176 if time_seconds > end {
177 return false;
178 }
179 }
180 true
181 }
182
183 #[must_use]
185 pub fn effective_opacity(&self, time_seconds: f64) -> f32 {
186 if self.is_visible_at(time_seconds) {
187 self.opacity
188 } else {
189 0.0
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct OverlayPipeline {
197 layers: Vec<WatermarkConfig>,
199 frame_width: u32,
201 frame_height: u32,
203}
204
205impl OverlayPipeline {
206 #[must_use]
208 pub fn new(width: u32, height: u32) -> Self {
209 Self {
210 layers: Vec::new(),
211 frame_width: width,
212 frame_height: height,
213 }
214 }
215
216 pub fn add_layer(&mut self, config: WatermarkConfig) {
218 self.layers.push(config);
219 }
220
221 #[must_use]
223 pub fn layer_count(&self) -> usize {
224 self.layers.len()
225 }
226
227 #[must_use]
229 pub fn visible_layers_at(&self, time_seconds: f64) -> Vec<&WatermarkConfig> {
230 self.layers
231 .iter()
232 .filter(|l| l.is_visible_at(time_seconds))
233 .collect()
234 }
235
236 pub fn clear(&mut self) {
238 self.layers.clear();
239 }
240
241 #[must_use]
243 pub fn frame_size(&self) -> (u32, u32) {
244 (self.frame_width, self.frame_height)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_position_display() {
254 assert_eq!(OverlayPosition::TopLeft.to_string(), "top-left");
255 assert_eq!(OverlayPosition::TopRight.to_string(), "top-right");
256 assert_eq!(OverlayPosition::BottomLeft.to_string(), "bottom-left");
257 assert_eq!(OverlayPosition::BottomRight.to_string(), "bottom-right");
258 assert_eq!(OverlayPosition::Center.to_string(), "center");
259 assert_eq!(
260 OverlayPosition::Custom { x: 10, y: 20 }.to_string(),
261 "custom(10,20)"
262 );
263 }
264
265 #[test]
266 fn test_position_resolve_top_left() {
267 let (x, y) = OverlayPosition::TopLeft.resolve(1920, 1080, 100, 50);
268 assert_eq!((x, y), (0, 0));
269 }
270
271 #[test]
272 fn test_position_resolve_bottom_right() {
273 let (x, y) = OverlayPosition::BottomRight.resolve(1920, 1080, 100, 50);
274 assert_eq!((x, y), (1820, 1030));
275 }
276
277 #[test]
278 fn test_position_resolve_center() {
279 let (x, y) = OverlayPosition::Center.resolve(1920, 1080, 100, 80);
280 assert_eq!((x, y), (910, 500));
281 }
282
283 #[test]
284 fn test_position_resolve_custom() {
285 let (x, y) = OverlayPosition::Custom { x: 42, y: 99 }.resolve(1920, 1080, 100, 100);
286 assert_eq!((x, y), (42, 99));
287 }
288
289 #[test]
290 fn test_text_watermark_defaults() {
291 let wm = WatermarkConfig::text("(c) 2024");
292 assert_eq!(wm.position, OverlayPosition::BottomRight);
293 assert!((wm.opacity - 0.5).abs() < f32::EPSILON);
294 assert!((wm.scale - 1.0).abs() < f32::EPSILON);
295 assert_eq!(wm.margin, 10);
296 assert!(wm.start_time.is_none());
297 }
298
299 #[test]
300 fn test_image_watermark_defaults() {
301 let wm = WatermarkConfig::image("/logo.png");
302 assert!((wm.opacity - 0.8).abs() < f32::EPSILON);
303 match &wm.content {
304 WatermarkContent::ImageFile(p) => assert_eq!(p, "/logo.png"),
305 _ => panic!("expected ImageFile"),
306 }
307 }
308
309 #[test]
310 fn test_opacity_clamp() {
311 let wm = WatermarkConfig::text("x").with_opacity(2.0);
312 assert!((wm.opacity - 1.0).abs() < f32::EPSILON);
313 let wm2 = WatermarkConfig::text("x").with_opacity(-1.0);
314 assert!((wm2.opacity - 0.0).abs() < f32::EPSILON);
315 }
316
317 #[test]
318 fn test_visibility_always() {
319 let wm = WatermarkConfig::text("x");
320 assert!(wm.is_visible_at(0.0));
321 assert!(wm.is_visible_at(9999.0));
322 }
323
324 #[test]
325 fn test_visibility_timed() {
326 let wm = WatermarkConfig::text("x").with_time_range(5.0, 10.0);
327 assert!(!wm.is_visible_at(3.0));
328 assert!(wm.is_visible_at(7.0));
329 assert!(!wm.is_visible_at(12.0));
330 }
331
332 #[test]
333 fn test_effective_opacity() {
334 let wm = WatermarkConfig::text("x")
335 .with_opacity(0.75)
336 .with_time_range(5.0, 10.0);
337 assert!((wm.effective_opacity(7.0) - 0.75).abs() < f32::EPSILON);
338 assert!((wm.effective_opacity(1.0) - 0.0).abs() < f32::EPSILON);
339 }
340
341 #[test]
342 fn test_pipeline_add_layers() {
343 let mut pipeline = OverlayPipeline::new(1920, 1080);
344 assert_eq!(pipeline.layer_count(), 0);
345 pipeline.add_layer(WatermarkConfig::text("A"));
346 pipeline.add_layer(WatermarkConfig::text("B"));
347 assert_eq!(pipeline.layer_count(), 2);
348 }
349
350 #[test]
351 fn test_pipeline_visible_layers() {
352 let mut pipeline = OverlayPipeline::new(1920, 1080);
353 pipeline.add_layer(WatermarkConfig::text("always"));
354 pipeline.add_layer(WatermarkConfig::text("timed").with_time_range(5.0, 10.0));
355
356 assert_eq!(pipeline.visible_layers_at(0.0).len(), 1);
357 assert_eq!(pipeline.visible_layers_at(7.0).len(), 2);
358 }
359
360 #[test]
361 fn test_pipeline_clear() {
362 let mut pipeline = OverlayPipeline::new(1920, 1080);
363 pipeline.add_layer(WatermarkConfig::text("x"));
364 pipeline.clear();
365 assert_eq!(pipeline.layer_count(), 0);
366 }
367
368 #[test]
369 fn test_pipeline_frame_size() {
370 let pipeline = OverlayPipeline::new(3840, 2160);
371 assert_eq!(pipeline.frame_size(), (3840, 2160));
372 }
373}