Skip to main content

ebustl_parser/
gsi.rs

1//! General Subtitle Information (GSI) block, Tech 3264 section 4.2.
2
3use std::fmt;
4
5use codepage_strings::Coding;
6use jiff::Zoned;
7
8use crate::{Date, InvalidFieldValue, LanguageCode, ParseError, Time, fields};
9
10/// Maximum Total Number of TTI Blocks (TNB)
11pub(crate) const MAX_TNB: u32 = 99_999;
12
13pub(crate) struct CodePageCodec {
14    coding: Coding,
15    codepage: u16,
16}
17
18impl CodePageCodec {
19    pub fn new(codepage: u16) -> Result<Self, ParseError> {
20        Ok(Self {
21            codepage,
22            coding: Coding::new(codepage).map_err(|_e| ParseError::CodePageNumber(codepage))?,
23        })
24    }
25
26    pub(crate) fn decode(&self, data: &[u8]) -> Result<String, ParseError> {
27        Ok(self.coding.decode_lossy(data).to_string())
28    }
29    pub(crate) fn encode(&self, value: &str) -> Result<Vec<u8>, ParseError> {
30        self.coding
31            .encode(value)
32            .map_err(|err| ParseError::CodePageEncoding {
33                codepage: self.codepage,
34                value: value.to_string(),
35                source: err,
36            })
37    }
38}
39
40/// Code Page Number (CPN), the code page of the text in the GSI block.
41#[derive(Debug, Clone, Copy, Eq, PartialEq)]
42#[allow(non_camel_case_types)]
43pub enum CodePageNumber {
44    /// Code page 437, United States
45    CPN_437,
46    /// Code page 850, Multilingual
47    CPN_850,
48    /// Code page 860, Portugal
49    CPN_860,
50    /// Code page 863, Canada-French
51    CPN_863,
52    /// Code page 865, Nordic
53    CPN_865,
54}
55
56impl CodePageNumber {
57    pub(crate) fn serialize(&self) -> Vec<u8> {
58        match *self {
59            CodePageNumber::CPN_437 => vec![0x34, 0x33, 0x37],
60            CodePageNumber::CPN_850 => vec![0x38, 0x35, 0x30],
61            CodePageNumber::CPN_860 => vec![0x38, 0x36, 0x30],
62            CodePageNumber::CPN_863 => vec![0x38, 0x36, 0x33],
63            CodePageNumber::CPN_865 => vec![0x38, 0x36, 0x35],
64        }
65    }
66
67    pub(crate) fn from_u16(codepage: u16) -> Result<CodePageNumber, ParseError> {
68        match codepage {
69            437 => Ok(CodePageNumber::CPN_437),
70            850 => Ok(CodePageNumber::CPN_850),
71            860 => Ok(CodePageNumber::CPN_860),
72            863 => Ok(CodePageNumber::CPN_863),
73            865 => Ok(CodePageNumber::CPN_865),
74            _ => Err(ParseError::CodePageNumber(codepage)),
75        }
76    }
77
78    fn to_u16(self) -> u16 {
79        match self {
80            CodePageNumber::CPN_437 => 437,
81            CodePageNumber::CPN_850 => 850,
82            CodePageNumber::CPN_860 => 860,
83            CodePageNumber::CPN_863 => 863,
84            CodePageNumber::CPN_865 => 865,
85        }
86    }
87}
88
89/// Display Standard Code (DSC).
90#[derive(Debug, PartialEq, Eq)]
91pub enum DisplayStandardCode {
92    /// Blank (20h), undefined
93    Blank,
94    /// Open subtitling (30h)
95    OpenSubtitling,
96    /// Level-1 teletext (31h)
97    Level1Teletext,
98    /// Level-2 teletext (32h)
99    Level2Teletext,
100}
101
102impl DisplayStandardCode {
103    pub(crate) fn parse(data: u8) -> Result<DisplayStandardCode, ParseError> {
104        match data {
105            0x20 => Ok(DisplayStandardCode::Blank),
106            0x30 => Ok(DisplayStandardCode::OpenSubtitling),
107            0x31 => Ok(DisplayStandardCode::Level1Teletext),
108            0x32 => Ok(DisplayStandardCode::Level2Teletext),
109            _ => Err(ParseError::DisplayStandardCode),
110        }
111    }
112
113    pub(crate) fn serialize(&self) -> u8 {
114        match *self {
115            DisplayStandardCode::Blank => 0x20,
116            DisplayStandardCode::OpenSubtitling => 0x30,
117            DisplayStandardCode::Level1Teletext => 0x31,
118            DisplayStandardCode::Level2Teletext => 0x32,
119        }
120    }
121}
122
123/// Time Code: Status (TCS), whether the time codes in the GSI and TTI blocks are intended for use.
124#[derive(Debug, PartialEq, Eq)]
125pub enum TimeCodeStatus {
126    /// 30h, the time codes are not intended for use
127    NotIntendedForUse,
128    /// 31h, the time codes are intended for use
129    IntendedForUse,
130}
131
132impl TimeCodeStatus {
133    pub(crate) fn parse(data: u8) -> Result<TimeCodeStatus, ParseError> {
134        match data {
135            0x30 => Ok(TimeCodeStatus::NotIntendedForUse),
136            0x31 => Ok(TimeCodeStatus::IntendedForUse),
137            _ => Err(ParseError::TimeCodeStatus),
138        }
139    }
140
141    pub(crate) fn serialize(&self) -> u8 {
142        match *self {
143            TimeCodeStatus::NotIntendedForUse => 0x30,
144            TimeCodeStatus::IntendedForUse => 0x31,
145        }
146    }
147}
148
149/// Character Code Table (CCT), the character set of the Text Fields in the TTI blocks.
150#[derive(Debug, Copy, Clone, PartialEq, Eq)]
151pub enum CharacterCodeTable {
152    /// 00, Latin alphabet (ISO 6937)
153    Latin,
154    /// 01, Latin/Cyrillic alphabet (ISO 8859-5)
155    LatinCyrillic,
156    /// 02, Latin/Arabic alphabet (ISO 8859-6)
157    LatinArabic,
158    /// 03, Latin/Greek alphabet (ISO 8859-7)
159    LatinGreek,
160    /// 04, Latin/Hebrew alphabet (ISO 8859-8)
161    LatinHebrew,
162}
163
164impl CharacterCodeTable {
165    pub(crate) fn parse(data: &[u8]) -> Result<CharacterCodeTable, ParseError> {
166        if data.len() != 2 {
167            return Err(ParseError::CharacterCodeTable);
168        }
169        if data[0] != 0x30 {
170            return Err(ParseError::CharacterCodeTable);
171        }
172        match data[1] {
173            0x30 => Ok(CharacterCodeTable::Latin),
174            0x31 => Ok(CharacterCodeTable::LatinCyrillic),
175            0x32 => Ok(CharacterCodeTable::LatinArabic),
176            0x33 => Ok(CharacterCodeTable::LatinGreek),
177            0x34 => Ok(CharacterCodeTable::LatinHebrew),
178            _ => Err(ParseError::CharacterCodeTable),
179        }
180    }
181
182    pub(crate) fn serialize(&self) -> Vec<u8> {
183        match *self {
184            CharacterCodeTable::Latin => vec![0x30, 0x30],
185            CharacterCodeTable::LatinCyrillic => vec![0x30, 0x31],
186            CharacterCodeTable::LatinArabic => vec![0x30, 0x32],
187            CharacterCodeTable::LatinGreek => vec![0x30, 0x33],
188            CharacterCodeTable::LatinHebrew => vec![0x30, 0x34],
189        }
190    }
191}
192
193/// Disk Format Code (DFC), the frame rate of the time codes.
194#[derive(Debug, PartialEq, Eq)]
195#[allow(non_camel_case_types)]
196pub enum DiskFormatCode {
197    /// `STL25.01`, 25 frames per second
198    STL25_01,
199    /// `STL30.01`, 30 frames per second
200    STL30_01,
201}
202
203impl DiskFormatCode {
204    pub(crate) fn parse(data: &str) -> Result<DiskFormatCode, ParseError> {
205        if data == "STL25.01" {
206            Ok(DiskFormatCode::STL25_01)
207        } else if data == "STL30.01" {
208            Ok(DiskFormatCode::STL30_01)
209        } else {
210            Err(ParseError::DiskFormatCode(data.to_string()))
211        }
212    }
213
214    pub(crate) fn serialize(&self) -> Vec<u8> {
215        match *self {
216            DiskFormatCode::STL25_01 => String::from("STL25.01").into_bytes(),
217            DiskFormatCode::STL30_01 => String::from("STL30.01").into_bytes(),
218        }
219    }
220
221    /// The number of frames per second, 25 or 30.
222    ///
223    /// `STL30.01` is commonly used for 29.97 frames per second (30 x 1000/1001), which the spec
224    /// does not distinguish from 30.
225    pub fn fps(&self) -> usize {
226        match self {
227            DiskFormatCode::STL25_01 => 25,
228            DiskFormatCode::STL30_01 => 30,
229        }
230    }
231}
232
233/// General Subtitle Information (GSI) block, the first 1024 bytes of an STL file.
234#[derive(Debug, PartialEq, Eq)]
235pub struct GsiBlock {
236    #[doc = "0..2 Code Page Number"]
237    pub(crate) cpn: CodePageNumber,
238    #[doc = "3..10 Disk Format Code"]
239    pub(crate) dfc: DiskFormatCode,
240    #[doc = "11 Display Standard Code"]
241    pub(crate) dsc: DisplayStandardCode,
242    #[doc = "12..13 Character Code Table Number"]
243    pub(crate) cct: CharacterCodeTable,
244    #[doc = "14..15 Language Code"]
245    pub(crate) lc: String,
246    #[doc = "16..47 Original Programme Title"]
247    pub(crate) opt: String,
248    #[doc = "48..79 Original Episode Title"]
249    pub(crate) oet: String,
250    #[doc = "80..111 Translated Programme Title"]
251    pub(crate) tpt: String,
252    #[doc = "112..143 Translated Episode Title"]
253    pub(crate) tet: String,
254    #[doc = "144..175 Translator's Name"]
255    pub(crate) tn: String,
256    #[doc = "176..207 Translator's Contact Details"]
257    pub(crate) tcd: String,
258    #[doc = "208..223 Subtitle List Reference Code"]
259    pub(crate) slr: String,
260    #[doc = "224..229 Creation Date"]
261    pub(crate) cd: String,
262    #[doc = "230..235 Revision Date"]
263    pub(crate) rd: String,
264    #[doc = "236..237 Revision Number"]
265    pub(crate) rn: String,
266    #[doc = "238..242 Total Number of Text and Timing Blocks. 0-99 999"]
267    pub(crate) tnb: u32,
268    #[doc = "243..247 Total Number of Subtitles. 0-99 999"]
269    pub(crate) tns: u32,
270    #[doc = "248..250 Total Number of Subtitle Groups"]
271    pub(crate) tng: u16,
272    #[doc = "251..252 Maximum Number of Displayable Characters in a Text Row"]
273    pub(crate) mnc: u16,
274    #[doc = "253..254 Maximum Number of Displayable Rows"]
275    pub(crate) mnr: u16,
276    #[doc = "255 Time Code Status"]
277    pub(crate) tcs: TimeCodeStatus,
278    #[doc = "256..263 Time Code: Start of Programme (format: HHMMSSFF)"]
279    pub(crate) tcp: String,
280    #[doc = "264..271 Time Code: First-in-Cue (format: HHMMSSFF)"]
281    pub(crate) tcf: String,
282    #[doc = "272 Total Number of Disks"]
283    pub(crate) tnd: u8,
284    #[doc = "273 Disk Sequence Number"]
285    pub(crate) dsn: u8,
286    #[doc = "274..276 Country of Origin"]
287    pub(crate) co: String, // TODO Type with country definitions
288    #[doc = "277..308 Publisher"]
289    pub(crate) pub_: String,
290    #[doc = "309..340 Editor's Name"]
291    pub(crate) en: String,
292    #[doc = "341..372 Editor's Contact Details"]
293    pub(crate) ecd: String,
294    #[doc = "373..447 Spare Bytes"]
295    pub(crate) _spare: String,
296    #[doc = "448..1023 User-Defined Area"]
297    pub(crate) uda: String,
298}
299
300// Accessors in the order of the fields in the block. Accessors that validate a field return
301// the value as stored in the file in the error, see `InvalidFieldValue`.
302impl GsiBlock {
303    /// Code Page Number (CPN), the code page of the text in the GSI block.
304    pub fn code_page_number(&self) -> &CodePageNumber {
305        &self.cpn
306    }
307
308    /// Disk Format Code (DFC), the frame rate of the time codes.
309    pub fn disk_format_code(&self) -> &DiskFormatCode {
310        &self.dfc
311    }
312
313    /// Display Standard Code (DSC).
314    pub fn display_standard_code(&self) -> &DisplayStandardCode {
315        &self.dsc
316    }
317
318    /// Character Code Table (CCT) of the Text Fields in the TTI blocks.
319    pub fn character_code_table(&self) -> &CharacterCodeTable {
320        &self.cct
321    }
322
323    /// Language Code (LC), validated as two hexadecimal digits 00h-7Fh, the range of the codes in
324    /// Appendix 3. A validated code may still be unassigned, see [`LanguageCode::name`]. `None` if
325    /// blank.
326    ///
327    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
328    ///
329    /// # Example
330    ///
331    /// ```rust
332    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
333    /// // The test file uses 2Fh, which is reserved for national assignment
334    /// let language = stl.gsi.language().unwrap().unwrap();
335    /// assert_eq!(0x2F, language.0);
336    /// assert_eq!(None, language.name());
337    /// ```
338    pub fn language(&self) -> Result<Option<LanguageCode>, InvalidFieldValue<String>> {
339        fields::parse_field("LC", &self.lc, fields::parse_language_code)
340    }
341
342    /// Original Programme Title (OPT), without the trailing space padding. `None` if blank.
343    ///
344    /// # Example
345    ///
346    /// ```rust
347    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
348    /// assert_eq!(Some("TEST"), stl.gsi.original_programme_title());
349    /// ```
350    pub fn original_programme_title(&self) -> Option<&str> {
351        fields::text(&self.opt)
352    }
353
354    /// Original Episode Title (OET), without the trailing space padding. `None` if blank.
355    ///
356    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
357    ///
358    /// # Example
359    ///
360    /// ```rust
361    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
362    /// assert_eq!(Some("TEST EPISODE"), stl.gsi.original_episode_title());
363    /// ```
364    pub fn original_episode_title(&self) -> Option<&str> {
365        fields::text(&self.oet)
366    }
367
368    /// Translated Programme Title (TPT), without the trailing space padding. `None` if blank.
369    ///
370    /// # Example
371    ///
372    /// ```rust
373    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
374    /// assert_eq!(Some("TEST"), stl.gsi.translated_programme_title());
375    /// ```
376    pub fn translated_programme_title(&self) -> Option<&str> {
377        fields::text(&self.tpt)
378    }
379
380    /// Translated Episode Title (TET), without the trailing space padding. `None` if blank.
381    ///
382    /// # Example
383    ///
384    /// ```rust
385    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
386    /// assert_eq!(Some("EPISODE DE TEST"), stl.gsi.translated_episode_title());
387    /// ```
388    pub fn translated_episode_title(&self) -> Option<&str> {
389        fields::text(&self.tet)
390    }
391
392    /// Translator's Name (TN), without the trailing space padding. `None` if blank.
393    ///
394    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
395    ///
396    /// # Example
397    ///
398    /// ```rust
399    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
400    /// assert_eq!(Some("C. Lebeaupin"), stl.gsi.translators_name());
401    /// ```
402    pub fn translators_name(&self) -> Option<&str> {
403        fields::text(&self.tn)
404    }
405
406    /// Translator's Contact Details (TCD), without the trailing space padding. `None` if blank.
407    ///
408    /// # Example
409    ///
410    /// ```rust
411    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
412    /// assert_eq!(None, stl.gsi.translators_contact_details());
413    /// ```
414    pub fn translators_contact_details(&self) -> Option<&str> {
415        fields::text(&self.tcd)
416    }
417
418    /// Subtitle List Reference Code (SLR), without the trailing space padding. `None` if blank.
419    ///
420    /// # Example
421    ///
422    /// ```rust
423    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
424    /// assert_eq!(None, stl.gsi.subtitle_list_reference_code());
425    /// ```
426    pub fn subtitle_list_reference_code(&self) -> Option<&str> {
427        fields::text(&self.slr)
428    }
429
430    /// Creation Date (CD), validated as an existing date in the format YYMMDD. `None` if blank.
431    ///
432    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
433    ///
434    /// # Example
435    ///
436    /// ```rust
437    /// use ebustl_parser::Date;
438    ///
439    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
440    /// let date = Date { year: 2013, month: 10, day: 10 };
441    /// assert_eq!(Ok(Some(date)), stl.gsi.creation_date());
442    /// ```
443    pub fn creation_date(&self) -> Result<Option<Date>, InvalidFieldValue<String>> {
444        fields::parse_field("CD", &self.cd, fields::parse_date)
445    }
446
447    /// Revision Date (RD), validated as an existing date in the format YYMMDD. `None` if blank.
448    ///
449    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
450    ///
451    /// # Example
452    ///
453    /// ```rust
454    /// use ebustl_parser::Date;
455    ///
456    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
457    /// let date = Date { year: 2013, month: 10, day: 17 };
458    /// assert_eq!(Ok(Some(date)), stl.gsi.revision_date());
459    /// ```
460    pub fn revision_date(&self) -> Result<Option<Date>, InvalidFieldValue<String>> {
461        fields::parse_field("RD", &self.rd, fields::parse_date)
462    }
463
464    /// Revision Number (RN), validated as 0-99, ignoring space padding. `None` if blank.
465    ///
466    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
467    ///
468    /// # Example
469    ///
470    /// ```rust
471    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
472    /// // Blank in the test file
473    /// assert_eq!(Ok(None), stl.gsi.revision_number());
474    /// ```
475    pub fn revision_number(&self) -> Result<Option<u8>, InvalidFieldValue<String>> {
476        fields::parse_field("RN", &self.rn, fields::parse_revision_number)
477    }
478
479    /// Total Number of TTI Blocks (TNB), including extension blocks.
480    pub fn total_number_of_text_and_timing_blocks(&self) -> u32 {
481        self.tnb
482    }
483
484    /// Total Number of Subtitles (TNS).
485    pub fn total_number_of_subtitles(&self) -> u32 {
486        self.tns
487    }
488
489    /// Total Number of Subtitle Groups (TNG).
490    pub fn total_number_of_subtitle_groups(&self) -> u16 {
491        self.tng
492    }
493
494    /// Maximum Number of Displayable Characters in any text row (MNC).
495    pub fn max_number_of_chars_in_row(&self) -> u16 {
496        self.mnc
497    }
498
499    /// Maximum Number of Displayable Rows (MNR).
500    pub fn max_number_of_rows(&self) -> u16 {
501        self.mnr
502    }
503
504    /// Time Code: Status (TCS), whether the time codes in the GSI and TTI blocks are intended for
505    /// use.
506    pub fn time_code_status(&self) -> &TimeCodeStatus {
507        &self.tcs
508    }
509
510    /// Time Code: Start-of-Programme (TCP), validated as HHMMSSFF with minutes and seconds 0-59 and
511    /// frames according to the Disk Format Code. Hours may exceed 23, e.g. for a programme that
512    /// continues past midnight. `None` if blank.
513    ///
514    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
515    ///
516    /// # Example
517    ///
518    /// ```rust
519    /// use ebustl_parser::Time;
520    ///
521    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
522    /// let time = Time { hours: 10, minutes: 0, seconds: 0, frames: 0 };
523    /// assert_eq!(Ok(Some(time)), stl.gsi.time_code_start_of_programme());
524    /// ```
525    pub fn time_code_start_of_programme(&self) -> Result<Option<Time>, InvalidFieldValue<String>> {
526        let fps = self.dfc.fps();
527        fields::parse_field("TCP", &self.tcp, |value| {
528            fields::parse_time_code(value, fps)
529        })
530    }
531
532    /// Time Code: First In-Cue (TCF), validated as HHMMSSFF with minutes and seconds 0-59 and
533    /// frames according to the Disk Format Code. Hours may exceed 23, e.g. for a programme that
534    /// continues past midnight. `None` if blank.
535    ///
536    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
537    ///
538    /// # Example
539    ///
540    /// ```rust
541    /// use ebustl_parser::Time;
542    ///
543    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
544    /// let time = Time { hours: 10, minutes: 0, seconds: 0, frames: 0 };
545    /// assert_eq!(Ok(Some(time)), stl.gsi.time_code_first_in_cue());
546    /// ```
547    pub fn time_code_first_in_cue(&self) -> Result<Option<Time>, InvalidFieldValue<String>> {
548        let fps = self.dfc.fps();
549        fields::parse_field("TCF", &self.tcf, |value| {
550            fields::parse_time_code(value, fps)
551        })
552    }
553
554    /// Total Number of Disks (TND) in the set holding the subtitle list.
555    pub fn total_number_of_disks(&self) -> u8 {
556        self.tnd
557    }
558
559    /// Disk Sequence Number (DSN) of this disk in the set, starting at 1.
560    pub fn disk_sequence_number(&self) -> u8 {
561        self.dsn
562    }
563
564    /// Country of Origin (CO), validated as three upper case letters (ISO 3166 alpha-3). `None` if
565    /// blank.
566    ///
567    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
568    ///
569    /// # Example
570    ///
571    /// ```rust
572    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
573    /// assert_eq!(Ok(Some("FRA")), stl.gsi.country_of_origin());
574    /// ```
575    pub fn country_of_origin(&self) -> Result<Option<&str>, InvalidFieldValue<String>> {
576        fields::parse_field("CO", &self.co, fields::parse_country_code)
577    }
578
579    /// Publisher (PUB), without the trailing space padding. `None` if blank.
580    ///
581    /// # Example
582    ///
583    /// ```rust
584    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
585    /// assert_eq!(Some("TESTSUB"), stl.gsi.publisher());
586    /// ```
587    pub fn publisher(&self) -> Option<&str> {
588        fields::text(&self.pub_)
589    }
590
591    /// Editor's Name (EN), without the trailing space padding. `None` if blank.
592    ///
593    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
594    ///
595    /// # Example
596    ///
597    /// ```rust
598    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
599    /// assert_eq!(Some("TESTSUB 1.0.1"), stl.gsi.editors_name());
600    /// ```
601    pub fn editors_name(&self) -> Option<&str> {
602        fields::text(&self.en)
603    }
604
605    /// Editor's Contact Details (ECD), without the trailing space padding. `None` if blank.
606    ///
607    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
608    ///
609    /// # Example
610    ///
611    /// ```rust
612    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
613    /// assert_eq!(None, stl.gsi.editors_contact_details());
614    /// ```
615    pub fn editors_contact_details(&self) -> Option<&str> {
616        fields::text(&self.ecd)
617    }
618
619    /// User-Defined Area (UDA), decoded with the code page of the GSI block.
620    pub fn user_defined_area(&self) -> &str {
621        &self.uda
622    }
623}
624
625fn push_string(v: &mut Vec<u8>, s: &str, len: usize) {
626    let addendum = s.to_owned().into_bytes();
627    let padding = len - addendum.len();
628    v.extend(addendum.iter().cloned());
629    v.extend(vec![0x20u8; padding]);
630}
631
632fn push_encoded_string(
633    v: &mut Vec<u8>,
634    c: &CodePageCodec,
635    s: &str,
636    len: usize,
637) -> Result<(), ParseError> {
638    let addendum = c.encode(s)?;
639    let padding = len - addendum.len();
640    v.extend(addendum.iter().cloned());
641    v.extend(vec![0x20u8; padding]);
642    Ok(())
643}
644
645impl GsiBlock {
646    /// Creates a GSI block for a teletext subtitle list.
647    ///
648    /// The defaults are code page 850, 25 frames per second, Level-1 teletext, the Latin character
649    /// code table, today as creation and revision date and time codes intended for use.
650    pub fn new() -> GsiBlock {
651        let date = Zoned::now();
652        let now = date.strftime("%y%m%d").to_string();
653        GsiBlock {
654            cpn: CodePageNumber::CPN_850,
655            dfc: DiskFormatCode::STL25_01,
656            dsc: DisplayStandardCode::Level1Teletext,
657            cct: CharacterCodeTable::Latin,
658            lc: "0F".to_string(), // FIXME: ok for default?
659            opt: "".to_string(),
660            oet: "".to_string(),
661            tpt: "".to_string(),
662            tet: "".to_string(),
663            tn: "".to_string(),
664            tcd: "".to_string(),
665            slr: "".to_string(),
666            cd: now.clone(),
667            rd: now,
668            rn: "00".to_string(),
669            tnb: 0,
670            tns: 0,
671            tng: 1,  // At least one group?
672            mnc: 40, // FIXME: ok for default?
673            mnr: 23, // FIXME: ok for default?
674            tcs: TimeCodeStatus::IntendedForUse,
675            tcp: "00000000".to_string(),
676            tcf: "00000000".to_string(),
677            tnd: 1,
678            dsn: 1,
679            co: "".to_string(),
680            pub_: "".to_string(),
681            en: "".to_string(),
682            ecd: "".to_string(),
683            _spare: "".to_string(),
684            uda: "".to_string(),
685        }
686    }
687
688    pub(crate) fn serialize(&self) -> Result<Vec<u8>, ParseError> {
689        let codepage = self.code_page_number().to_u16();
690        let coding = CodePageCodec::new(codepage).expect("codepage");
691        let mut res = Vec::with_capacity(1024);
692        res.extend(self.cpn.serialize());
693        res.extend(self.dfc.serialize().iter().cloned());
694        res.push(self.dsc.serialize());
695        res.extend(self.cct.serialize());
696        // be careful for the length of following: must force padding
697        push_encoded_string(&mut res, &coding, &self.lc, 15 - 14 + 1)?;
698        push_encoded_string(&mut res, &coding, &self.opt, 47 - 16 + 1)?;
699        push_encoded_string(&mut res, &coding, &self.oet, 79 - 48 + 1)?;
700        push_encoded_string(&mut res, &coding, &self.tpt, 111 - 80 + 1)?;
701        push_encoded_string(&mut res, &coding, &self.tet, 143 - 112 + 1)?;
702        push_encoded_string(&mut res, &coding, &self.tn, 175 - 144 + 1)?;
703        push_encoded_string(&mut res, &coding, &self.tcd, 207 - 176 + 1)?;
704        push_encoded_string(&mut res, &coding, &self.slr, 223 - 208 + 1)?;
705        push_encoded_string(&mut res, &coding, &self.cd, 229 - 224 + 1)?;
706        push_encoded_string(&mut res, &coding, &self.rd, 235 - 230 + 1)?;
707        push_encoded_string(&mut res, &coding, &self.rn, 237 - 236 + 1)?;
708
709        push_string(&mut res, &format!("{:05}", self.tnb), 242 - 238 + 1);
710        push_string(&mut res, &format!("{:05}", self.tns), 247 - 243 + 1);
711        push_string(&mut res, &format!("{:03}", self.tng), 250 - 248 + 1);
712        push_string(&mut res, &format!("{:02}", self.mnc), 252 - 251 + 1);
713        push_string(&mut res, &format!("{:02}", self.mnr), 254 - 253 + 1);
714
715        res.push(self.tcs.serialize());
716        push_encoded_string(&mut res, &coding, &self.tcp, 263 - 256 + 1)?;
717        push_encoded_string(&mut res, &coding, &self.tcf, 271 - 264 + 1)?;
718        push_string(&mut res, &format!("{:1}", self.tnd), 1);
719        push_string(&mut res, &format!("{:1}", self.dsn), 1);
720        push_encoded_string(&mut res, &coding, &self.co, 276 - 274 + 1)?;
721        push_encoded_string(&mut res, &coding, &self.pub_, 308 - 277 + 1)?;
722        push_encoded_string(&mut res, &coding, &self.en, 340 - 309 + 1)?;
723        push_encoded_string(&mut res, &coding, &self.ecd, 372 - 341 + 1)?;
724        push_encoded_string(&mut res, &coding, &self._spare, 447 - 373 + 1)?;
725        push_encoded_string(&mut res, &coding, &self.uda, 1023 - 448 + 1)?;
726
727        Ok(res)
728    }
729}
730
731impl Default for GsiBlock {
732    fn default() -> Self {
733        Self::new()
734    }
735}
736
737impl fmt::Display for GsiBlock {
738    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
739        write!(
740            f,
741            "Program Title: {}\nEpisode Title: {}\ncct:{:?} lc:{}\n",
742            self.opt, self.oet, self.cct, self.lc
743        )
744    }
745}