Skip to main content

ass_editor/formats/srt/
import.rs

1//! SRT import: parse SRT entries into an ASS [`EditorDocument`].
2
3use super::SrtFormat;
4use crate::core::{EditorDocument, EditorError};
5use crate::formats::{FormatImporter, FormatInfo, FormatOptions, FormatResult};
6use ass_core::parser::Script;
7use std::io::Read;
8
9impl SrtFormat {
10    /// Parse SRT subtitle entry
11    fn parse_srt_subtitle(
12        lines: &[String],
13        start_idx: usize,
14    ) -> Result<(usize, String), EditorError> {
15        if start_idx >= lines.len() {
16            return Err(EditorError::InvalidFormat(
17                "Unexpected end of file".to_string(),
18            ));
19        }
20
21        let mut idx = start_idx;
22
23        // Skip empty lines
24        while idx < lines.len() && lines[idx].trim().is_empty() {
25            idx += 1;
26        }
27
28        if idx >= lines.len() {
29            return Err(EditorError::InvalidFormat(
30                "Unexpected end of file".to_string(),
31            ));
32        }
33
34        // Parse subtitle number (optional validation)
35        let _subtitle_num = lines[idx].trim();
36        idx += 1;
37
38        if idx >= lines.len() {
39            return Err(EditorError::InvalidFormat(
40                "Missing timestamp line".to_string(),
41            ));
42        }
43
44        // Parse timestamp line
45        let timestamp_line = &lines[idx];
46        if !timestamp_line.contains("-->") {
47            return Err(EditorError::InvalidFormat(format!(
48                "Invalid timestamp line: {timestamp_line}"
49            )));
50        }
51
52        let parts: Vec<&str> = timestamp_line.split("-->").collect();
53        if parts.len() != 2 {
54            return Err(EditorError::InvalidFormat(format!(
55                "Invalid timestamp format: {timestamp_line}"
56            )));
57        }
58
59        let start_time = Self::parse_srt_time(parts[0])?;
60        let end_time = Self::parse_srt_time(parts[1])?;
61
62        idx += 1;
63
64        // Collect subtitle text lines
65        let mut text_lines = Vec::new();
66        while idx < lines.len() && !lines[idx].trim().is_empty() {
67            let styled_text = Self::convert_srt_to_ass_styling(&lines[idx]);
68            text_lines.push(styled_text);
69            idx += 1;
70        }
71
72        if text_lines.is_empty() {
73            return Err(EditorError::InvalidFormat(
74                "Empty subtitle text".to_string(),
75            ));
76        }
77
78        let text = text_lines.join("\\N"); // ASS line break
79        let dialogue_line = format!("Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{text}");
80
81        Ok((idx, dialogue_line))
82    }
83}
84
85impl FormatImporter for SrtFormat {
86    fn format_info(&self) -> &FormatInfo {
87        &self.info
88    }
89
90    fn import_from_reader(
91        &self,
92        reader: &mut dyn Read,
93        options: &FormatOptions,
94    ) -> Result<(EditorDocument, FormatResult), EditorError> {
95        // Read the entire content
96        let mut content = String::new();
97        reader
98            .read_to_string(&mut content)
99            .map_err(|e| EditorError::IoError(format!("Failed to read SRT content: {e}")))?;
100
101        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
102        let mut warnings = Vec::new();
103        let mut dialogues = Vec::new();
104        let mut idx = 0;
105        let mut subtitle_count = 0;
106
107        // Parse all SRT subtitles
108        while idx < lines.len() {
109            match Self::parse_srt_subtitle(&lines, idx) {
110                Ok((next_idx, dialogue)) => {
111                    dialogues.push(dialogue);
112                    idx = next_idx;
113                    subtitle_count += 1;
114                }
115                Err(e) => {
116                    if idx < lines.len() {
117                        warnings.push(format!(
118                            "Skipping invalid subtitle at line {}: {e}",
119                            idx + 1
120                        ));
121                        idx += 1;
122                    } else {
123                        break;
124                    }
125                }
126            }
127        }
128
129        // Build ASS script content
130        let mut ass_content = String::new();
131
132        // Add script info section
133        ass_content.push_str("[Script Info]\n");
134        ass_content.push_str("Title: Converted from SRT\n");
135        ass_content.push_str("ScriptType: v4.00+\n");
136        ass_content.push_str("Collisions: Normal\n");
137        ass_content.push_str("PlayDepth: 0\n");
138        ass_content.push_str("Timer: 100.0000\n");
139        ass_content.push_str("Video Aspect Ratio: 0\n");
140        ass_content.push_str("Video Zoom: 6\n");
141        ass_content.push_str("Video Position: 0\n\n");
142
143        // Add styles section with basic default style
144        ass_content.push_str("[V4+ Styles]\n");
145        ass_content.push_str("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n");
146        ass_content.push_str("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n");
147
148        // Add events section
149        ass_content.push_str("[Events]\n");
150        ass_content.push_str(
151            "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
152        );
153
154        for dialogue in dialogues {
155            ass_content.push_str(&dialogue);
156            ass_content.push('\n');
157        }
158
159        // Validate the generated ASS content
160        let _script = Script::parse(&ass_content)?;
161
162        // Create EditorDocument
163        let document = EditorDocument::from_content(&ass_content)?;
164
165        // Create result with metadata
166        let mut result = FormatResult::success(subtitle_count)
167            .with_metadata("original_format".to_string(), "SRT".to_string())
168            .with_metadata("subtitles_count".to_string(), subtitle_count.to_string())
169            .with_metadata("encoding".to_string(), options.encoding.clone());
170
171        if !warnings.is_empty() {
172            result = result.with_warnings(warnings);
173        }
174
175        Ok((document, result))
176    }
177}