Skip to main content

rassa_parse/
lib.rs

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                        &current_style,
628                        pending_karaoke,
629                        drawing_scale,
630                        &current_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                    &current_style,
650                    pending_karaoke,
651                    drawing_scale,
652                    &current_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        &current_style,
665        pending_karaoke,
666        drawing_scale,
667        &current_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}