Skip to main content

rassa_parse/
lib.rs

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