1use crate::Result;
16use crate::core::formats::{
17 Subtitle, SubtitleEntry, SubtitleFormat, SubtitleFormatType, SubtitleMetadata,
18};
19use crate::error::SubXError;
20use std::time::Duration;
21
22#[derive(Debug, Clone)]
24pub struct AssStyle {
25 pub name: String,
27 pub font_name: String,
29 pub font_size: u32,
31 pub primary_color: Color,
33 pub secondary_color: Color,
35 pub outline_color: Color,
37 pub shadow_color: Color,
39 pub bold: bool,
41 pub italic: bool,
43 pub underline: bool,
45 pub alignment: i32,
47}
48
49#[derive(Debug, Clone)]
51pub struct Color {
52 pub r: u8,
54 pub g: u8,
56 pub b: u8,
58}
59
60impl Color {
61 pub fn white() -> Self {
63 Color {
64 r: 255,
65 g: 255,
66 b: 255,
67 }
68 }
69
70 pub fn black() -> Self {
72 Color { r: 0, g: 0, b: 0 }
73 }
74
75 pub fn red() -> Self {
77 Color { r: 255, g: 0, b: 0 }
78 }
79}
80
81pub struct AssFormat;
86
87impl SubtitleFormat for AssFormat {
88 fn parse(&self, content: &str) -> Result<Subtitle> {
89 let mut entries = Vec::new();
90 let mut in_events = false;
91 let mut fields: Vec<&str> = Vec::new();
92 for line in content.lines() {
93 let l = line.trim_start();
94 if l.eq_ignore_ascii_case("[events]") {
95 in_events = true;
96 continue;
97 }
98 if !in_events {
99 continue;
100 }
101 if l.to_lowercase().starts_with("format:") {
102 fields = l["Format:".len()..].split(',').map(|s| s.trim()).collect();
103 continue;
104 }
105 if l.to_lowercase().starts_with("dialogue:") {
106 let data = l["Dialogue:".len()..].trim();
107 let parts: Vec<&str> = data.splitn(fields.len(), ',').collect();
108 if parts.len() < fields.len() {
109 continue;
110 }
111 let start_index = fields
112 .iter()
113 .position(|&f| f.eq_ignore_ascii_case("start"))
114 .ok_or_else(|| {
115 SubXError::subtitle_format(
116 "ASS",
117 "Missing 'Start' field in Format declaration",
118 )
119 })?;
120 let end_index = fields
121 .iter()
122 .position(|&f| f.eq_ignore_ascii_case("end"))
123 .ok_or_else(|| {
124 SubXError::subtitle_format(
125 "ASS",
126 "Missing 'End' field in Format declaration",
127 )
128 })?;
129 let text_index = fields
130 .iter()
131 .position(|&f| f.eq_ignore_ascii_case("text"))
132 .ok_or_else(|| {
133 SubXError::subtitle_format(
134 "ASS",
135 "Missing 'Text' field in Format declaration",
136 )
137 })?;
138 let start = parts[start_index].trim();
139 let end = parts[end_index].trim();
140 let text = parts[text_index..].join(",").replace("\\N", "\n");
141 let start_time = parse_ass_time(start)?;
142 let end_time = parse_ass_time(end)?;
143 entries.push(SubtitleEntry {
144 index: entries.len() + 1,
145 start_time,
146 end_time,
147 text,
148 styling: None,
149 });
150 }
151 }
152 Ok(Subtitle {
153 entries,
154 metadata: SubtitleMetadata {
155 title: None,
156 language: None,
157 encoding: "utf-8".to_string(),
158 frame_rate: None,
159 original_format: SubtitleFormatType::Ass,
160 },
161 format: SubtitleFormatType::Ass,
162 })
163 }
164
165 fn serialize(&self, subtitle: &Subtitle) -> Result<String> {
166 let mut output = String::new();
167 output.push_str("[Script Info]\n");
168 output.push_str("; Script generated by SubX\n");
169 output.push_str("ScriptType: v4.00+\n\n");
170 output.push_str("[V4+ Styles]\n");
171 output.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");
172 output.push_str("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n");
173 output.push_str("[Events]\n");
174 output.push_str("Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\n");
175 for entry in &subtitle.entries {
176 let text = entry.text.replace('\n', "\\N");
177 let start = format_ass_time(entry.start_time);
178 let end = format_ass_time(entry.end_time);
179 output.push_str(&format!(
180 "Dialogue: 0,{},{},Default,,0000,0000,0000,,{}\n",
181 start, end, text
182 ));
183 }
184 Ok(output)
185 }
186
187 fn detect(&self, content: &str) -> bool {
188 content.contains("[Script Info]") || content.contains("Dialogue:")
189 }
190
191 fn format_name(&self) -> &'static str {
192 "ASS"
193 }
194
195 fn file_extensions(&self) -> &'static [&'static str] {
196 &["ass", "ssa"]
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 const SAMPLE_ASS: &str = "[Script Info]\nScriptType: v4.00+\n\n[V4+ Styles]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n\n[Events]\nFormat: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\nDialogue: 0,0:00:01.00,0:00:02.50,Default,,0000,0000,0000,,Hello\\NASS\n";
205
206 #[test]
207 fn test_detect_ass() {
208 let fmt = AssFormat;
209 assert!(fmt.detect(SAMPLE_ASS));
210 assert!(!fmt.detect("Not ASS content"));
211 }
212
213 #[test]
214 fn test_parse_ass_and_serialize() {
215 let fmt = AssFormat;
216 let subtitle = fmt.parse(SAMPLE_ASS).expect("ASS parse failed");
217 assert_eq!(subtitle.entries.len(), 1);
218 assert_eq!(subtitle.entries[0].text, "Hello\nASS");
219 let out = fmt.serialize(&subtitle).expect("ASS serialize failed");
220 assert!(out.contains("Dialogue: 0,0:00:01.00,0:00:02.50"));
221 assert!(out.contains("Hello\\NASS"));
222 }
223
224 #[test]
225 fn test_parse_ass_missing_start_field() {
226 let fmt = AssFormat;
227 let content = "[Events]\nFormat: Layer,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\nDialogue: 0,0:00:02.50,Default,,0000,0000,0000,,Hi\n";
229 let err = fmt.parse(content).expect_err("missing Start must error");
230 let msg = err.to_string();
231 assert!(msg.contains("Start"), "unexpected error: {}", msg);
232 }
233
234 #[test]
235 fn test_parse_ass_missing_end_field() {
236 let fmt = AssFormat;
237 let content = "[Events]\nFormat: Layer,Start,Style,Name,MarginL,MarginR,MarginV,Effect,Text\nDialogue: 0,0:00:01.00,Default,,0000,0000,0000,,Hi\n";
238 let err = fmt.parse(content).expect_err("missing End must error");
239 assert!(err.to_string().contains("End"));
240 }
241
242 #[test]
243 fn test_parse_ass_missing_text_field() {
244 let fmt = AssFormat;
245 let content = "[Events]\nFormat: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect\nDialogue: 0,0:00:01.00,0:00:02.50,Default,,0000,0000,0000,\n";
246 let err = fmt.parse(content).expect_err("missing Text must error");
247 assert!(err.to_string().contains("Text"));
248 }
249
250 #[test]
251 fn test_parse_ass_time_overflow() {
252 let overflow_time = format!("{}:00:00.00", u64::MAX);
254 let err = parse_ass_time(&overflow_time).expect_err("overflow must be detected");
255 assert!(err.to_string().to_lowercase().contains("overflow"));
256 }
257
258 #[test]
259 fn test_parse_ass_time_valid() {
260 let d = parse_ass_time("1:02:03.45").expect("valid time");
261 assert_eq!(
262 d,
263 Duration::from_millis(3_600_000 + 2 * 60_000 + 3 * 1000 + 450)
264 );
265 }
266}
267
268fn parse_ass_time(time: &str) -> Result<Duration> {
269 let parts: Vec<&str> = time.split(&[':', '.'][..]).collect();
270 if parts.len() != 4 {
271 return Err(SubXError::subtitle_format(
272 "ASS",
273 format!("Invalid time format: {}", time),
274 ));
275 }
276 let hours: u64 = parts[0]
277 .parse()
278 .map_err(|e: std::num::ParseIntError| SubXError::subtitle_format("ASS", e.to_string()))?;
279 let minutes: u64 = parts[1]
280 .parse()
281 .map_err(|e: std::num::ParseIntError| SubXError::subtitle_format("ASS", e.to_string()))?;
282 let seconds: u64 = parts[2]
283 .parse()
284 .map_err(|e: std::num::ParseIntError| SubXError::subtitle_format("ASS", e.to_string()))?;
285 let centi: u64 = parts[3]
286 .parse()
287 .map_err(|e: std::num::ParseIntError| SubXError::subtitle_format("ASS", e.to_string()))?;
288
289 let overflow =
290 || SubXError::subtitle_format("ASS", format!("Timestamp arithmetic overflow: {}", time));
291 let total_ms = hours
292 .checked_mul(3_600_000)
293 .ok_or_else(overflow)?
294 .checked_add(minutes.checked_mul(60_000).ok_or_else(overflow)?)
295 .ok_or_else(overflow)?
296 .checked_add(seconds.checked_mul(1_000).ok_or_else(overflow)?)
297 .ok_or_else(overflow)?
298 .checked_add(centi.checked_mul(10).ok_or_else(overflow)?)
299 .ok_or_else(overflow)?;
300 Ok(Duration::from_millis(total_ms))
301}
302
303fn format_ass_time(duration: Duration) -> String {
304 let total_ms = duration.as_millis();
305 let hours = total_ms / 3600000;
306 let minutes = (total_ms % 3600000) / 60000;
307 let seconds = (total_ms % 60000) / 1000;
308 let centi = (total_ms % 1000) / 10;
309 format!("{}:{:02}:{:02}.{:02}", hours, minutes, seconds, centi)
310}