1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub struct Color {
6 pub r: u8,
8 pub g: u8,
10 pub b: u8,
12 pub a: u8,
14}
15
16impl Color {
17 #[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 #[must_use]
25 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
26 Self::new(r, g, b, 255)
27 }
28
29 #[must_use]
31 pub const fn white() -> Self {
32 Self::rgb(255, 255, 255)
33 }
34
35 #[must_use]
37 pub const fn black() -> Self {
38 Self::rgb(0, 0, 0)
39 }
40
41 #[must_use]
43 pub const fn transparent() -> Self {
44 Self::new(0, 0, 0, 0)
45 }
46
47 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 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
118pub enum Alignment {
119 Left,
121 #[default]
123 Center,
124 Right,
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
130pub enum VerticalAlignment {
131 Top,
133 Middle,
135 #[default]
137 Bottom,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct Position {
143 pub x: f32,
145 pub y: f32,
147 pub alignment: Alignment,
149 pub vertical_alignment: VerticalAlignment,
151}
152
153impl Position {
154 #[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 #[must_use]
167 pub const fn bottom_center() -> Self {
168 Self::new(0.5, 0.9)
169 }
170
171 #[must_use]
173 pub const fn top_center() -> Self {
174 Self::new(0.5, 0.1)
175 }
176
177 #[must_use]
179 pub const fn middle_center() -> Self {
180 Self::new(0.5, 0.5)
181 }
182
183 #[must_use]
185 pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
186 self.alignment = alignment;
187 self
188 }
189
190 #[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#[derive(Clone, Copy, Debug, PartialEq)]
206pub struct OutlineStyle {
207 pub color: Color,
209 pub width: f32,
211}
212
213impl OutlineStyle {
214 #[must_use]
216 pub const fn new(color: Color, width: f32) -> Self {
217 Self { color, width }
218 }
219
220 #[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#[derive(Clone, Copy, Debug, PartialEq)]
235pub struct ShadowStyle {
236 pub color: Color,
238 pub offset_x: f32,
240 pub offset_y: f32,
242 pub blur: f32,
244}
245
246impl ShadowStyle {
247 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
273pub enum FontWeight {
274 Thin,
276 ExtraLight,
278 Light,
280 #[default]
282 Normal,
283 Medium,
285 SemiBold,
287 Bold,
289 ExtraBold,
291 Black,
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
297pub enum FontStyle {
298 #[default]
300 Normal,
301 Italic,
303 Oblique,
305}
306
307#[derive(Clone, Debug, PartialEq)]
309pub enum Animation {
310 FadeIn(i64),
312 FadeOut(i64),
314 Move {
316 from: Position,
318 to: Position,
320 duration: i64,
322 },
323 Karaoke {
325 color: Color,
327 timings: Vec<i64>,
329 },
330 Scale {
332 from: f32,
334 to: f32,
336 duration: i64,
338 },
339 Rotate {
341 from: f32,
343 to: f32,
345 duration: i64,
347 },
348}
349
350#[derive(Clone, Debug, PartialEq)]
352pub struct SubtitleStyle {
353 pub font_size: f32,
355 pub font_weight: FontWeight,
357 pub font_style: FontStyle,
359 pub primary_color: Color,
361 pub secondary_color: Color,
363 pub outline: Option<OutlineStyle>,
365 pub shadow: Option<ShadowStyle>,
367 pub alignment: Alignment,
369 pub vertical_alignment: VerticalAlignment,
371 pub position: Position,
373 pub margin_left: u32,
375 pub margin_right: u32,
377 pub margin_top: u32,
379 pub margin_bottom: u32,
381 pub line_spacing: f32,
383 pub word_wrap: bool,
385 pub max_width: u32,
387 pub background_color: Option<Color>,
389 pub background_padding: f32,
391}
392
393impl SubtitleStyle {
394 #[must_use]
396 pub fn new() -> Self {
397 Self::default()
398 }
399
400 #[must_use]
402 pub const fn with_font_size(mut self, size: f32) -> Self {
403 self.font_size = size;
404 self
405 }
406
407 #[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 #[must_use]
416 pub fn with_outline(mut self, outline: OutlineStyle) -> Self {
417 self.outline = Some(outline);
418 self
419 }
420
421 #[must_use]
423 pub fn with_shadow(mut self, shadow: ShadowStyle) -> Self {
424 self.shadow = Some(shadow);
425 self
426 }
427
428 #[must_use]
430 pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
431 self.alignment = alignment;
432 self
433 }
434
435 #[must_use]
437 pub const fn with_position(mut self, position: Position) -> Self {
438 self.position = position;
439 self
440 }
441
442 #[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 #[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), 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}