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