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