1use crate::utils::rgb_to_rgba;
6
7#[derive(Debug, Clone)]
9pub struct VobSubPalette {
10 pub rgba: [u32; 16],
12}
13
14impl Default for VobSubPalette {
15 fn default() -> Self {
16 Self {
18 rgba: [
19 0x00000000, 0xFFFFFFFF, 0xFF000000, 0xFF808080, 0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000,
24 0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000,
25 ],
26 }
27 }
28}
29
30#[derive(Debug, Clone, Copy)]
32pub struct VobSubTimestamp {
33 pub timestamp_ms: u32,
35 pub file_position: u64,
37}
38
39#[derive(Debug, Clone)]
41pub struct VobSubMetadata {
42 pub width: u16,
44 pub height: u16,
46 pub language: Option<String>,
48 pub id: Option<String>,
50}
51
52impl Default for VobSubMetadata {
53 fn default() -> Self {
54 Self {
55 width: 720,
56 height: 480,
57 language: None,
58 id: None,
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct IdxParseResult {
66 pub palette: VobSubPalette,
67 pub timestamps: Vec<VobSubTimestamp>,
68 pub metadata: VobSubMetadata,
69}
70
71pub fn parse_idx(idx_content: &str) -> IdxParseResult {
73 let mut result = IdxParseResult {
74 palette: VobSubPalette::default(),
75 timestamps: Vec::new(),
76 metadata: VobSubMetadata::default(),
77 };
78
79 for line in idx_content.lines() {
80 let trimmed = line.trim();
81 if trimmed.is_empty() || trimmed.starts_with('#') {
82 continue;
83 }
84
85 if let Some(rest) = trimmed.strip_prefix("size:") {
87 if let Some((w_str, h_str)) = rest.trim().split_once('x')
88 && let (Ok(w), Ok(h)) = (w_str.trim().parse::<u16>(), h_str.trim().parse::<u16>())
89 {
90 result.metadata.width = w;
91 result.metadata.height = h;
92 }
93 continue;
94 }
95
96 if let Some(rest) = trimmed.strip_prefix("palette:") {
98 let colors: Vec<&str> = rest.split(',').map(|s| s.trim()).collect();
99 for (i, color_hex) in colors.iter().enumerate().take(16) {
100 let hex = color_hex.trim_start_matches('#');
101 if hex.len() == 6
102 && let Ok(rgb) = u32::from_str_radix(hex, 16)
103 {
104 let r = ((rgb >> 16) & 0xFF) as u8;
105 let g = ((rgb >> 8) & 0xFF) as u8;
106 let b = (rgb & 0xFF) as u8;
107 result.palette.rgba[i] = rgb_to_rgba(r, g, b, 255);
108 }
109 }
110 continue;
111 }
112
113 if let Some(rest) = trimmed.strip_prefix("id:") {
115 let parts: Vec<&str> = rest.split(',').collect();
116 if !parts.is_empty() {
117 result.metadata.language = Some(parts[0].trim().to_string());
118 }
119 if let Some(idx_part) = parts.get(1)
120 && let Some(idx_str) = idx_part.trim().strip_prefix("index:")
121 {
122 result.metadata.id = Some(idx_str.trim().to_string());
123 }
124 continue;
125 }
126
127 if let Some(rest) = trimmed.strip_prefix("timestamp:")
130 && let Some((time_part, filepos_part)) = rest.split_once(',')
131 {
132 let time_str = time_part.trim();
133 let filepos_str = filepos_part
134 .trim()
135 .strip_prefix("filepos:")
136 .map(|s| s.trim());
137
138 if let Some(filepos_hex) = filepos_str {
139 let parts: Vec<&str> = time_str.split(':').collect();
141 if parts.len() == 4
142 && let (Ok(h), Ok(m), Ok(s), Ok(ms)) = (
143 parts[0].parse::<u32>(),
144 parts[1].parse::<u32>(),
145 parts[2].parse::<u32>(),
146 parts[3].parse::<u32>(),
147 )
148 {
149 let timestamp_ms = h * 3600000 + m * 60000 + s * 1000 + ms;
150
151 if let Ok(file_position) = u64::from_str_radix(filepos_hex, 16) {
153 result.timestamps.push(VobSubTimestamp {
154 timestamp_ms,
155 file_position,
156 });
157 }
158 }
159 }
160 }
161 }
162
163 result
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_parse_idx_basic() {
172 let idx = r#"
173# VobSub index file
174size: 720x480
175palette: 000000, ffffff, 808080, 404040, 000000, 000000, 000000, 000000, 000000, 000000, 000000, 000000, 000000, 000000, 000000, 000000
176id: en, index: 0
177timestamp: 00:00:01:000, filepos: 00000000
178timestamp: 00:00:05:500, filepos: 00001000
179"#;
180
181 let result = parse_idx(idx);
182
183 assert_eq!(result.metadata.width, 720);
184 assert_eq!(result.metadata.height, 480);
185 assert_eq!(result.metadata.language, Some("en".to_string()));
186 assert_eq!(result.timestamps.len(), 2);
187 assert_eq!(result.timestamps[0].timestamp_ms, 1000);
188 assert_eq!(result.timestamps[1].timestamp_ms, 5500);
189 }
190
191 #[test]
192 fn test_parse_real_vobsub_durations() {
193 use crate::vobsub::parse_subtitle_packet;
194
195 let idx_content = include_str!("../testfiles/vobsub.idx");
196 let sub_data = include_bytes!("../testfiles/vobsub.sub");
197
198 let idx = parse_idx(idx_content);
199
200 println!("\n=== VobSub Duration Analysis ===");
201 println!("Total timestamps: {}", idx.timestamps.len());
202
203 for (i, ts) in idx.timestamps.iter().take(10).enumerate() {
205 let next_ts = idx.timestamps.get(i + 1);
206 let gap_to_next = next_ts.map(|n| n.timestamp_ms - ts.timestamp_ms);
207
208 if let Some((packet, _)) =
209 parse_subtitle_packet(sub_data, ts.file_position as usize, &idx.palette)
210 {
211 println!(
213 "Sub {}: start={}ms, ctrl_dur={}ms, gap={}ms",
214 i,
215 ts.timestamp_ms,
216 packet.duration_ms,
217 gap_to_next.unwrap_or(0)
218 );
219
220 if let Some(gap) = gap_to_next {
223 assert!(
224 packet.duration_ms <= gap || packet.duration_ms == 5000,
225 "Duration {} should be <= gap {} (or default 5000)",
226 packet.duration_ms,
227 gap
228 );
229 }
230 } else {
231 println!(
232 "Subtitle {}: FAILED TO PARSE at offset 0x{:X}",
233 i, ts.file_position
234 );
235 }
236 }
237
238 println!("\n=== Durations are now correctly parsed from control sequences ===");
239 }
240}