Skip to main content

oximedia_subtitle/
style.rs

1//! Subtitle styling definitions.
2
3/// RGBA color value.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub struct Color {
6    /// Red component (0-255).
7    pub r: u8,
8    /// Green component (0-255).
9    pub g: u8,
10    /// Blue component (0-255).
11    pub b: u8,
12    /// Alpha component (0-255, 0=transparent, 255=opaque).
13    pub a: u8,
14}
15
16impl Color {
17    /// Create a new color.
18    #[must_use]
19    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
20        Self { r, g, b, a }
21    }
22
23    /// Create an opaque color.
24    #[must_use]
25    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
26        Self::new(r, g, b, 255)
27    }
28
29    /// White color.
30    #[must_use]
31    pub const fn white() -> Self {
32        Self::rgb(255, 255, 255)
33    }
34
35    /// Black color.
36    #[must_use]
37    pub const fn black() -> Self {
38        Self::rgb(0, 0, 0)
39    }
40
41    /// Transparent color.
42    #[must_use]
43    pub const fn transparent() -> Self {
44        Self::new(0, 0, 0, 0)
45    }
46
47    /// Parse from hex string (e.g., "#FFFFFF" or "#FFFFFFFF").
48    ///
49    /// # Errors
50    ///
51    /// Returns error if the string is not a valid hex color.
52    pub fn from_hex(hex: &str) -> Result<Self, crate::SubtitleError> {
53        let hex = hex.trim_start_matches('#');
54
55        let (r, g, b, a) = match hex.len() {
56            6 => {
57                let r = u8::from_str_radix(&hex[0..2], 16)
58                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
59                let g = u8::from_str_radix(&hex[2..4], 16)
60                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
61                let b = u8::from_str_radix(&hex[4..6], 16)
62                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
63                (r, g, b, 255)
64            }
65            8 => {
66                let r = u8::from_str_radix(&hex[0..2], 16)
67                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
68                let g = u8::from_str_radix(&hex[2..4], 16)
69                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
70                let b = u8::from_str_radix(&hex[4..6], 16)
71                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
72                let a = u8::from_str_radix(&hex[6..8], 16)
73                    .map_err(|_| crate::SubtitleError::InvalidColor(hex.to_string()))?;
74                (r, g, b, a)
75            }
76            _ => return Err(crate::SubtitleError::InvalidColor(hex.to_string())),
77        };
78
79        Ok(Self::new(r, g, b, a))
80    }
81
82    /// Blend this color with another using alpha compositing.
83    #[must_use]
84    pub fn blend_over(&self, background: Color) -> Color {
85        if self.a == 255 {
86            return *self;
87        }
88        if self.a == 0 {
89            return background;
90        }
91
92        let alpha = f32::from(self.a) / 255.0;
93        let inv_alpha = 1.0 - alpha;
94
95        Color::new(
96            (f32::from(self.r) * alpha + f32::from(background.r) * inv_alpha) as u8,
97            (f32::from(self.g) * alpha + f32::from(background.g) * inv_alpha) as u8,
98            (f32::from(self.b) * alpha + f32::from(background.b) * inv_alpha) as u8,
99            255,
100        )
101    }
102
103    /// Create a color with modified alpha.
104    #[must_use]
105    pub const fn with_alpha(&self, a: u8) -> Self {
106        Self::new(self.r, self.g, self.b, a)
107    }
108}
109
110impl Default for Color {
111    fn default() -> Self {
112        Self::white()
113    }
114}
115
116/// Text alignment.
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
118pub enum Alignment {
119    /// Left aligned.
120    Left,
121    /// Center aligned.
122    #[default]
123    Center,
124    /// Right aligned.
125    Right,
126}
127
128/// Vertical alignment.
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
130pub enum VerticalAlignment {
131    /// Top aligned.
132    Top,
133    /// Middle aligned.
134    Middle,
135    /// Bottom aligned.
136    #[default]
137    Bottom,
138}
139
140/// Subtitle position on screen.
141#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct Position {
143    /// Horizontal position (0.0 = left, 1.0 = right).
144    pub x: f32,
145    /// Vertical position (0.0 = top, 1.0 = bottom).
146    pub y: f32,
147    /// Horizontal alignment.
148    pub alignment: Alignment,
149    /// Vertical alignment.
150    pub vertical_alignment: VerticalAlignment,
151}
152
153impl Position {
154    /// Create a new position.
155    #[must_use]
156    pub const fn new(x: f32, y: f32) -> Self {
157        Self {
158            x,
159            y,
160            alignment: Alignment::Center,
161            vertical_alignment: VerticalAlignment::Bottom,
162        }
163    }
164
165    /// Bottom center position (default).
166    #[must_use]
167    pub const fn bottom_center() -> Self {
168        Self::new(0.5, 0.9)
169    }
170
171    /// Top center position.
172    #[must_use]
173    pub const fn top_center() -> Self {
174        Self::new(0.5, 0.1)
175    }
176
177    /// Middle center position.
178    #[must_use]
179    pub const fn middle_center() -> Self {
180        Self::new(0.5, 0.5)
181    }
182
183    /// Set alignment.
184    #[must_use]
185    pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
186        self.alignment = alignment;
187        self
188    }
189
190    /// Set vertical alignment.
191    #[must_use]
192    pub const fn with_vertical_alignment(mut self, vertical_alignment: VerticalAlignment) -> Self {
193        self.vertical_alignment = vertical_alignment;
194        self
195    }
196}
197
198impl Default for Position {
199    fn default() -> Self {
200        Self::bottom_center()
201    }
202}
203
204/// Outline style for text.
205#[derive(Clone, Copy, Debug, PartialEq)]
206pub struct OutlineStyle {
207    /// Outline color.
208    pub color: Color,
209    /// Outline width in pixels.
210    pub width: f32,
211}
212
213impl OutlineStyle {
214    /// Create a new outline style.
215    #[must_use]
216    pub const fn new(color: Color, width: f32) -> Self {
217        Self { color, width }
218    }
219
220    /// Default black outline.
221    #[must_use]
222    pub const fn black(width: f32) -> Self {
223        Self::new(Color::black(), width)
224    }
225}
226
227impl Default for OutlineStyle {
228    fn default() -> Self {
229        Self::black(2.0)
230    }
231}
232
233/// Shadow style for text.
234#[derive(Clone, Copy, Debug, PartialEq)]
235pub struct ShadowStyle {
236    /// Shadow color.
237    pub color: Color,
238    /// Shadow offset X in pixels.
239    pub offset_x: f32,
240    /// Shadow offset Y in pixels.
241    pub offset_y: f32,
242    /// Shadow blur radius in pixels.
243    pub blur: f32,
244}
245
246impl ShadowStyle {
247    /// Create a new shadow style.
248    #[must_use]
249    pub const fn new(color: Color, offset_x: f32, offset_y: f32, blur: f32) -> Self {
250        Self {
251            color,
252            offset_x,
253            offset_y,
254            blur,
255        }
256    }
257
258    /// Default drop shadow.
259    #[must_use]
260    pub const fn default_shadow() -> Self {
261        Self::new(Color::new(0, 0, 0, 128), 2.0, 2.0, 0.0)
262    }
263}
264
265impl Default for ShadowStyle {
266    fn default() -> Self {
267        Self::default_shadow()
268    }
269}
270
271/// Font weight.
272#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
273pub enum FontWeight {
274    /// Thin (100).
275    Thin,
276    /// Extra light (200).
277    ExtraLight,
278    /// Light (300).
279    Light,
280    /// Normal (400).
281    #[default]
282    Normal,
283    /// Medium (500).
284    Medium,
285    /// Semi bold (600).
286    SemiBold,
287    /// Bold (700).
288    Bold,
289    /// Extra bold (800).
290    ExtraBold,
291    /// Black (900).
292    Black,
293}
294
295/// Font style.
296#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
297pub enum FontStyle {
298    /// Normal/Roman style.
299    #[default]
300    Normal,
301    /// Italic style.
302    Italic,
303    /// Oblique style.
304    Oblique,
305}
306
307/// Animation effect for subtitles.
308#[derive(Clone, Debug, PartialEq)]
309pub enum Animation {
310    /// Fade in over duration (milliseconds).
311    FadeIn(i64),
312    /// Fade out over duration (milliseconds).
313    FadeOut(i64),
314    /// Move from position to position over duration.
315    Move {
316        /// Start position.
317        from: Position,
318        /// End position.
319        to: Position,
320        /// Duration in milliseconds.
321        duration: i64,
322    },
323    /// Karaoke effect - highlight words as they're sung.
324    Karaoke {
325        /// Highlight color.
326        color: Color,
327        /// Timing for each syllable/word in milliseconds.
328        timings: Vec<i64>,
329    },
330    /// Scale animation.
331    Scale {
332        /// Start scale (1.0 = normal).
333        from: f32,
334        /// End scale.
335        to: f32,
336        /// Duration in milliseconds.
337        duration: i64,
338    },
339    /// Rotation animation (degrees).
340    Rotate {
341        /// Start angle.
342        from: f32,
343        /// End angle.
344        to: f32,
345        /// Duration in milliseconds.
346        duration: i64,
347    },
348}
349
350/// Complete subtitle style configuration.
351#[derive(Clone, Debug, PartialEq)]
352pub struct SubtitleStyle {
353    /// Font size in pixels.
354    pub font_size: f32,
355    /// Font weight.
356    pub font_weight: FontWeight,
357    /// Font style.
358    pub font_style: FontStyle,
359    /// Primary text color.
360    pub primary_color: Color,
361    /// Secondary color (for karaoke, gradients).
362    pub secondary_color: Color,
363    /// Outline style.
364    pub outline: Option<OutlineStyle>,
365    /// Shadow style.
366    pub shadow: Option<ShadowStyle>,
367    /// Text alignment.
368    pub alignment: Alignment,
369    /// Vertical alignment.
370    pub vertical_alignment: VerticalAlignment,
371    /// Default position.
372    pub position: Position,
373    /// Margin from edges (in pixels).
374    pub margin_left: u32,
375    /// Right margin.
376    pub margin_right: u32,
377    /// Top margin.
378    pub margin_top: u32,
379    /// Bottom margin.
380    pub margin_bottom: u32,
381    /// Line spacing multiplier (1.0 = normal).
382    pub line_spacing: f32,
383    /// Enable word wrapping.
384    pub word_wrap: bool,
385    /// Maximum width for wrapping (0 = use frame width with margins).
386    pub max_width: u32,
387    /// Background box color (if any).
388    pub background_color: Option<Color>,
389    /// Background box padding.
390    pub background_padding: f32,
391}
392
393impl SubtitleStyle {
394    /// Create a new subtitle style with defaults.
395    #[must_use]
396    pub fn new() -> Self {
397        Self::default()
398    }
399
400    /// Set font size.
401    #[must_use]
402    pub const fn with_font_size(mut self, size: f32) -> Self {
403        self.font_size = size;
404        self
405    }
406
407    /// Set primary color.
408    #[must_use]
409    pub fn with_color(mut self, r: u8, g: u8, b: u8, a: u8) -> Self {
410        self.primary_color = Color::new(r, g, b, a);
411        self
412    }
413
414    /// Set outline.
415    #[must_use]
416    pub fn with_outline(mut self, outline: OutlineStyle) -> Self {
417        self.outline = Some(outline);
418        self
419    }
420
421    /// Set shadow.
422    #[must_use]
423    pub fn with_shadow(mut self, shadow: ShadowStyle) -> Self {
424        self.shadow = Some(shadow);
425        self
426    }
427
428    /// Set alignment.
429    #[must_use]
430    pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
431        self.alignment = alignment;
432        self
433    }
434
435    /// Set position.
436    #[must_use]
437    pub const fn with_position(mut self, position: Position) -> Self {
438        self.position = position;
439        self
440    }
441
442    /// Set margins.
443    #[must_use]
444    pub const fn with_margins(mut self, left: u32, right: u32, top: u32, bottom: u32) -> Self {
445        self.margin_left = left;
446        self.margin_right = right;
447        self.margin_top = top;
448        self.margin_bottom = bottom;
449        self
450    }
451
452    /// Enable background box.
453    #[must_use]
454    pub fn with_background(mut self, color: Color, padding: f32) -> Self {
455        self.background_color = Some(color);
456        self.background_padding = padding;
457        self
458    }
459}
460
461impl Default for SubtitleStyle {
462    fn default() -> Self {
463        Self {
464            font_size: 48.0,
465            font_weight: FontWeight::Normal,
466            font_style: FontStyle::Normal,
467            primary_color: Color::white(),
468            secondary_color: Color::rgb(255, 255, 0), // Yellow for karaoke
469            outline: Some(OutlineStyle::default()),
470            shadow: Some(ShadowStyle::default()),
471            alignment: Alignment::Center,
472            vertical_alignment: VerticalAlignment::Bottom,
473            position: Position::default(),
474            margin_left: 40,
475            margin_right: 40,
476            margin_top: 40,
477            margin_bottom: 40,
478            line_spacing: 1.2,
479            word_wrap: true,
480            max_width: 0,
481            background_color: None,
482            background_padding: 4.0,
483        }
484    }
485}