oxihuman_export/
ass_export.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone)]
9pub struct AssDialogue {
10 pub layer: u32,
12 pub start_cs: u64,
14 pub end_cs: u64,
16 pub style: String,
18 pub name: String,
20 pub text: String,
22}
23
24#[derive(Debug, Clone)]
26pub struct AssStyle {
27 pub name: String,
28 pub fontname: String,
29 pub fontsize: u32,
30 pub primary_colour: u32,
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct AssDocument {
36 pub styles: Vec<AssStyle>,
37 pub dialogues: Vec<AssDialogue>,
38 pub title: String,
39}
40
41impl AssDocument {
42 pub fn add_dialogue(&mut self, start_cs: u64, end_cs: u64, text: impl Into<String>) {
44 self.dialogues.push(AssDialogue {
45 layer: 0,
46 start_cs,
47 end_cs,
48 style: "Default".to_string(),
49 name: String::new(),
50 text: text.into(),
51 });
52 }
53
54 pub fn dialogue_count(&self) -> usize {
56 self.dialogues.len()
57 }
58}
59
60pub fn cs_to_ass_time(cs: u64) -> String {
62 let h = cs / 360_000;
63 let m = (cs % 360_000) / 6_000;
64 let s = (cs % 6_000) / 100;
65 let c = cs % 100;
66 format!("{h}:{m:02}:{s:02}.{c:02}")
67}
68
69pub fn render_script_info(doc: &AssDocument) -> String {
71 format!(
72 "[Script Info]\nTitle: {}\nScriptType: v4.00+\n\n",
73 doc.title
74 )
75}
76
77pub fn render_dialogues(doc: &AssDocument) -> String {
79 let mut out = String::from("[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n");
80 for d in &doc.dialogues {
81 out.push_str(&format!(
82 "Dialogue: {},{},{},{},{},0000,0000,0000,,{}\n",
83 d.layer,
84 cs_to_ass_time(d.start_cs),
85 cs_to_ass_time(d.end_cs),
86 d.style,
87 d.name,
88 d.text,
89 ));
90 }
91 out
92}
93
94pub fn render_ass(doc: &AssDocument) -> String {
96 render_script_info(doc) + &render_dialogues(doc)
97}
98
99pub fn validate_ass(doc: &AssDocument) -> bool {
101 doc.dialogues
102 .iter()
103 .all(|d| d.start_cs < d.end_cs && !d.text.is_empty())
104}
105
106pub fn total_duration_cs(doc: &AssDocument) -> u64 {
108 doc.dialogues.iter().map(|d| d.end_cs).max().unwrap_or(0)
109}
110
111pub fn default_style() -> AssStyle {
113 AssStyle {
114 name: "Default".to_string(),
115 fontname: "Arial".to_string(),
116 fontsize: 20,
117 primary_colour: 0x00FFFFFF,
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn sample_doc() -> AssDocument {
126 let mut d = AssDocument {
127 title: "Test".into(),
128 ..Default::default()
129 };
130 d.add_dialogue(0, 200, "Hello ASS");
131 d.add_dialogue(300, 500, "Second line");
132 d
133 }
134
135 #[test]
136 fn dialogue_count() {
137 assert_eq!(sample_doc().dialogue_count(), 2);
138 }
139
140 #[test]
141 fn cs_to_ass_time_format() {
142 assert_eq!(cs_to_ass_time(0), "0:00:00.00");
144 }
145
146 #[test]
147 fn cs_to_ass_time_nonzero() {
148 assert_eq!(cs_to_ass_time(360_000), "1:00:00.00");
150 }
151
152 #[test]
153 fn render_script_info_header() {
154 let s = render_script_info(&sample_doc());
155 assert!(s.contains("[Script Info]"));
156 }
157
158 #[test]
159 fn render_dialogues_contains_event() {
160 let s = render_dialogues(&sample_doc());
161 assert!(s.contains("Dialogue:"));
162 }
163
164 #[test]
165 fn render_ass_complete() {
166 let s = render_ass(&sample_doc());
167 assert!(s.contains("[Script Info]"));
168 assert!(s.contains("[Events]"));
169 }
170
171 #[test]
172 fn validate_ok() {
173 assert!(validate_ass(&sample_doc()));
174 }
175
176 #[test]
177 fn validate_bad() {
178 let mut d = AssDocument::default();
179 d.dialogues.push(AssDialogue {
180 layer: 0,
181 start_cs: 500,
182 end_cs: 100,
183 style: "Default".into(),
184 name: String::new(),
185 text: "bad".into(),
186 });
187 assert!(!validate_ass(&d));
188 }
189
190 #[test]
191 fn total_duration_correct() {
192 assert_eq!(total_duration_cs(&sample_doc()), 500);
193 }
194
195 #[test]
196 fn default_style_name() {
197 assert_eq!(default_style().name, "Default");
198 }
199}