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        let fraction = format!("{fraction:0<2}");
1010        (
1011            seconds.trim().parse::<i64>().ok()?,
1012            fraction[..2].parse::<i64>().ok()?,
1013        )
1014    } else {
1015        (seconds.parse::<i64>().ok()?, 0)
1016    };
1017    Some((((hours * 60 + minutes) * 60) + seconds) * 1000 + centiseconds * 10)
1018}
1019
1020fn parse_style_reference(value: &str, styles: &[ParsedStyle]) -> i32 {
1021    let style_name = value.trim();
1022    if style_name.is_empty() {
1023        return 0;
1024    }
1025
1026    styles
1027        .iter()
1028        .position(|style| style.name.eq_ignore_ascii_case(style_name))
1029        .map(|index| index as i32)
1030        .unwrap_or(0)
1031}
1032
1033#[allow(clippy::too_many_arguments)]
1034fn apply_override_block(
1035    block: &str,
1036    base_style: &ParsedStyle,
1037    styles: &[ParsedStyle],
1038    current_style: &mut ParsedSpanStyle,
1039    parsed: &mut ParsedDialogueText,
1040    buffer: &mut String,
1041    active_line: &mut ParsedTextLine,
1042    pending_karaoke: &mut Option<ParsedKaraokeSpan>,
1043    karaoke_cursor_ms: &mut i32,
1044    drawing_scale: &mut i32,
1045    current_transforms: &mut Vec<ParsedSpanTransform>,
1046    current_wrap_style: &mut i32,
1047) {
1048    for raw_tag in split_override_tags(block) {
1049        let tag = raw_tag.trim();
1050        if tag.is_empty() {
1051            continue;
1052        }
1053
1054        let previous = current_style.clone();
1055        let previous_transforms = current_transforms.clone();
1056        if let Some(rest) = tag.strip_prefix("fn") {
1057            let family = rest.trim();
1058            if !family.is_empty() {
1059                current_style.font_name = family.to_string();
1060            }
1061        } else if let Some(rest) = tag.strip_prefix("fe") {
1062            current_style.encoding = parse_i32(rest, current_style.encoding);
1063        } else if let Some(rest) = tag.strip_prefix("kt") {
1064            flush_span(
1065                buffer,
1066                &previous,
1067                *pending_karaoke,
1068                *drawing_scale,
1069                &previous_transforms,
1070                active_line,
1071            );
1072            *karaoke_cursor_ms = parse_karaoke_duration(rest).unwrap_or(0);
1073            *pending_karaoke = None;
1074        } else if let Some((rest, mode)) = tag
1075            .strip_prefix("kf")
1076            .map(|rest| (rest, ParsedKaraokeMode::Sweep))
1077            .or_else(|| {
1078                tag.strip_prefix("ko")
1079                    .map(|rest| (rest, ParsedKaraokeMode::OutlineToggle))
1080            })
1081            .or_else(|| {
1082                tag.strip_prefix('K')
1083                    .map(|rest| (rest, ParsedKaraokeMode::Sweep))
1084            })
1085            .or_else(|| {
1086                tag.strip_prefix('k')
1087                    .map(|rest| (rest, ParsedKaraokeMode::FillSwap))
1088            })
1089        {
1090            flush_span(
1091                buffer,
1092                &previous,
1093                *pending_karaoke,
1094                *drawing_scale,
1095                &previous_transforms,
1096                active_line,
1097            );
1098            if let Some(duration_ms) = parse_karaoke_duration(rest) {
1099                *pending_karaoke = Some(ParsedKaraokeSpan {
1100                    start_ms: *karaoke_cursor_ms,
1101                    duration_ms,
1102                    mode,
1103                });
1104                *karaoke_cursor_ms += duration_ms;
1105            }
1106        } else if let Some(rest) = tag.strip_prefix("fscx") {
1107            current_style.scale_x = parse_scale(rest, base_style.scale_x);
1108        } else if let Some(rest) = tag.strip_prefix("fscy") {
1109            current_style.scale_y = parse_scale(rest, base_style.scale_y);
1110        } else if tag == "fsc" {
1111            current_style.scale_x = base_style.scale_x;
1112            current_style.scale_y = base_style.scale_y;
1113        } else if let Some(rest) = tag.strip_prefix("fsp") {
1114            current_style.spacing = parse_f64(rest, current_style.spacing);
1115        } else if let Some(rest) = tag.strip_prefix("frx") {
1116            current_style.rotation_x = parse_f64(rest, current_style.rotation_x);
1117        } else if let Some(rest) = tag.strip_prefix("fry") {
1118            current_style.rotation_y = parse_f64(rest, current_style.rotation_y);
1119        } else if let Some(rest) = tag.strip_prefix("frz").or_else(|| tag.strip_prefix("fr")) {
1120            current_style.rotation_z = parse_f64(rest, current_style.rotation_z);
1121        } else if let Some(rest) = tag.strip_prefix("fax") {
1122            current_style.shear_x = parse_f64(rest, current_style.shear_x);
1123        } else if let Some(rest) = tag.strip_prefix("fay") {
1124            current_style.shear_y = parse_f64(rest, current_style.shear_y);
1125        } else if let Some(rest) = tag.strip_prefix("fs") {
1126            current_style.font_size =
1127                parse_font_size_override(rest, current_style.font_size, base_style.font_size);
1128        } else if let Some(rest) = tag.strip_prefix("iclip") {
1129            if let Some(rect) = parse_rect_clip(rest) {
1130                parsed.clip_rect = Some(rect);
1131                parsed.clip_rect_exact = parse_rect_clip_exact(rest);
1132                parsed.vector_clip = None;
1133                parsed.inverse_clip = true;
1134            } else if let Some(rect) = parse_rect_clip_exact(rest) {
1135                parsed.clip_rect = None;
1136                parsed.clip_rect_exact = Some(rect);
1137                parsed.vector_clip = None;
1138                parsed.inverse_clip = true;
1139            } else if let Some(vector) = parse_vector_clip(rest) {
1140                parsed.clip_rect = None;
1141                parsed.clip_rect_exact = None;
1142                parsed.vector_clip = Some(vector);
1143                parsed.inverse_clip = true;
1144            }
1145        } else if let Some(rest) = tag.strip_prefix("move") {
1146            if parsed.position.is_none()
1147                && parsed.position_exact.is_none()
1148                && parsed.movement.is_none()
1149                && parsed.movement_exact.is_none()
1150            {
1151                parsed.movement = parse_move(rest);
1152                parsed.movement_exact = parse_move_exact(rest);
1153            }
1154        } else if let Some(rest) = tag.strip_prefix("fade") {
1155            parsed.fade = parse_fade(rest);
1156        } else if let Some(rest) = tag.strip_prefix("fad") {
1157            parsed.fade = parse_fad(rest);
1158        } else if let Some(rest) = tag.strip_prefix("clip") {
1159            if let Some(rect) = parse_rect_clip(rest) {
1160                parsed.clip_rect = Some(rect);
1161                parsed.clip_rect_exact = parse_rect_clip_exact(rest);
1162                parsed.vector_clip = None;
1163                parsed.inverse_clip = false;
1164            } else if let Some(rect) = parse_rect_clip_exact(rest) {
1165                parsed.clip_rect = None;
1166                parsed.clip_rect_exact = Some(rect);
1167                parsed.vector_clip = None;
1168                parsed.inverse_clip = false;
1169            } else if let Some(vector) = parse_vector_clip(rest) {
1170                parsed.clip_rect = None;
1171                parsed.clip_rect_exact = None;
1172                parsed.vector_clip = Some(vector);
1173                parsed.inverse_clip = false;
1174            }
1175        } else if let Some(rest) = tag.strip_prefix("1c").or_else(|| tag.strip_prefix('c')) {
1176            current_style.primary_colour = parse_override_color(rest, current_style.primary_colour);
1177        } else if let Some(rest) = tag.strip_prefix("2c") {
1178            current_style.secondary_colour =
1179                parse_override_color(rest, current_style.secondary_colour);
1180        } else if let Some(rest) = tag.strip_prefix("3c") {
1181            current_style.outline_colour = parse_override_color(rest, current_style.outline_colour);
1182        } else if let Some(rest) = tag.strip_prefix("4c") {
1183            current_style.back_colour = parse_override_color(rest, current_style.back_colour);
1184        } else if let Some(rest) = tag.strip_prefix("alpha") {
1185            let alpha = parse_alpha_tag(rest, alpha_of(current_style.primary_colour));
1186            current_style.primary_colour = with_alpha(current_style.primary_colour, alpha);
1187            current_style.secondary_colour = with_alpha(current_style.secondary_colour, alpha);
1188            current_style.outline_colour = with_alpha(current_style.outline_colour, alpha);
1189            current_style.back_colour = with_alpha(current_style.back_colour, alpha);
1190        } else if let Some(rest) = tag.strip_prefix("1a") {
1191            let alpha = parse_alpha_tag(rest, alpha_of(current_style.primary_colour));
1192            current_style.primary_colour = with_alpha(current_style.primary_colour, alpha);
1193        } else if let Some(rest) = tag.strip_prefix("2a") {
1194            let alpha = parse_alpha_tag(rest, alpha_of(current_style.secondary_colour));
1195            current_style.secondary_colour = with_alpha(current_style.secondary_colour, alpha);
1196        } else if let Some(rest) = tag.strip_prefix("3a") {
1197            let alpha = parse_alpha_tag(rest, alpha_of(current_style.outline_colour));
1198            current_style.outline_colour = with_alpha(current_style.outline_colour, alpha);
1199        } else if let Some(rest) = tag.strip_prefix("4a") {
1200            let alpha = parse_alpha_tag(rest, alpha_of(current_style.back_colour));
1201            current_style.back_colour = with_alpha(current_style.back_colour, alpha);
1202        } else if let Some(rest) = tag.strip_prefix("xbord") {
1203            current_style.border_x = parse_f64(rest, current_style.border_x);
1204        } else if let Some(rest) = tag.strip_prefix("ybord") {
1205            current_style.border_y = parse_f64(rest, current_style.border_y);
1206        } else if let Some(rest) = tag.strip_prefix("bord") {
1207            current_style.border = parse_f64(rest, current_style.border);
1208            current_style.border_x = current_style.border;
1209            current_style.border_y = current_style.border;
1210        } else if let Some(rest) = tag.strip_prefix("xshad") {
1211            current_style.shadow_x = parse_f64(rest, current_style.shadow_x);
1212        } else if let Some(rest) = tag.strip_prefix("yshad") {
1213            current_style.shadow_y = parse_f64(rest, current_style.shadow_y);
1214        } else if let Some(rest) = tag.strip_prefix("shad") {
1215            current_style.shadow = parse_f64(rest, current_style.shadow);
1216            current_style.shadow_x = current_style.shadow;
1217            current_style.shadow_y = current_style.shadow;
1218        } else if let Some(rest) = tag.strip_prefix("blur") {
1219            current_style.blur = parse_f64(rest, current_style.blur);
1220        } else if let Some(rest) = tag.strip_prefix("be") {
1221            current_style.be = parse_f64(rest, current_style.be);
1222        } else if let Some(rest) = tag.strip_prefix('t') {
1223            if let Some(transform) = parse_transform(rest, current_style) {
1224                current_transforms.push(transform);
1225            }
1226        } else if let Some(rest) = tag.strip_prefix('u') {
1227            current_style.underline = parse_override_bool(rest, current_style.underline);
1228        } else if let Some(rest) = tag.strip_prefix('s') {
1229            current_style.strike_out = parse_override_bool(rest, current_style.strike_out);
1230        } else if let Some(rest) = tag.strip_prefix('b') {
1231            current_style.font_weight = parse_override_bold_weight(rest, current_style.font_weight);
1232            current_style.bold = bold_weight_is_active(current_style.font_weight);
1233        } else if let Some(rest) = tag.strip_prefix('i') {
1234            current_style.italic = parse_override_bool(rest, current_style.italic);
1235        } else if let Some(rest) = tag.strip_prefix("an") {
1236            if let Ok(value) = rest.trim().parse::<i32>() {
1237                parsed.alignment = alignment_from_an(value);
1238            }
1239        } else if let Some(rest) = tag.strip_prefix('a') {
1240            if let Ok(value) = rest.trim().parse::<i32>() {
1241                parsed.alignment = alignment_from_legacy_a(value);
1242            }
1243        } else if let Some(rest) = tag.strip_prefix('q') {
1244            if let Ok(value) = rest.trim().parse::<i32>() {
1245                let value = value.clamp(0, 3);
1246                parsed.wrap_style = Some(value);
1247                *current_wrap_style = value;
1248            }
1249        } else if let Some(rest) = tag.strip_prefix("org") {
1250            parsed.origin = parse_pos(rest);
1251            parsed.origin_exact = parse_pos_exact(rest);
1252        } else if let Some(rest) = tag.strip_prefix("pos") {
1253            if parsed.position.is_none()
1254                && parsed.position_exact.is_none()
1255                && parsed.movement.is_none()
1256                && parsed.movement_exact.is_none()
1257            {
1258                parsed.position = parse_pos(rest);
1259                parsed.position_exact = parse_pos_exact(rest);
1260            }
1261        } else if let Some(rest) = tag.strip_prefix("pbo") {
1262            current_style.pbo = parse_f64(rest, current_style.pbo);
1263        } else if let Some(rest) = tag.strip_prefix('p') {
1264            flush_span(
1265                buffer,
1266                &previous,
1267                *pending_karaoke,
1268                *drawing_scale,
1269                &previous_transforms,
1270                active_line,
1271            );
1272            *drawing_scale = parse_i32(rest, *drawing_scale).max(0);
1273        } else if let Some(rest) = tag.strip_prefix('r') {
1274            *current_style = resolve_reset_style(rest, base_style, styles);
1275            current_transforms.clear();
1276        }
1277
1278        suppress_transform_fields_for_override(tag, current_transforms);
1279
1280        if *current_style != previous || *current_transforms != previous_transforms {
1281            flush_span(
1282                buffer,
1283                &previous,
1284                *pending_karaoke,
1285                *drawing_scale,
1286                &previous_transforms,
1287                active_line,
1288            );
1289        }
1290    }
1291}
1292
1293fn suppress_transform_fields_for_override(
1294    tag: &str,
1295    current_transforms: &mut Vec<ParsedSpanTransform>,
1296) {
1297    if current_transforms.is_empty() || tag.strip_prefix('t').is_some() {
1298        return;
1299    }
1300
1301    for transform in current_transforms.iter_mut() {
1302        let style = &mut transform.style;
1303        if tag
1304            .strip_prefix("1c")
1305            .or_else(|| tag.strip_prefix('c'))
1306            .is_some()
1307        {
1308            style.primary_colour = None;
1309        } else if tag.strip_prefix("2c").is_some() {
1310            style.secondary_colour = None;
1311        } else if tag.strip_prefix("3c").is_some() {
1312            style.outline_colour = None;
1313        } else if tag.strip_prefix("4c").is_some() {
1314            style.back_colour = None;
1315        } else if tag.strip_prefix("alpha").is_some() {
1316            style.clear_colours();
1317        } else if tag.strip_prefix("1a").is_some() {
1318            style.primary_colour = None;
1319        } else if tag.strip_prefix("2a").is_some() {
1320            style.secondary_colour = None;
1321        } else if tag.strip_prefix("3a").is_some() {
1322            style.outline_colour = None;
1323        } else if tag.strip_prefix("4a").is_some() {
1324            style.back_colour = None;
1325        } else if tag.strip_prefix("fscx").is_some() {
1326            style.scale_x = None;
1327        } else if tag.strip_prefix("fscy").is_some() {
1328            style.scale_y = None;
1329        } else if tag == "fsc" {
1330            style.scale_x = None;
1331            style.scale_y = None;
1332        } else if tag.strip_prefix("fsp").is_some() {
1333            style.spacing = None;
1334        } else if tag.strip_prefix("frx").is_some() {
1335            style.rotation_x = None;
1336        } else if tag.strip_prefix("fry").is_some() {
1337            style.rotation_y = None;
1338        } else if tag
1339            .strip_prefix("frz")
1340            .or_else(|| tag.strip_prefix("fr"))
1341            .is_some()
1342        {
1343            style.rotation_z = None;
1344        } else if tag.strip_prefix("fax").is_some() {
1345            style.shear_x = None;
1346        } else if tag.strip_prefix("fay").is_some() {
1347            style.shear_y = None;
1348        } else if tag.strip_prefix("fs").is_some() {
1349            style.font_size = None;
1350        } else if tag.strip_prefix("xbord").is_some() {
1351            style.border_x = None;
1352        } else if tag.strip_prefix("ybord").is_some() {
1353            style.border_y = None;
1354        } else if tag.strip_prefix("bord").is_some() {
1355            style.border = None;
1356            style.border_x = None;
1357            style.border_y = None;
1358        } else if tag.strip_prefix("xshad").is_some() {
1359            style.shadow_x = None;
1360        } else if tag.strip_prefix("yshad").is_some() {
1361            style.shadow_y = None;
1362        } else if tag.strip_prefix("shad").is_some() {
1363            style.shadow = None;
1364            style.shadow_x = None;
1365            style.shadow_y = None;
1366        } else if tag.strip_prefix("blur").is_some() {
1367            style.blur = None;
1368        } else if tag.strip_prefix("be").is_some() {
1369            style.be = None;
1370        }
1371    }
1372
1373    current_transforms.retain(|transform| !transform.style.is_empty());
1374}
1375
1376fn parse_transform(value: &str, current_style: &ParsedSpanStyle) -> Option<ParsedSpanTransform> {
1377    let inside = value.trim().strip_prefix('(')?.strip_suffix(')')?.trim();
1378    let tag_start = inside.find('\\')?;
1379    let (timing_part, tags_part) = inside.split_at(tag_start);
1380    let params = timing_part
1381        .split(',')
1382        .map(str::trim)
1383        .filter(|part| !part.is_empty())
1384        .collect::<Vec<_>>();
1385
1386    let (start_ms, end_ms, accel) = match params.as_slice() {
1387        [] => (0, None, 1.0),
1388        [accel] => (0, None, parse_f64(accel, 1.0)),
1389        [start, end] => (
1390            parse_i32(start, 0).max(0),
1391            Some(parse_i32(end, 0).max(parse_i32(start, 0))),
1392            1.0,
1393        ),
1394        [start, end, accel, ..] => (
1395            parse_i32(start, 0).max(0),
1396            Some(parse_i32(end, 0).max(parse_i32(start, 0))),
1397            parse_f64(accel, 1.0),
1398        ),
1399    };
1400
1401    let mut target_style = current_style.clone();
1402    for raw_tag in split_override_tags(tags_part) {
1403        apply_transform_tag(raw_tag.trim(), &mut target_style);
1404    }
1405
1406    let animated = diff_animated_style(current_style, &target_style);
1407    (!animated.is_empty()).then_some(ParsedSpanTransform {
1408        start_ms,
1409        end_ms,
1410        accel: if accel > 0.0 { accel } else { 1.0 },
1411        style: animated,
1412    })
1413}
1414
1415fn split_override_tags(block: &str) -> Vec<&str> {
1416    let mut tags = Vec::new();
1417    let mut start = None;
1418    let mut depth = 0_i32;
1419
1420    for (index, character) in block.char_indices() {
1421        match character {
1422            '\\' if depth == 0 => {
1423                if let Some(tag_start) = start.take() {
1424                    let tag = block[tag_start..index].trim();
1425                    if !tag.is_empty() {
1426                        tags.push(tag);
1427                    }
1428                }
1429                start = Some(index + character.len_utf8());
1430            }
1431            '(' => depth += 1,
1432            ')' => depth = (depth - 1).max(0),
1433            _ => {}
1434        }
1435    }
1436
1437    if let Some(tag_start) = start {
1438        let tag = block[tag_start..].trim();
1439        if !tag.is_empty() {
1440            tags.push(tag);
1441        }
1442    }
1443
1444    tags
1445}
1446
1447fn apply_transform_tag(tag: &str, style: &mut ParsedSpanStyle) {
1448    if let Some(rest) = tag.strip_prefix("1c").or_else(|| tag.strip_prefix('c')) {
1449        style.primary_colour = parse_override_color(rest, style.primary_colour);
1450    } else if let Some(rest) = tag.strip_prefix("2c") {
1451        style.secondary_colour = parse_override_color(rest, style.secondary_colour);
1452    } else if let Some(rest) = tag.strip_prefix("3c") {
1453        style.outline_colour = parse_override_color(rest, style.outline_colour);
1454    } else if let Some(rest) = tag.strip_prefix("4c") {
1455        style.back_colour = parse_override_color(rest, style.back_colour);
1456    } else if let Some(rest) = tag.strip_prefix("alpha") {
1457        let alpha = parse_alpha_tag(rest, alpha_of(style.primary_colour));
1458        style.primary_colour = with_alpha(style.primary_colour, alpha);
1459        style.secondary_colour = with_alpha(style.secondary_colour, alpha);
1460        style.outline_colour = with_alpha(style.outline_colour, alpha);
1461        style.back_colour = with_alpha(style.back_colour, alpha);
1462    } else if let Some(rest) = tag.strip_prefix("1a") {
1463        style.primary_colour = with_alpha(
1464            style.primary_colour,
1465            parse_alpha_tag(rest, alpha_of(style.primary_colour)),
1466        );
1467    } else if let Some(rest) = tag.strip_prefix("2a") {
1468        style.secondary_colour = with_alpha(
1469            style.secondary_colour,
1470            parse_alpha_tag(rest, alpha_of(style.secondary_colour)),
1471        );
1472    } else if let Some(rest) = tag.strip_prefix("3a") {
1473        style.outline_colour = with_alpha(
1474            style.outline_colour,
1475            parse_alpha_tag(rest, alpha_of(style.outline_colour)),
1476        );
1477    } else if let Some(rest) = tag.strip_prefix("4a") {
1478        style.back_colour = with_alpha(
1479            style.back_colour,
1480            parse_alpha_tag(rest, alpha_of(style.back_colour)),
1481        );
1482    } else if let Some(rest) = tag.strip_prefix("fscx") {
1483        style.scale_x = parse_scale(rest, style.scale_x);
1484    } else if let Some(rest) = tag.strip_prefix("fscy") {
1485        style.scale_y = parse_scale(rest, style.scale_y);
1486    } else if let Some(rest) = tag.strip_prefix("fsp") {
1487        style.spacing = parse_f64(rest, style.spacing);
1488    } else if let Some(rest) = tag.strip_prefix("frx") {
1489        style.rotation_x = parse_f64(rest, style.rotation_x);
1490    } else if let Some(rest) = tag.strip_prefix("fry") {
1491        style.rotation_y = parse_f64(rest, style.rotation_y);
1492    } else if let Some(rest) = tag.strip_prefix("frz").or_else(|| tag.strip_prefix("fr")) {
1493        style.rotation_z = parse_f64(rest, style.rotation_z);
1494    } else if let Some(rest) = tag.strip_prefix("fax") {
1495        style.shear_x = parse_f64(rest, style.shear_x);
1496    } else if let Some(rest) = tag.strip_prefix("fay") {
1497        style.shear_y = parse_f64(rest, style.shear_y);
1498    } else if let Some(rest) = tag.strip_prefix("fs") {
1499        style.font_size = parse_f64(rest, style.font_size);
1500    } else if let Some(rest) = tag.strip_prefix("xbord") {
1501        style.border_x = parse_f64(rest, style.border_x);
1502    } else if let Some(rest) = tag.strip_prefix("ybord") {
1503        style.border_y = parse_f64(rest, style.border_y);
1504    } else if let Some(rest) = tag.strip_prefix("bord") {
1505        style.border = parse_f64(rest, style.border);
1506        style.border_x = style.border;
1507        style.border_y = style.border;
1508    } else if let Some(rest) = tag.strip_prefix("xshad") {
1509        style.shadow_x = parse_f64(rest, style.shadow_x);
1510    } else if let Some(rest) = tag.strip_prefix("yshad") {
1511        style.shadow_y = parse_f64(rest, style.shadow_y);
1512    } else if let Some(rest) = tag.strip_prefix("shad") {
1513        style.shadow = parse_f64(rest, style.shadow);
1514        style.shadow_x = style.shadow;
1515        style.shadow_y = style.shadow;
1516    } else if let Some(rest) = tag.strip_prefix("blur") {
1517        style.blur = parse_f64(rest, style.blur);
1518    } else if let Some(rest) = tag.strip_prefix("be") {
1519        style.be = parse_f64(rest, style.be);
1520    }
1521}
1522
1523fn diff_animated_style(base: &ParsedSpanStyle, target: &ParsedSpanStyle) -> ParsedAnimatedStyle {
1524    ParsedAnimatedStyle {
1525        font_size: ((target.font_size - base.font_size).abs() > f64::EPSILON)
1526            .then_some(target.font_size),
1527        scale_x: ((target.scale_x - base.scale_x).abs() > f64::EPSILON).then_some(target.scale_x),
1528        scale_y: ((target.scale_y - base.scale_y).abs() > f64::EPSILON).then_some(target.scale_y),
1529        spacing: ((target.spacing - base.spacing).abs() > f64::EPSILON).then_some(target.spacing),
1530        rotation_x: ((target.rotation_x - base.rotation_x).abs() > f64::EPSILON)
1531            .then_some(target.rotation_x),
1532        rotation_y: ((target.rotation_y - base.rotation_y).abs() > f64::EPSILON)
1533            .then_some(target.rotation_y),
1534        rotation_z: ((target.rotation_z - base.rotation_z).abs() > f64::EPSILON)
1535            .then_some(target.rotation_z),
1536        shear_x: ((target.shear_x - base.shear_x).abs() > f64::EPSILON).then_some(target.shear_x),
1537        shear_y: ((target.shear_y - base.shear_y).abs() > f64::EPSILON).then_some(target.shear_y),
1538        primary_colour: (target.primary_colour != base.primary_colour)
1539            .then_some(target.primary_colour),
1540        secondary_colour: (target.secondary_colour != base.secondary_colour)
1541            .then_some(target.secondary_colour),
1542        outline_colour: (target.outline_colour != base.outline_colour)
1543            .then_some(target.outline_colour),
1544        back_colour: (target.back_colour != base.back_colour).then_some(target.back_colour),
1545        border: ((target.border - base.border).abs() > f64::EPSILON).then_some(target.border),
1546        border_x: ((target.border_x - base.border_x).abs() > f64::EPSILON)
1547            .then_some(target.border_x),
1548        border_y: ((target.border_y - base.border_y).abs() > f64::EPSILON)
1549            .then_some(target.border_y),
1550        shadow: ((target.shadow - base.shadow).abs() > f64::EPSILON).then_some(target.shadow),
1551        shadow_x: ((target.shadow_x - base.shadow_x).abs() > f64::EPSILON)
1552            .then_some(target.shadow_x),
1553        shadow_y: ((target.shadow_y - base.shadow_y).abs() > f64::EPSILON)
1554            .then_some(target.shadow_y),
1555        blur: ((target.blur - base.blur).abs() > f64::EPSILON).then_some(target.blur),
1556        be: ((target.be - base.be).abs() > f64::EPSILON).then_some(target.be),
1557    }
1558}
1559
1560fn parse_font_size_override(value: &str, current: f64, base: f64) -> f64 {
1561    let trimmed = value.trim();
1562    if trimmed.is_empty() {
1563        return base;
1564    }
1565
1566    let parsed = trimmed.parse::<f64>().unwrap_or(0.0);
1567    let resolved = if trimmed.starts_with(['+', '-']) {
1568        current * (1.0 + parsed / 10.0)
1569    } else {
1570        parsed
1571    };
1572
1573    if resolved > 0.0 { resolved } else { base }
1574}
1575
1576fn parse_karaoke_duration(value: &str) -> Option<i32> {
1577    value
1578        .trim()
1579        .parse::<i32>()
1580        .ok()
1581        .map(|centiseconds| centiseconds.max(0) * 10)
1582}
1583
1584fn parse_override_color(value: &str, fallback: u32) -> u32 {
1585    let trimmed = value.trim();
1586    let trimmed = trimmed.trim_matches('&').trim_start_matches(['H', 'h']);
1587    if trimmed.is_empty() {
1588        return fallback;
1589    }
1590
1591    u32::from_str_radix(trimmed, 16).unwrap_or(fallback)
1592}
1593
1594fn parse_alpha_tag(value: &str, fallback: u8) -> u8 {
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    u8::from_str_radix(trimmed, 16).unwrap_or(fallback)
1601}
1602
1603fn alpha_of(color: u32) -> u8 {
1604    ((color >> 24) & 0xFF) as u8
1605}
1606
1607fn with_alpha(color: u32, alpha: u8) -> u32 {
1608    (color & 0x00FF_FFFF) | (u32::from(alpha) << 24)
1609}
1610
1611fn parse_override_bool(value: &str, fallback: bool) -> bool {
1612    let trimmed = value.trim();
1613    if trimmed.is_empty() {
1614        true
1615    } else {
1616        parse_bool(trimmed, fallback)
1617    }
1618}
1619
1620fn alignment_from_an(value: i32) -> Option<i32> {
1621    Some(match value {
1622        1 => ass::VALIGN_SUB | ass::HALIGN_LEFT,
1623        2 => ass::VALIGN_SUB | ass::HALIGN_CENTER,
1624        3 => ass::VALIGN_SUB | ass::HALIGN_RIGHT,
1625        4 => ass::VALIGN_CENTER | ass::HALIGN_LEFT,
1626        5 => ass::VALIGN_CENTER | ass::HALIGN_CENTER,
1627        6 => ass::VALIGN_CENTER | ass::HALIGN_RIGHT,
1628        7 => ass::VALIGN_TOP | ass::HALIGN_LEFT,
1629        8 => ass::VALIGN_TOP | ass::HALIGN_CENTER,
1630        9 => ass::VALIGN_TOP | ass::HALIGN_RIGHT,
1631        _ => return None,
1632    })
1633}
1634
1635fn alignment_from_legacy_a(value: i32) -> Option<i32> {
1636    let halign = match value & 0x3 {
1637        1 => ass::HALIGN_LEFT,
1638        2 => ass::HALIGN_CENTER,
1639        3 => ass::HALIGN_RIGHT,
1640        _ => return None,
1641    };
1642    let valign = if value & 0x4 != 0 {
1643        ass::VALIGN_TOP
1644    } else if value & 0x8 != 0 {
1645        ass::VALIGN_CENTER
1646    } else {
1647        ass::VALIGN_SUB
1648    };
1649    Some(valign | halign)
1650}
1651
1652fn parse_pos(value: &str) -> Option<(i32, i32)> {
1653    let trimmed = value.trim();
1654    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1655    let mut parts = inside.split(',').map(str::trim);
1656    let x = parts.next()?.parse::<i32>().ok()?;
1657    let y = parts.next()?.parse::<i32>().ok()?;
1658    Some((x, y))
1659}
1660
1661fn parse_pos_exact(value: &str) -> Option<(f64, f64)> {
1662    let trimmed = value.trim();
1663    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1664    let mut parts = inside.split(',').map(str::trim);
1665    let x = parts.next()?.parse::<f64>().ok()?;
1666    let y = parts.next()?.parse::<f64>().ok()?;
1667    if parts.next().is_some() || !x.is_finite() || !y.is_finite() {
1668        return None;
1669    }
1670    Some((x, y))
1671}
1672
1673fn parse_rect_clip(value: &str) -> Option<Rect> {
1674    let trimmed = value.trim();
1675    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1676    let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
1677    if parts.len() != 4 {
1678        return None;
1679    }
1680    let x_min = parts[0].parse::<i32>().ok()?;
1681    let y_min = parts[1].parse::<i32>().ok()?;
1682    let x_max = parts[2].parse::<i32>().ok()?;
1683    let y_max = parts[3].parse::<i32>().ok()?;
1684    Some(Rect {
1685        x_min,
1686        y_min,
1687        x_max,
1688        y_max,
1689    })
1690}
1691
1692fn parse_rect_clip_exact(value: &str) -> Option<ParsedRectF64> {
1693    let trimmed = value.trim();
1694    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
1695    let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
1696    if parts.len() != 4 {
1697        return None;
1698    }
1699    let x_min = parts[0].parse::<f64>().ok()?;
1700    let y_min = parts[1].parse::<f64>().ok()?;
1701    let x_max = parts[2].parse::<f64>().ok()?;
1702    let y_max = parts[3].parse::<f64>().ok()?;
1703    if !x_min.is_finite() || !y_min.is_finite() || !x_max.is_finite() || !y_max.is_finite() {
1704        return None;
1705    }
1706    Some(ParsedRectF64 {
1707        x_min,
1708        y_min,
1709        x_max,
1710        y_max,
1711    })
1712}
1713
1714fn parse_vector_clip(value: &str) -> Option<ParsedVectorClip> {
1715    let trimmed = value.trim();
1716    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?.trim();
1717    if inside.is_empty() {
1718        return None;
1719    }
1720
1721    let (scale, drawing) = if let Some((scale, drawing)) = inside.split_once(',') {
1722        if let Ok(scale) = scale.trim().parse::<i32>() {
1723            (scale.max(1), drawing.trim())
1724        } else {
1725            (1, inside)
1726        }
1727    } else {
1728        (1, inside)
1729    };
1730
1731    let polygons = parse_drawing_polygons(drawing, scale)?;
1732    if polygons.is_empty() {
1733        return None;
1734    }
1735
1736    Some(ParsedVectorClip { scale, polygons })
1737}
1738
1739fn parse_drawing_polygons(drawing: &str, scale: i32) -> Option<Vec<Vec<Point>>> {
1740    let tokens = drawing.split_whitespace().collect::<Vec<_>>();
1741    if tokens.is_empty() {
1742        return None;
1743    }
1744
1745    let mut polygons = Vec::new();
1746    let mut current = Vec::new();
1747    let mut spline_state: Option<SplineState> = None;
1748    let mut index = 0;
1749    while index < tokens.len() {
1750        match tokens[index].to_ascii_lowercase().as_str() {
1751            "m" | "n" => {
1752                spline_state = None;
1753                if current.len() >= 3 {
1754                    polygons.push(std::mem::take(&mut current));
1755                }
1756                index += 1;
1757                let (point, next_index) = parse_drawing_point(&tokens, index, scale)?;
1758                current.push(point);
1759                index = next_index;
1760                while let Some((point, next_index)) =
1761                    parse_drawing_point_optional(&tokens, index, scale)
1762                {
1763                    current.push(point);
1764                    index = next_index;
1765                }
1766            }
1767            "l" => {
1768                spline_state = None;
1769                if current.is_empty() {
1770                    return None;
1771                }
1772                index += 1;
1773                let mut consumed = false;
1774                while let Some((point, next_index)) =
1775                    parse_drawing_point_optional(&tokens, index, scale)
1776                {
1777                    current.push(point);
1778                    index = next_index;
1779                    consumed = true;
1780                }
1781                if !consumed {
1782                    return None;
1783                }
1784            }
1785            "b" => {
1786                spline_state = None;
1787                if current.is_empty() {
1788                    return None;
1789                }
1790                index += 1;
1791                let mut consumed = false;
1792                while let Some(((control1, control2, end), next_index)) =
1793                    parse_bezier_segment(&tokens, index, scale)
1794                {
1795                    let start = *current.last()?;
1796                    current.extend(approximate_cubic_bezier(start, control1, control2, end, 16));
1797                    index = next_index;
1798                    consumed = true;
1799                }
1800                if !consumed {
1801                    return None;
1802                }
1803            }
1804            "s" => {
1805                if current.is_empty() {
1806                    return None;
1807                }
1808                index += 1;
1809                let (point1, next_index) = parse_drawing_point(&tokens, index, scale)?;
1810                let (point2, next_index) = parse_drawing_point(&tokens, next_index, scale)?;
1811                let (point3, next_index) = parse_drawing_point(&tokens, next_index, scale)?;
1812                let start = *current.last()?;
1813                current.extend(approximate_spline_segment(
1814                    start, point1, point2, point3, 16,
1815                ));
1816                spline_state = Some(SplineState {
1817                    first_three: [point1, point2, point3],
1818                    history: vec![start, point1, point2, point3],
1819                });
1820                index = next_index;
1821            }
1822            "p" => {
1823                let state = spline_state.as_mut()?;
1824                index += 1;
1825                let mut consumed = false;
1826                while let Some((point, next_index)) =
1827                    parse_drawing_point_optional(&tokens, index, scale)
1828                {
1829                    let len = state.history.len();
1830                    current.extend(approximate_spline_segment(
1831                        state.history[len - 3],
1832                        state.history[len - 2],
1833                        state.history[len - 1],
1834                        point,
1835                        16,
1836                    ));
1837                    state.history.push(point);
1838                    index = next_index;
1839                    consumed = true;
1840                }
1841                if !consumed {
1842                    return None;
1843                }
1844            }
1845            "c" => {
1846                let state = spline_state.take()?;
1847                for point in state.first_three {
1848                    let len = state.history.len();
1849                    current.extend(approximate_spline_segment(
1850                        state.history[len - 3],
1851                        state.history[len - 2],
1852                        state.history[len - 1],
1853                        point,
1854                        16,
1855                    ));
1856                }
1857                index += 1;
1858            }
1859            _ => return None,
1860        }
1861    }
1862
1863    if current.len() >= 3 {
1864        polygons.push(current);
1865    }
1866
1867    Some(polygons)
1868}
1869
1870#[derive(Clone, Debug)]
1871struct SplineState {
1872    first_three: [Point; 3],
1873    history: Vec<Point>,
1874}
1875
1876fn parse_drawing_point(tokens: &[&str], index: usize, scale: i32) -> Option<(Point, usize)> {
1877    let x = tokens.get(index)?.parse::<i32>().ok()?;
1878    let y = tokens.get(index + 1)?.parse::<i32>().ok()?;
1879    Some((scale_drawing_point(x, y, scale), index + 2))
1880}
1881
1882fn parse_drawing_point_optional(
1883    tokens: &[&str],
1884    index: usize,
1885    scale: i32,
1886) -> Option<(Point, usize)> {
1887    let x = tokens.get(index)?;
1888    let y = tokens.get(index + 1)?;
1889    if x.chars().any(|character| character.is_ascii_alphabetic())
1890        || y.chars().any(|character| character.is_ascii_alphabetic())
1891    {
1892        return None;
1893    }
1894    parse_drawing_point(tokens, index, scale)
1895}
1896
1897fn parse_bezier_segment(
1898    tokens: &[&str],
1899    index: usize,
1900    scale: i32,
1901) -> Option<((Point, Point, Point), usize)> {
1902    let (control1, next_index) = parse_drawing_point(tokens, index, scale)?;
1903    let (control2, next_index) = parse_drawing_point(tokens, next_index, scale)?;
1904    let (end, next_index) = parse_drawing_point(tokens, next_index, scale)?;
1905    Some(((control1, control2, end), next_index))
1906}
1907
1908fn approximate_cubic_bezier(
1909    start: Point,
1910    control1: Point,
1911    control2: Point,
1912    end: Point,
1913    segments: usize,
1914) -> Vec<Point> {
1915    let segments = segments.max(1);
1916    let mut points = Vec::with_capacity(segments);
1917    for step in 1..=segments {
1918        let t = step as f64 / segments as f64;
1919        let one_minus_t = 1.0 - t;
1920        let x = one_minus_t.powi(3) * f64::from(start.x)
1921            + 3.0 * one_minus_t.powi(2) * t * f64::from(control1.x)
1922            + 3.0 * one_minus_t * t.powi(2) * f64::from(control2.x)
1923            + t.powi(3) * f64::from(end.x);
1924        let y = one_minus_t.powi(3) * f64::from(start.y)
1925            + 3.0 * one_minus_t.powi(2) * t * f64::from(control1.y)
1926            + 3.0 * one_minus_t * t.powi(2) * f64::from(control2.y)
1927            + t.powi(3) * f64::from(end.y);
1928        let point = Point {
1929            x: x.round() as i32,
1930            y: y.round() as i32,
1931        };
1932        if points.last().copied() != Some(point) {
1933            points.push(point);
1934        }
1935    }
1936    points
1937}
1938
1939fn approximate_spline_segment(
1940    previous: Point,
1941    point1: Point,
1942    point2: Point,
1943    point3: Point,
1944    segments: usize,
1945) -> Vec<Point> {
1946    let x01 = (point1.x - previous.x) / 3;
1947    let y01 = (point1.y - previous.y) / 3;
1948    let x12 = (point2.x - point1.x) / 3;
1949    let y12 = (point2.y - point1.y) / 3;
1950    let x23 = (point3.x - point2.x) / 3;
1951    let y23 = (point3.y - point2.y) / 3;
1952
1953    let start = Point {
1954        x: point1.x + ((x12 - x01) >> 1),
1955        y: point1.y + ((y12 - y01) >> 1),
1956    };
1957    let control1 = Point {
1958        x: point1.x + x12,
1959        y: point1.y + y12,
1960    };
1961    let control2 = Point {
1962        x: point2.x - x12,
1963        y: point2.y - y12,
1964    };
1965    let end = Point {
1966        x: point2.x + ((x23 - x12) >> 1),
1967        y: point2.y + ((y23 - y12) >> 1),
1968    };
1969
1970    approximate_cubic_bezier(start, control1, control2, end, segments)
1971}
1972
1973fn scale_drawing_point(x: i32, y: i32, scale: i32) -> Point {
1974    let factor = 1_i32
1975        .checked_shl(scale.saturating_sub(1) as u32)
1976        .unwrap_or(1)
1977        .max(1);
1978    Point {
1979        x: x / factor,
1980        y: y / factor,
1981    }
1982}
1983
1984fn bounds_from_polygons(polygons: &[Vec<Point>]) -> Option<Rect> {
1985    let mut points = polygons.iter().flat_map(|polygon| polygon.iter().copied());
1986    let first = points.next()?;
1987    let mut x_min = first.x;
1988    let mut y_min = first.y;
1989    let mut x_max = first.x;
1990    let mut y_max = first.y;
1991    for point in points {
1992        x_min = x_min.min(point.x);
1993        y_min = y_min.min(point.y);
1994        x_max = x_max.max(point.x);
1995        y_max = y_max.max(point.y);
1996    }
1997    Some(Rect {
1998        x_min,
1999        y_min,
2000        x_max: x_max + 1,
2001        y_max: y_max + 1,
2002    })
2003}
2004
2005fn parse_move(value: &str) -> Option<ParsedMovement> {
2006    let trimmed = value.trim();
2007    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
2008    let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
2009    let (x1, y1, x2, y2, t1_ms, t2_ms) = match parts.as_slice() {
2010        [x1, y1, x2, y2] => (
2011            x1.parse::<i32>().ok()?,
2012            y1.parse::<i32>().ok()?,
2013            x2.parse::<i32>().ok()?,
2014            y2.parse::<i32>().ok()?,
2015            0,
2016            0,
2017        ),
2018        [x1, y1, x2, y2, t1, t2] => {
2019            let mut t1_ms = t1.parse::<i32>().ok()?;
2020            let mut t2_ms = t2.parse::<i32>().ok()?;
2021            if t1_ms > t2_ms {
2022                std::mem::swap(&mut t1_ms, &mut t2_ms);
2023            }
2024            (
2025                x1.parse::<i32>().ok()?,
2026                y1.parse::<i32>().ok()?,
2027                x2.parse::<i32>().ok()?,
2028                y2.parse::<i32>().ok()?,
2029                t1_ms,
2030                t2_ms,
2031            )
2032        }
2033        _ => return None,
2034    };
2035
2036    Some(ParsedMovement {
2037        start: (x1, y1),
2038        end: (x2, y2),
2039        t1_ms,
2040        t2_ms,
2041    })
2042}
2043
2044fn parse_move_exact(value: &str) -> Option<ParsedMovementExact> {
2045    let trimmed = value.trim();
2046    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
2047    let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
2048    let (x1, y1, x2, y2, t1_ms, t2_ms) = match parts.as_slice() {
2049        [x1, y1, x2, y2] => (
2050            x1.parse::<f64>().ok()?,
2051            y1.parse::<f64>().ok()?,
2052            x2.parse::<f64>().ok()?,
2053            y2.parse::<f64>().ok()?,
2054            0,
2055            0,
2056        ),
2057        [x1, y1, x2, y2, t1, t2] => {
2058            let mut t1_ms = t1.parse::<i32>().ok()?;
2059            let mut t2_ms = t2.parse::<i32>().ok()?;
2060            if t1_ms > t2_ms {
2061                std::mem::swap(&mut t1_ms, &mut t2_ms);
2062            }
2063            (
2064                x1.parse::<f64>().ok()?,
2065                y1.parse::<f64>().ok()?,
2066                x2.parse::<f64>().ok()?,
2067                y2.parse::<f64>().ok()?,
2068                t1_ms,
2069                t2_ms,
2070            )
2071        }
2072        _ => return None,
2073    };
2074    if !x1.is_finite() || !y1.is_finite() || !x2.is_finite() || !y2.is_finite() {
2075        return None;
2076    }
2077
2078    Some(ParsedMovementExact {
2079        start: (x1, y1),
2080        end: (x2, y2),
2081        t1_ms,
2082        t2_ms,
2083    })
2084}
2085
2086fn parse_fad(value: &str) -> Option<ParsedFade> {
2087    let trimmed = value.trim();
2088    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
2089    let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
2090    let [fade_in, fade_out] = parts.as_slice() else {
2091        return None;
2092    };
2093
2094    Some(ParsedFade::Simple {
2095        fade_in_ms: fade_in.parse::<i32>().ok()?,
2096        fade_out_ms: fade_out.parse::<i32>().ok()?,
2097    })
2098}
2099
2100fn parse_fade(value: &str) -> Option<ParsedFade> {
2101    let trimmed = value.trim();
2102    let inside = trimmed.strip_prefix('(')?.strip_suffix(')')?;
2103    let parts = inside.split(',').map(str::trim).collect::<Vec<_>>();
2104    let [a1, a2, a3, t1, t2, t3, t4] = parts.as_slice() else {
2105        return None;
2106    };
2107
2108    Some(ParsedFade::Complex {
2109        alpha1: a1.parse::<i32>().ok()?.clamp(0, 255),
2110        alpha2: a2.parse::<i32>().ok()?.clamp(0, 255),
2111        alpha3: a3.parse::<i32>().ok()?.clamp(0, 255),
2112        t1_ms: t1.parse::<i32>().ok()?,
2113        t2_ms: t2.parse::<i32>().ok()?,
2114        t3_ms: t3.parse::<i32>().ok()?,
2115        t4_ms: t4.parse::<i32>().ok()?,
2116    })
2117}
2118
2119fn resolve_reset_style(
2120    value: &str,
2121    base_style: &ParsedStyle,
2122    styles: &[ParsedStyle],
2123) -> ParsedSpanStyle {
2124    let name = value.trim();
2125    if name.is_empty() {
2126        return ParsedSpanStyle::from_style(base_style);
2127    }
2128
2129    styles
2130        .iter()
2131        .find(|style| style.name.eq_ignore_ascii_case(name))
2132        .map(ParsedSpanStyle::from_style)
2133        .unwrap_or_else(|| ParsedSpanStyle::from_style(base_style))
2134}
2135
2136fn flush_span(
2137    buffer: &mut String,
2138    style: &ParsedSpanStyle,
2139    karaoke: Option<ParsedKaraokeSpan>,
2140    drawing_scale: i32,
2141    transforms: &[ParsedSpanTransform],
2142    line: &mut ParsedTextLine,
2143) {
2144    if buffer.is_empty() {
2145        return;
2146    }
2147    let text = std::mem::take(buffer);
2148    let drawing = (drawing_scale > 0)
2149        .then(|| parse_drawing_polygons(&text, drawing_scale))
2150        .flatten()
2151        .map(|polygons| ParsedDrawing {
2152            scale: drawing_scale,
2153            polygons,
2154        });
2155    line.text.push_str(&text);
2156    line.spans.push(ParsedTextSpan {
2157        text,
2158        style: style.clone(),
2159        transforms: transforms.to_vec(),
2160        karaoke,
2161        drawing,
2162    });
2163}
2164
2165fn push_line(parsed: &mut ParsedDialogueText, line: &mut ParsedTextLine) {
2166    if line.text.is_empty() && line.spans.is_empty() && !parsed.lines.is_empty() {
2167        return;
2168    }
2169    parsed.lines.push(std::mem::take(line));
2170}
2171
2172fn parse_matrix(value: &str) -> YCbCrMatrix {
2173    match value.trim().to_ascii_lowercase().as_str() {
2174        "none" => YCbCrMatrix::None,
2175        "tv.601" | "bt601(tv)" | "bt.601(tv)" => YCbCrMatrix::Bt601Tv,
2176        "pc.601" | "bt601(pc)" | "bt.601(pc)" => YCbCrMatrix::Bt601Pc,
2177        "tv.709" | "bt709(tv)" | "bt.709(tv)" => YCbCrMatrix::Bt709Tv,
2178        "pc.709" | "bt709(pc)" | "bt.709(pc)" => YCbCrMatrix::Bt709Pc,
2179        "tv.240m" | "smpte240m(tv)" => YCbCrMatrix::Smpte240mTv,
2180        "pc.240m" | "smpte240m(pc)" => YCbCrMatrix::Smpte240mPc,
2181        "tv.fcc" | "fcc(tv)" => YCbCrMatrix::FccTv,
2182        "pc.fcc" | "fcc(pc)" => YCbCrMatrix::FccPc,
2183        "" => YCbCrMatrix::Default,
2184        _ => YCbCrMatrix::Unknown,
2185    }
2186}
2187
2188#[cfg(test)]
2189mod tests {
2190    use super::*;
2191
2192    #[test]
2193    fn parses_basic_ass_script() {
2194        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!";
2195        let track = parse_script_text(input).expect("script should parse");
2196
2197        assert_eq!(track.play_res_x, 1280);
2198        assert_eq!(track.play_res_y, 720);
2199        assert_eq!(track.styles.len(), 1);
2200        assert_eq!(track.events.len(), 1);
2201        assert_eq!(track.events[0].start, 1000);
2202        assert_eq!(track.events[0].duration, 2500);
2203        assert_eq!(track.events[0].style, 0);
2204        assert_eq!(track.events[0].text, "Hello, world!");
2205        assert_eq!(
2206            track.styles[0].alignment,
2207            ass::VALIGN_SUB | ass::HALIGN_CENTER
2208        );
2209    }
2210
2211    #[test]
2212    fn decodes_legacy_codepage_bytes_before_parsing() {
2213        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();
2214        input.extend_from_slice(&[
2215            68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 44, 48, 58, 48, 48, 58, 48, 48, 46,
2216            48, 48, 44, 48, 58, 48, 48, 58, 48, 49, 46, 48, 48, 44, 68, 101, 102, 97, 117, 108,
2217            116, 44, 44, 48, 44, 48, 44, 48, 44, 44, 147, 250, 150, 123, 140, 234,
2218        ]);
2219
2220        let track = parse_script_bytes_with_codepage(&input, Some("SHIFT_JIS"))
2221            .expect("Shift-JIS script should parse");
2222
2223        assert_eq!(track.events.len(), 1);
2224        assert_eq!(track.events[0].text, "日本語");
2225    }
2226
2227    #[test]
2228    fn normalizes_style_alignment_numbers_to_libass_bits() {
2229        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";
2230        let track = parse_script_text(input).expect("script should parse");
2231
2232        assert_eq!(
2233            track.styles[0].alignment,
2234            ass::VALIGN_CENTER | ass::HALIGN_CENTER
2235        );
2236    }
2237
2238    #[test]
2239    fn resolves_event_style_by_name() {
2240        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";
2241        let track = parse_script_text(input).expect("script should parse");
2242
2243        assert_eq!(track.styles.len(), 2);
2244        assert_eq!(track.events.len(), 1);
2245        assert_eq!(track.events[0].style, 1);
2246    }
2247
2248    #[test]
2249    fn parses_dialogue_overrides_into_spans_and_event_metadata() {
2250        let base_style = ParsedStyle {
2251            font_name: "Arial".to_string(),
2252            font_size: 20.0,
2253            ..ParsedStyle::default()
2254        };
2255        let alt_style = ParsedStyle {
2256            name: "Alt".to_string(),
2257            font_name: "DejaVu Sans".to_string(),
2258            font_size: 28.0,
2259            ..ParsedStyle::default()
2260        };
2261        let parsed = parse_dialogue_text(
2262            "{\\fnLiberation Sans\\fs32\\fscx150\\fscy75\\fsp3\\an7}Hello{\\rAlt} world\\N{\\pos(120,48)}again",
2263            &base_style,
2264            &[base_style.clone(), alt_style.clone()],
2265        );
2266
2267        assert_eq!(parsed.alignment, Some(ass::VALIGN_TOP | ass::HALIGN_LEFT));
2268        assert_eq!(parsed.position, Some((120, 48)));
2269        assert_eq!(parsed.lines.len(), 2);
2270        assert_eq!(parsed.lines[0].spans.len(), 2);
2271        assert_eq!(parsed.lines[0].spans[0].style.font_name, "Liberation Sans");
2272        assert_eq!(parsed.lines[0].spans[0].style.font_size, 32.0);
2273        assert_eq!(parsed.lines[0].spans[0].style.scale_x, 1.5);
2274        assert_eq!(parsed.lines[0].spans[0].style.scale_y, 0.75);
2275        assert_eq!(parsed.lines[0].spans[0].style.spacing, 3.0);
2276        assert_eq!(parsed.lines[0].spans[1].style.font_name, "DejaVu Sans");
2277        assert_eq!(parsed.lines[1].text, "again");
2278    }
2279
2280    #[test]
2281    fn fe_override_updates_span_encoding() {
2282        let base_style = ParsedStyle {
2283            encoding: 1,
2284            ..ParsedStyle::default()
2285        };
2286        let parsed = parse_dialogue_text("{\\fe128}encoded", &base_style, &[]);
2287
2288        assert_eq!(parsed.lines[0].spans[0].style.encoding, 128);
2289    }
2290
2291    #[test]
2292    fn numeric_bold_preserves_weight_and_matches_libass_thresholds() {
2293        let style = ParsedStyle::default();
2294        for (tag, expected_bold, expected_weight) in [
2295            ("0", false, 400),
2296            ("1", true, 700),
2297            ("100", false, 100),
2298            ("400", false, 400),
2299            ("500", false, 500),
2300            ("700", true, 700),
2301            ("900", true, 900),
2302        ] {
2303            let parsed = parse_dialogue_text(&format!("{{\\b{tag}}}bold"), &style, &[]);
2304            let span_style = &parsed.lines[0].spans[0].style;
2305            assert_eq!(
2306                span_style.bold, expected_bold,
2307                "unexpected bold state for \\b{tag}"
2308            );
2309            assert_eq!(
2310                span_style.font_weight, expected_weight,
2311                "unexpected preserved font weight for \\b{tag}"
2312            );
2313        }
2314    }
2315
2316    #[test]
2317    fn parse_text_preserves_unknown_literal_backslash_escapes() {
2318        let style = ParsedStyle::default();
2319        let parsed = parse_dialogue_text("animated \\t and drawing \\p", &style, &[]);
2320
2321        assert_eq!(parsed.lines.len(), 1);
2322        assert_eq!(parsed.lines[0].spans.len(), 1);
2323        assert_eq!(
2324            parsed.lines[0].spans[0].text,
2325            "animated \\t and drawing \\p"
2326        );
2327    }
2328
2329    #[test]
2330    fn override_alpha_tags_update_ass_alpha_byte() {
2331        let style = ParsedStyle::default();
2332        let parsed = parse_dialogue_text(
2333            "{\\alpha&H40&\\1a&H00&\\3a&H20&\\4a&H80&}alpha",
2334            &style,
2335            &[],
2336        );
2337        let span_style = &parsed.lines[0].spans[0].style;
2338
2339        assert_eq!((span_style.primary_colour >> 24) & 0xff, 0x00);
2340        assert_eq!((span_style.secondary_colour >> 24) & 0xff, 0x40);
2341        assert_eq!((span_style.outline_colour >> 24) & 0xff, 0x20);
2342        assert_eq!((span_style.back_colour >> 24) & 0xff, 0x80);
2343    }
2344
2345    #[test]
2346    fn parses_rectangular_clip_overrides() {
2347        let base_style = ParsedStyle::default();
2348        let parsed = parse_dialogue_text("{\\clip(10,20,30,40)}Clip", &base_style, &[]);
2349        let inverse = parse_dialogue_text("{\\iclip(1,2,3,4)}Clip", &base_style, &[]);
2350
2351        assert_eq!(
2352            parsed.clip_rect,
2353            Some(Rect {
2354                x_min: 10,
2355                y_min: 20,
2356                x_max: 30,
2357                y_max: 40
2358            })
2359        );
2360        assert_eq!(
2361            parsed.clip_rect_exact,
2362            Some(ParsedRectF64 {
2363                x_min: 10.0,
2364                y_min: 20.0,
2365                x_max: 30.0,
2366                y_max: 40.0,
2367            })
2368        );
2369        assert!(!parsed.inverse_clip);
2370        assert_eq!(
2371            inverse.clip_rect,
2372            Some(Rect {
2373                x_min: 1,
2374                y_min: 2,
2375                x_max: 3,
2376                y_max: 4
2377            })
2378        );
2379        assert_eq!(
2380            inverse.clip_rect_exact,
2381            Some(ParsedRectF64 {
2382                x_min: 1.0,
2383                y_min: 2.0,
2384                x_max: 3.0,
2385                y_max: 4.0,
2386            })
2387        );
2388        assert!(inverse.inverse_clip);
2389    }
2390
2391    #[test]
2392    fn decimal_position_origin_move_and_clip_preserve_exact_coordinates() {
2393        let base_style = ParsedStyle::default();
2394        let positioned =
2395            parse_dialogue_text("{\\pos(10.25,20.75)\\org(4.5,8.125)}Pos", &base_style, &[]);
2396        let moved = parse_dialogue_text(
2397            "{\\move(1.5,2.25,30.75,40.125,900,100)}Move",
2398            &base_style,
2399            &[],
2400        );
2401        let clipped = parse_dialogue_text("{\\clip(1.5,2.5,30.25,40.75)}Clip", &base_style, &[]);
2402
2403        assert_eq!(positioned.position_exact, Some((10.25, 20.75)));
2404        assert_eq!(positioned.origin_exact, Some((4.5, 8.125)));
2405        assert_eq!(positioned.position, None);
2406        assert_eq!(positioned.origin, None);
2407        assert_eq!(
2408            moved.movement_exact,
2409            Some(ParsedMovementExact {
2410                start: (1.5, 2.25),
2411                end: (30.75, 40.125),
2412                t1_ms: 100,
2413                t2_ms: 900,
2414            })
2415        );
2416        assert_eq!(moved.movement, None);
2417        assert_eq!(
2418            clipped.clip_rect_exact,
2419            Some(ParsedRectF64 {
2420                x_min: 1.5,
2421                y_min: 2.5,
2422                x_max: 30.25,
2423                y_max: 40.75,
2424            })
2425        );
2426        assert_eq!(clipped.clip_rect, None);
2427    }
2428
2429    #[test]
2430    fn parses_vector_clip_overrides() {
2431        let base_style = ParsedStyle::default();
2432        let parsed = parse_dialogue_text("{\\clip(m 0 0 l 10 0 10 10 0 10)}Clip", &base_style, &[]);
2433
2434        assert!(parsed.clip_rect.is_none());
2435        assert_eq!(
2436            parsed.vector_clip,
2437            Some(ParsedVectorClip {
2438                scale: 1,
2439                polygons: vec![vec![
2440                    Point { x: 0, y: 0 },
2441                    Point { x: 10, y: 0 },
2442                    Point { x: 10, y: 10 },
2443                    Point { x: 0, y: 10 },
2444                ]],
2445            })
2446        );
2447        assert!(!parsed.inverse_clip);
2448    }
2449
2450    #[test]
2451    fn parses_move_overrides() {
2452        let base_style = ParsedStyle::default();
2453        let parsed = parse_dialogue_text("{\\move(10,20,110,220,50,150)}Move", &base_style, &[]);
2454
2455        assert_eq!(
2456            parsed.movement,
2457            Some(ParsedMovement {
2458                start: (10, 20),
2459                end: (110, 220),
2460                t1_ms: 50,
2461                t2_ms: 150,
2462            })
2463        );
2464        assert!(parsed.position.is_none());
2465    }
2466
2467    #[test]
2468    fn parses_fad_overrides() {
2469        let base_style = ParsedStyle::default();
2470        let parsed = parse_dialogue_text("{\\fad(120,240)}Fade", &base_style, &[]);
2471
2472        assert_eq!(
2473            parsed.fade,
2474            Some(ParsedFade::Simple {
2475                fade_in_ms: 120,
2476                fade_out_ms: 240,
2477            })
2478        );
2479    }
2480
2481    #[test]
2482    fn parses_full_fade_overrides() {
2483        let base_style = ParsedStyle::default();
2484        let parsed = parse_dialogue_text("{\\fade(10,20,30,40,50,60,70)}Fade", &base_style, &[]);
2485
2486        assert_eq!(
2487            parsed.fade,
2488            Some(ParsedFade::Complex {
2489                alpha1: 10,
2490                alpha2: 20,
2491                alpha3: 30,
2492                t1_ms: 40,
2493                t2_ms: 50,
2494                t3_ms: 60,
2495                t4_ms: 70,
2496            })
2497        );
2498    }
2499
2500    #[test]
2501    fn parses_karaoke_spans() {
2502        let base_style = ParsedStyle::default();
2503        let parsed = parse_dialogue_text("{\\k10}Ka{\\K20}ra{\\ko30}oke", &base_style, &[]);
2504
2505        assert_eq!(parsed.lines.len(), 1);
2506        assert_eq!(parsed.lines[0].spans.len(), 3);
2507        assert_eq!(
2508            parsed.lines[0].spans[0].karaoke,
2509            Some(ParsedKaraokeSpan {
2510                start_ms: 0,
2511                duration_ms: 100,
2512                mode: ParsedKaraokeMode::FillSwap,
2513            })
2514        );
2515        assert_eq!(
2516            parsed.lines[0].spans[1].karaoke,
2517            Some(ParsedKaraokeSpan {
2518                start_ms: 100,
2519                duration_ms: 200,
2520                mode: ParsedKaraokeMode::Sweep,
2521            })
2522        );
2523        assert_eq!(
2524            parsed.lines[0].spans[2].karaoke,
2525            Some(ParsedKaraokeSpan {
2526                start_ms: 300,
2527                duration_ms: 300,
2528                mode: ParsedKaraokeMode::OutlineToggle,
2529            })
2530        );
2531    }
2532
2533    #[test]
2534    fn parses_kt_karaoke_timing_reset() {
2535        let base_style = ParsedStyle::default();
2536        let parsed = parse_dialogue_text("{\\k10}A{\\kt50\\k10}B", &base_style, &[]);
2537
2538        assert_eq!(parsed.lines.len(), 1);
2539        assert_eq!(parsed.lines[0].spans.len(), 2);
2540        assert_eq!(
2541            parsed.lines[0].spans[0].karaoke,
2542            Some(ParsedKaraokeSpan {
2543                start_ms: 0,
2544                duration_ms: 100,
2545                mode: ParsedKaraokeMode::FillSwap,
2546            })
2547        );
2548        assert_eq!(
2549            parsed.lines[0].spans[1].karaoke,
2550            Some(ParsedKaraokeSpan {
2551                start_ms: 500,
2552                duration_ms: 100,
2553                mode: ParsedKaraokeMode::FillSwap,
2554            })
2555        );
2556    }
2557
2558    #[test]
2559    fn parses_font_size_relative_and_scale_reset_overrides() {
2560        let base_style = ParsedStyle {
2561            font_size: 20.0,
2562            scale_x: 1.2,
2563            scale_y: 0.8,
2564            ..ParsedStyle::default()
2565        };
2566        let parsed = parse_dialogue_text(
2567            "{\\fs+5}Bigger{\\fs-2}Smaller{\\fs0}Reset{\\fscx150\\fscy50}Scaled{\\fsc}Base",
2568            &base_style,
2569            &[],
2570        );
2571
2572        assert_eq!(parsed.lines[0].spans[0].style.font_size, 30.0);
2573        assert_eq!(parsed.lines[0].spans[1].style.font_size, 24.0);
2574        assert_eq!(parsed.lines[0].spans[2].style.font_size, 20.0);
2575        assert_eq!(parsed.lines[0].spans[3].style.scale_x, 1.5);
2576        assert_eq!(parsed.lines[0].spans[3].style.scale_y, 0.5);
2577        assert_eq!(parsed.lines[0].spans[4].style.scale_x, 1.2);
2578        assert_eq!(parsed.lines[0].spans[4].style.scale_y, 0.8);
2579    }
2580
2581    #[test]
2582    fn parses_backslash_n_as_space_unless_wrap_style_two() {
2583        let base_style = ParsedStyle::default();
2584        let normal = parse_dialogue_text("one\\ntwo", &base_style, &[]);
2585        assert_eq!(normal.lines.len(), 1);
2586        assert_eq!(normal.lines[0].spans[0].text, "one two");
2587
2588        let q2 = parse_dialogue_text("{\\q2}one\\ntwo", &base_style, &[]);
2589        assert_eq!(q2.lines.len(), 2);
2590        assert_eq!(q2.lines[0].spans[0].text, "one");
2591        assert_eq!(q2.lines[1].spans[0].text, "two");
2592    }
2593
2594    #[test]
2595    fn drawing_mode_treats_newline_escapes_as_path_whitespace() {
2596        let base_style = ParsedStyle::default();
2597        let parsed = parse_dialogue_text("{\\p1}m 0 0 l 10 0\\N l 10 10 l 0 10", &base_style, &[]);
2598
2599        assert_eq!(parsed.lines.len(), 1);
2600        assert_eq!(parsed.lines[0].spans.len(), 1);
2601        let drawing = parsed.lines[0].spans[0]
2602            .drawing
2603            .as_ref()
2604            .expect("drawing should continue across \\N like libass");
2605        assert_eq!(drawing.polygons.len(), 1);
2606        assert_eq!(drawing.bounds().expect("bounds").x_max, 11);
2607        assert_eq!(drawing.bounds().expect("bounds").y_max, 11);
2608    }
2609
2610    #[test]
2611    fn parses_drawing_spans_in_p_mode() {
2612        let base_style = ParsedStyle::default();
2613        let parsed = parse_dialogue_text("{\\p1}m 0 0 l 10 0 10 10 0 10", &base_style, &[]);
2614
2615        assert_eq!(parsed.lines.len(), 1);
2616        assert_eq!(parsed.lines[0].spans.len(), 1);
2617        let drawing = parsed.lines[0].spans[0]
2618            .drawing
2619            .as_ref()
2620            .expect("drawing span");
2621        assert_eq!(drawing.scale, 1);
2622        assert_eq!(drawing.polygons.len(), 1);
2623        assert_eq!(
2624            drawing.bounds(),
2625            Some(Rect {
2626                x_min: 0,
2627                y_min: 0,
2628                x_max: 11,
2629                y_max: 11
2630            })
2631        );
2632    }
2633
2634    #[test]
2635    fn parses_bezier_drawing_spans_in_p_mode() {
2636        let base_style = ParsedStyle::default();
2637        let parsed = parse_dialogue_text("{\\p1}m 0 0 b 10 0 10 10 0 10", &base_style, &[]);
2638
2639        let drawing = parsed.lines[0].spans[0]
2640            .drawing
2641            .as_ref()
2642            .expect("drawing span");
2643        assert_eq!(drawing.polygons.len(), 1);
2644        assert!(drawing.polygons[0].len() > 4);
2645        assert_eq!(
2646            drawing.polygons[0].first().copied(),
2647            Some(Point { x: 0, y: 0 })
2648        );
2649        assert_eq!(
2650            drawing.polygons[0].last().copied(),
2651            Some(Point { x: 0, y: 10 })
2652        );
2653    }
2654
2655    #[test]
2656    fn parses_spline_drawing_spans_in_p_mode() {
2657        let base_style = ParsedStyle::default();
2658        let parsed =
2659            parse_dialogue_text("{\\p1}m 0 0 s 10 0 10 10 0 10 p -5 5 c", &base_style, &[]);
2660
2661        let drawing = parsed.lines[0].spans[0]
2662            .drawing
2663            .as_ref()
2664            .expect("drawing span");
2665        assert_eq!(drawing.polygons.len(), 1);
2666        assert!(drawing.polygons[0].len() > 8);
2667    }
2668
2669    #[test]
2670    fn parses_non_closing_move_drawing_spans_in_p_mode() {
2671        let base_style = ParsedStyle::default();
2672        let parsed = parse_dialogue_text(
2673            "{\\p1}m 0 0 l 10 0 10 10 0 10 n 20 20 l 30 20 30 30 20 30",
2674            &base_style,
2675            &[],
2676        );
2677
2678        let drawing = parsed.lines[0].spans[0]
2679            .drawing
2680            .as_ref()
2681            .expect("drawing span");
2682        assert_eq!(drawing.polygons.len(), 2);
2683        assert_eq!(
2684            drawing.polygons[0].first().copied(),
2685            Some(Point { x: 0, y: 0 })
2686        );
2687        assert_eq!(
2688            drawing.polygons[1].first().copied(),
2689            Some(Point { x: 20, y: 20 })
2690        );
2691    }
2692
2693    #[test]
2694    fn parses_timed_transform_overrides() {
2695        let base_style = ParsedStyle::default();
2696        let parsed = parse_dialogue_text(
2697            "{\\t(100,300,2,\\1c&H112233&\\fs48\\fscx150\\fscy50\\fsp4\\bord6\\blur2)}Text",
2698            &base_style,
2699            &[],
2700        );
2701
2702        let transforms = &parsed.lines[0].spans[0].transforms;
2703        assert_eq!(transforms.len(), 1);
2704        assert_eq!(transforms[0].start_ms, 100);
2705        assert_eq!(transforms[0].end_ms, Some(300));
2706        assert_eq!(transforms[0].accel, 2.0);
2707        assert_eq!(transforms[0].style.font_size, Some(48.0));
2708        assert_eq!(transforms[0].style.scale_x, Some(1.5));
2709        assert_eq!(transforms[0].style.scale_y, Some(0.5));
2710        assert_eq!(transforms[0].style.spacing, Some(4.0));
2711        assert_eq!(transforms[0].style.primary_colour, Some(0x0011_2233));
2712        assert_eq!(transforms[0].style.border, Some(6.0));
2713        assert_eq!(transforms[0].style.blur, Some(2.0));
2714    }
2715
2716    #[test]
2717    fn parses_z_rotation_overrides_and_transforms() {
2718        let base_style = ParsedStyle::default();
2719        let parsed = parse_dialogue_text("{\\frz15\\t(0,1000,\\frz45)}Text", &base_style, &[]);
2720
2721        let span = &parsed.lines[0].spans[0];
2722        assert_eq!(span.style.rotation_z, 15.0);
2723        assert_eq!(span.transforms.len(), 1);
2724        assert_eq!(span.transforms[0].style.rotation_z, Some(45.0));
2725    }
2726
2727    #[test]
2728    fn later_override_removes_same_field_from_active_transform() {
2729        let base_style = ParsedStyle::default();
2730        let parsed = parse_dialogue_text(
2731            "{\\t(1000,3000,\\1c&H0000FF&\\frz45\\bord8)\\1c&H00FF00&\\frz15}Text",
2732            &base_style,
2733            &[],
2734        );
2735
2736        let span = &parsed.lines[0].spans[0];
2737        assert_eq!(span.style.primary_colour, 0x0000_ff00);
2738        assert_eq!(span.style.rotation_z, 15.0);
2739        assert_eq!(span.transforms.len(), 1);
2740        assert_eq!(span.transforms[0].style.primary_colour, None);
2741        assert_eq!(span.transforms[0].style.rotation_z, None);
2742        assert_eq!(span.transforms[0].style.border, Some(8.0));
2743    }
2744
2745    #[test]
2746    fn parses_color_and_shadow_overrides() {
2747        let base_style = ParsedStyle::default();
2748        let parsed = parse_dialogue_text(
2749            "{\\1c&H112233&\\4c&H445566&\\1a&H80&\\shad3.5\\blur1.5}Color",
2750            &base_style,
2751            &[],
2752        );
2753
2754        assert_eq!(parsed.lines.len(), 1);
2755        assert_eq!(parsed.lines[0].spans.len(), 1);
2756        assert_eq!(parsed.lines[0].spans[0].style.primary_colour, 0x8011_2233);
2757        assert_eq!(parsed.lines[0].spans[0].style.back_colour, 0x0044_5566);
2758        assert_eq!(parsed.lines[0].spans[0].style.shadow, 3.5);
2759        assert_eq!(parsed.lines[0].spans[0].style.blur, 1.5);
2760    }
2761
2762    #[test]
2763    fn parses_missing_override_metadata_tags() {
2764        let base_style = ParsedStyle {
2765            underline: false,
2766            strike_out: false,
2767            ..ParsedStyle::default()
2768        };
2769        let parsed = parse_dialogue_text(
2770            "{\\u1\\s1\\a10\\q2\\org(320,240)\\frx12\\fry-8\\fax0.25\\fay-0.5\\xbord3\\ybord4\\xshad5\\yshad-6\\be2\\pbo7}Meta",
2771            &base_style,
2772            &[],
2773        );
2774
2775        assert_eq!(
2776            parsed.alignment,
2777            Some(ass::VALIGN_CENTER | ass::HALIGN_CENTER)
2778        );
2779        assert_eq!(parsed.wrap_style, Some(2));
2780        assert_eq!(parsed.origin, Some((320, 240)));
2781        let style = &parsed.lines[0].spans[0].style;
2782        assert!(style.underline);
2783        assert!(style.strike_out);
2784        assert_eq!(style.rotation_x, 12.0);
2785        assert_eq!(style.rotation_y, -8.0);
2786        assert_eq!(style.shear_x, 0.25);
2787        assert_eq!(style.shear_y, -0.5);
2788        assert_eq!(style.border_x, 3.0);
2789        assert_eq!(style.border_y, 4.0);
2790        assert_eq!(style.shadow_x, 5.0);
2791        assert_eq!(style.shadow_y, -6.0);
2792        assert_eq!(style.be, 2.0);
2793        assert_eq!(style.pbo, 7.0);
2794    }
2795
2796    #[test]
2797    fn parses_font_attachments_from_fonts_section() {
2798        let encoded = encode_font_bytes(b"ABC");
2799        let input = format!(
2800            "[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"
2801        );
2802        let track = parse_script_text(&input).expect("script should parse");
2803
2804        assert_eq!(track.attachments.len(), 1);
2805        assert_eq!(track.attachments[0].name, "DemoFont.ttf");
2806        assert_eq!(track.attachments[0].data, b"ABC");
2807    }
2808
2809    fn encode_font_bytes(bytes: &[u8]) -> String {
2810        let mut encoded = String::new();
2811        for chunk in bytes.chunks(3) {
2812            let value = match chunk.len() {
2813                1 => u32::from(chunk[0]) << 16,
2814                2 => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8),
2815                _ => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]),
2816            };
2817            let output_len = match chunk.len() {
2818                1 => 2,
2819                2 => 3,
2820                _ => 4,
2821            };
2822            for shift_index in 0..output_len {
2823                let shift = 6 * (3 - shift_index);
2824                let six_bits = ((value >> shift) & 63) as u8;
2825                encoded.push(char::from(six_bits + 33));
2826            }
2827        }
2828        encoded
2829    }
2830}