oximedia_timecode/
burn_in.rs1#[allow(dead_code)]
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum BurnInStyle {
9 Classic,
11 Modern,
13 Minimal,
15}
16
17impl BurnInStyle {
18 #[allow(clippy::cast_precision_loss)]
20 #[must_use]
21 pub fn font_scale(&self) -> f32 {
22 match self {
23 BurnInStyle::Classic => 2.0,
24 BurnInStyle::Modern => 1.5,
25 BurnInStyle::Minimal => 1.0,
26 }
27 }
28}
29
30#[allow(dead_code)]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum BurnInPosition {
34 TopLeft,
36 TopCenter,
38 TopRight,
40 BottomLeft,
42 BottomCenter,
44 BottomRight,
46}
47
48impl BurnInPosition {
49 #[must_use]
51 pub fn is_top(&self) -> bool {
52 matches!(
53 self,
54 BurnInPosition::TopLeft | BurnInPosition::TopCenter | BurnInPosition::TopRight
55 )
56 }
57
58 #[must_use]
60 pub fn is_right(&self) -> bool {
61 matches!(self, BurnInPosition::TopRight | BurnInPosition::BottomRight)
62 }
63}
64
65#[allow(dead_code)]
66#[derive(Debug, Clone)]
68pub struct TimecodeOverlay {
69 pub style: BurnInStyle,
71 pub position: BurnInPosition,
73 pub background_alpha: f32,
75 pub color: [u8; 3],
77}
78
79impl TimecodeOverlay {
80 #[must_use]
82 pub fn default_broadcast() -> Self {
83 Self {
84 style: BurnInStyle::Classic,
85 position: BurnInPosition::BottomCenter,
86 background_alpha: 0.5,
87 color: [255, 255, 255],
88 }
89 }
90
91 #[must_use]
93 pub fn default_dailies() -> Self {
94 Self {
95 style: BurnInStyle::Modern,
96 position: BurnInPosition::TopLeft,
97 background_alpha: 0.0,
98 color: [255, 255, 0],
99 }
100 }
101}
102
103#[allow(clippy::cast_precision_loss)]
109#[must_use]
110pub fn render_timecode_text(
111 tc: &str,
112 frame_width: u32,
113 frame_height: u32,
114 overlay: &TimecodeOverlay,
115) -> (u32, u32) {
116 let char_w = (12.0 * overlay.style.font_scale()) as u32;
118 let char_h = (20.0 * overlay.style.font_scale()) as u32;
119 let text_w = char_w * tc.len() as u32;
120 let margin = 16u32;
121
122 let x = match overlay.position {
123 BurnInPosition::TopLeft | BurnInPosition::BottomLeft => margin,
124 BurnInPosition::TopCenter | BurnInPosition::BottomCenter => {
125 frame_width.saturating_sub(text_w) / 2
126 }
127 BurnInPosition::TopRight | BurnInPosition::BottomRight => {
128 frame_width.saturating_sub(text_w + margin)
129 }
130 };
131
132 let y = if overlay.position.is_top() {
133 margin
134 } else {
135 frame_height.saturating_sub(char_h + margin)
136 };
137
138 (x, y)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_burn_in_style_classic_scale() {
147 assert!((BurnInStyle::Classic.font_scale() - 2.0).abs() < f32::EPSILON);
148 }
149
150 #[test]
151 fn test_burn_in_style_modern_scale() {
152 assert!((BurnInStyle::Modern.font_scale() - 1.5).abs() < f32::EPSILON);
153 }
154
155 #[test]
156 fn test_burn_in_style_minimal_scale() {
157 assert!((BurnInStyle::Minimal.font_scale() - 1.0).abs() < f32::EPSILON);
158 }
159
160 #[test]
161 fn test_position_is_top_true() {
162 assert!(BurnInPosition::TopLeft.is_top());
163 assert!(BurnInPosition::TopCenter.is_top());
164 assert!(BurnInPosition::TopRight.is_top());
165 }
166
167 #[test]
168 fn test_position_is_top_false() {
169 assert!(!BurnInPosition::BottomLeft.is_top());
170 assert!(!BurnInPosition::BottomCenter.is_top());
171 assert!(!BurnInPosition::BottomRight.is_top());
172 }
173
174 #[test]
175 fn test_position_is_right_true() {
176 assert!(BurnInPosition::TopRight.is_right());
177 assert!(BurnInPosition::BottomRight.is_right());
178 }
179
180 #[test]
181 fn test_position_is_right_false() {
182 assert!(!BurnInPosition::TopLeft.is_right());
183 assert!(!BurnInPosition::BottomCenter.is_right());
184 }
185
186 #[test]
187 fn test_default_broadcast_style() {
188 let o = TimecodeOverlay::default_broadcast();
189 assert_eq!(o.style, BurnInStyle::Classic);
190 assert_eq!(o.position, BurnInPosition::BottomCenter);
191 assert!(!o.position.is_top());
192 }
193
194 #[test]
195 fn test_default_dailies_style() {
196 let o = TimecodeOverlay::default_dailies();
197 assert_eq!(o.style, BurnInStyle::Modern);
198 assert_eq!(o.position, BurnInPosition::TopLeft);
199 assert!(o.position.is_top());
200 }
201
202 #[test]
203 fn test_render_timecode_text_bottom_center() {
204 let overlay = TimecodeOverlay::default_broadcast();
205 let (x, y) = render_timecode_text("01:02:03:04", 1920, 1080, &overlay);
206 assert!(x < 1920);
208 assert!(y > 1080 / 2);
210 }
211
212 #[test]
213 fn test_render_timecode_text_top_left() {
214 let overlay = TimecodeOverlay::default_dailies();
215 let (x, y) = render_timecode_text("01:02:03:04", 1920, 1080, &overlay);
216 assert!(x < 100);
218 assert!(y < 100);
220 }
221
222 #[test]
223 fn test_render_timecode_text_top_right() {
224 let overlay = TimecodeOverlay {
225 style: BurnInStyle::Minimal,
226 position: BurnInPosition::TopRight,
227 background_alpha: 0.0,
228 color: [255, 255, 255],
229 };
230 let (x, _y) = render_timecode_text("01:02:03:04", 1920, 1080, &overlay);
231 assert!(x > 1920 / 2);
233 }
234}