1use std::iter::FusedIterator;
10
11use crate::{TextElement, TtiBlock, text_field};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Subtitle<'a> {
18 blocks: &'a [TtiBlock],
19}
20
21impl<'a> Subtitle<'a> {
22 pub fn blocks(&self) -> &'a [TtiBlock] {
24 self.blocks
25 }
26
27 pub fn subtitle_number(&self) -> u16 {
37 self.blocks[0].sn
38 }
39
40 pub fn header_block(&self) -> &'a TtiBlock {
58 self.text_blocks().next().unwrap_or(&self.blocks[0])
59 }
60
61 pub fn is_comment(&self) -> bool {
71 self.header_block().is_comment()
72 }
73
74 pub fn text_blocks(&self) -> impl Iterator<Item = &'a TtiBlock> + use<'a> {
76 self.blocks.iter().filter(|block| block.is_text_block())
77 }
78
79 pub fn user_data_blocks(&self) -> impl Iterator<Item = &'a TtiBlock> + use<'a> {
81 self.blocks.iter().filter(|block| block.is_user_data())
82 }
83
84 pub fn text(&self) -> String {
98 text_field::decode(&self.concatenated_text_field(), self.blocks[0].cct)
99 }
100
101 pub fn text_elements(&self) -> Vec<TextElement> {
123 text_field::decode_elements(&self.concatenated_text_field(), self.blocks[0].cct)
124 }
125
126 fn concatenated_text_field(&self) -> Vec<u8> {
128 self.text_blocks()
129 .flat_map(|block| block.text_field_content())
130 .copied()
131 .collect()
132 }
133}
134
135#[derive(Debug, Clone)]
138pub struct Subtitles<'a> {
139 remaining: &'a [TtiBlock],
140}
141
142impl<'a> Subtitles<'a> {
143 pub(crate) fn new(blocks: &'a [TtiBlock]) -> Self {
144 Self { remaining: blocks }
145 }
146}
147
148impl<'a> Iterator for Subtitles<'a> {
149 type Item = Subtitle<'a>;
150
151 fn next(&mut self) -> Option<Self::Item> {
152 if self.remaining.is_empty() {
153 return None;
154 }
155 let (blocks, remaining) = self.remaining.split_at(subtitle_len(self.remaining));
156 self.remaining = remaining;
157 Some(Subtitle { blocks })
158 }
159}
160
161impl FusedIterator for Subtitles<'_> {}
162
163fn subtitle_len(blocks: &[TtiBlock]) -> usize {
169 let Some(first) = blocks.first() else {
170 return 0;
171 };
172 let mut ended = first.is_last_block();
173 let mut len = 1;
174 for block in &blocks[1..] {
175 if block.sn != first.sn || (ended && block.is_text_block()) {
176 break;
177 }
178 ended |= block.is_last_block();
179 len += 1;
180 }
181 len
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::{CharacterCodeTable, CumulativeStatus, Stl, Time};
188
189 const TF_LENGTH: usize = 112;
190
191 fn time(seconds: u8) -> Time {
192 Time {
193 hours: 0,
194 minutes: 0,
195 seconds,
196 frames: 0,
197 }
198 }
199
200 fn block(sn: u16, ebn: u8, text: &[u8]) -> TtiBlock {
202 assert!(text.len() <= TF_LENGTH);
203 let mut tf = text.to_vec();
204 tf.resize(TF_LENGTH, 0x8F);
205 TtiBlock {
206 sgn: 0,
207 sn,
208 ebn,
209 cs: CumulativeStatus::NotPartOfASet,
210 tci: time(1),
211 tco: time(2),
212 vp: 20,
213 jc: 2,
214 cf: 0,
215 tf,
216 cct: CharacterCodeTable::Latin,
217 }
218 }
219
220 fn stl(ttis: Vec<TtiBlock>) -> Stl {
221 Stl {
222 gsi: crate::GsiBlock::new(),
223 ttis,
224 }
225 }
226
227 fn block_counts(stl: &Stl) -> Vec<usize> {
228 stl.subtitles().map(|s| s.blocks().len()).collect()
229 }
230
231 #[test]
232 fn no_blocks_gives_no_subtitles() {
233 assert_eq!(0, stl(vec![]).subtitles().count());
234 }
235
236 #[test]
237 fn single_block_subtitles() {
238 let stl = stl(vec![
239 block(1, 0xFF, b"one"),
240 block(2, 0xFF, b"two"),
241 block(3, 0xFF, b"three"),
242 ]);
243
244 let texts: Vec<String> = stl.subtitles().map(|s| s.text()).collect();
245 assert_eq!(vec!["one", "two", "three"], texts);
246 let numbers: Vec<u16> = stl.subtitles().map(|s| s.subtitle_number()).collect();
247 assert_eq!(vec![1, 2, 3], numbers);
248 }
249
250 #[test]
251 fn extension_blocks_are_merged() {
252 let mut first = b"Line one".to_vec();
255 first.push(0x8A);
256 first.resize(TF_LENGTH - 3, b'x');
257 first.extend(b"Bee");
258 let stl = stl(vec![
259 block(1, 0x00, &first),
260 block(1, 0x01, b"kem"),
261 block(1, 0xFF, b"olen."),
262 block(2, 0xFF, b"next"),
263 ]);
264
265 assert_eq!(vec![3, 1], block_counts(&stl));
266 let text = stl.subtitles().next().unwrap().text();
267 assert!(text.starts_with("Line one\r\nxxx"), "{text:?}");
268 assert!(text.ends_with("xxxBeekemolen."), "{text:?}");
269 }
270
271 #[test]
272 fn character_split_across_blocks_is_decoded() {
273 let stl = stl(vec![block(1, 0x00, b"B\xC8"), block(1, 0xFF, b"ar")]);
275
276 assert_eq!("Bär", stl.subtitles().next().unwrap().text());
277 }
278
279 #[test]
280 fn user_data_is_not_part_of_the_text() {
281 let stl = stl(vec![
282 block(1, 0xFE, b"user data first"),
283 block(1, 0x00, b"Hello "),
284 block(1, 0xFE, b"user data between"),
285 block(1, 0xFF, b"world"),
286 block(1, 0xFE, b"user data last"),
287 block(2, 0xFF, b"next"),
288 ]);
289
290 assert_eq!(vec![5, 1], block_counts(&stl));
291 let subtitle = stl.subtitles().next().unwrap();
292 assert_eq!("Hello world", subtitle.text());
293 let user_data: Vec<String> = subtitle
294 .user_data_blocks()
295 .map(|block| block.text())
296 .collect();
297 assert_eq!(
298 vec!["user data first", "user data between", "user data last"],
299 user_data
300 );
301 assert_eq!(2, subtitle.text_blocks().count());
302 }
303
304 #[test]
305 fn reserved_blocks_are_not_part_of_the_text() {
306 let stl = stl(vec![
307 block(1, 0x00, b"Hello "),
308 block(1, 0xF0, b"reserved"),
309 block(1, 0xFD, b"reserved"),
310 block(1, 0xFF, b"world"),
311 ]);
312
313 let subtitle = stl.subtitles().next().unwrap();
314 assert_eq!(4, subtitle.blocks().len());
315 assert_eq!("Hello world", subtitle.text());
316 }
317
318 #[test]
319 fn header_block_is_first_text_block() {
320 let mut text_block = block(1, 0xFF, b"text");
321 text_block.tci = time(10);
322 let stl = stl(vec![block(1, 0xFE, b"user data"), text_block]);
323
324 let subtitle = stl.subtitles().next().unwrap();
325 assert_eq!(time(10), subtitle.header_block().tci);
326 }
327
328 #[test]
329 fn header_block_without_text_blocks_is_first_block() {
330 let stl = stl(vec![block(1, 0xFE, b"user data")]);
331
332 let subtitle = stl.subtitles().next().unwrap();
333 assert!(subtitle.header_block().is_user_data());
334 assert_eq!("", subtitle.text());
335 }
336
337 #[test]
338 fn same_subtitle_number_after_last_block_starts_new_subtitle() {
339 let stl = stl(vec![
340 block(0, 0xFF, b"one"),
341 block(0, 0xFF, b"two"),
342 block(0, 0x00, b"thr"),
343 block(0, 0xFF, b"ee"),
344 ]);
345
346 let texts: Vec<String> = stl.subtitles().map(|s| s.text()).collect();
347 assert_eq!(vec!["one", "two", "three"], texts);
348 }
349
350 #[test]
351 fn subtitle_without_last_block_ends_at_next_subtitle_number() {
352 let stl = stl(vec![block(1, 0x00, b"one"), block(2, 0xFF, b"two")]);
353
354 assert_eq!(vec![1, 1], block_counts(&stl));
355 }
356
357 #[test]
358 fn comment_flag() {
359 let mut comment = block(1, 0x00, b"comment ");
360 comment.cf = 1;
361 let stl = stl(vec![
363 comment,
364 block(1, 0xFF, b"text"),
365 block(2, 0xFF, b"sub"),
366 ]);
367
368 let comments: Vec<bool> = stl.subtitles().map(|s| s.is_comment()).collect();
369 assert_eq!(vec![true, false], comments);
370 }
371
372 #[test]
373 fn block_text_filling_the_whole_field_is_decoded() {
374 let text = [b'a'; TF_LENGTH];
375 assert_eq!("a".repeat(TF_LENGTH), block(1, 0xFF, &text).text());
376 }
377
378 #[test]
379 fn block_text_stops_at_unused_space() {
380 let tti = block(1, 0xFF, b"one\x8Atwo\x8Fignored");
381 assert_eq!("one\r\ntwo", tti.text());
382 }
383
384 #[test]
385 fn parsed_file_has_one_subtitle_per_block() {
386 let stl = crate::parse_stl_from_file("stls/test.stl").expect("Parse stls/test.stl");
387
388 assert_eq!(stl.ttis.len(), stl.subtitles().count());
389 for (subtitle, tti) in stl.subtitles().zip(&stl.ttis) {
390 assert_eq!(tti.text(), subtitle.text());
391 }
392 }
393
394 #[test]
395 fn text_elements_continue_across_blocks() {
396 use crate::OpenControl;
397
398 let stl = stl(vec![block(1, 0x00, b"\x80Hel"), block(1, 0xFF, b"lo\x81")]);
399
400 assert_eq!(
401 vec![
402 TextElement::Open(OpenControl::ItalicsOn),
403 TextElement::Text("Hello".to_string()),
404 TextElement::Open(OpenControl::ItalicsOff),
405 ],
406 stl.subtitles().next().unwrap().text_elements()
407 );
408 }
409}