1use rassa_core::{
2 Point, RassaResult, Rect,
3 ass::{self, TrackType, YCbCrMatrix},
4};
5
6#[derive(Clone, Debug, Default, PartialEq, Eq)]
7pub struct ParsedAttachment {
8 pub name: String,
9 pub data: Vec<u8>,
10}
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct ParsedStyle {
14 pub name: String,
15 pub font_name: String,
16 pub font_size: f64,
17 pub primary_colour: u32,
18 pub secondary_colour: u32,
19 pub outline_colour: u32,
20 pub back_colour: u32,
21 pub bold: bool,
22 pub italic: bool,
23 pub underline: bool,
24 pub strike_out: bool,
25 pub scale_x: f64,
26 pub scale_y: f64,
27 pub spacing: f64,
28 pub angle: f64,
29 pub border_style: i32,
30 pub outline: f64,
31 pub shadow: f64,
32 pub alignment: i32,
33 pub margin_l: i32,
34 pub margin_r: i32,
35 pub margin_v: i32,
36 pub encoding: i32,
37 pub treat_fontname_as_pattern: i32,
38 pub blur: f64,
39 pub justify: i32,
40}
41
42impl Default for ParsedStyle {
43 fn default() -> Self {
44 Self {
45 name: "Default".to_string(),
46 font_name: "Arial".to_string(),
47 font_size: 20.0,
48 primary_colour: 0x0000_00ff,
49 secondary_colour: 0x0000_ffff,
50 outline_colour: 0x0000_0000,
51 back_colour: 0x0000_0000,
52 bold: false,
53 italic: false,
54 underline: false,
55 strike_out: false,
56 scale_x: 1.0,
57 scale_y: 1.0,
58 spacing: 0.0,
59 angle: 0.0,
60 border_style: 1,
61 outline: 2.0,
62 shadow: 2.0,
63 alignment: ass::VALIGN_SUB | ass::HALIGN_CENTER,
64 margin_l: 10,
65 margin_r: 10,
66 margin_v: 10,
67 encoding: 1,
68 treat_fontname_as_pattern: 0,
69 blur: 0.0,
70 justify: ass::ASS_JUSTIFY_AUTO,
71 }
72 }
73}
74
75#[derive(Clone, Debug, Default, PartialEq, Eq)]
76pub struct ParsedEvent {
77 pub start: i64,
78 pub duration: i64,
79 pub read_order: i32,
80 pub layer: i32,
81 pub style: i32,
82 pub name: String,
83 pub margin_l: i32,
84 pub margin_r: i32,
85 pub margin_v: i32,
86 pub effect: String,
87 pub text: String,
88}
89
90#[derive(Clone, Debug, PartialEq)]
91pub struct ParsedSpanStyle {
92 pub font_name: String,
93 pub font_size: f64,
94 pub scale_x: f64,
95 pub scale_y: f64,
96 pub spacing: f64,
97 pub underline: bool,
98 pub strike_out: bool,
99 pub rotation_x: f64,
100 pub rotation_y: f64,
101 pub rotation_z: f64,
102 pub shear_x: f64,
103 pub shear_y: f64,
104 pub bold: bool,
105 pub italic: bool,
106 pub primary_colour: u32,
107 pub secondary_colour: u32,
108 pub outline_colour: u32,
109 pub back_colour: u32,
110 pub border: f64,
111 pub border_x: f64,
112 pub border_y: f64,
113 pub shadow: f64,
114 pub shadow_x: f64,
115 pub shadow_y: f64,
116 pub blur: f64,
117 pub be: f64,
118 pub pbo: f64,
119}
120
121#[derive(Clone, Debug, Default, PartialEq)]
122pub struct ParsedAnimatedStyle {
123 pub font_size: Option<f64>,
124 pub scale_x: Option<f64>,
125 pub scale_y: Option<f64>,
126 pub spacing: Option<f64>,
127 pub rotation_x: Option<f64>,
128 pub rotation_y: Option<f64>,
129 pub rotation_z: Option<f64>,
130 pub shear_x: Option<f64>,
131 pub shear_y: Option<f64>,
132 pub primary_colour: Option<u32>,
133 pub secondary_colour: Option<u32>,
134 pub outline_colour: Option<u32>,
135 pub back_colour: Option<u32>,
136 pub border: Option<f64>,
137 pub border_x: Option<f64>,
138 pub border_y: Option<f64>,
139 pub shadow: Option<f64>,
140 pub shadow_x: Option<f64>,
141 pub shadow_y: Option<f64>,
142 pub blur: Option<f64>,
143 pub be: Option<f64>,
144}
145
146impl ParsedAnimatedStyle {
147 fn is_empty(&self) -> bool {
148 self.font_size.is_none()
149 && self.scale_x.is_none()
150 && self.scale_y.is_none()
151 && self.spacing.is_none()
152 && self.rotation_x.is_none()
153 && self.rotation_y.is_none()
154 && self.rotation_z.is_none()
155 && self.shear_x.is_none()
156 && self.shear_y.is_none()
157 && self.primary_colour.is_none()
158 && self.secondary_colour.is_none()
159 && self.outline_colour.is_none()
160 && self.back_colour.is_none()
161 && self.border.is_none()
162 && self.border_x.is_none()
163 && self.border_y.is_none()
164 && self.shadow.is_none()
165 && self.shadow_x.is_none()
166 && self.shadow_y.is_none()
167 && self.blur.is_none()
168 && self.be.is_none()
169 }
170}
171
172#[derive(Clone, Debug, PartialEq)]
173pub struct ParsedSpanTransform {
174 pub start_ms: i32,
175 pub end_ms: Option<i32>,
176 pub accel: f64,
177 pub style: ParsedAnimatedStyle,
178}
179
180impl Default for ParsedSpanStyle {
181 fn default() -> Self {
182 Self {
183 font_name: ParsedStyle::default().font_name,
184 font_size: ParsedStyle::default().font_size,
185 scale_x: ParsedStyle::default().scale_x,
186 scale_y: ParsedStyle::default().scale_y,
187 spacing: ParsedStyle::default().spacing,
188 underline: false,
189 strike_out: false,
190 rotation_x: 0.0,
191 rotation_y: 0.0,
192 rotation_z: ParsedStyle::default().angle,
193 shear_x: 0.0,
194 shear_y: 0.0,
195 bold: false,
196 italic: false,
197 primary_colour: ParsedStyle::default().primary_colour,
198 secondary_colour: ParsedStyle::default().secondary_colour,
199 outline_colour: ParsedStyle::default().outline_colour,
200 back_colour: ParsedStyle::default().back_colour,
201 border: ParsedStyle::default().outline,
202 border_x: ParsedStyle::default().outline,
203 border_y: ParsedStyle::default().outline,
204 shadow: ParsedStyle::default().shadow,
205 shadow_x: ParsedStyle::default().shadow,
206 shadow_y: ParsedStyle::default().shadow,
207 blur: ParsedStyle::default().blur,
208 be: 0.0,
209 pbo: 0.0,
210 }
211 }
212}
213
214impl ParsedSpanStyle {
215 fn from_style(style: &ParsedStyle) -> Self {
216 Self {
217 font_name: style.font_name.clone(),
218 font_size: style.font_size,
219 scale_x: style.scale_x,
220 scale_y: style.scale_y,
221 spacing: style.spacing,
222 underline: style.underline,
223 strike_out: style.strike_out,
224 rotation_x: 0.0,
225 rotation_y: 0.0,
226 rotation_z: style.angle,
227 shear_x: 0.0,
228 shear_y: 0.0,
229 bold: style.bold,
230 italic: style.italic,
231 primary_colour: style.primary_colour,
232 secondary_colour: style.secondary_colour,
233 outline_colour: style.outline_colour,
234 back_colour: style.back_colour,
235 border: style.outline,
236 border_x: style.outline,
237 border_y: style.outline,
238 shadow: style.shadow,
239 shadow_x: style.shadow,
240 shadow_y: style.shadow,
241 blur: style.blur,
242 be: 0.0,
243 pbo: 0.0,
244 }
245 }
246}
247
248#[derive(Clone, Debug, Default, PartialEq)]
249pub struct ParsedTextSpan {
250 pub text: String,
251 pub style: ParsedSpanStyle,
252 pub transforms: Vec<ParsedSpanTransform>,
253 pub karaoke: Option<ParsedKaraokeSpan>,
254 pub drawing: Option<ParsedDrawing>,
255}
256
257#[derive(Clone, Debug, Default, PartialEq)]
258pub struct ParsedTextLine {
259 pub text: String,
260 pub spans: Vec<ParsedTextSpan>,
261}
262
263#[derive(Clone, Debug, Default, PartialEq)]
264pub struct ParsedDialogueText {
265 pub lines: Vec<ParsedTextLine>,
266 pub alignment: Option<i32>,
267 pub position: Option<(i32, i32)>,
268 pub movement: Option<ParsedMovement>,
269 pub fade: Option<ParsedFade>,
270 pub clip_rect: Option<Rect>,
271 pub vector_clip: Option<ParsedVectorClip>,
272 pub inverse_clip: bool,
273 pub wrap_style: Option<i32>,
274 pub origin: Option<(i32, i32)>,
275}
276
277#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
278pub struct ParsedMovement {
279 pub start: (i32, i32),
280 pub end: (i32, i32),
281 pub t1_ms: i32,
282 pub t2_ms: i32,
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub enum ParsedFade {
287 Simple {
288 fade_in_ms: i32,
289 fade_out_ms: i32,
290 },
291 Complex {
292 alpha1: i32,
293 alpha2: i32,
294 alpha3: i32,
295 t1_ms: i32,
296 t2_ms: i32,
297 t3_ms: i32,
298 t4_ms: i32,
299 },
300}
301
302#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
303pub enum ParsedKaraokeMode {
304 #[default]
305 FillSwap,
306 Sweep,
307 OutlineToggle,
308}
309
310#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
311pub struct ParsedKaraokeSpan {
312 pub start_ms: i32,
313 pub duration_ms: i32,
314 pub mode: ParsedKaraokeMode,
315}
316
317#[derive(Clone, Debug, Default, PartialEq, Eq)]
318pub struct ParsedVectorClip {
319 pub scale: i32,
320 pub polygons: Vec<Vec<Point>>,
321}
322
323#[derive(Clone, Debug, Default, PartialEq, Eq)]
324pub struct ParsedDrawing {
325 pub scale: i32,
326 pub polygons: Vec<Vec<Point>>,
327}
328
329impl ParsedVectorClip {
330 pub fn bounds(&self) -> Option<Rect> {
331 bounds_from_polygons(&self.polygons)
332 }
333}
334
335impl ParsedDrawing {
336 pub fn bounds(&self) -> Option<Rect> {
337 bounds_from_polygons(&self.polygons)
338 }
339}
340
341#[derive(Clone, Debug, PartialEq)]
342pub struct ParsedTrack {
343 pub styles: Vec<ParsedStyle>,
344 pub events: Vec<ParsedEvent>,
345 pub attachments: Vec<ParsedAttachment>,
346 pub style_format: String,
347 pub event_format: String,
348 pub track_type: TrackType,
349 pub play_res_x: i32,
350 pub play_res_y: i32,
351 pub timer: f64,
352 pub wrap_style: i32,
353 pub scaled_border_and_shadow: bool,
354 pub kerning: bool,
355 pub language: String,
356 pub ycbcr_matrix: YCbCrMatrix,
357 pub default_style: i32,
358 pub layout_res_x: i32,
359 pub layout_res_y: i32,
360}
361
362impl Default for ParsedTrack {
363 fn default() -> Self {
364 Self {
365 styles: Vec::new(),
366 events: Vec::new(),
367 attachments: Vec::new(),
368 style_format: String::new(),
369 event_format: String::new(),
370 track_type: TrackType::Unknown,
371 play_res_x: 384,
372 play_res_y: 288,
373 timer: 100.0,
374 wrap_style: 0,
375 scaled_border_and_shadow: true,
376 kerning: true,
377 language: String::new(),
378 ycbcr_matrix: YCbCrMatrix::Default,
379 default_style: 0,
380 layout_res_x: 0,
381 layout_res_y: 0,
382 }
383 }
384}
385
386pub fn parse_script_bytes(bytes: &[u8]) -> RassaResult<ParsedTrack> {
387 match std::str::from_utf8(bytes) {
388 Ok(text) => parse_script_text(text),
389 Err(_) => parse_script_text(&String::from_utf8_lossy(bytes)),
390 }
391}
392
393pub fn parse_script_text(text: &str) -> RassaResult<ParsedTrack> {
394 let mut track = ParsedTrack::default();
395 let mut section = String::new();
396 let mut style_format: Vec<String> = Vec::new();
397 let mut event_format: Vec<String> = Vec::new();
398 let mut pending_font_name: Option<String> = None;
399 let mut pending_font_data = String::new();
400
401 for raw_line in text.lines() {
402 let line = raw_line.trim_matches(|character| character == '\u{feff}' || character == '\r');
403 let line = line.trim();
404 if line.is_empty() || line.starts_with(';') {
405 continue;
406 }
407
408 if line.starts_with('[') && line.ends_with(']') {
409 flush_font_attachment(&mut track, &mut pending_font_name, &mut pending_font_data);
410 section.clear();
411 section.push_str(&line[1..line.len() - 1].to_ascii_lowercase());
412 if section == "v4+ styles" {
413 track.track_type = TrackType::Ass;
414 } else if section == "v4 styles" && track.track_type == TrackType::Unknown {
415 track.track_type = TrackType::Ssa;
416 }
417 continue;
418 }
419
420 if section == "fonts" {
421 process_font_line(
422 line,
423 &mut track,
424 &mut pending_font_name,
425 &mut pending_font_data,
426 );
427 continue;
428 }
429
430 let Some((key, value)) = split_once_colon(line) else {
431 continue;
432 };
433
434 match section.as_str() {
435 "script info" => apply_script_info_field(&mut track, key, value),
436 "v4+ styles" | "v4 styles" => {
437 if key.eq_ignore_ascii_case("Format") {
438 track.style_format = value.trim().to_string();
439 style_format = parse_format_fields(value);
440 } else if key.eq_ignore_ascii_case("Style") {
441 if style_format.is_empty() {
442 style_format = default_style_format();
443 if track.style_format.is_empty() {
444 track.style_format = style_format.join(", ");
445 }
446 }
447 if let Some(style) = parse_style_line(value, &style_format) {
448 track.styles.push(style);
449 }
450 }
451 }
452 "events" => {
453 if key.eq_ignore_ascii_case("Format") {
454 track.event_format = value.trim().to_string();
455 event_format = parse_format_fields(value);
456 } else if key.eq_ignore_ascii_case("Dialogue") {
457 if event_format.is_empty() {
458 event_format = default_event_format();
459 if track.event_format.is_empty() {
460 track.event_format = event_format.join(", ");
461 }
462 }
463 if let Some(event) = parse_event_line(
464 value,
465 &event_format,
466 track.events.len() as i32,
467 &track.styles,
468 ) {
469 track.events.push(event);
470 }
471 }
472 }
473 _ => {}
474 }
475 }
476
477 flush_font_attachment(&mut track, &mut pending_font_name, &mut pending_font_data);
478
479 if track.styles.is_empty() {
480 track.styles.push(ParsedStyle::default());
481 }
482
483 if track.style_format.is_empty() {
484 track.style_format = default_style_format().join(", ");
485 }
486 if track.event_format.is_empty() {
487 track.event_format = default_event_format().join(", ");
488 }
489
490 Ok(track)
491}
492
493fn process_font_line(
494 line: &str,
495 track: &mut ParsedTrack,
496 pending_font_name: &mut Option<String>,
497 pending_font_data: &mut String,
498) {
499 if let Some(name) = line.strip_prefix("fontname:") {
500 flush_font_attachment(track, pending_font_name, pending_font_data);
501 *pending_font_name = Some(name.trim().to_string());
502 return;
503 }
504
505 if pending_font_name.is_some() {
506 pending_font_data.push_str(line.trim());
507 }
508}
509
510fn flush_font_attachment(
511 track: &mut ParsedTrack,
512 pending_font_name: &mut Option<String>,
513 pending_font_data: &mut String,
514) {
515 let Some(name) = pending_font_name.take() else {
516 pending_font_data.clear();
517 return;
518 };
519
520 let encoded = std::mem::take(pending_font_data);
521 if let Some(data) = decode_embedded_font(&encoded) {
522 track.attachments.push(ParsedAttachment { name, data });
523 }
524}
525
526fn decode_embedded_font(encoded: &str) -> Option<Vec<u8>> {
527 let encoded = encoded.trim();
528 if encoded.is_empty() {
529 return Some(Vec::new());
530 }
531 if encoded.len() % 4 == 1 {
532 return None;
533 }
534
535 let bytes = encoded.as_bytes();
536 let mut decoded = Vec::with_capacity(encoded.len() / 4 * 3 + encoded.len() % 4);
537 let mut offset = 0;
538 while offset + 4 <= bytes.len() {
539 decode_chars(&bytes[offset..offset + 4], &mut decoded);
540 offset += 4;
541 }
542 match bytes.len() - offset {
543 0 => {}
544 2 => decode_chars(&bytes[offset..offset + 2], &mut decoded),
545 3 => decode_chars(&bytes[offset..offset + 3], &mut decoded),
546 _ => return None,
547 }
548
549 Some(decoded)
550}
551
552fn decode_chars(src: &[u8], dst: &mut Vec<u8>) {
553 let mut value = 0_u32;
554 for (index, byte) in src.iter().enumerate() {
555 value |= u32::from(byte.saturating_sub(33) & 63) << (6 * (3 - index));
556 }
557
558 dst.push((value >> 16) as u8);
559 if src.len() >= 3 {
560 dst.push(((value >> 8) & 0xFF) as u8);
561 }
562 if src.len() >= 4 {
563 dst.push((value & 0xFF) as u8);
564 }
565}
566
567pub fn parse_dialogue_text(
568 text: &str,
569 base_style: &ParsedStyle,
570 styles: &[ParsedStyle],
571) -> ParsedDialogueText {
572 let mut parsed = ParsedDialogueText::default();
573 let mut current_style = ParsedSpanStyle::from_style(base_style);
574 let mut active_line = ParsedTextLine::default();
575 let mut buffer = String::new();
576 let mut pending_karaoke = None;
577 let mut karaoke_cursor_ms = 0;
578 let mut drawing_scale = 0;
579 let mut current_transforms = Vec::new();
580 let mut characters = text.chars().peekable();
581
582 while let Some(character) = characters.next() {
583 match character {
584 '{' => {
585 let mut tag_block = String::new();
586 for next in characters.by_ref() {
587 if next == '}' {
588 break;
589 }
590 tag_block.push(next);
591 }
592 apply_override_block(
593 &tag_block,
594 base_style,
595 styles,
596 &mut current_style,
597 &mut parsed,
598 &mut buffer,
599 &mut active_line,
600 &mut pending_karaoke,
601 &mut karaoke_cursor_ms,
602 &mut drawing_scale,
603 &mut current_transforms,
604 );
605 }
606 '\\' => match characters.peek().copied() {
607 Some('N') | Some('n') => {
608 characters.next();
609 flush_span(
610 &mut buffer,
611 ¤t_style,
612 pending_karaoke,
613 drawing_scale,
614 ¤t_transforms,
615 &mut active_line,
616 );
617 push_line(&mut parsed, &mut active_line);
618 }
619 Some('h') => {
620 characters.next();
621 buffer.push('\u{00A0}');
622 }
623 Some(next) => {
624 characters.next();
625 buffer.push(next);
626 }
627 None => buffer.push(character),
628 },
629 '\n' => {
630 flush_span(
631 &mut buffer,
632 ¤t_style,
633 pending_karaoke,
634 drawing_scale,
635 ¤t_transforms,
636 &mut active_line,
637 );
638 push_line(&mut parsed, &mut active_line);
639 }
640 '\r' => {}
641 _ => buffer.push(character),
642 }
643 }
644
645 flush_span(
646 &mut buffer,
647 ¤t_style,
648 pending_karaoke,
649 drawing_scale,
650 ¤t_transforms,
651 &mut active_line,
652 );
653 push_line(&mut parsed, &mut active_line);
654 if parsed.lines.is_empty() {
655 parsed.lines.push(ParsedTextLine::default());
656 }
657 parsed
658}
659
660fn split_once_colon(line: &str) -> Option<(&str, &str)> {
661 let (key, value) = line.split_once(':')?;
662 Some((key.trim(), value.trim_start()))
663}
664
665fn parse_format_fields(value: &str) -> Vec<String> {
666 value
667 .split(',')
668 .map(|field| field.trim().to_string())
669 .filter(|field| !field.is_empty())
670 .collect()
671}
672
673fn default_style_format() -> Vec<String> {
674 [
675 "Name",
676 "Fontname",
677 "Fontsize",
678 "PrimaryColour",
679 "SecondaryColour",
680 "OutlineColour",
681 "BackColour",
682 "Bold",
683 "Italic",
684 "Underline",
685 "StrikeOut",
686 "ScaleX",
687 "ScaleY",
688 "Spacing",
689 "Angle",
690 "BorderStyle",
691 "Outline",
692 "Shadow",
693 "Alignment",
694 "MarginL",
695 "MarginR",
696 "MarginV",
697 "Encoding",
698 "Blur",
699 "Justify",
700 ]
701 .into_iter()
702 .map(str::to_string)
703 .collect()
704}
705
706fn default_event_format() -> Vec<String> {
707 [
708 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect", "Text",
709 ]
710 .into_iter()
711 .map(str::to_string)
712 .collect()
713}
714
715fn parse_style_line(value: &str, format: &[String]) -> Option<ParsedStyle> {
716 let fields = split_fields(value, format.len());
717 if fields.len() != format.len() {
718 return None;
719 }
720
721 let mut style = ParsedStyle::default();
722 for (key, raw_value) in format.iter().zip(fields) {
723 let lowered = key.to_ascii_lowercase();
724 match lowered.as_str() {
725 "name" => style.name = raw_value.trim().to_string(),
726 "fontname" => style.font_name = raw_value.trim().to_string(),
727 "fontsize" => style.font_size = parse_f64(raw_value, style.font_size),
728 "primarycolour" | "primarycolor" => {
729 style.primary_colour = parse_color(raw_value, style.primary_colour)
730 }
731 "secondarycolour" | "secondarycolor" => {
732 style.secondary_colour = parse_color(raw_value, style.secondary_colour)
733 }
734 "outlinecolour" | "outlinecolor" => {
735 style.outline_colour = parse_color(raw_value, style.outline_colour)
736 }
737 "backcolour" | "backcolor" => {
738 style.back_colour = parse_color(raw_value, style.back_colour)
739 }
740 "bold" => style.bold = parse_bool(raw_value, style.bold),
741 "italic" => style.italic = parse_bool(raw_value, style.italic),
742 "underline" => style.underline = parse_bool(raw_value, style.underline),
743 "strikeout" => style.strike_out = parse_bool(raw_value, style.strike_out),
744 "scalex" => style.scale_x = parse_scale(raw_value, style.scale_x),
745 "scaley" => style.scale_y = parse_scale(raw_value, style.scale_y),
746 "spacing" => style.spacing = parse_f64(raw_value, style.spacing),
747 "angle" => style.angle = parse_f64(raw_value, style.angle),
748 "borderstyle" => style.border_style = parse_i32(raw_value, style.border_style),
749 "outline" => style.outline = parse_f64(raw_value, style.outline),
750 "shadow" => style.shadow = parse_f64(raw_value, style.shadow),
751 "alignment" => {
752 let raw_alignment = parse_i32(raw_value, style.alignment);
753 style.alignment = alignment_from_an(raw_alignment).unwrap_or(style.alignment);
754 }
755 "marginl" => style.margin_l = parse_i32(raw_value, style.margin_l),
756 "marginr" => style.margin_r = parse_i32(raw_value, style.margin_r),
757 "marginv" => style.margin_v = parse_i32(raw_value, style.margin_v),
758 "encoding" => style.encoding = parse_i32(raw_value, style.encoding),
759 "treat_fontname_as_pattern" => {
760 style.treat_fontname_as_pattern =
761 parse_i32(raw_value, style.treat_fontname_as_pattern)
762 }
763 "blur" => style.blur = parse_f64(raw_value, style.blur),
764 "justify" => style.justify = parse_i32(raw_value, style.justify),
765 _ => {}
766 }
767 }
768
769 Some(style)
770}
771
772fn parse_event_line(
773 value: &str,
774 format: &[String],
775 read_order: i32,
776 styles: &[ParsedStyle],
777) -> Option<ParsedEvent> {
778 let fields = split_fields(value, format.len());
779 if fields.len() != format.len() {
780 return None;
781 }
782
783 let mut event = ParsedEvent {
784 read_order,
785 ..ParsedEvent::default()
786 };
787 let mut end = 0_i64;
788
789 for (key, raw_value) in format.iter().zip(fields) {
790 let lowered = key.to_ascii_lowercase();
791 match lowered.as_str() {
792 "layer" => event.layer = parse_i32(raw_value, event.layer),
793 "start" => event.start = parse_timestamp(raw_value).unwrap_or(event.start),
794 "end" => end = parse_timestamp(raw_value).unwrap_or(end),
795 "style" => event.style = parse_style_reference(raw_value, styles),
796 "name" => event.name = raw_value.trim().to_string(),
797 "marginl" => event.margin_l = parse_i32(raw_value, event.margin_l),
798 "marginr" => event.margin_r = parse_i32(raw_value, event.margin_r),
799 "marginv" => event.margin_v = parse_i32(raw_value, event.margin_v),
800 "effect" => event.effect = raw_value.to_string(),
801 "text" => event.text = raw_value.to_string(),
802 _ => {}
803 }
804 }
805
806 event.duration = (end - event.start).max(0);
807 Some(event)
808}
809
810fn split_fields(input: &str, field_count: usize) -> Vec<&str> {
811 if field_count == 0 {
812 return Vec::new();
813 }
814
815 let mut fields = Vec::with_capacity(field_count);
816 let mut remainder = input;
817 for _ in 0..field_count.saturating_sub(1) {
818 if let Some((head, tail)) = remainder.split_once(',') {
819 fields.push(head.trim());
820 remainder = tail;
821 } else {
822 fields.push(remainder.trim());
823 remainder = "";
824 }
825 }
826 fields.push(remainder.trim());
827 fields
828}
829
830fn apply_script_info_field(track: &mut ParsedTrack, key: &str, value: &str) {
831 match key.to_ascii_lowercase().as_str() {
832 "playresx" => track.play_res_x = parse_i32(value, track.play_res_x),
833 "playresy" => track.play_res_y = parse_i32(value, track.play_res_y),
834 "timer" => track.timer = parse_f64(value, track.timer),
835 "wrapstyle" => track.wrap_style = parse_i32(value, track.wrap_style),
836 "scaledborderandshadow" => {
837 track.scaled_border_and_shadow = parse_bool(value, track.scaled_border_and_shadow)
838 }
839 "kerning" => track.kerning = parse_bool(value, track.kerning),
840 "language" => track.language = value.trim().to_string(),
841 "layoutresx" => track.layout_res_x = parse_i32(value, track.layout_res_x),
842 "layoutresy" => track.layout_res_y = parse_i32(value, track.layout_res_y),
843 "ycbcr matrix" => track.ycbcr_matrix = parse_matrix(value),
844 _ => {}
845 }
846}
847
848fn parse_bool(value: &str, fallback: bool) -> bool {
849 match value.trim().parse::<i32>() {
850 Ok(parsed) => parsed != 0,
851 Err(_) => match value.trim().to_ascii_lowercase().as_str() {
852 "yes" | "true" => true,
853 "no" | "false" => false,
854 _ => fallback,
855 },
856 }
857}
858
859fn parse_i32(value: &str, fallback: i32) -> i32 {
860 value.trim().parse().unwrap_or(fallback)
861}
862
863fn parse_f64(value: &str, fallback: f64) -> f64 {
864 value.trim().parse().unwrap_or(fallback)
865}
866
867fn parse_scale(value: &str, fallback: f64) -> f64 {
868 let parsed = parse_f64(value, fallback * 100.0);
869 if parsed > 10.0 {
870 parsed / 100.0
871 } else {
872 parsed
873 }
874}
875
876fn parse_color(value: &str, fallback: u32) -> u32 {
877 let trimmed = value.trim();
878 if let Some(hex) = trimmed
879 .strip_prefix("&H")
880 .or_else(|| trimmed.strip_prefix("&h"))
881 {
882 let hex = hex.trim_end_matches('&');
883 u32::from_str_radix(hex, 16).unwrap_or(fallback)
884 } else {
885 trimmed.parse().unwrap_or(fallback)
886 }
887}
888
889fn parse_timestamp(value: &str) -> Option<i64> {
890 let mut parts = value.trim().split(':');
891 let hours = parts.next()?.trim().parse::<i64>().ok()?;
892 let minutes = parts.next()?.trim().parse::<i64>().ok()?;
893 let seconds = parts.next()?.trim();
894 let (seconds, centiseconds) = if let Some((seconds, fraction)) = seconds.split_once('.') {
895 let fraction = format!("{fraction:0<2}");
896 (
897 seconds.trim().parse::<i64>().ok()?,
898 fraction[..2].parse::<i64>().ok()?,
899 )
900 } else {
901 (seconds.parse::<i64>().ok()?, 0)
902 };
903 Some((((hours * 60 + minutes) * 60) + seconds) * 1000 + centiseconds * 10)
904}
905
906fn parse_style_reference(value: &str, styles: &[ParsedStyle]) -> i32 {
907 let style_name = value.trim();
908 if style_name.is_empty() {
909 return 0;
910 }
911
912 styles
913 .iter()
914 .position(|style| style.name.eq_ignore_ascii_case(style_name))
915 .map(|index| index as i32)
916 .unwrap_or(0)
917}
918
919#[allow(clippy::too_many_arguments)]
920fn apply_override_block(
921 block: &str,
922 base_style: &ParsedStyle,
923 styles: &[ParsedStyle],
924 current_style: &mut ParsedSpanStyle,
925 parsed: &mut ParsedDialogueText,
926 buffer: &mut String,
927 active_line: &mut ParsedTextLine,
928 pending_karaoke: &mut Option<ParsedKaraokeSpan>,
929 karaoke_cursor_ms: &mut i32,
930 drawing_scale: &mut i32,
931 current_transforms: &mut Vec<ParsedSpanTransform>,
932) {
933 for raw_tag in split_override_tags(block) {
934 let tag = raw_tag.trim();
935 if tag.is_empty() {
936 continue;
937 }
938
939 let previous = current_style.clone();
940 let previous_transforms = current_transforms.clone();
941 if let Some(rest) = tag.strip_prefix("fn") {
942 let family = rest.trim();
943 if !family.is_empty() {
944 current_style.font_name = family.to_string();
945 }
946 } else if let Some(rest) = tag.strip_prefix("kt") {
947 flush_span(
948 buffer,
949 &previous,
950 *pending_karaoke,
951 *drawing_scale,
952 &previous_transforms,
953 active_line,
954 );
955 *karaoke_cursor_ms = parse_karaoke_duration(rest).unwrap_or(0);
956 *pending_karaoke = None;
957 } else if let Some((rest, mode)) = tag
958 .strip_prefix("kf")
959 .map(|rest| (rest, ParsedKaraokeMode::Sweep))
960 .or_else(|| {
961 tag.strip_prefix("ko")
962 .map(|rest| (rest, ParsedKaraokeMode::OutlineToggle))
963 })
964 .or_else(|| {
965 tag.strip_prefix('K')
966 .map(|rest| (rest, ParsedKaraokeMode::Sweep))
967 })
968 .or_else(|| {
969 tag.strip_prefix('k')
970 .map(|rest| (rest, ParsedKaraokeMode::FillSwap))
971 })
972 {
973 flush_span(
974 buffer,
975 &previous,
976 *pending_karaoke,
977 *drawing_scale,
978 &previous_transforms,
979 active_line,
980 );
981 if let Some(duration_ms) = parse_karaoke_duration(rest) {
982 *pending_karaoke = Some(ParsedKaraokeSpan {
983 start_ms: *karaoke_cursor_ms,
984 duration_ms,
985 mode,
986 });
987 *karaoke_cursor_ms += duration_ms;
988 }
989 } else if let Some(rest) = tag.strip_prefix("fscx") {
990 current_style.scale_x = parse_scale(rest, base_style.scale_x);
991 } else if let Some(rest) = tag.strip_prefix("fscy") {
992 current_style.scale_y = parse_scale(rest, base_style.scale_y);
993 } else if tag == "fsc" {
994 current_style.scale_x = base_style.scale_x;
995 current_style.scale_y = base_style.scale_y;
996 } else if let Some(rest) = tag.strip_prefix("fsp") {
997 current_style.spacing = parse_f64(rest, current_style.spacing);
998 } else if let Some(rest) = tag.strip_prefix("frx") {
999 current_style.rotation_x = parse_f64(rest, current_style.rotation_x);
1000 } else if let Some(rest) = tag.strip_prefix("fry") {
1001 current_style.rotation_y = parse_f64(rest, current_style.rotation_y);
1002 } else if let Some(rest) = tag.strip_prefix("frz").or_else(|| tag.strip_prefix("fr")) {
1003 current_style.rotation_z = parse_f64(rest, current_style.rotation_z);
1004 } else if let Some(rest) = tag.strip_prefix("fax") {
1005 current_style.shear_x = parse_f64(rest, current_style.shear_x);
1006 } else if let Some(rest) = tag.strip_prefix("fay") {
1007 current_style.shear_y = parse_f64(rest, current_style.shear_y);
1008 } else if let Some(rest) = tag.strip_prefix("fs") {
1009 current_style.font_size =
1010 parse_font_size_override(rest, current_style.font_size, base_style.font_size);
1011 } else if let Some(rest) = tag.strip_prefix("iclip") {
1012 if let Some(rect) = parse_rect_clip(rest) {
1013 parsed.clip_rect = Some(rect);
1014 parsed.vector_clip = None;
1015 parsed.inverse_clip = true;
1016 } else if let Some(vector) = parse_vector_clip(rest) {
1017 parsed.clip_rect = None;
1018 parsed.vector_clip = Some(vector);
1019 parsed.inverse_clip = true;
1020 }
1021 } else if let Some(rest) = tag.strip_prefix("move") {
1022 if parsed.position.is_none() && parsed.movement.is_none() {
1023 parsed.movement = parse_move(rest);
1024 }
1025 } else if let Some(rest) = tag.strip_prefix("fade") {
1026 parsed.fade = parse_fade(rest);
1027 } else if let Some(rest) = tag.strip_prefix("fad") {
1028 parsed.fade = parse_fad(rest);
1029 } else if let Some(rest) = tag.strip_prefix("clip") {
1030 if let Some(rect) = parse_rect_clip(rest) {
1031 parsed.clip_rect = Some(rect);
1032 parsed.vector_clip = None;
1033 parsed.inverse_clip = false;
1034 } else if let Some(vector) = parse_vector_clip(rest) {
1035 parsed.clip_rect = None;
1036 parsed.vector_clip = Some(vector);
1037 parsed.inverse_clip = false;
1038 }
1039 } else if let Some(rest) = tag.strip_prefix("1c").or_else(|| tag.strip_prefix('c')) {
1040 current_style.primary_colour = parse_override_color(rest, current_style.primary_colour);
1041 } else if let Some(rest) = tag.strip_prefix("2c") {
1042 current_style.secondary_colour =
1043 parse_override_color(rest, current_style.secondary_colour);
1044 } else if let Some(rest) = tag.strip_prefix("3c") {
1045 current_style.outline_colour = parse_override_color(rest, current_style.outline_colour);
1046 } else if let Some(rest) = tag.strip_prefix("4c") {
1047 current_style.back_colour = parse_override_color(rest, current_style.back_colour);
1048 } else if let Some(rest) = tag.strip_prefix("alpha") {
1049 let alpha = parse_alpha_tag(rest, alpha_of(current_style.primary_colour));
1050 current_style.primary_colour = with_alpha(current_style.primary_colour, alpha);
1051 current_style.secondary_colour = with_alpha(current_style.secondary_colour, alpha);
1052 current_style.outline_colour = with_alpha(current_style.outline_colour, alpha);
1053 current_style.back_colour = with_alpha(current_style.back_colour, alpha);
1054 } else if let Some(rest) = tag.strip_prefix("1a") {
1055 let alpha = parse_alpha_tag(rest, alpha_of(current_style.primary_colour));
1056 current_style.primary_colour = with_alpha(current_style.primary_colour, alpha);
1057 } else if let Some(rest) = tag.strip_prefix("2a") {
1058 let alpha = parse_alpha_tag(rest, alpha_of(current_style.secondary_colour));
1059 current_style.secondary_colour = with_alpha(current_style.secondary_colour, alpha);
1060 } else if let Some(rest) = tag.strip_prefix("3a") {
1061 let alpha = parse_alpha_tag(rest, alpha_of(current_style.outline_colour));
1062 current_style.outline_colour = with_alpha(current_style.outline_colour, alpha);
1063 } else if let Some(rest) = tag.strip_prefix("4a") {
1064 let alpha = parse_alpha_tag(rest, alpha_of(current_style.back_colour));
1065 current_style.back_colour = with_alpha(current_style.back_colour, alpha);
1066 } else if let Some(rest) = tag.strip_prefix("xbord") {
1067 current_style.border_x = parse_f64(rest, current_style.border_x);
1068 } else if let Some(rest) = tag.strip_prefix("ybord") {
1069 current_style.border_y = parse_f64(rest, current_style.border_y);
1070 } else if let Some(rest) = tag.strip_prefix("bord") {
1071 current_style.border = parse_f64(rest, current_style.border);
1072 current_style.border_x = current_style.border;
1073 current_style.border_y = current_style.border;
1074 } else if let Some(rest) = tag.strip_prefix("xshad") {
1075 current_style.shadow_x = parse_f64(rest, current_style.shadow_x);
1076 } else if let Some(rest) = tag.strip_prefix("yshad") {
1077 current_style.shadow_y = parse_f64(rest, current_style.shadow_y);
1078 } else if let Some(rest) = tag.strip_prefix("shad") {
1079 current_style.shadow = parse_f64(rest, current_style.shadow);
1080 current_style.shadow_x = current_style.shadow;
1081 current_style.shadow_y = current_style.shadow;
1082 } else if let Some(rest) = tag.strip_prefix("blur") {
1083 current_style.blur = parse_f64(rest, current_style.blur);
1084 } else if let Some(rest) = tag.strip_prefix("be") {
1085 current_style.be = parse_f64(rest, current_style.be);
1086 } else if let Some(rest) = tag.strip_prefix('t') {
1087 if let Some(transform) = parse_transform(rest, current_style) {
1088 current_transforms.push(transform);
1089 }
1090 } else if let Some(rest) = tag.strip_prefix('u') {
1091 current_style.underline = parse_override_bool(rest, current_style.underline);
1092 } else if let Some(rest) = tag.strip_prefix('s') {
1093 current_style.strike_out = parse_override_bool(rest, current_style.strike_out);
1094 } else if let Some(rest) = tag.strip_prefix('b') {
1095 current_style.bold = parse_override_bool(rest, current_style.bold);
1096 } else if let Some(rest) = tag.strip_prefix('i') {
1097 current_style.italic = parse_override_bool(rest, current_style.italic);
1098 } else if let Some(rest) = tag.strip_prefix("an") {
1099 if let Ok(value) = rest.trim().parse::<i32>() {
1100 parsed.alignment = alignment_from_an(value);
1101 }
1102 } else if let Some(rest) = tag.strip_prefix('a') {
1103 if let Ok(value) = rest.trim().parse::<i32>() {
1104 parsed.alignment = alignment_from_legacy_a(value);
1105 }
1106 } else if let Some(rest) = tag.strip_prefix('q') {
1107 if let Ok(value) = rest.trim().parse::<i32>() {
1108 parsed.wrap_style = Some(value.clamp(0, 3));
1109 }
1110 } else if let Some(rest) = tag.strip_prefix("org") {
1111 parsed.origin = parse_pos(rest);
1112 } else if let Some(rest) = tag.strip_prefix("pos") {
1113 if parsed.position.is_none() && parsed.movement.is_none() {
1114 if let Some(position) = parse_pos(rest) {
1115 parsed.position = Some(position);
1116 }
1117 }
1118 } else if let Some(rest) = tag.strip_prefix("pbo") {
1119 current_style.pbo = parse_f64(rest, current_style.pbo);
1120 } else if let Some(rest) = tag.strip_prefix('p') {
1121 flush_span(
1122 buffer,
1123 &previous,
1124 *pending_karaoke,
1125 *drawing_scale,
1126 &previous_transforms,
1127 active_line,
1128 );
1129 *drawing_scale = parse_i32(rest, *drawing_scale).max(0);
1130 } else if let Some(rest) = tag.strip_prefix('r') {
1131 *current_style = resolve_reset_style(rest, base_style, styles);
1132 current_transforms.clear();
1133 }
1134
1135 if *current_style != previous || *current_transforms != previous_transforms {
1136 flush_span(
1137 buffer,
1138 &previous,
1139 *pending_karaoke,
1140 *drawing_scale,
1141 &previous_transforms,
1142 active_line,
1143 );
1144 }
1145 }
1146}
1147
1148fn parse_transform(value: &str, current_style: &ParsedSpanStyle) -> Option<ParsedSpanTransform> {
1149 let inside = value.trim().strip_prefix('(')?.strip_suffix(')')?.trim();
1150 let tag_start = inside.find('\\')?;
1151 let (timing_part, tags_part) = inside.split_at(tag_start);
1152 let params = timing_part
1153 .split(',')
1154 .map(str::trim)
1155 .filter(|part| !part.is_empty())
1156 .collect::<Vec<_>>();
1157
1158 let (start_ms, end_ms, accel) = match params.as_slice() {
1159 [] => (0, None, 1.0),
1160 [accel] => (0, None, parse_f64(accel, 1.0)),
1161 [start, end] => (
1162 parse_i32(start, 0).max(0),
1163 Some(parse_i32(end, 0).max(parse_i32(start, 0))),
1164 1.0,
1165 ),
1166 [start, end, accel, ..] => (
1167 parse_i32(start, 0).max(0),
1168 Some(parse_i32(end, 0).max(parse_i32(start, 0))),
1169 parse_f64(accel, 1.0),
1170 ),
1171 };
1172
1173 let mut target_style = current_style.clone();
1174 for raw_tag in split_override_tags(tags_part) {
1175 apply_transform_tag(raw_tag.trim(), &mut target_style);
1176 }
1177
1178 let animated = diff_animated_style(current_style, &target_style);
1179 (!animated.is_empty()).then_some(ParsedSpanTransform {
1180 start_ms,
1181 end_ms,
1182 accel: if accel > 0.0 { accel } else { 1.0 },
1183 style: animated,
1184 })
1185}
1186
1187fn split_override_tags(block: &str) -> Vec<&str> {
1188 let mut tags = Vec::new();
1189 let mut start = None;
1190 let mut depth = 0_i32;
1191
1192 for (index, character) in block.char_indices() {
1193 match character {
1194 '\\' if depth == 0 => {
1195 if let Some(tag_start) = start.take() {
1196 let tag = block[tag_start..index].trim();
1197 if !tag.is_empty() {
1198 tags.push(tag);
1199 }
1200 }
1201 start = Some(index + character.len_utf8());
1202 }
1203 '(' => depth += 1,
1204 ')' => depth = (depth - 1).max(0),
1205 _ => {}
1206 }
1207 }
1208
1209 if let Some(tag_start) = start {
1210 let tag = block[tag_start..].trim();
1211 if !tag.is_empty() {
1212 tags.push(tag);
1213 }
1214 }
1215
1216 tags
1217}
1218
1219fn apply_transform_tag(tag: &str, style: &mut ParsedSpanStyle) {
1220 if let Some(rest) = tag.strip_prefix("1c").or_else(|| tag.strip_prefix('c')) {
1221 style.primary_colour = parse_override_color(rest, style.primary_colour);
1222 } else if let Some(rest) = tag.strip_prefix("2c") {
1223 style.secondary_colour = parse_override_color(rest, style.secondary_colour);
1224 } else if let Some(rest) = tag.strip_prefix("3c") {
1225 style.outline_colour = parse_override_color(rest, style.outline_colour);
1226 } else if let Some(rest) = tag.strip_prefix("4c") {
1227 style.back_colour = parse_override_color(rest, style.back_colour);
1228 } else if let Some(rest) = tag.strip_prefix("alpha") {
1229 let alpha = parse_alpha_tag(rest, alpha_of(style.primary_colour));
1230 style.primary_colour = with_alpha(style.primary_colour, alpha);
1231 style.secondary_colour = with_alpha(style.secondary_colour, alpha);
1232 style.outline_colour = with_alpha(style.outline_colour, alpha);
1233 style.back_colour = with_alpha(style.back_colour, alpha);
1234 } else if let Some(rest) = tag.strip_prefix("1a") {
1235 style.primary_colour = with_alpha(
1236 style.primary_colour,
1237 parse_alpha_tag(rest, alpha_of(style.primary_colour)),
1238 );
1239 } else if let Some(rest) = tag.strip_prefix("2a") {
1240 style.secondary_colour = with_alpha(
1241 style.secondary_colour,
1242 parse_alpha_tag(rest, alpha_of(style.secondary_colour)),
1243 );
1244 } else if let Some(rest) = tag.strip_prefix("3a") {
1245 style.outline_colour = with_alpha(
1246 style.outline_colour,
1247 parse_alpha_tag(rest, alpha_of(style.outline_colour)),
1248 );
1249 } else if let Some(rest) = tag.strip_prefix("4a") {
1250 style.back_colour = with_alpha(
1251 style.back_colour,
1252 parse_alpha_tag(rest, alpha_of(style.back_colour)),
1253 );
1254 } else if let Some(rest) = tag.strip_prefix("fscx") {
1255 style.scale_x = parse_scale(rest, style.scale_x);
1256 } else if let Some(rest) = tag.strip_prefix("fscy") {
1257 style.scale_y = parse_scale(rest, style.scale_y);
1258 } else if let Some(rest) = tag.strip_prefix("fsp") {
1259 style.spacing = parse_f64(rest, style.spacing);
1260 } else if let Some(rest) = tag.strip_prefix("frx") {
1261 style.rotation_x = parse_f64(rest, style.rotation_x);
1262 } else if let Some(rest) = tag.strip_prefix("fry") {
1263 style.rotation_y = parse_f64(rest, style.rotation_y);
1264 } else if let Some(rest) = tag.strip_prefix("frz").or_else(|| tag.strip_prefix("fr")) {
1265 style.rotation_z = parse_f64(rest, style.rotation_z);
1266 } else if let Some(rest) = tag.strip_prefix("fax") {
1267 style.shear_x = parse_f64(rest, style.shear_x);
1268 } else if let Some(rest) = tag.strip_prefix("fay") {
1269 style.shear_y = parse_f64(rest, style.shear_y);
1270 } else if let Some(rest) = tag.strip_prefix("fs") {
1271 style.font_size = parse_f64(rest, style.font_size);
1272 } else if let Some(rest) = tag.strip_prefix("xbord") {
1273 style.border_x = parse_f64(rest, style.border_x);
1274 } else if let Some(rest) = tag.strip_prefix("ybord") {
1275 style.border_y = parse_f64(rest, style.border_y);
1276 } else if let Some(rest) = tag.strip_prefix("bord") {
1277 style.border = parse_f64(rest, style.border);
1278 style.border_x = style.border;
1279 style.border_y = style.border;
1280 } else if let Some(rest) = tag.strip_prefix("xshad") {
1281 style.shadow_x = parse_f64(rest, style.shadow_x);
1282 } else if let Some(rest) = tag.strip_prefix("yshad") {
1283 style.shadow_y = parse_f64(rest, style.shadow_y);
1284 } else if let Some(rest) = tag.strip_prefix("shad") {
1285 style.shadow = parse_f64(rest, style.shadow);
1286 style.shadow_x = style.shadow;
1287 style.shadow_y = style.shadow;
1288 } else if let Some(rest) = tag.strip_prefix("blur") {
1289 style.blur = parse_f64(rest, style.blur);
1290 } else if let Some(rest) = tag.strip_prefix("be") {
1291 style.be = parse_f64(rest, style.be);
1292 }
1293}
1294
1295fn diff_animated_style(base: &ParsedSpanStyle, target: &ParsedSpanStyle) -> ParsedAnimatedStyle {
1296 ParsedAnimatedStyle {
1297 font_size: ((target.font_size - base.font_size).abs() > f64::EPSILON)
1298 .then_some(target.font_size),
1299 scale_x: ((target.scale_x - base.scale_x).abs() > f64::EPSILON).then_some(target.scale_x),
1300 scale_y: ((target.scale_y - base.scale_y).abs() > f64::EPSILON).then_some(target.scale_y),
1301 spacing: ((target.spacing - base.spacing).abs() > f64::EPSILON).then_some(target.spacing),
1302 rotation_x: ((target.rotation_x - base.rotation_x).abs() > f64::EPSILON)
1303 .then_some(target.rotation_x),
1304 rotation_y: ((target.rotation_y - base.rotation_y).abs() > f64::EPSILON)
1305 .then_some(target.rotation_y),
1306 rotation_z: ((target.rotation_z - base.rotation_z).abs() > f64::EPSILON)
1307 .then_some(target.rotation_z),
1308 shear_x: ((target.shear_x - base.shear_x).abs() > f64::EPSILON).then_some(target.shear_x),
1309 shear_y: ((target.shear_y - base.shear_y).abs() > f64::EPSILON).then_some(target.shear_y),
1310 primary_colour: (target.primary_colour != base.primary_colour)
1311 .then_some(target.primary_colour),
1312 secondary_colour: (target.secondary_colour != base.secondary_colour)
1313 .then_some(target.secondary_colour),
1314 outline_colour: (target.outline_colour != base.outline_colour)
1315 .then_some(target.outline_colour),
1316 back_colour: (target.back_colour != base.back_colour).then_some(target.back_colour),
1317 border: ((target.border - base.border).abs() > f64::EPSILON).then_some(target.border),
1318 border_x: ((target.border_x - base.border_x).abs() > f64::EPSILON)
1319 .then_some(target.border_x),
1320 border_y: ((target.border_y - base.border_y).abs() > f64::EPSILON)
1321 .then_some(target.border_y),
1322 shadow: ((target.shadow - base.shadow).abs() > f64::EPSILON).then_some(target.shadow),
1323 shadow_x: ((target.shadow_x - base.shadow_x).abs() > f64::EPSILON)
1324 .then_some(target.shadow_x),
1325 shadow_y: ((target.shadow_y - base.shadow_y).abs() > f64::EPSILON)
1326 .then_some(target.shadow_y),
1327 blur: ((target.blur - base.blur).abs() > f64::EPSILON).then_some(target.blur),
1328 be: ((target.be - base.be).abs() > f64::EPSILON).then_some(target.be),
1329 }
1330}
1331
1332fn parse_font_size_override(value: &str, current: f64, base: f64) -> f64 {
1333 let trimmed = value.trim();
1334 if trimmed.is_empty() {
1335 return base;
1336 }
1337
1338 let parsed = trimmed.parse::<f64>().unwrap_or(0.0);
1339 let resolved = if trimmed.starts_with(['+', '-']) {
1340 current * (1.0 + parsed / 10.0)
1341 } else {
1342 parsed
1343 };
1344
1345 if resolved > 0.0 { resolved } else { base }
1346}
1347
1348fn parse_karaoke_duration(value: &str) -> Option<i32> {
1349 value
1350 .trim()
1351 .parse::<i32>()
1352 .ok()
1353 .map(|centiseconds| centiseconds.max(0) * 10)
1354}
1355
1356fn parse_override_color(value: &str, fallback: u32) -> u32 {
1357 let trimmed = value.trim();
1358 let trimmed = trimmed.trim_matches('&').trim_start_matches(['H', 'h']);
1359 if trimmed.is_empty() {
1360 return fallback;
1361 }
1362
1363 u32::from_str_radix(trimmed, 16).unwrap_or(fallback)
1364}
1365
1366fn parse_alpha_tag(value: &str, fallback: u8) -> u8 {
1367 let trimmed = value.trim();
1368 let trimmed = trimmed.trim_matches('&').trim_start_matches(['H', 'h']);
1369 if trimmed.is_empty() {
1370 return fallback;
1371 }
1372 u8::from_str_radix(trimmed, 16).unwrap_or(fallback)
1373}
1374
1375fn alpha_of(color: u32) -> u8 {
1376 (color & 0xFF) as u8
1377}
1378
1379fn with_alpha(color: u32, alpha: u8) -> u32 {
1380 (color & 0xFFFF_FF00) | u32::from(alpha)
1381}
1382
1383fn parse_override_bool(value: &str, fallback: bool) -> bool {
1384 let trimmed = value.trim();
1385 if trimmed.is_empty() {
1386 true
1387 } else {
1388 parse_bool(trimmed, fallback)
1389 }
1390}
1391
1392fn alignment_from_an(value: i32) -> Option<i32> {
1393 Some(match value {
1394 1 => ass::VALIGN_SUB | ass::HALIGN_LEFT,
1395 2 => ass::VALIGN_SUB | ass::HALIGN_CENTER,
1396 3 => ass::VALIGN_SUB | ass::HALIGN_RIGHT,
1397 4 => ass::VALIGN_CENTER | ass::HALIGN_LEFT,
1398 5 => ass::VALIGN_CENTER | ass::HALIGN_CENTER,
1399 6 => ass::VALIGN_CENTER | ass::HALIGN_RIGHT,
1400 7 => ass::VALIGN_TOP | ass::HALIGN_LEFT,
1401 8 => ass::VALIGN_TOP | ass::HALIGN_CENTER,
1402 9 => ass::VALIGN_TOP | ass::HALIGN_RIGHT,
1403 _ => return None,
1404 })
1405}
1406
1407fn alignment_from_legacy_a(value: i32) -> Option<i32> {
1408 let halign = match value & 0x3 {
1409 1 => ass::HALIGN_LEFT,
1410 2 => ass::HALIGN_CENTER,
1411 3 => ass::HALIGN_RIGHT,
1412 _ => return None,
1413 };
1414 let valign = if value & 0x4 != 0 {
1415 ass::VALIGN_TOP
1416 } else if value & 0x8 != 0 {
1417 ass::VALIGN_CENTER
1418 } else {
1419 ass::VALIGN_SUB
1420 };
1421 Some(valign | halign)
1422}
1423
1424fn parse_pos(value: &str) -> Option<(i32, i32)> {
1425 let trimmed = value.trim();
1426 let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1427 let mut parts = inside.split(',').map(str::trim);
1428 let x = parts.next()?.parse::<i32>().ok()?;
1429 let y = parts.next()?.parse::<i32>().ok()?;
1430 Some((x, y))
1431}
1432
1433fn parse_rect_clip(value: &str) -> Option<Rect> {
1434 let trimmed = value.trim();
1435 let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1436 let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
1437 if parts.len() != 4 {
1438 return None;
1439 }
1440 let x_min = parts[0].parse::<i32>().ok()?;
1441 let y_min = parts[1].parse::<i32>().ok()?;
1442 let x_max = parts[2].parse::<i32>().ok()?;
1443 let y_max = parts[3].parse::<i32>().ok()?;
1444 Some(Rect {
1445 x_min,
1446 y_min,
1447 x_max,
1448 y_max,
1449 })
1450}
1451
1452fn parse_vector_clip(value: &str) -> Option<ParsedVectorClip> {
1453 let trimmed = value.trim();
1454 let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?.trim();
1455 if inside.is_empty() {
1456 return None;
1457 }
1458
1459 let (scale, drawing) = if let Some((scale, drawing)) = inside.split_once(',') {
1460 if let Ok(scale) = scale.trim().parse::<i32>() {
1461 (scale.max(1), drawing.trim())
1462 } else {
1463 (1, inside)
1464 }
1465 } else {
1466 (1, inside)
1467 };
1468
1469 let polygons = parse_drawing_polygons(drawing, scale)?;
1470 if polygons.is_empty() {
1471 return None;
1472 }
1473
1474 Some(ParsedVectorClip { scale, polygons })
1475}
1476
1477fn parse_drawing_polygons(drawing: &str, scale: i32) -> Option<Vec<Vec<Point>>> {
1478 let tokens = drawing.split_whitespace().collect::<Vec<_>>();
1479 if tokens.is_empty() {
1480 return None;
1481 }
1482
1483 let mut polygons = Vec::new();
1484 let mut current = Vec::new();
1485 let mut spline_state: Option<SplineState> = None;
1486 let mut index = 0;
1487 while index < tokens.len() {
1488 match tokens[index].to_ascii_lowercase().as_str() {
1489 "m" | "n" => {
1490 spline_state = None;
1491 if current.len() >= 3 {
1492 polygons.push(std::mem::take(&mut current));
1493 }
1494 index += 1;
1495 let (point, next_index) = parse_drawing_point(&tokens, index, scale)?;
1496 current.push(point);
1497 index = next_index;
1498 while let Some((point, next_index)) =
1499 parse_drawing_point_optional(&tokens, index, scale)
1500 {
1501 current.push(point);
1502 index = next_index;
1503 }
1504 }
1505 "l" => {
1506 spline_state = None;
1507 if current.is_empty() {
1508 return None;
1509 }
1510 index += 1;
1511 let mut consumed = false;
1512 while let Some((point, next_index)) =
1513 parse_drawing_point_optional(&tokens, index, scale)
1514 {
1515 current.push(point);
1516 index = next_index;
1517 consumed = true;
1518 }
1519 if !consumed {
1520 return None;
1521 }
1522 }
1523 "b" => {
1524 spline_state = None;
1525 if current.is_empty() {
1526 return None;
1527 }
1528 index += 1;
1529 let mut consumed = false;
1530 while let Some(((control1, control2, end), next_index)) =
1531 parse_bezier_segment(&tokens, index, scale)
1532 {
1533 let start = *current.last()?;
1534 current.extend(approximate_cubic_bezier(start, control1, control2, end, 16));
1535 index = next_index;
1536 consumed = true;
1537 }
1538 if !consumed {
1539 return None;
1540 }
1541 }
1542 "s" => {
1543 if current.is_empty() {
1544 return None;
1545 }
1546 index += 1;
1547 let (point1, next_index) = parse_drawing_point(&tokens, index, scale)?;
1548 let (point2, next_index) = parse_drawing_point(&tokens, next_index, scale)?;
1549 let (point3, next_index) = parse_drawing_point(&tokens, next_index, scale)?;
1550 let start = *current.last()?;
1551 current.extend(approximate_spline_segment(
1552 start, point1, point2, point3, 16,
1553 ));
1554 spline_state = Some(SplineState {
1555 first_three: [point1, point2, point3],
1556 history: vec![start, point1, point2, point3],
1557 });
1558 index = next_index;
1559 }
1560 "p" => {
1561 let state = spline_state.as_mut()?;
1562 index += 1;
1563 let mut consumed = false;
1564 while let Some((point, next_index)) =
1565 parse_drawing_point_optional(&tokens, index, scale)
1566 {
1567 let len = state.history.len();
1568 current.extend(approximate_spline_segment(
1569 state.history[len - 3],
1570 state.history[len - 2],
1571 state.history[len - 1],
1572 point,
1573 16,
1574 ));
1575 state.history.push(point);
1576 index = next_index;
1577 consumed = true;
1578 }
1579 if !consumed {
1580 return None;
1581 }
1582 }
1583 "c" => {
1584 let state = spline_state.take()?;
1585 for point in state.first_three {
1586 let len = state.history.len();
1587 current.extend(approximate_spline_segment(
1588 state.history[len - 3],
1589 state.history[len - 2],
1590 state.history[len - 1],
1591 point,
1592 16,
1593 ));
1594 }
1595 index += 1;
1596 }
1597 _ => return None,
1598 }
1599 }
1600
1601 if current.len() >= 3 {
1602 polygons.push(current);
1603 }
1604
1605 Some(polygons)
1606}
1607
1608#[derive(Clone, Debug)]
1609struct SplineState {
1610 first_three: [Point; 3],
1611 history: Vec<Point>,
1612}
1613
1614fn parse_drawing_point(tokens: &[&str], index: usize, scale: i32) -> Option<(Point, usize)> {
1615 let x = tokens.get(index)?.parse::<i32>().ok()?;
1616 let y = tokens.get(index + 1)?.parse::<i32>().ok()?;
1617 Some((scale_drawing_point(x, y, scale), index + 2))
1618}
1619
1620fn parse_drawing_point_optional(
1621 tokens: &[&str],
1622 index: usize,
1623 scale: i32,
1624) -> Option<(Point, usize)> {
1625 let x = tokens.get(index)?;
1626 let y = tokens.get(index + 1)?;
1627 if x.chars().any(|character| character.is_ascii_alphabetic())
1628 || y.chars().any(|character| character.is_ascii_alphabetic())
1629 {
1630 return None;
1631 }
1632 parse_drawing_point(tokens, index, scale)
1633}
1634
1635fn parse_bezier_segment(
1636 tokens: &[&str],
1637 index: usize,
1638 scale: i32,
1639) -> Option<((Point, Point, Point), usize)> {
1640 let (control1, next_index) = parse_drawing_point(tokens, index, scale)?;
1641 let (control2, next_index) = parse_drawing_point(tokens, next_index, scale)?;
1642 let (end, next_index) = parse_drawing_point(tokens, next_index, scale)?;
1643 Some(((control1, control2, end), next_index))
1644}
1645
1646fn approximate_cubic_bezier(
1647 start: Point,
1648 control1: Point,
1649 control2: Point,
1650 end: Point,
1651 segments: usize,
1652) -> Vec<Point> {
1653 let segments = segments.max(1);
1654 let mut points = Vec::with_capacity(segments);
1655 for step in 1..=segments {
1656 let t = step as f64 / segments as f64;
1657 let one_minus_t = 1.0 - t;
1658 let x = one_minus_t.powi(3) * f64::from(start.x)
1659 + 3.0 * one_minus_t.powi(2) * t * f64::from(control1.x)
1660 + 3.0 * one_minus_t * t.powi(2) * f64::from(control2.x)
1661 + t.powi(3) * f64::from(end.x);
1662 let y = one_minus_t.powi(3) * f64::from(start.y)
1663 + 3.0 * one_minus_t.powi(2) * t * f64::from(control1.y)
1664 + 3.0 * one_minus_t * t.powi(2) * f64::from(control2.y)
1665 + t.powi(3) * f64::from(end.y);
1666 let point = Point {
1667 x: x.round() as i32,
1668 y: y.round() as i32,
1669 };
1670 if points.last().copied() != Some(point) {
1671 points.push(point);
1672 }
1673 }
1674 points
1675}
1676
1677fn approximate_spline_segment(
1678 previous: Point,
1679 point1: Point,
1680 point2: Point,
1681 point3: Point,
1682 segments: usize,
1683) -> Vec<Point> {
1684 let x01 = (point1.x - previous.x) / 3;
1685 let y01 = (point1.y - previous.y) / 3;
1686 let x12 = (point2.x - point1.x) / 3;
1687 let y12 = (point2.y - point1.y) / 3;
1688 let x23 = (point3.x - point2.x) / 3;
1689 let y23 = (point3.y - point2.y) / 3;
1690
1691 let start = Point {
1692 x: point1.x + ((x12 - x01) >> 1),
1693 y: point1.y + ((y12 - y01) >> 1),
1694 };
1695 let control1 = Point {
1696 x: point1.x + x12,
1697 y: point1.y + y12,
1698 };
1699 let control2 = Point {
1700 x: point2.x - x12,
1701 y: point2.y - y12,
1702 };
1703 let end = Point {
1704 x: point2.x + ((x23 - x12) >> 1),
1705 y: point2.y + ((y23 - y12) >> 1),
1706 };
1707
1708 approximate_cubic_bezier(start, control1, control2, end, segments)
1709}
1710
1711fn scale_drawing_point(x: i32, y: i32, scale: i32) -> Point {
1712 let factor = 1_i32
1713 .checked_shl(scale.saturating_sub(1) as u32)
1714 .unwrap_or(1)
1715 .max(1);
1716 Point {
1717 x: x / factor,
1718 y: y / factor,
1719 }
1720}
1721
1722fn bounds_from_polygons(polygons: &[Vec<Point>]) -> Option<Rect> {
1723 let mut points = polygons.iter().flat_map(|polygon| polygon.iter().copied());
1724 let first = points.next()?;
1725 let mut x_min = first.x;
1726 let mut y_min = first.y;
1727 let mut x_max = first.x;
1728 let mut y_max = first.y;
1729 for point in points {
1730 x_min = x_min.min(point.x);
1731 y_min = y_min.min(point.y);
1732 x_max = x_max.max(point.x);
1733 y_max = y_max.max(point.y);
1734 }
1735 Some(Rect {
1736 x_min,
1737 y_min,
1738 x_max: x_max + 1,
1739 y_max: y_max + 1,
1740 })
1741}
1742
1743fn parse_move(value: &str) -> Option<ParsedMovement> {
1744 let trimmed = value.trim();
1745 let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1746 let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
1747 let (x1, y1, x2, y2, t1_ms, t2_ms) = match parts.as_slice() {
1748 [x1, y1, x2, y2] => (
1749 x1.parse::<i32>().ok()?,
1750 y1.parse::<i32>().ok()?,
1751 x2.parse::<i32>().ok()?,
1752 y2.parse::<i32>().ok()?,
1753 0,
1754 0,
1755 ),
1756 [x1, y1, x2, y2, t1, t2] => {
1757 let mut t1_ms = t1.parse::<i32>().ok()?;
1758 let mut t2_ms = t2.parse::<i32>().ok()?;
1759 if t1_ms > t2_ms {
1760 std::mem::swap(&mut t1_ms, &mut t2_ms);
1761 }
1762 (
1763 x1.parse::<i32>().ok()?,
1764 y1.parse::<i32>().ok()?,
1765 x2.parse::<i32>().ok()?,
1766 y2.parse::<i32>().ok()?,
1767 t1_ms,
1768 t2_ms,
1769 )
1770 }
1771 _ => return None,
1772 };
1773
1774 Some(ParsedMovement {
1775 start: (x1, y1),
1776 end: (x2, y2),
1777 t1_ms,
1778 t2_ms,
1779 })
1780}
1781
1782fn parse_fad(value: &str) -> Option<ParsedFade> {
1783 let trimmed = value.trim();
1784 let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1785 let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
1786 let [fade_in, fade_out] = parts.as_slice() else {
1787 return None;
1788 };
1789
1790 Some(ParsedFade::Simple {
1791 fade_in_ms: fade_in.parse::<i32>().ok()?.max(0),
1792 fade_out_ms: fade_out.parse::<i32>().ok()?.max(0),
1793 })
1794}
1795
1796fn parse_fade(value: &str) -> Option<ParsedFade> {
1797 let trimmed = value.trim();
1798 let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1799 let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
1800 let [a1, a2, a3, t1, t2, t3, t4] = parts.as_slice() else {
1801 return None;
1802 };
1803
1804 Some(ParsedFade::Complex {
1805 alpha1: a1.parse::<i32>().ok()?.clamp(0, 255),
1806 alpha2: a2.parse::<i32>().ok()?.clamp(0, 255),
1807 alpha3: a3.parse::<i32>().ok()?.clamp(0, 255),
1808 t1_ms: t1.parse::<i32>().ok()?,
1809 t2_ms: t2.parse::<i32>().ok()?,
1810 t3_ms: t3.parse::<i32>().ok()?,
1811 t4_ms: t4.parse::<i32>().ok()?,
1812 })
1813}
1814
1815fn resolve_reset_style(
1816 value: &str,
1817 base_style: &ParsedStyle,
1818 styles: &[ParsedStyle],
1819) -> ParsedSpanStyle {
1820 let name = value.trim();
1821 if name.is_empty() {
1822 return ParsedSpanStyle::from_style(base_style);
1823 }
1824
1825 styles
1826 .iter()
1827 .find(|style| style.name.eq_ignore_ascii_case(name))
1828 .map(ParsedSpanStyle::from_style)
1829 .unwrap_or_else(|| ParsedSpanStyle::from_style(base_style))
1830}
1831
1832fn flush_span(
1833 buffer: &mut String,
1834 style: &ParsedSpanStyle,
1835 karaoke: Option<ParsedKaraokeSpan>,
1836 drawing_scale: i32,
1837 transforms: &[ParsedSpanTransform],
1838 line: &mut ParsedTextLine,
1839) {
1840 if buffer.is_empty() {
1841 return;
1842 }
1843 let text = std::mem::take(buffer);
1844 let drawing = (drawing_scale > 0)
1845 .then(|| parse_drawing_polygons(&text, drawing_scale))
1846 .flatten()
1847 .map(|polygons| ParsedDrawing {
1848 scale: drawing_scale,
1849 polygons,
1850 });
1851 line.text.push_str(&text);
1852 line.spans.push(ParsedTextSpan {
1853 text,
1854 style: style.clone(),
1855 transforms: transforms.to_vec(),
1856 karaoke,
1857 drawing,
1858 });
1859}
1860
1861fn push_line(parsed: &mut ParsedDialogueText, line: &mut ParsedTextLine) {
1862 if line.text.is_empty() && line.spans.is_empty() && !parsed.lines.is_empty() {
1863 return;
1864 }
1865 parsed.lines.push(std::mem::take(line));
1866}
1867
1868fn parse_matrix(value: &str) -> YCbCrMatrix {
1869 match value.trim().to_ascii_lowercase().as_str() {
1870 "none" => YCbCrMatrix::None,
1871 "tv.601" | "bt601(tv)" | "bt.601(tv)" => YCbCrMatrix::Bt601Tv,
1872 "pc.601" | "bt601(pc)" | "bt.601(pc)" => YCbCrMatrix::Bt601Pc,
1873 "tv.709" | "bt709(tv)" | "bt.709(tv)" => YCbCrMatrix::Bt709Tv,
1874 "pc.709" | "bt709(pc)" | "bt.709(pc)" => YCbCrMatrix::Bt709Pc,
1875 "tv.240m" | "smpte240m(tv)" => YCbCrMatrix::Smpte240mTv,
1876 "pc.240m" | "smpte240m(pc)" => YCbCrMatrix::Smpte240mPc,
1877 "tv.fcc" | "fcc(tv)" => YCbCrMatrix::FccTv,
1878 "pc.fcc" | "fcc(pc)" => YCbCrMatrix::FccPc,
1879 "" => YCbCrMatrix::Default,
1880 _ => YCbCrMatrix::Unknown,
1881 }
1882}
1883
1884#[cfg(test)]
1885mod tests {
1886 use super::*;
1887
1888 #[test]
1889 fn parses_basic_ass_script() {
1890 let input = "[Script Info]\nPlayResX: 1280\nPlayResY: 720\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,42,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:01.00,0:00:03.50,Default,,0000,0000,0000,,Hello, world!";
1891 let track = parse_script_text(input).expect("script should parse");
1892
1893 assert_eq!(track.play_res_x, 1280);
1894 assert_eq!(track.play_res_y, 720);
1895 assert_eq!(track.styles.len(), 1);
1896 assert_eq!(track.events.len(), 1);
1897 assert_eq!(track.events[0].start, 1000);
1898 assert_eq!(track.events[0].duration, 2500);
1899 assert_eq!(track.events[0].style, 0);
1900 assert_eq!(track.events[0].text, "Hello, world!");
1901 assert_eq!(
1902 track.styles[0].alignment,
1903 ass::VALIGN_SUB | ass::HALIGN_CENTER
1904 );
1905 }
1906
1907 #[test]
1908 fn normalizes_style_alignment_numbers_to_libass_bits() {
1909 let input = "[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Mid,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,5,10,10,10,1";
1910 let track = parse_script_text(input).expect("script should parse");
1911
1912 assert_eq!(
1913 track.styles[0].alignment,
1914 ass::VALIGN_CENTER | ass::HALIGN_CENTER
1915 );
1916 }
1917
1918 #[test]
1919 fn resolves_event_style_by_name() {
1920 let input = "[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\nStyle: Sign,DejaVu Sans,28,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,8,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Sign,,0000,0000,0000,,Visible text";
1921 let track = parse_script_text(input).expect("script should parse");
1922
1923 assert_eq!(track.styles.len(), 2);
1924 assert_eq!(track.events.len(), 1);
1925 assert_eq!(track.events[0].style, 1);
1926 }
1927
1928 #[test]
1929 fn parses_dialogue_overrides_into_spans_and_event_metadata() {
1930 let base_style = ParsedStyle {
1931 font_name: "Arial".to_string(),
1932 font_size: 20.0,
1933 ..ParsedStyle::default()
1934 };
1935 let alt_style = ParsedStyle {
1936 name: "Alt".to_string(),
1937 font_name: "DejaVu Sans".to_string(),
1938 font_size: 28.0,
1939 ..ParsedStyle::default()
1940 };
1941 let parsed = parse_dialogue_text(
1942 "{\\fnLiberation Sans\\fs32\\fscx150\\fscy75\\fsp3\\an7}Hello{\\rAlt} world\\N{\\pos(120,48)}again",
1943 &base_style,
1944 &[base_style.clone(), alt_style.clone()],
1945 );
1946
1947 assert_eq!(parsed.alignment, Some(ass::VALIGN_TOP | ass::HALIGN_LEFT));
1948 assert_eq!(parsed.position, Some((120, 48)));
1949 assert_eq!(parsed.lines.len(), 2);
1950 assert_eq!(parsed.lines[0].spans.len(), 2);
1951 assert_eq!(parsed.lines[0].spans[0].style.font_name, "Liberation Sans");
1952 assert_eq!(parsed.lines[0].spans[0].style.font_size, 32.0);
1953 assert_eq!(parsed.lines[0].spans[0].style.scale_x, 1.5);
1954 assert_eq!(parsed.lines[0].spans[0].style.scale_y, 0.75);
1955 assert_eq!(parsed.lines[0].spans[0].style.spacing, 3.0);
1956 assert_eq!(parsed.lines[0].spans[1].style.font_name, "DejaVu Sans");
1957 assert_eq!(parsed.lines[1].text, "again");
1958 }
1959
1960 #[test]
1961 fn parses_rectangular_clip_overrides() {
1962 let base_style = ParsedStyle::default();
1963 let parsed = parse_dialogue_text("{\\clip(10,20,30,40)}Clip", &base_style, &[]);
1964 let inverse = parse_dialogue_text("{\\iclip(1,2,3,4)}Clip", &base_style, &[]);
1965
1966 assert_eq!(
1967 parsed.clip_rect,
1968 Some(Rect {
1969 x_min: 10,
1970 y_min: 20,
1971 x_max: 30,
1972 y_max: 40
1973 })
1974 );
1975 assert!(!parsed.inverse_clip);
1976 assert_eq!(
1977 inverse.clip_rect,
1978 Some(Rect {
1979 x_min: 1,
1980 y_min: 2,
1981 x_max: 3,
1982 y_max: 4
1983 })
1984 );
1985 assert!(inverse.inverse_clip);
1986 }
1987
1988 #[test]
1989 fn parses_vector_clip_overrides() {
1990 let base_style = ParsedStyle::default();
1991 let parsed = parse_dialogue_text("{\\clip(m 0 0 l 10 0 10 10 0 10)}Clip", &base_style, &[]);
1992
1993 assert!(parsed.clip_rect.is_none());
1994 assert_eq!(
1995 parsed.vector_clip,
1996 Some(ParsedVectorClip {
1997 scale: 1,
1998 polygons: vec![vec![
1999 Point { x: 0, y: 0 },
2000 Point { x: 10, y: 0 },
2001 Point { x: 10, y: 10 },
2002 Point { x: 0, y: 10 },
2003 ]],
2004 })
2005 );
2006 assert!(!parsed.inverse_clip);
2007 }
2008
2009 #[test]
2010 fn parses_move_overrides() {
2011 let base_style = ParsedStyle::default();
2012 let parsed = parse_dialogue_text("{\\move(10,20,110,220,50,150)}Move", &base_style, &[]);
2013
2014 assert_eq!(
2015 parsed.movement,
2016 Some(ParsedMovement {
2017 start: (10, 20),
2018 end: (110, 220),
2019 t1_ms: 50,
2020 t2_ms: 150,
2021 })
2022 );
2023 assert!(parsed.position.is_none());
2024 }
2025
2026 #[test]
2027 fn parses_fad_overrides() {
2028 let base_style = ParsedStyle::default();
2029 let parsed = parse_dialogue_text("{\\fad(120,240)}Fade", &base_style, &[]);
2030
2031 assert_eq!(
2032 parsed.fade,
2033 Some(ParsedFade::Simple {
2034 fade_in_ms: 120,
2035 fade_out_ms: 240,
2036 })
2037 );
2038 }
2039
2040 #[test]
2041 fn parses_full_fade_overrides() {
2042 let base_style = ParsedStyle::default();
2043 let parsed = parse_dialogue_text("{\\fade(10,20,30,40,50,60,70)}Fade", &base_style, &[]);
2044
2045 assert_eq!(
2046 parsed.fade,
2047 Some(ParsedFade::Complex {
2048 alpha1: 10,
2049 alpha2: 20,
2050 alpha3: 30,
2051 t1_ms: 40,
2052 t2_ms: 50,
2053 t3_ms: 60,
2054 t4_ms: 70,
2055 })
2056 );
2057 }
2058
2059 #[test]
2060 fn parses_karaoke_spans() {
2061 let base_style = ParsedStyle::default();
2062 let parsed = parse_dialogue_text("{\\k10}Ka{\\K20}ra{\\ko30}oke", &base_style, &[]);
2063
2064 assert_eq!(parsed.lines.len(), 1);
2065 assert_eq!(parsed.lines[0].spans.len(), 3);
2066 assert_eq!(
2067 parsed.lines[0].spans[0].karaoke,
2068 Some(ParsedKaraokeSpan {
2069 start_ms: 0,
2070 duration_ms: 100,
2071 mode: ParsedKaraokeMode::FillSwap,
2072 })
2073 );
2074 assert_eq!(
2075 parsed.lines[0].spans[1].karaoke,
2076 Some(ParsedKaraokeSpan {
2077 start_ms: 100,
2078 duration_ms: 200,
2079 mode: ParsedKaraokeMode::Sweep,
2080 })
2081 );
2082 assert_eq!(
2083 parsed.lines[0].spans[2].karaoke,
2084 Some(ParsedKaraokeSpan {
2085 start_ms: 300,
2086 duration_ms: 300,
2087 mode: ParsedKaraokeMode::OutlineToggle,
2088 })
2089 );
2090 }
2091
2092 #[test]
2093 fn parses_kt_karaoke_timing_reset() {
2094 let base_style = ParsedStyle::default();
2095 let parsed = parse_dialogue_text("{\\k10}A{\\kt50\\k10}B", &base_style, &[]);
2096
2097 assert_eq!(parsed.lines.len(), 1);
2098 assert_eq!(parsed.lines[0].spans.len(), 2);
2099 assert_eq!(
2100 parsed.lines[0].spans[0].karaoke,
2101 Some(ParsedKaraokeSpan {
2102 start_ms: 0,
2103 duration_ms: 100,
2104 mode: ParsedKaraokeMode::FillSwap,
2105 })
2106 );
2107 assert_eq!(
2108 parsed.lines[0].spans[1].karaoke,
2109 Some(ParsedKaraokeSpan {
2110 start_ms: 500,
2111 duration_ms: 100,
2112 mode: ParsedKaraokeMode::FillSwap,
2113 })
2114 );
2115 }
2116
2117 #[test]
2118 fn parses_font_size_relative_and_scale_reset_overrides() {
2119 let base_style = ParsedStyle {
2120 font_size: 20.0,
2121 scale_x: 1.2,
2122 scale_y: 0.8,
2123 ..ParsedStyle::default()
2124 };
2125 let parsed = parse_dialogue_text(
2126 "{\\fs+5}Bigger{\\fs-2}Smaller{\\fs0}Reset{\\fscx150\\fscy50}Scaled{\\fsc}Base",
2127 &base_style,
2128 &[],
2129 );
2130
2131 assert_eq!(parsed.lines[0].spans[0].style.font_size, 30.0);
2132 assert_eq!(parsed.lines[0].spans[1].style.font_size, 24.0);
2133 assert_eq!(parsed.lines[0].spans[2].style.font_size, 20.0);
2134 assert_eq!(parsed.lines[0].spans[3].style.scale_x, 1.5);
2135 assert_eq!(parsed.lines[0].spans[3].style.scale_y, 0.5);
2136 assert_eq!(parsed.lines[0].spans[4].style.scale_x, 1.2);
2137 assert_eq!(parsed.lines[0].spans[4].style.scale_y, 0.8);
2138 }
2139
2140 #[test]
2141 fn parses_drawing_spans_in_p_mode() {
2142 let base_style = ParsedStyle::default();
2143 let parsed = parse_dialogue_text("{\\p1}m 0 0 l 10 0 10 10 0 10", &base_style, &[]);
2144
2145 assert_eq!(parsed.lines.len(), 1);
2146 assert_eq!(parsed.lines[0].spans.len(), 1);
2147 let drawing = parsed.lines[0].spans[0]
2148 .drawing
2149 .as_ref()
2150 .expect("drawing span");
2151 assert_eq!(drawing.scale, 1);
2152 assert_eq!(drawing.polygons.len(), 1);
2153 assert_eq!(
2154 drawing.bounds(),
2155 Some(Rect {
2156 x_min: 0,
2157 y_min: 0,
2158 x_max: 11,
2159 y_max: 11
2160 })
2161 );
2162 }
2163
2164 #[test]
2165 fn parses_bezier_drawing_spans_in_p_mode() {
2166 let base_style = ParsedStyle::default();
2167 let parsed = parse_dialogue_text("{\\p1}m 0 0 b 10 0 10 10 0 10", &base_style, &[]);
2168
2169 let drawing = parsed.lines[0].spans[0]
2170 .drawing
2171 .as_ref()
2172 .expect("drawing span");
2173 assert_eq!(drawing.polygons.len(), 1);
2174 assert!(drawing.polygons[0].len() > 4);
2175 assert_eq!(
2176 drawing.polygons[0].first().copied(),
2177 Some(Point { x: 0, y: 0 })
2178 );
2179 assert_eq!(
2180 drawing.polygons[0].last().copied(),
2181 Some(Point { x: 0, y: 10 })
2182 );
2183 }
2184
2185 #[test]
2186 fn parses_spline_drawing_spans_in_p_mode() {
2187 let base_style = ParsedStyle::default();
2188 let parsed =
2189 parse_dialogue_text("{\\p1}m 0 0 s 10 0 10 10 0 10 p -5 5 c", &base_style, &[]);
2190
2191 let drawing = parsed.lines[0].spans[0]
2192 .drawing
2193 .as_ref()
2194 .expect("drawing span");
2195 assert_eq!(drawing.polygons.len(), 1);
2196 assert!(drawing.polygons[0].len() > 8);
2197 }
2198
2199 #[test]
2200 fn parses_non_closing_move_drawing_spans_in_p_mode() {
2201 let base_style = ParsedStyle::default();
2202 let parsed = parse_dialogue_text(
2203 "{\\p1}m 0 0 l 10 0 10 10 0 10 n 20 20 l 30 20 30 30 20 30",
2204 &base_style,
2205 &[],
2206 );
2207
2208 let drawing = parsed.lines[0].spans[0]
2209 .drawing
2210 .as_ref()
2211 .expect("drawing span");
2212 assert_eq!(drawing.polygons.len(), 2);
2213 assert_eq!(
2214 drawing.polygons[0].first().copied(),
2215 Some(Point { x: 0, y: 0 })
2216 );
2217 assert_eq!(
2218 drawing.polygons[1].first().copied(),
2219 Some(Point { x: 20, y: 20 })
2220 );
2221 }
2222
2223 #[test]
2224 fn parses_timed_transform_overrides() {
2225 let base_style = ParsedStyle::default();
2226 let parsed = parse_dialogue_text(
2227 "{\\t(100,300,2,\\1c&H112233&\\fs48\\fscx150\\fscy50\\fsp4\\bord6\\blur2)}Text",
2228 &base_style,
2229 &[],
2230 );
2231
2232 let transforms = &parsed.lines[0].spans[0].transforms;
2233 assert_eq!(transforms.len(), 1);
2234 assert_eq!(transforms[0].start_ms, 100);
2235 assert_eq!(transforms[0].end_ms, Some(300));
2236 assert_eq!(transforms[0].accel, 2.0);
2237 assert_eq!(transforms[0].style.font_size, Some(48.0));
2238 assert_eq!(transforms[0].style.scale_x, Some(1.5));
2239 assert_eq!(transforms[0].style.scale_y, Some(0.5));
2240 assert_eq!(transforms[0].style.spacing, Some(4.0));
2241 assert_eq!(transforms[0].style.primary_colour, Some(0x0011_2233));
2242 assert_eq!(transforms[0].style.border, Some(6.0));
2243 assert_eq!(transforms[0].style.blur, Some(2.0));
2244 }
2245
2246 #[test]
2247 fn parses_z_rotation_overrides_and_transforms() {
2248 let base_style = ParsedStyle::default();
2249 let parsed = parse_dialogue_text("{\\frz15\\t(0,1000,\\frz45)}Text", &base_style, &[]);
2250
2251 let span = &parsed.lines[0].spans[0];
2252 assert_eq!(span.style.rotation_z, 15.0);
2253 assert_eq!(span.transforms.len(), 1);
2254 assert_eq!(span.transforms[0].style.rotation_z, Some(45.0));
2255 }
2256
2257 #[test]
2258 fn parses_color_and_shadow_overrides() {
2259 let base_style = ParsedStyle::default();
2260 let parsed = parse_dialogue_text(
2261 "{\\1c&H112233&\\4c&H445566&\\1a&H80&\\shad3.5\\blur1.5}Color",
2262 &base_style,
2263 &[],
2264 );
2265
2266 assert_eq!(parsed.lines.len(), 1);
2267 assert_eq!(parsed.lines[0].spans.len(), 1);
2268 assert_eq!(parsed.lines[0].spans[0].style.primary_colour, 0x0011_2280);
2269 assert_eq!(parsed.lines[0].spans[0].style.back_colour, 0x0044_5566);
2270 assert_eq!(parsed.lines[0].spans[0].style.shadow, 3.5);
2271 assert_eq!(parsed.lines[0].spans[0].style.blur, 1.5);
2272 }
2273
2274 #[test]
2275 fn parses_missing_override_metadata_tags() {
2276 let base_style = ParsedStyle {
2277 underline: false,
2278 strike_out: false,
2279 ..ParsedStyle::default()
2280 };
2281 let parsed = parse_dialogue_text(
2282 "{\\u1\\s1\\a10\\q2\\org(320,240)\\frx12\\fry-8\\fax0.25\\fay-0.5\\xbord3\\ybord4\\xshad5\\yshad-6\\be2\\pbo7}Meta",
2283 &base_style,
2284 &[],
2285 );
2286
2287 assert_eq!(
2288 parsed.alignment,
2289 Some(ass::VALIGN_CENTER | ass::HALIGN_CENTER)
2290 );
2291 assert_eq!(parsed.wrap_style, Some(2));
2292 assert_eq!(parsed.origin, Some((320, 240)));
2293 let style = &parsed.lines[0].spans[0].style;
2294 assert!(style.underline);
2295 assert!(style.strike_out);
2296 assert_eq!(style.rotation_x, 12.0);
2297 assert_eq!(style.rotation_y, -8.0);
2298 assert_eq!(style.shear_x, 0.25);
2299 assert_eq!(style.shear_y, -0.5);
2300 assert_eq!(style.border_x, 3.0);
2301 assert_eq!(style.border_y, 4.0);
2302 assert_eq!(style.shadow_x, 5.0);
2303 assert_eq!(style.shadow_y, -6.0);
2304 assert_eq!(style.be, 2.0);
2305 assert_eq!(style.pbo, 7.0);
2306 }
2307
2308 #[test]
2309 fn parses_font_attachments_from_fonts_section() {
2310 let encoded = encode_font_bytes(b"ABC");
2311 let input = format!(
2312 "[Fonts]\nfontname: DemoFont.ttf\n{encoded}\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1"
2313 );
2314 let track = parse_script_text(&input).expect("script should parse");
2315
2316 assert_eq!(track.attachments.len(), 1);
2317 assert_eq!(track.attachments[0].name, "DemoFont.ttf");
2318 assert_eq!(track.attachments[0].data, b"ABC");
2319 }
2320
2321 fn encode_font_bytes(bytes: &[u8]) -> String {
2322 let mut encoded = String::new();
2323 for chunk in bytes.chunks(3) {
2324 let value = match chunk.len() {
2325 1 => u32::from(chunk[0]) << 16,
2326 2 => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8),
2327 _ => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]),
2328 };
2329 let output_len = match chunk.len() {
2330 1 => 2,
2331 2 => 3,
2332 _ => 4,
2333 };
2334 for shift_index in 0..output_len {
2335 let shift = 6 * (3 - shift_index);
2336 let six_bits = ((value >> shift) & 63) as u8;
2337 encoded.push(char::from(six_bits + 33));
2338 }
2339 }
2340 encoded
2341 }
2342}