ebustl_parser/
lib.rs

1use std::fs::File;
2use std::io;
3use std::io::prelude::*;
4use std::str;
5use std::{fmt, path::Path};
6
7use codepage_strings::Coding;
8use jiff::Zoned;
9use textcode::{iso6937, iso8859_5, iso8859_6, iso8859_7, iso8859_8};
10pub mod parser;
11pub use crate::parser::ParseError;
12use crate::parser::parse_stl_from_slice;
13
14/// A representation of a STL File
15/// See the [Spec](https://tech.ebu.ch/docs/tech/tech3264.pdf) for details
16#[derive(Debug, PartialEq, Eq)]
17pub struct Stl {
18    pub gsi: GsiBlock,
19    pub ttis: Vec<TtiBlock>,
20}
21
22impl fmt::Display for Stl {
23    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24        write!(f, "{}\n{:?}\n", self.gsi, self.ttis)
25    }
26}
27
28/// Text format information
29pub struct TtiFormat {
30    #[doc = "Justification Code"]
31    pub jc: u8,
32    #[doc = "Vertical Position"]
33    pub vp: u8,
34    #[doc = "Double Height"]
35    pub dh: bool,
36}
37
38impl Stl {
39    pub fn new() -> Stl {
40        Stl {
41            gsi: GsiBlock::new(),
42            ttis: vec![],
43        }
44    }
45
46    pub fn write_to_file<P: AsRef<Path>>(&self, filename: P) -> Result<(), ParseError> {
47        let mut f = File::create(filename)?;
48        f.write_all(&self.gsi.serialize()?)?;
49        for tti in self.ttis.iter() {
50            f.write_all(&tti.serialize())?;
51        }
52        Ok(())
53    }
54
55    pub fn add_sub(&mut self, tci: Time, tco: Time, txt: &str, opt: TtiFormat) {
56        if txt.len() > 112 {
57            //TODO: if txt.len() > 112 split in multiple
58            println!("Warning: sub text is too long!");
59        }
60        self.gsi.tnb += 1; // First TTI has sn=1
61        let tti = TtiBlock::new(self.gsi.tnb, tci, tco, txt, opt, self.gsi.cct);
62        self.gsi.tns += 1;
63        self.ttis.push(tti);
64    }
65}
66
67impl Default for Stl {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73/// Reads an STL file and parse it to a [Stl] struct.
74///
75/// Use [`crate::parser::parse_stl_from_slice`] to parse in memory data
76///
77/// # Example
78///
79/// ```rust,no_run
80/// use ebustl_parser::parse_stl_from_file;
81///
82/// let stl = parse_stl_from_file("/path/to/subtiltle.stl").expect("Parse stl from file");
83/// println!("{:?}", stl);
84/// ```
85pub fn parse_stl_from_file<P: AsRef<Path>>(filename: P) -> Result<Stl, ParseError> {
86    let mut f = File::open(filename)?;
87    let mut buffer = vec![];
88    f.read_to_end(&mut buffer)?;
89
90    Ok(parse_stl_from_slice(&mut buffer.as_slice())?)
91}
92
93struct CodePageCodec {
94    coding: Coding,
95    codepage: u16,
96}
97
98impl CodePageCodec {
99    pub fn new(codepage: u16) -> Result<Self, ParseError> {
100        Ok(Self {
101            codepage,
102            coding: Coding::new(codepage).map_err(|_e| ParseError::CodePageNumber(codepage))?,
103        })
104    }
105
106    fn decode(&self, data: &[u8]) -> Result<String, ParseError> {
107        Ok(self.coding.decode_lossy(data).to_string())
108    }
109    fn encode(&self, value: &str) -> Result<Vec<u8>, ParseError> {
110        self.coding
111            .encode(value)
112            .map_err(|err| ParseError::CodePageEncoding {
113                codepage: self.codepage,
114                value: value.to_string(),
115                source: err,
116            })
117    }
118}
119
120// GSI Block
121
122///The codepage numbers an stl file can use for strings
123#[derive(Debug, Clone, Copy, Eq, PartialEq)]
124#[allow(non_camel_case_types)]
125pub enum CodePageNumber {
126    CPN_437,
127    CPN_850,
128    CPN_860,
129    CPN_863,
130    CPN_865,
131}
132
133impl CodePageNumber {
134    fn serialize(&self) -> Vec<u8> {
135        match *self {
136            CodePageNumber::CPN_437 => vec![0x34, 0x33, 0x37],
137            CodePageNumber::CPN_850 => vec![0x38, 0x35, 0x30],
138            CodePageNumber::CPN_860 => vec![0x38, 0x36, 0x30],
139            CodePageNumber::CPN_863 => vec![0x38, 0x36, 0x33],
140            CodePageNumber::CPN_865 => vec![0x38, 0x36, 0x35],
141        }
142    }
143
144    pub(crate) fn from_u16(codepage: u16) -> Result<CodePageNumber, ParseError> {
145        match codepage {
146            437 => Ok(CodePageNumber::CPN_437),
147            850 => Ok(CodePageNumber::CPN_850),
148            860 => Ok(CodePageNumber::CPN_860),
149            863 => Ok(CodePageNumber::CPN_863),
150            865 => Ok(CodePageNumber::CPN_865),
151            _ => Err(ParseError::CodePageNumber(codepage)),
152        }
153    }
154
155    fn to_u16(self) -> u16 {
156        match self {
157            CodePageNumber::CPN_437 => 437,
158            CodePageNumber::CPN_850 => 850,
159            CodePageNumber::CPN_860 => 860,
160            CodePageNumber::CPN_863 => 863,
161            CodePageNumber::CPN_865 => 865,
162        }
163    }
164}
165
166/// The four display modes from the spec.
167#[derive(Debug, PartialEq, Eq)]
168pub enum DisplayStandardCode {
169    Blank,
170    OpenSubtitling,
171    Level1Teletext,
172    Level2Teletext,
173}
174
175impl DisplayStandardCode {
176    fn parse(data: u8) -> Result<DisplayStandardCode, ParseError> {
177        match data {
178            0x20 => Ok(DisplayStandardCode::Blank),
179            0x30 => Ok(DisplayStandardCode::OpenSubtitling),
180            0x31 => Ok(DisplayStandardCode::Level1Teletext),
181            0x32 => Ok(DisplayStandardCode::Level2Teletext),
182            _ => Err(ParseError::DisplayStandardCode),
183        }
184    }
185
186    fn serialize(&self) -> u8 {
187        match *self {
188            DisplayStandardCode::Blank => 0x20,
189            DisplayStandardCode::OpenSubtitling => 0x30,
190            DisplayStandardCode::Level1Teletext => 0x31,
191            DisplayStandardCode::Level2Teletext => 0x32,
192        }
193    }
194}
195
196/// A Status to indicate the validity of the information in GSI and TTI blocks.
197#[derive(Debug, PartialEq, Eq)]
198pub enum TimeCodeStatus {
199    NotIntendedForUse,
200    IntendedForUse,
201}
202
203impl TimeCodeStatus {
204    fn parse(data: u8) -> Result<TimeCodeStatus, ParseError> {
205        match data {
206            0x30 => Ok(TimeCodeStatus::NotIntendedForUse),
207            0x31 => Ok(TimeCodeStatus::IntendedForUse),
208            _ => Err(ParseError::TimeCodeStatus),
209        }
210    }
211
212    fn serialize(&self) -> u8 {
213        match *self {
214            TimeCodeStatus::NotIntendedForUse => 0x30,
215            TimeCodeStatus::IntendedForUse => 0x31,
216        }
217    }
218}
219
220/// The five ISO Standard character code tables can be used to define the text in the Text Field of
221/// the TTI Blocks
222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
223pub enum CharacterCodeTable {
224    Latin,
225    LatinCyrillic,
226    LatinArabic,
227    LatinGreek,
228    LatinHebrew,
229}
230
231impl CharacterCodeTable {
232    fn parse(data: &[u8]) -> Result<CharacterCodeTable, ParseError> {
233        if data.len() != 2 {
234            return Err(ParseError::CharacterCodeTable);
235        }
236        if data[0] != 0x30 {
237            return Err(ParseError::CharacterCodeTable);
238        }
239        match data[1] {
240            0x30 => Ok(CharacterCodeTable::Latin),
241            0x31 => Ok(CharacterCodeTable::LatinCyrillic),
242            0x32 => Ok(CharacterCodeTable::LatinArabic),
243            0x33 => Ok(CharacterCodeTable::LatinGreek),
244            0x34 => Ok(CharacterCodeTable::LatinHebrew),
245            _ => Err(ParseError::CharacterCodeTable),
246        }
247    }
248
249    fn serialize(&self) -> Vec<u8> {
250        match *self {
251            CharacterCodeTable::Latin => vec![0x30, 0x30],
252            CharacterCodeTable::LatinCyrillic => vec![0x30, 0x31],
253            CharacterCodeTable::LatinArabic => vec![0x30, 0x32],
254            CharacterCodeTable::LatinGreek => vec![0x30, 0x33],
255            CharacterCodeTable::LatinHebrew => vec![0x30, 0x34],
256        }
257    }
258}
259
260/// The television frame-rates the STL spec allows
261#[derive(Debug, PartialEq, Eq)]
262#[allow(non_camel_case_types)]
263pub enum DiskFormatCode {
264    STL25_01,
265    STL30_01,
266}
267
268impl DiskFormatCode {
269    fn parse(data: &str) -> Result<DiskFormatCode, ParseError> {
270        if data == "STL25.01" {
271            Ok(DiskFormatCode::STL25_01)
272        } else if data == "STL30.01" {
273            Ok(DiskFormatCode::STL30_01)
274        } else {
275            Err(ParseError::DiskFormatCode(data.to_string()))
276        }
277    }
278
279    fn serialize(&self) -> Vec<u8> {
280        match *self {
281            DiskFormatCode::STL25_01 => String::from("STL25.01").into_bytes(),
282            DiskFormatCode::STL30_01 => String::from("STL30.01").into_bytes(),
283        }
284    }
285
286    pub fn get_fps(&self) -> usize {
287        match self {
288            DiskFormatCode::STL25_01 => 25,
289            DiskFormatCode::STL30_01 => 30,
290        }
291    }
292}
293
294/// General Subtitle Information (GSI) block
295#[derive(Debug, PartialEq, Eq)]
296pub struct GsiBlock {
297    #[doc = "0..2 Code Page Number"]
298    cpn: CodePageNumber,
299    #[doc = "3..10 Disk Format Code"]
300    dfc: DiskFormatCode,
301    #[doc = "11 Display Standard Code"]
302    dsc: DisplayStandardCode,
303    #[doc = "12..13 Character Code Table Number"]
304    cct: CharacterCodeTable,
305    #[doc = "14..15 Language Code"]
306    lc: String,
307    #[doc = "16..47 Original Program Title"]
308    opt: String,
309    #[doc = "48..79 Original Episode Title"]
310    oet: String,
311    #[doc = "80..111 Translated Program Title"]
312    tpt: String,
313    #[doc = "112..143 Translated Episode Title"]
314    tet: String,
315    #[doc = "144..175 Translator's Name"]
316    tn: String,
317    #[doc = "176..207 Translator's Contact Details"]
318    tcd: String,
319    #[doc = "208..223 Subtitle List Reference Code"]
320    slr: String,
321    #[doc = "224..229 Creation Date"]
322    cd: String,
323    #[doc = "230..235 Revision Date"]
324    rd: String,
325    #[doc = "236..237 Revision Number"]
326    rn: String,
327    #[doc = "238..242 Total Number of Text and Timing Blocks"]
328    tnb: u16,
329    #[doc = "243..247 Total Number of Subtitles"]
330    tns: u16,
331    #[doc = "248..250 Total Number of Subtitle Groups"]
332    tng: u16,
333    #[doc = "251..252 Maximum Number of Displayable Characters in a Text Row"]
334    mnc: u16,
335    #[doc = "253..254 Maximum Number of Displayable Rows"]
336    mnr: u16,
337    #[doc = "255 Time Code Status"]
338    tcs: TimeCodeStatus,
339    #[doc = "256..263 Time Code: Start of Programme (format: HHMMSSFF)"]
340    tcp: String,
341    #[doc = "264..271 Time Code: First-in-Cue (format: HHMMSSFF)"]
342    tcf: String,
343    #[doc = "272 Total Number of Disks"]
344    tnd: u8,
345    #[doc = "273 Disk Sequence Number"]
346    dsn: u8,
347    #[doc = "274..276 Country of Origin"]
348    co: String, // TODO Type with country definitions
349    #[doc = "277..308 Publisher"]
350    pub_: String,
351    #[doc = "309..340 Editor's Name"]
352    en: String,
353    #[doc = "341..372 Editor's Contact Details"]
354    ecd: String,
355    #[doc = "373..447 Spare Bytes"]
356    _spare: String,
357    #[doc = "448..1023 User-Defined Area"]
358    uda: String,
359}
360
361impl GsiBlock {
362    pub fn get_code_page_number(&self) -> &CodePageNumber {
363        &self.cpn
364    }
365    pub fn get_disk_format_code(&self) -> &DiskFormatCode {
366        &self.dfc
367    }
368    pub fn get_display_standard_code(&self) -> &DisplayStandardCode {
369        &self.dsc
370    }
371    pub fn get_character_code_table(&self) -> &CharacterCodeTable {
372        &self.cct
373    }
374    pub fn get_language_code(&self) -> &str {
375        &self.lc
376    }
377    pub fn get_original_program_title(&self) -> &str {
378        &self.opt
379    }
380    pub fn get_original_episode_title(&self) -> &str {
381        &self.oet
382    }
383    pub fn get_translated_program_title(&self) -> &str {
384        &self.tpt
385    }
386    pub fn get_translated_episode_title(&self) -> &str {
387        &self.tet
388    }
389    pub fn get_translators_name(&self) -> &str {
390        &self.tn
391    }
392    pub fn get_translators_contact_details(&self) -> &str {
393        &self.tcd
394    }
395    pub fn get_subtitle_list_reference_code(&self) -> &str {
396        &self.slr
397    }
398    pub fn get_creation_date(&self) -> &str {
399        &self.cd
400    }
401    pub fn get_revision_date(&self) -> &str {
402        &self.rd
403    }
404    pub fn get_revision_number(&self) -> &str {
405        &self.rn
406    }
407    pub fn get_total_number_of_text_and_timing_blocks(&self) -> u16 {
408        self.tnb
409    }
410    pub fn get_total_number_of_subtitles(&self) -> u16 {
411        self.tns
412    }
413    pub fn get_total_number_of_chars_in_row(&self) -> u16 {
414        self.tng
415    }
416    pub fn get_max_number_of_chars_in_row(&self) -> u16 {
417        self.mnc
418    }
419    pub fn get_max_number_of_rows(&self) -> u16 {
420        self.mnr
421    }
422    pub fn get_timecode_status(&self) -> &TimeCodeStatus {
423        &self.tcs
424    }
425    pub fn get_timecode_start_of_program(&self) -> &str {
426        &self.tcp
427    }
428    pub fn get_timecode_first_in_cue(&self) -> &str {
429        &self.tcf
430    }
431    pub fn get_total_number_of_disks(&self) -> u8 {
432        self.tnd
433    }
434    pub fn get_disk_sequence_number(&self) -> u8 {
435        self.dsn
436    }
437    pub fn get_country_of_origin(&self) -> &str {
438        &self.co
439    }
440    pub fn get_publisher(&self) -> &str {
441        &self.pub_
442    }
443    pub fn get_editors_name(&self) -> &str {
444        &self.en
445    }
446    pub fn get_editors_contact_details(&self) -> &str {
447        &self.ecd
448    }
449    pub fn get_user_defined_area(&self) -> &str {
450        &self.uda
451    }
452}
453
454fn push_string(v: &mut Vec<u8>, s: &str, len: usize) {
455    let addendum = s.to_owned().into_bytes();
456    let padding = len - addendum.len();
457    v.extend(addendum.iter().cloned());
458    v.extend(vec![0x20u8; padding]);
459}
460
461fn push_encoded_string(
462    v: &mut Vec<u8>,
463    c: &CodePageCodec,
464    s: &str,
465    len: usize,
466) -> Result<(), ParseError> {
467    let addendum = c.encode(s)?;
468    let padding = len - addendum.len();
469    v.extend(addendum.iter().cloned());
470    v.extend(vec![0x20u8; padding]);
471    Ok(())
472}
473
474impl GsiBlock {
475    pub fn new() -> GsiBlock {
476        let date = Zoned::now();
477        let now = date.strftime("%y%m%d").to_string();
478        GsiBlock {
479            cpn: CodePageNumber::CPN_850,
480            dfc: DiskFormatCode::STL25_01,
481            dsc: DisplayStandardCode::Level1Teletext,
482            cct: CharacterCodeTable::Latin,
483            lc: "0F".to_string(), // FIXME: ok for default?
484            opt: "".to_string(),
485            oet: "".to_string(),
486            tpt: "".to_string(),
487            tet: "".to_string(),
488            tn: "".to_string(),
489            tcd: "".to_string(),
490            slr: "".to_string(),
491            cd: now.clone(),
492            rd: now,
493            rn: "00".to_string(),
494            tnb: 0,
495            tns: 0,
496            tng: 1,  // At least one group?
497            mnc: 40, // FIXME: ok for default?
498            mnr: 23, // FIXME: ok for default?
499            tcs: TimeCodeStatus::IntendedForUse,
500            tcp: "00000000".to_string(),
501            tcf: "00000000".to_string(),
502            tnd: 1,
503            dsn: 1,
504            co: "".to_string(),
505            pub_: "".to_string(),
506            en: "".to_string(),
507            ecd: "".to_string(),
508            _spare: "".to_string(),
509            uda: "".to_string(),
510        }
511    }
512
513    fn serialize(&self) -> Result<Vec<u8>, ParseError> {
514        let codepage = self.get_code_page_number().to_u16();
515        let coding = CodePageCodec::new(codepage).expect("codepage");
516        let mut res = Vec::with_capacity(1024);
517        res.extend(self.cpn.serialize());
518        res.extend(self.dfc.serialize().iter().cloned());
519        res.push(self.dsc.serialize());
520        res.extend(self.cct.serialize());
521        // be careful for the length of following: must force padding
522        push_encoded_string(&mut res, &coding, &self.lc, 15 - 14 + 1)?;
523        push_encoded_string(&mut res, &coding, &self.opt, 47 - 16 + 1)?;
524        push_encoded_string(&mut res, &coding, &self.oet, 79 - 48 + 1)?;
525        push_encoded_string(&mut res, &coding, &self.tpt, 111 - 80 + 1)?;
526        push_encoded_string(&mut res, &coding, &self.tet, 143 - 112 + 1)?;
527        push_encoded_string(&mut res, &coding, &self.tn, 175 - 144 + 1)?;
528        push_encoded_string(&mut res, &coding, &self.tcd, 207 - 176 + 1)?;
529        push_encoded_string(&mut res, &coding, &self.slr, 223 - 208 + 1)?;
530        push_encoded_string(&mut res, &coding, &self.cd, 229 - 224 + 1)?;
531        push_encoded_string(&mut res, &coding, &self.rd, 235 - 230 + 1)?;
532        push_encoded_string(&mut res, &coding, &self.rn, 237 - 236 + 1)?;
533
534        push_string(&mut res, &format!("{:05}", self.tnb), 242 - 238 + 1);
535        push_string(&mut res, &format!("{:05}", self.tns), 247 - 243 + 1);
536        push_string(&mut res, &format!("{:03}", self.tng), 250 - 248 + 1);
537        push_string(&mut res, &format!("{:02}", self.mnc), 252 - 251 + 1);
538        push_string(&mut res, &format!("{:02}", self.mnr), 254 - 253 + 1);
539
540        res.push(self.tcs.serialize());
541        push_encoded_string(&mut res, &coding, &self.tcp, 263 - 256 + 1)?;
542        push_encoded_string(&mut res, &coding, &self.tcf, 271 - 264 + 1)?;
543        push_string(&mut res, &format!("{:1}", self.tnd), 1);
544        push_string(&mut res, &format!("{:1}", self.dsn), 1);
545        push_encoded_string(&mut res, &coding, &self.co, 276 - 274 + 1)?;
546        push_encoded_string(&mut res, &coding, &self.pub_, 308 - 277 + 1)?;
547        push_encoded_string(&mut res, &coding, &self.en, 340 - 309 + 1)?;
548        push_encoded_string(&mut res, &coding, &self.ecd, 372 - 341 + 1)?;
549        push_encoded_string(&mut res, &coding, &self._spare, 447 - 373 + 1)?;
550        push_encoded_string(&mut res, &coding, &self.uda, 1023 - 448 + 1)?;
551
552        Ok(res)
553    }
554}
555
556impl Default for GsiBlock {
557    fn default() -> Self {
558        Self::new()
559    }
560}
561
562impl fmt::Display for GsiBlock {
563    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
564        write!(
565            f,
566            "Program Title: {}\nEpisode Title: {}\ncct:{:?} lc:{}\n",
567            self.opt, self.oet, self.cct, self.lc
568        )
569    }
570}
571
572// TTI Block
573
574/// A status to indicate if a subtitle is part of a cumulative set of subtitles
575#[derive(Debug, PartialEq, Eq)]
576pub enum CumulativeStatus {
577    NotPartOfASet,
578    FirstInSet,
579    IntermediateInSet,
580    LastInSet,
581}
582
583impl CumulativeStatus {
584    fn parse(d: u8) -> Result<CumulativeStatus, ParseError> {
585        match d {
586            0 => Ok(CumulativeStatus::NotPartOfASet),
587            1 => Ok(CumulativeStatus::FirstInSet),
588            2 => Ok(CumulativeStatus::IntermediateInSet),
589            3 => Ok(CumulativeStatus::LastInSet),
590            _ => Err(ParseError::CumulativeStatus),
591        }
592    }
593
594    fn serialize(&self) -> u8 {
595        match *self {
596            CumulativeStatus::NotPartOfASet => 0,
597            CumulativeStatus::FirstInSet => 1,
598            CumulativeStatus::IntermediateInSet => 2,
599            CumulativeStatus::LastInSet => 3,
600        }
601    }
602}
603
604/// The horizontal alignment of a displayed subtitle
605pub enum Justification {
606    Unchanged,
607    Left,
608    Centered,
609    Right,
610}
611
612/// A representation of a Time Code
613#[derive(Debug, PartialEq, Eq)]
614pub struct Time {
615    pub hours: u8,
616    pub minutes: u8,
617    pub seconds: u8,
618    pub frames: u8,
619}
620
621impl Time {
622    pub fn format_fps(&self, fps: usize) -> String {
623        format!(
624            "{}:{}:{},{}",
625            self.hours,
626            self.minutes,
627            self.seconds,
628            self.frames as usize * 1000 / fps
629        )
630    }
631    fn serialize(&self) -> Vec<u8> {
632        vec![self.hours, self.minutes, self.seconds, self.frames]
633    }
634}
635
636impl fmt::Display for Time {
637    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
638        write!(
639            f,
640            "{}:{}:{}/{})",
641            self.hours, self.minutes, self.seconds, self.frames
642        )
643    }
644}
645
646/// Text and Timing Information (TTI) block
647#[derive(PartialEq, Eq)]
648pub struct TtiBlock {
649    #[doc = "0 Subtitle Group Number. 00h-FFh"]
650    sgn: u8,
651    #[doc = "1..2 Subtitle Number range. 0000h-FFFFh"]
652    sn: u16,
653    #[doc = "3 Extension Block Number. 00h-FFh"]
654    ebn: u8,
655    #[doc = "4 Cumulative Status. 00-03h"]
656    cs: CumulativeStatus,
657    #[doc = "5..8 Time Code In"]
658    tci: Time,
659    #[doc = "9..12 Time Code Out"]
660    tco: Time,
661    #[doc = "13 Vertical Position"]
662    vp: u8,
663    #[doc = "14 Justification Code"]
664    jc: u8,
665    #[doc = "15 Comment Flag"]
666    cf: u8,
667    #[doc = "16..127 Text Field encoded as the Character Code Table"]
668    tf: Vec<u8>,
669    #[doc = "Duplication of the CharacterCodeTable in GsiBlock, do be able to decode/encode text independent of the GsiBlock"]
670    cct: CharacterCodeTable, //Needed for Display/Debug without access to GsiBlock
671}
672
673impl TtiBlock {
674    pub fn get_subtitle_group_number(&self) -> u8 {
675        self.sgn
676    }
677    pub fn get_subtitle_number_range(&self) -> u16 {
678        self.sn
679    }
680    pub fn get_extension_block_number(&self) -> u8 {
681        self.ebn
682    }
683    pub fn get_cumulative_status(&self) -> &CumulativeStatus {
684        &self.cs
685    }
686    pub fn get_time_code_in(&self) -> &Time {
687        &self.tci
688    }
689    pub fn get_time_code_out(&self) -> &Time {
690        &self.tco
691    }
692    pub fn get_vertical_position(&self) -> u8 {
693        self.vp
694    }
695    pub fn get_justification_code(&self) -> u8 {
696        self.jc
697    }
698    pub fn get_comment_flag(&self) -> u8 {
699        self.cf
700    }
701}
702
703impl TtiBlock {
704    pub fn new(
705        idx: u16,
706        tci: Time,
707        tco: Time,
708        txt: &str,
709        opt: TtiFormat,
710        cct: CharacterCodeTable,
711    ) -> TtiBlock {
712        TtiBlock {
713            sgn: 0,
714            sn: idx,
715            ebn: 0xff,
716            cs: CumulativeStatus::NotPartOfASet,
717            tci,
718            tco,
719            vp: opt.vp,
720            jc: opt.jc,
721            cf: 0,
722            tf: TtiBlock::encode_text(txt, opt.dh, cct),
723            cct, //Needed for Display/Debug without access to GsiBlock
724        }
725    }
726
727    fn encode_text(txt: &str, dh: bool, cct: CharacterCodeTable) -> Vec<u8> {
728        const TF_LENGTH: usize = 112;
729
730        let text = match cct {
731            CharacterCodeTable::Latin => iso6937::encode_to_vec(txt),
732            CharacterCodeTable::LatinCyrillic => iso8859_5::encode_to_vec(txt),
733            CharacterCodeTable::LatinArabic => iso8859_6::encode_to_vec(txt),
734            CharacterCodeTable::LatinGreek => iso8859_7::encode_to_vec(txt),
735            CharacterCodeTable::LatinHebrew => iso8859_8::encode_to_vec(txt),
736        };
737        let mut res = Vec::with_capacity(TF_LENGTH);
738        if dh {
739            res.push(0x0d);
740        }
741        res.push(0x0b);
742        res.push(0x0b);
743        res.extend(text);
744
745        // Make sure size does not exceeds 112 bytes, FIXME: and what if!
746        let max_size = TF_LENGTH - 3; // 3 trailing teletext codes to add.
747        if res.len() > max_size {
748            println!("!!! subtitle length is too long, truncating!");
749        }
750        res.truncate(max_size);
751        res.push(0x0A);
752        res.push(0x0A);
753        res.push(0x8A);
754        let padding = TF_LENGTH - res.len();
755        res.extend(vec![0x8Fu8; padding]);
756        res
757    }
758
759    pub fn get_text(&self) -> String {
760        let mut result = String::from("");
761        let mut first = 0;
762        for i in 0..self.tf.len() {
763            let c = self.tf[i];
764            if match c {
765                0x0..=0x1f => true, //TODO: decode teletext control codes
766                0x20..=0x7f => false,
767                0x80..=0x9f => true, // TODO: decode codes
768                0xa0..=0xff => false,
769            } {
770                if first != i {
771                    let data = &self.tf[first..i];
772                    let decoded_string = match self.cct {
773                        CharacterCodeTable::Latin => iso6937::decode_to_string(data),
774                        CharacterCodeTable::LatinCyrillic => iso8859_5::decode_to_string(data),
775                        CharacterCodeTable::LatinArabic => iso8859_6::decode_to_string(data),
776                        CharacterCodeTable::LatinGreek => iso8859_7::decode_to_string(data),
777                        CharacterCodeTable::LatinHebrew => iso8859_8::decode_to_string(data),
778                    };
779                    result.push_str(&decoded_string);
780                }
781                if c == 0x8f {
782                    break;
783                } else if c == 0x8a {
784                    result.push_str("\r\n");
785                }
786                first = i + 1;
787            }
788        }
789        result
790    }
791
792    #[allow(clippy::vec_init_then_push)]
793    fn serialize(&self) -> Vec<u8> {
794        let mut res = vec![];
795        res.push(self.sgn);
796        res.push((self.sn & 0xff) as u8);
797        res.push((self.sn >> 8) as u8);
798        res.push(self.ebn);
799        res.push(self.cs.serialize());
800        res.extend(self.tci.serialize().iter().cloned());
801        res.extend(self.tco.serialize().iter().cloned());
802        res.push(self.vp);
803        res.push(self.jc);
804        res.push(self.cf);
805        res.extend(self.tf.iter().cloned());
806        res
807    }
808}
809
810impl fmt::Debug for TtiBlock {
811    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
812        write!(
813            f,
814            "\n{}-->{} sgn:{} sn:{} ebn:{} cs:{:?} vp:{} jc:{} cf:{} [{}]",
815            self.tci,
816            self.tco,
817            self.sgn,
818            self.sn,
819            self.ebn,
820            self.cs,
821            self.vp,
822            self.jc,
823            self.cf,
824            self.get_text()
825        )
826    }
827}
828
829impl fmt::Display for TtiBlock {
830    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
831        write!(
832            f,
833            "\n{} {} {} {} {:?} [{}]",
834            self.tci,
835            self.sgn,
836            self.sn,
837            self.ebn,
838            self.cs,
839            self.get_text()
840        )
841    }
842}