1use std::{borrow::Cow, fmt::Display, fs::File, io::BufReader, path::Path, str::FromStr};
2
3use buildstructor::Builder;
4use encoding_rs::Encoding;
5use encoding_rs_io::DecodeReaderBytesBuilder;
6
7use crate::{
8 encoding::detect_file_encoding,
9 errors::Error,
10 plain::PlainSubtitle,
11 subrip::convert::srt_to_ass_formatting,
12 substation::common::data::{SubStationEventKind, SubStationFont, SubStationGraphic},
13 traits::TimedSubtitle,
14 webvtt::convert::vtt_to_ass_formatting,
15 Moment, SsaSubtitle, SubRipSubtitle, Subtitle, TextEvent, TextEventInterface, TextSubtitle,
16 TimedEvent, TimedEventInterface, TimedMicroDvdSubtitle, TimedSubtitleFile, WebVttSubtitle,
17};
18
19use super::{convert::strip_formatting_tags, parse::parse_ass};
20
21#[derive(Debug, Builder)]
23pub struct AssSubtitle {
24 script_info: AssScriptInfo,
26 dialogue: Vec<AssEvent>,
29 pictures: Vec<AssEvent>,
31 sounds: Vec<AssEvent>,
33 movies: Vec<AssEvent>,
35 commands: Vec<AssEvent>,
37 styles: Vec<AssStyle>,
39 fonts: Vec<SubStationFont>,
41 graphics: Vec<SubStationGraphic>,
43}
44
45#[derive(Debug)]
47pub struct AssEvent {
48 pub kind: SubStationEventKind,
50 pub layer: i64,
53 pub start: Moment,
55 pub end: Moment,
57 pub style: Option<String>,
59 pub name: Option<String>,
61 pub margin_l: i64,
63 pub margin_r: i64,
65 pub margin_v: i64,
67 pub effect: Option<String>,
69 pub text: String,
72}
73
74#[derive(Debug, Builder)]
78pub struct AssScriptInfo {
79 pub title: Option<String>,
81 pub original_script: Option<String>,
83 pub original_translation: Option<String>,
85 pub original_editing: Option<String>,
87 pub original_timing: Option<String>,
89 pub synch_point: Option<String>,
91 pub script_updated_by: Option<String>,
93 pub update_details: Option<String>,
95 pub script_type: Option<String>,
98 pub collisions: Option<String>,
100 pub play_res_y: Option<String>,
102 pub play_res_x: Option<String>,
104 pub play_depth: Option<String>,
106 pub timer: Option<String>,
108 pub wrap_style: Option<String>,
110}
111
112#[derive(Debug)]
114pub struct AssStyle {
115 pub name: String,
117 pub fontname: String,
119 pub fontsize: i64,
121 pub primary_colour: String,
123 pub secondary_colour: String,
125 pub outline_colour: String,
127 pub back_colour: String,
129 pub bold: bool,
131 pub italic: bool,
133 pub underline: bool,
135 pub strike_out: bool,
137 pub scale_x: i64,
139 pub scale_y: i64,
141 pub spacing: i64,
143 pub angle: f64,
145 pub border_style: i64,
147 pub outline: i64,
149 pub shadow: i64,
151 pub alignment: i64,
153 pub margin_l: i64,
155 pub margin_r: i64,
157 pub margin_v: i64,
159 pub encoding: i64,
161}
162
163impl AssSubtitle {
164 #[must_use]
166 pub fn pictures(&self) -> &[AssEvent] {
167 self.pictures.as_slice()
168 }
169
170 pub fn pictures_mut(&mut self) -> &mut [AssEvent] {
172 self.pictures.as_mut_slice()
173 }
174
175 #[must_use]
177 pub fn picture(&self, index: usize) -> Option<&AssEvent> {
178 self.pictures.get(index)
179 }
180
181 pub fn picture_mut(&mut self, index: usize) -> Option<&mut AssEvent> {
183 self.pictures.get_mut(index)
184 }
185
186 #[must_use]
188 pub fn sounds(&self) -> &[AssEvent] {
189 self.sounds.as_slice()
190 }
191
192 pub fn sounds_mut(&mut self) -> &mut [AssEvent] {
194 self.sounds.as_mut_slice()
195 }
196
197 #[must_use]
199 pub fn sound(&self, index: usize) -> Option<&AssEvent> {
200 self.sounds.get(index)
201 }
202
203 pub fn sound_mut(&mut self, index: usize) -> Option<&mut AssEvent> {
205 self.sounds.get_mut(index)
206 }
207
208 #[must_use]
210 pub fn movies(&self) -> &[AssEvent] {
211 self.movies.as_slice()
212 }
213
214 pub fn movies_mut(&mut self) -> &mut [AssEvent] {
216 self.movies.as_mut_slice()
217 }
218
219 #[must_use]
221 pub fn movie(&self, index: usize) -> Option<&AssEvent> {
222 self.movies.get(index)
223 }
224
225 pub fn movie_mut(&mut self, index: usize) -> Option<&mut AssEvent> {
227 self.movies.get_mut(index)
228 }
229
230 #[must_use]
232 pub fn commands(&self) -> &[AssEvent] {
233 self.commands.as_slice()
234 }
235
236 pub fn commands_mut(&mut self) -> &mut [AssEvent] {
238 self.commands.as_mut_slice()
239 }
240
241 #[must_use]
243 pub fn command(&self, index: usize) -> Option<&AssEvent> {
244 self.commands.get(index)
245 }
246
247 pub fn command_mut(&mut self, index: usize) -> Option<&mut AssEvent> {
249 self.commands.get_mut(index)
250 }
251
252 #[must_use]
254 pub fn script_info(&self) -> &AssScriptInfo {
255 &self.script_info
256 }
257
258 pub fn script_info_mut(&mut self) -> &mut AssScriptInfo {
260 &mut self.script_info
261 }
262
263 #[must_use]
265 pub fn styles(&self) -> &[AssStyle] {
266 self.styles.as_slice()
267 }
268
269 pub fn styles_mut(&mut self) -> &mut [AssStyle] {
271 self.styles.as_mut_slice()
272 }
273
274 #[must_use]
276 pub fn fonts(&self) -> &[SubStationFont] {
277 self.fonts.as_slice()
278 }
279
280 pub fn fonts_mut(&mut self) -> &mut [SubStationFont] {
282 self.fonts.as_mut_slice()
283 }
284
285 #[must_use]
287 pub fn graphics(&self) -> &[SubStationGraphic] {
288 self.graphics.as_slice()
289 }
290
291 pub fn graphics_mut(&mut self) -> &mut [SubStationGraphic] {
293 self.graphics.as_mut_slice()
294 }
295
296 fn open_file_with_encoding(
297 path: &Path,
298 encoding: Option<&'static Encoding>,
299 ) -> Result<Self, Error> {
300 let file = File::open(path)?;
301 let transcoded = DecodeReaderBytesBuilder::new()
302 .encoding(encoding)
303 .build(file);
304 let reader = BufReader::new(transcoded);
305
306 Ok(parse_ass(reader))
307 }
308}
309
310impl Subtitle for AssSubtitle {
311 type Event = AssEvent;
312
313 fn from_path_with_encoding(
314 path: impl AsRef<Path>,
315 encoding: Option<&'static Encoding>,
316 ) -> Result<Self, Error> {
317 let mut enc = encoding.or_else(|| detect_file_encoding(path.as_ref(), Some(40)).ok());
318 let mut result = Self::open_file_with_encoding(path.as_ref(), enc);
319
320 if encoding.is_none() && result.is_err() {
321 enc = encoding.or_else(|| detect_file_encoding(path.as_ref(), None).ok());
322 result = Self::open_file_with_encoding(path.as_ref(), enc);
323 }
324
325 result
326 }
327
328 fn events(&self) -> &[AssEvent] {
329 self.dialogue.as_slice()
330 }
331
332 fn events_mut(&mut self) -> &mut [AssEvent] {
333 self.dialogue.as_mut_slice()
334 }
335}
336
337impl TextSubtitle for AssSubtitle {
338 fn strip_formatting(&mut self) {
340 for event in self.events_mut() {
341 event.strip_formatting();
342 }
343 self.styles.clear();
344 }
345}
346
347impl TimedSubtitle for AssSubtitle {}
348
349impl Display for AssSubtitle {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 writeln!(f, "{}", self.script_info)?;
352 writeln!(f)?;
353 if !self.styles.is_empty() {
354 writeln!(f, "[V4+ Styles]")?;
355 writeln!(f, "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding")?;
356 for style in &self.styles {
357 writeln!(f, "{style}")?;
358 }
359 writeln!(f)?;
360 }
361 if !self.fonts.is_empty() {
362 writeln!(f)?;
363 for font in &self.fonts {
364 writeln!(f, "{font}")?;
365 }
366 }
367 if !self.graphics.is_empty() {
368 writeln!(f)?;
369 for graphic in &self.graphics {
370 writeln!(f, "{graphic}")?;
371 }
372 }
373 if !self.dialogue.is_empty() {
374 writeln!(f, "[Events]")?;
375 writeln!(
376 f,
377 "Format: Layer, Start, End, Style, Actor, MarginL, MarginR, MarginV, Effect, Text"
378 )?;
379 for event in &self.dialogue {
380 writeln!(f, "{event}")?;
381 }
382 for event in &self.pictures {
383 writeln!(f, "{event}")?;
384 }
385 for event in &self.sounds {
386 writeln!(f, "{event}")?;
387 }
388 for event in &self.movies {
389 writeln!(f, "{event}")?;
390 }
391 for event in &self.commands {
392 writeln!(f, "{event}")?;
393 }
394 }
395
396 Ok(())
397 }
398}
399
400impl FromStr for AssSubtitle {
401 type Err = Error;
402
403 fn from_str(s: &str) -> Result<Self, Self::Err> {
404 let reader = BufReader::new(s.as_bytes());
405
406 Ok(parse_ass(reader))
407 }
408}
409
410impl Default for AssSubtitle {
411 fn default() -> Self {
412 Self::builder()
413 .script_info(AssScriptInfo::default())
414 .build()
415 }
416}
417
418impl From<&TimedMicroDvdSubtitle> for AssSubtitle {
419 fn from(value: &TimedMicroDvdSubtitle) -> Self {
420 AssSubtitle::builder()
421 .script_info(AssScriptInfo::default())
422 .dialogue(
423 value
424 .events()
425 .iter()
426 .map(|line| AssEvent {
427 kind: SubStationEventKind::Dialogue,
428 layer: 0,
429 start: line.start,
430 end: line.end,
431 style: None,
432 name: None,
433 margin_l: 0,
434 margin_r: 0,
435 margin_v: 0,
436 effect: None,
437 text: line.text.replace('|', "\\N"),
438 })
439 .collect(),
440 )
441 .build()
442 }
443}
444
445impl From<&SsaSubtitle> for AssSubtitle {
447 fn from(value: &SsaSubtitle) -> Self {
448 AssSubtitle::builder()
449 .script_info(AssScriptInfo::default())
450 .dialogue(
451 value
452 .events()
453 .iter()
454 .map(|event| AssEvent {
455 kind: SubStationEventKind::Dialogue,
456 layer: 0,
457 start: event.start,
458 end: event.end,
459 style: event.style.clone(),
460 name: event.name.clone(),
461 margin_l: event.margin_l,
462 margin_r: event.margin_r,
463 margin_v: event.margin_v,
464 effect: event.effect.clone(),
465 text: event.text.clone(),
466 })
467 .collect(),
468 )
469 .build()
470 }
471}
472
473impl From<&SubRipSubtitle> for AssSubtitle {
474 fn from(value: &SubRipSubtitle) -> Self {
478 AssSubtitle::builder()
479 .script_info(AssScriptInfo::default())
480 .dialogue(
481 value
482 .events()
483 .iter()
484 .map(|line| {
485 let mut text = line.text.replace('\n', "\\N");
486 if let Ok((_, converted)) = srt_to_ass_formatting(text.as_str()) {
487 text = converted;
488 }
489 AssEvent {
490 kind: SubStationEventKind::Dialogue,
491 layer: 0,
492 start: line.start,
493 end: line.end,
494 style: None,
495 name: None,
496 margin_l: 0,
497 margin_r: 0,
498 margin_v: 0,
499 effect: None,
500 text,
501 }
502 })
503 .collect(),
504 )
505 .build()
506 }
507}
508
509impl From<&WebVttSubtitle> for AssSubtitle {
510 fn from(value: &WebVttSubtitle) -> Self {
517 AssSubtitle::builder()
518 .script_info(AssScriptInfo::builder().and_title(value.header()).build())
519 .dialogue(
520 value
521 .events()
522 .iter()
523 .map(|cue| {
524 let mut text = cue.text.replace('\n', "\\N");
525 if let Ok((_, converted)) = vtt_to_ass_formatting(text.as_str()) {
526 text = converted;
527 }
528 AssEvent {
529 kind: SubStationEventKind::Dialogue,
530 layer: 0,
531 start: cue.start,
532 end: cue.end,
533 style: None,
534 name: None,
535 margin_l: 0,
536 margin_r: 0,
537 margin_v: 0,
538 effect: None,
539 text,
540 }
541 })
542 .collect(),
543 )
544 .build()
545 }
546}
547
548impl From<TimedMicroDvdSubtitle> for AssSubtitle {
549 fn from(value: TimedMicroDvdSubtitle) -> Self {
550 Self::from(&value)
551 }
552}
553
554impl From<SsaSubtitle> for AssSubtitle {
555 fn from(value: SsaSubtitle) -> Self {
556 Self::from(&value)
557 }
558}
559
560impl From<SubRipSubtitle> for AssSubtitle {
561 fn from(value: SubRipSubtitle) -> Self {
562 Self::from(&value)
563 }
564}
565
566impl From<WebVttSubtitle> for AssSubtitle {
567 fn from(value: WebVttSubtitle) -> Self {
568 Self::from(&value)
569 }
570}
571
572impl From<TimedSubtitleFile> for AssSubtitle {
573 fn from(value: TimedSubtitleFile) -> Self {
574 match value {
575 TimedSubtitleFile::Ass(data) => data,
576 TimedSubtitleFile::MicroDvd(data) => data.into(),
577 TimedSubtitleFile::Ssa(data) => data.into(),
578 TimedSubtitleFile::SubRip(data) => data.into(),
579 TimedSubtitleFile::WebVtt(data) => data.into(),
580 }
581 }
582}
583
584impl From<PlainSubtitle> for AssSubtitle {
585 fn from(value: PlainSubtitle) -> Self {
586 AssSubtitle::builder()
587 .script_info(AssScriptInfo::default())
588 .dialogue(
589 value
590 .events()
591 .iter()
592 .map(|event| AssEvent {
593 kind: SubStationEventKind::Dialogue,
594 layer: 0,
595 start: event.start,
596 end: event.end,
597 style: None,
598 name: None,
599 margin_l: 0,
600 margin_r: 0,
601 margin_v: 0,
602 effect: None,
603 text: event.text.replace('\n', "\\N"),
604 })
605 .collect(),
606 )
607 .build()
608 }
609}
610
611impl TextEvent for AssEvent {
612 fn unformatted_text(&self) -> Cow<'_, String> {
613 let Ok((_, stripped)) = strip_formatting_tags(self.text.as_str()) else {
614 return Cow::Borrowed(&self.text);
615 };
616
617 Cow::Owned(stripped)
618 }
619
620 fn as_plaintext(&self) -> Cow<'_, String> {
621 Cow::Owned(self.unformatted_text().replace("\\N", "\n"))
622 }
623}
624
625impl TimedEvent for AssEvent {}
626
627impl TextEventInterface for AssEvent {
628 fn text(&self) -> String {
629 self.text.clone()
630 }
631
632 fn set_text(&mut self, text: String) {
633 self.text = text;
634 }
635}
636
637impl TimedEventInterface for AssEvent {
638 fn start(&self) -> Moment {
639 self.start
640 }
641
642 fn end(&self) -> Moment {
643 self.end
644 }
645
646 fn set_start(&mut self, moment: Moment) {
647 self.start = moment;
648 }
649
650 fn set_end(&mut self, moment: Moment) {
651 self.end = moment;
652 }
653}
654
655impl Display for AssEvent {
656 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657 write!(
658 f,
659 "{}: {},{},{},{},{},{},{},{},{},{}",
660 self.kind,
661 self.layer,
662 self.start.as_substation_timestamp(),
663 self.end.as_substation_timestamp(),
664 self.style.as_deref().unwrap_or_default(),
665 self.name.as_deref().unwrap_or_default(),
666 self.margin_l,
667 self.margin_r,
668 self.margin_v,
669 self.effect.as_deref().unwrap_or_default(),
670 self.text
671 )
672 }
673}
674
675impl Display for AssScriptInfo {
676 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677 write!(f, "[Script Info]")?;
678 if let Some(title) = &self.title {
679 write!(f, "\nTitle: {title}")?;
680 }
681 if let Some(original_script) = &self.original_script {
682 write!(f, "\nOriginal Script: {original_script}")?;
683 }
684 if let Some(original_translation) = &self.original_translation {
685 write!(f, "\nOriginal Translation: {original_translation}")?;
686 }
687 if let Some(original_editing) = &self.original_editing {
688 write!(f, "\nOriginal Editing: {original_editing}")?;
689 }
690 if let Some(original_timing) = &self.original_timing {
691 write!(f, "\nOriginal Timing: {original_timing}")?;
692 }
693 if let Some(synch_point) = &self.synch_point {
694 write!(f, "\nSynch Point: {synch_point}")?;
695 }
696 if let Some(script_updated_by) = &self.script_updated_by {
697 write!(f, "\nScript Updated By: {script_updated_by}")?;
698 }
699 if let Some(update_details) = &self.update_details {
700 write!(f, "\nUpdate Details: {update_details}")?;
701 }
702 if let Some(script_type) = &self.script_type {
703 write!(f, "\nScript Type: {script_type}")?;
704 }
705 if let Some(collisions) = &self.collisions {
706 write!(f, "\nCollisions: {collisions}")?;
707 }
708 if let Some(play_res_y) = &self.play_res_y {
709 write!(f, "\nPlayResY: {play_res_y}")?;
710 }
711 if let Some(play_res_x) = &self.play_res_x {
712 write!(f, "\nPlayResX: {play_res_x}")?;
713 }
714 if let Some(play_depth) = &self.play_depth {
715 write!(f, "\nPlayDepth: {play_depth}")?;
716 }
717 if let Some(timer) = &self.timer {
718 write!(f, "\nTimer: {timer}")?;
719 }
720 if let Some(wrap_style) = &self.wrap_style {
721 write!(f, "\nWrapStyle: {wrap_style}")?;
722 }
723
724 Ok(())
725 }
726}
727
728impl Default for AssScriptInfo {
729 fn default() -> Self {
730 AssScriptInfo::builder().script_type("v4.00+").build()
731 }
732}
733
734impl Display for AssStyle {
735 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
736 write!(
737 f,
738 "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
739 self.name,
740 self.fontname,
741 self.fontsize,
742 self.primary_colour,
743 self.secondary_colour,
744 self.outline_colour,
745 self.back_colour,
746 self.bold,
747 self.italic,
748 self.underline,
749 self.strike_out,
750 self.scale_x,
751 self.scale_y,
752 self.spacing,
753 self.angle,
754 self.border_style,
755 self.outline,
756 self.shadow,
757 self.alignment,
758 self.margin_l,
759 self.margin_r,
760 self.margin_v,
761 self.encoding
762 )
763 }
764}