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