Skip to main content

ass_editor/formats/webvtt/
importer.rs

1//! WebVTT import: parse `.vtt` content into an `EditorDocument`.
2//!
3//! Implements [`FormatImporter`] for [`WebVttFormat`], converting WebVTT cues
4//! into an ASS script and validating the generated result.
5
6use crate::core::{EditorDocument, EditorError};
7use crate::formats::{FormatImporter, FormatInfo, FormatOptions, FormatResult};
8use ass_core::parser::Script;
9use std::io::Read;
10
11use super::WebVttFormat;
12
13impl FormatImporter for WebVttFormat {
14    fn format_info(&self) -> &FormatInfo {
15        &self.info
16    }
17
18    fn import_from_reader(
19        &self,
20        reader: &mut dyn Read,
21        options: &FormatOptions,
22    ) -> Result<(EditorDocument, FormatResult), EditorError> {
23        // Read the entire content
24        let mut content = String::new();
25        reader
26            .read_to_string(&mut content)
27            .map_err(|e| EditorError::IoError(format!("Failed to read WebVTT content: {e}")))?;
28
29        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
30        let mut warnings = Vec::new();
31        let mut dialogues = Vec::new();
32        let mut idx = 0;
33        let mut cue_count = 0;
34
35        // Check for WebVTT header
36        if lines.is_empty() || !lines[0].trim().starts_with("WEBVTT") {
37            warnings.push("Missing or invalid WebVTT header".to_string());
38        } else {
39            idx = 1; // Skip header
40        }
41
42        // Parse all WebVTT cues
43        while idx < lines.len() {
44            match Self::parse_vtt_cue(&lines, idx) {
45                Ok((next_idx, dialogue)) => {
46                    dialogues.push(dialogue);
47                    idx = next_idx;
48                    cue_count += 1;
49                }
50                Err(e) => {
51                    if idx < lines.len() {
52                        warnings.push(format!("Skipping invalid cue at line {}: {e}", idx + 1));
53                        idx += 1;
54                    } else {
55                        break;
56                    }
57                }
58            }
59        }
60
61        // Build ASS script content
62        let mut ass_content = String::new();
63
64        // Add script info section
65        ass_content.push_str("[Script Info]\n");
66        ass_content.push_str("Title: Converted from WebVTT\n");
67        ass_content.push_str("ScriptType: v4.00+\n");
68        ass_content.push_str("Collisions: Normal\n");
69        ass_content.push_str("PlayDepth: 0\n");
70        ass_content.push_str("Timer: 100.0000\n");
71        ass_content.push_str("Video Aspect Ratio: 0\n");
72        ass_content.push_str("Video Zoom: 6\n");
73        ass_content.push_str("Video Position: 0\n");
74        ass_content.push_str("PlayResX: 640\n");
75        ass_content.push_str("PlayResY: 480\n\n");
76
77        // Add styles section with basic default style
78        ass_content.push_str("[V4+ Styles]\n");
79        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");
80        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");
81
82        // Add events section
83        ass_content.push_str("[Events]\n");
84        ass_content.push_str(
85            "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
86        );
87
88        for dialogue in dialogues {
89            ass_content.push_str(&dialogue);
90            ass_content.push('\n');
91        }
92
93        // Validate the generated ASS content
94        let _script = Script::parse(&ass_content)?;
95
96        // Create EditorDocument
97        let document = EditorDocument::from_content(&ass_content)?;
98
99        // Create result with metadata
100        let mut result = FormatResult::success(cue_count)
101            .with_metadata("original_format".to_string(), "WebVTT".to_string())
102            .with_metadata("cues_count".to_string(), cue_count.to_string())
103            .with_metadata("encoding".to_string(), options.encoding.clone());
104
105        if !warnings.is_empty() {
106            result = result.with_warnings(warnings);
107        }
108
109        Ok((document, result))
110    }
111}