1#![warn(missing_docs)]
26
27use std::fs::File;
28use std::io::prelude::*;
29use std::{fmt, path::Path};
30
31mod fields;
32mod gsi;
33mod latin;
34pub mod parser;
36mod subtitle;
37mod text_field;
38mod time;
39mod tti;
40mod validate;
41
42pub use crate::fields::{Date, InvalidFieldValue, Justification, LanguageCode};
43use crate::gsi::MAX_TNB;
44pub use crate::gsi::{
45 CharacterCodeTable, CodePageNumber, DiskFormatCode, DisplayStandardCode, GsiBlock,
46 TimeCodeStatus,
47};
48pub use crate::parser::ParseError;
49pub use crate::subtitle::{Subtitle, Subtitles};
50pub use crate::text_field::{Color, OpenControl, TeletextControl, TextElement};
51pub use crate::time::Time;
52pub use crate::tti::{CumulativeStatus, TtiBlock, TtiFormat};
53use crate::tti::{EBN_LAST_BLOCK, MAX_TEXT_BLOCKS};
54pub use crate::validate::ValidationIssue;
55
56use crate::parser::parse_stl_from_slice;
57use crate::text_field::Presentation;
58
59#[derive(Debug, PartialEq, Eq)]
61pub struct Stl {
62 pub gsi: GsiBlock,
64 pub ttis: Vec<TtiBlock>,
66}
67
68impl fmt::Display for Stl {
69 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70 write!(f, "{}\n{:?}\n", self.gsi, self.ttis)
71 }
72}
73
74impl Stl {
75 pub fn new() -> Stl {
77 Stl {
78 gsi: GsiBlock::new(),
79 ttis: vec![],
80 }
81 }
82
83 pub fn write_to_file<P: AsRef<Path>>(&self, filename: P) -> Result<(), ParseError> {
107 let mut f = File::create(filename)?;
108 f.write_all(&self.gsi.serialize()?)?;
109 for tti in self.ttis.iter() {
110 f.write_all(&tti.serialize())?;
111 }
112 Ok(())
113 }
114
115 pub fn add_sub(
146 &mut self,
147 tci: Time,
148 tco: Time,
149 txt: &str,
150 opt: TtiFormat,
151 ) -> Result<(), ParseError> {
152 let presentation = match self.gsi.dsc {
153 DisplayStandardCode::OpenSubtitling => Presentation::Open,
154 _ => Presentation::Teletext {
155 double_height: opt.dh,
156 },
157 };
158 let text_fields = text_field::encode_text_fields(txt, presentation, self.gsi.cct);
159 if text_fields.len() > MAX_TEXT_BLOCKS {
160 return Err(ParseError::SubtitleTextTooLong {
161 blocks: text_fields.len(),
162 });
163 }
164 let tnb = u32::try_from(text_fields.len())
165 .ok()
166 .and_then(|blocks| self.gsi.tnb.checked_add(blocks))
167 .filter(|&tnb| tnb <= MAX_TNB)
168 .ok_or(ParseError::TooManyTtiBlocks)?;
169 let sn = match self.ttis.last() {
170 Some(last) => last
171 .sn
172 .checked_add(1)
173 .ok_or(ParseError::SubtitleNumberOverflow)?,
174 None => 1,
175 };
176 if self.ttis.is_empty() {
177 self.gsi.tcf = format!(
179 "{:02}{:02}{:02}{:02}",
180 tci.hours, tci.minutes, tci.seconds, tci.frames
181 );
182 }
183
184 let last_block = text_fields.len() - 1;
185 for (i, (tf, ebn)) in text_fields.into_iter().zip(0_u8..).enumerate() {
186 self.ttis.push(TtiBlock {
187 sgn: 0,
188 sn,
189 ebn: if i == last_block { EBN_LAST_BLOCK } else { ebn },
190 cs: CumulativeStatus::NotPartOfASet,
191 tci,
192 tco,
193 vp: opt.vp,
194 jc: opt.jc,
195 cf: 0,
196 tf,
197 cct: self.gsi.cct,
198 });
199 }
200 self.gsi.tnb = tnb;
201 self.gsi.tns += 1;
202 Ok(())
203 }
204
205 pub fn subtitles(&self) -> Subtitles<'_> {
225 Subtitles::new(&self.ttis)
226 }
227}
228
229impl Default for Stl {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235pub fn parse_stl_from_file<P: AsRef<Path>>(filename: P) -> Result<Stl, ParseError> {
252 let mut f = File::open(filename)?;
253 let mut buffer = vec![];
254 f.read_to_end(&mut buffer)?;
255
256 Ok(parse_stl_from_slice(&mut buffer.as_slice())?)
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn time(seconds: u8) -> Time {
264 Time {
265 hours: 10,
266 minutes: 0,
267 seconds,
268 frames: 0,
269 }
270 }
271
272 fn format(dh: bool) -> TtiFormat {
273 TtiFormat { jc: 2, vp: 20, dh }
274 }
275
276 fn add(stl: &mut Stl, text: &str) -> Result<(), ParseError> {
277 stl.add_sub(time(1), time(2), text, format(false))
278 }
279
280 fn ebns(stl: &Stl) -> Vec<u8> {
281 stl.ttis.iter().map(|tti| tti.ebn).collect()
282 }
283
284 #[test]
285 fn add_sub_with_rows() {
286 let mut stl = Stl::new();
287 add(&mut stl, "One\nTwo").unwrap();
288 stl.add_sub(time(3), time(4), "Three\nFour", format(true))
289 .unwrap();
290
291 let texts: Vec<String> = stl.subtitles().map(|s| s.text()).collect();
292 assert_eq!(vec!["One\r\nTwo", "Three\r\n\r\nFour"], texts);
293 assert_eq!(vec![0xFF, 0xFF], ebns(&stl));
294 }
295
296 #[test]
297 fn add_sub_numbers_subtitles_from_one() {
298 let mut stl = Stl::new();
299 add(&mut stl, "One").unwrap();
300 add(&mut stl, "Two").unwrap();
301
302 let numbers: Vec<u16> = stl.ttis.iter().map(|tti| tti.sn).collect();
303 assert_eq!(vec![1, 2], numbers);
304 }
305
306 #[test]
307 fn add_sub_continues_subtitle_number_of_last_block() {
308 let mut stl = parse_stl_from_file("stls/test.stl").unwrap();
309 let last_sn = stl.ttis.last().unwrap().sn;
310
311 add(&mut stl, "Added").unwrap();
312 assert_eq!(last_sn + 1, stl.ttis.last().unwrap().sn);
313 }
314
315 #[test]
316 fn add_sub_long_text_uses_extension_blocks() {
317 let text = "A long subtitle text, ".repeat(12);
318 let mut stl = Stl::new();
319 add(&mut stl, &text).unwrap();
320 add(&mut stl, "Next").unwrap();
321
322 assert_eq!(vec![0x00, 0x01, 0xFF, 0xFF], ebns(&stl));
324 assert_eq!(
325 vec![1, 1, 1, 2],
326 stl.ttis.iter().map(|t| t.sn).collect::<Vec<_>>()
327 );
328 assert_eq!(4, stl.gsi.tnb);
329 assert_eq!(2, stl.gsi.tns);
330 let subtitle = stl.subtitles().next().unwrap();
331 assert_eq!(text.trim_end(), subtitle.text().trim_end());
332 assert!(stl.ttis.iter().all(|tti| tti.tci == time(1)));
333 }
334
335 #[test]
336 fn add_sub_open_subtitles_have_no_teletext_codes() {
337 let mut stl = Stl::new();
338 stl.gsi.dsc = DisplayStandardCode::OpenSubtitling;
339 stl.add_sub(time(1), time(2), "One\nTwo", format(true))
340 .unwrap();
341
342 assert_eq!(b"One\x8ATwo\x8F", &stl.ttis[0].tf[..8]);
343 }
344
345 #[test]
346 fn add_sub_too_long_text_is_an_error() {
347 let mut stl = Stl::new();
348 let text = "a".repeat(MAX_TEXT_BLOCKS * text_field::TF_LENGTH);
349
350 let err = add(&mut stl, &text).unwrap_err();
351 assert!(
352 matches!(err, ParseError::SubtitleTextTooLong { blocks: 242 }),
353 "{err:?}"
354 );
355 assert!(stl.ttis.is_empty());
356 assert_eq!(0, stl.gsi.tnb);
357 assert_eq!(0, stl.gsi.tns);
358 }
359
360 #[test]
361 fn add_sub_longest_allowed_text() {
362 let mut stl = Stl::new();
363 let text = "a".repeat(MAX_TEXT_BLOCKS * text_field::TF_LENGTH - 5);
365
366 add(&mut stl, &text).unwrap();
367 assert_eq!(MAX_TEXT_BLOCKS, stl.ttis.len());
368 assert_eq!(0xEF, stl.ttis[MAX_TEXT_BLOCKS - 2].ebn);
369 assert_eq!(0xFF, stl.ttis[MAX_TEXT_BLOCKS - 1].ebn);
370 }
371
372 #[test]
373 fn add_sub_tnb_overflow_is_an_error() {
374 let mut stl = Stl::new();
375 stl.gsi.tnb = MAX_TNB;
376
377 assert!(matches!(
378 add(&mut stl, "One"),
379 Err(ParseError::TooManyTtiBlocks)
380 ));
381 assert!(stl.ttis.is_empty());
382 }
383
384 #[test]
385 fn add_sub_subtitle_number_overflow_is_an_error() {
386 let mut stl = Stl::new();
387 stl.ttis.push(TtiBlock::new(
388 u16::MAX,
389 time(1),
390 time(2),
391 "Last",
392 format(false),
393 CharacterCodeTable::Latin,
394 ));
395
396 assert!(matches!(
397 add(&mut stl, "One"),
398 Err(ParseError::SubtitleNumberOverflow)
399 ));
400 assert_eq!(1, stl.ttis.len());
401 }
402
403 #[test]
404 fn written_subtitles_are_parsed_back() {
405 let mut stl = Stl::new();
406 add(&mut stl, "Première ligne\nDeuxième ligne à $5").unwrap();
407 add(&mut stl, &"Long text ".repeat(20)).unwrap();
408
409 let path = std::env::temp_dir().join("ebustl_parser_add_sub.stl");
410 stl.write_to_file(&path).unwrap();
411 let parsed = parse_stl_from_file(&path);
412 std::fs::remove_file(&path).ok();
413 let parsed = parsed.unwrap();
414
415 assert_eq!(stl.ttis, parsed.ttis);
416 assert_eq!(stl.gsi.tnb, parsed.gsi.tnb);
417 assert_eq!(stl.gsi.tns, parsed.gsi.tns);
418 let texts: Vec<String> = parsed.subtitles().map(|s| s.text()).collect();
419 assert_eq!("Première ligne\r\nDeuxième ligne à $5", texts[0]);
420 assert_eq!("Long text ".repeat(20).trim_end(), texts[1].trim_end());
421 }
422
423 #[test]
424 fn tti_block_new_truncates_to_one_block() {
425 let tti = TtiBlock::new(
426 1,
427 time(1),
428 time(2),
429 &"a".repeat(200),
430 format(false),
431 CharacterCodeTable::Latin,
432 );
433
434 assert_eq!(text_field::TF_LENGTH, tti.tf.len());
435 assert_eq!(
436 text_field::TF_UNUSED_SPACE,
437 tti.tf[text_field::TF_LENGTH - 1]
438 );
439 assert_eq!("a".repeat(text_field::TF_LENGTH - 3), tti.text());
440 }
441
442 #[test]
443 fn renamed_getters() {
444 let stl = parse_stl_from_file("stls/test.stl").unwrap();
445 assert_eq!(1, stl.gsi.total_number_of_subtitle_groups());
447 assert_eq!(40, stl.gsi.max_number_of_chars_in_row());
448
449 let tti = &stl.ttis[1];
450 assert_eq!(tti.sn, tti.subtitle_number());
451 }
452}