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