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