Skip to main content

ass_editor/formats/srt/
export.rs

1//! SRT export: serialize an ASS [`EditorDocument`] into SRT text.
2
3use super::SrtFormat;
4use crate::core::{EditorDocument, EditorError};
5use crate::formats::{FormatExporter, FormatInfo, FormatOptions, FormatResult};
6use std::io::Write;
7
8impl FormatExporter for SrtFormat {
9    fn format_info(&self) -> &FormatInfo {
10        &self.info
11    }
12
13    fn export_to_writer(
14        &self,
15        document: &EditorDocument,
16        writer: &mut dyn Write,
17        options: &FormatOptions,
18    ) -> Result<FormatResult, EditorError> {
19        // Parse the ASS content to extract events
20        let events = document.parse_script_with(|script| {
21            // Find events section and collect owned data
22            if let Some(ass_core::parser::ast::Section::Events(events)) =
23                script.find_section(ass_core::parser::ast::SectionType::Events)
24            {
25                // Convert to owned data to avoid lifetime issues
26                events
27                    .iter()
28                    .map(|event| {
29                        (
30                            event.event_type,
31                            event.start.to_string(),
32                            event.end.to_string(),
33                            event.text.to_string(),
34                        )
35                    })
36                    .collect::<Vec<_>>()
37            } else {
38                Vec::new()
39            }
40        })?;
41
42        let mut srt_content = String::new();
43        let mut subtitle_num = 1;
44        let mut warnings = Vec::new();
45
46        for (event_type, start, end, text) in &events {
47            // Only export dialogue events
48            if event_type.as_str() != "Dialogue" {
49                continue;
50            }
51
52            // Parse start and end times
53            let start_time = match Self::format_srt_time(start) {
54                Ok(time) => time,
55                Err(e) => {
56                    warnings.push(format!(
57                        "Invalid start time for subtitle {subtitle_num}: {e}"
58                    ));
59                    continue;
60                }
61            };
62
63            let end_time = match Self::format_srt_time(end) {
64                Ok(time) => time,
65                Err(e) => {
66                    warnings.push(format!("Invalid end time for subtitle {subtitle_num}: {e}"));
67                    continue;
68                }
69            };
70
71            // Convert ASS text to SRT format
72            let mut text = text.clone();
73
74            // Convert ASS line breaks to actual line breaks
75            text = text.replace("\\N", "\n");
76            text = text.replace("\\n", "\n");
77
78            // Convert ASS styling to SRT styling
79            text = Self::convert_ass_to_srt_styling(&text);
80
81            // Write SRT subtitle entry
82            srt_content.push_str(&format!("{subtitle_num}\n"));
83            srt_content.push_str(&format!("{start_time} --> {end_time}\n"));
84            srt_content.push_str(&text);
85            srt_content.push_str("\n\n");
86
87            subtitle_num += 1;
88        }
89
90        // Write content with proper encoding
91        let bytes = if options.encoding.eq_ignore_ascii_case("UTF-8") {
92            srt_content.into_bytes()
93        } else {
94            warnings.push(format!(
95                "Encoding '{}' not supported, using UTF-8 instead",
96                options.encoding
97            ));
98            srt_content.into_bytes()
99        };
100
101        writer
102            .write_all(&bytes)
103            .map_err(|e| EditorError::IoError(format!("Failed to write SRT content: {e}")))?;
104
105        let mut result = FormatResult::success(subtitle_num - 1)
106            .with_metadata("exported_format".to_string(), "SRT".to_string())
107            .with_metadata(
108                "subtitles_exported".to_string(),
109                (subtitle_num - 1).to_string(),
110            );
111
112        if !warnings.is_empty() {
113            result = result.with_warnings(warnings);
114        }
115
116        Ok(result)
117    }
118}