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