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