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 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}