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