Skip to main content

ebustl_parser/
tti.rs

1//! Text and Timing Information (TTI) block, Tech 3264 section 4.3.
2
3use std::fmt;
4
5use crate::text_field::{self, Presentation};
6use crate::{
7    CharacterCodeTable, DiskFormatCode, InvalidFieldValue, Justification, ParseError, TextElement,
8    Time, fields,
9};
10
11/// Presentation of a subtitle created with [`Stl::add_sub`](crate::Stl::add_sub) or
12/// [`TtiBlock::new`].
13pub struct TtiFormat {
14    #[doc = "Justification Code (JC), 00h-03h"]
15    pub jc: u8,
16    #[doc = "Vertical Position (VP), the row of the first subtitle row"]
17    pub vp: u8,
18    #[doc = "Double height, only used for teletext subtitles"]
19    pub dh: bool,
20}
21
22/// Cumulative Status (CS), whether a subtitle is part of a cumulative set of subtitles.
23#[derive(Debug, PartialEq, Eq)]
24pub enum CumulativeStatus {
25    /// 00h, the subtitle is not part of a cumulative set
26    NotPartOfASet,
27    /// 01h, the first subtitle of a cumulative set
28    FirstInSet,
29    /// 02h, an intermediate subtitle of a cumulative set
30    IntermediateInSet,
31    /// 03h, the last subtitle of a cumulative set
32    LastInSet,
33}
34
35impl CumulativeStatus {
36    pub(crate) fn parse(d: u8) -> Result<CumulativeStatus, ParseError> {
37        match d {
38            0 => Ok(CumulativeStatus::NotPartOfASet),
39            1 => Ok(CumulativeStatus::FirstInSet),
40            2 => Ok(CumulativeStatus::IntermediateInSet),
41            3 => Ok(CumulativeStatus::LastInSet),
42            _ => Err(ParseError::CumulativeStatus),
43        }
44    }
45
46    pub(crate) fn serialize(&self) -> u8 {
47        match *self {
48            CumulativeStatus::NotPartOfASet => 0,
49            CumulativeStatus::FirstInSet => 1,
50            CumulativeStatus::IntermediateInSet => 2,
51            CumulativeStatus::LastInSet => 3,
52        }
53    }
54}
55
56/// Text and Timing Information (TTI) block, 128 bytes with the text and timing of a subtitle.
57#[derive(PartialEq, Eq)]
58pub struct TtiBlock {
59    #[doc = "0 Subtitle Group Number. 00h-FFh"]
60    pub(crate) sgn: u8,
61    #[doc = "1..2 Subtitle Number. 0000h-FFFFh"]
62    pub(crate) sn: u16,
63    #[doc = "3 Extension Block Number. 00h-FFh"]
64    pub(crate) ebn: u8,
65    #[doc = "4 Cumulative Status. 00-03h"]
66    pub(crate) cs: CumulativeStatus,
67    #[doc = "5..8 Time Code In"]
68    pub(crate) tci: Time,
69    #[doc = "9..12 Time Code Out"]
70    pub(crate) tco: Time,
71    #[doc = "13 Vertical Position"]
72    pub(crate) vp: u8,
73    #[doc = "14 Justification Code"]
74    pub(crate) jc: u8,
75    #[doc = "15 Comment Flag"]
76    pub(crate) cf: u8,
77    #[doc = "16..127 Text Field, encoded with the Character Code Table"]
78    pub(crate) tf: Vec<u8>,
79    #[doc = "Copy of the Character Code Table of the GSI block, to decode the Text Field"]
80    pub(crate) cct: CharacterCodeTable,
81}
82
83// Accessors in the order of the fields in the block. Accessors that validate a field return
84// the value as stored in the file in the error, see `InvalidFieldValue`.
85impl TtiBlock {
86    /// Subtitle Group Number (SGN).
87    pub fn subtitle_group_number(&self) -> u8 {
88        self.sgn
89    }
90
91    /// Subtitle Number (SN).
92    pub fn subtitle_number(&self) -> u16 {
93        self.sn
94    }
95
96    /// Extension Block Number (EBN), see also [`TtiBlock::is_last_block`] and
97    /// [`TtiBlock::is_user_data`].
98    pub fn extension_block_number(&self) -> u8 {
99        self.ebn
100    }
101
102    /// Cumulative Status (CS).
103    pub fn cumulative_status(&self) -> &CumulativeStatus {
104        &self.cs
105    }
106
107    /// Time Code In (TCI), validated against the ranges in the spec: hours 0-23, minutes
108    /// and seconds 0-59 and frames according to the Disk Format Code.
109    ///
110    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
111    ///
112    /// # Example
113    ///
114    /// ```rust
115    /// use ebustl_parser::Time;
116    ///
117    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
118    /// let dfc = stl.gsi.disk_format_code();
119    /// let time = Time { hours: 10, minutes: 0, seconds: 0, frames: 0 };
120    /// assert_eq!(Ok(time), stl.ttis[0].time_code_in(dfc));
121    /// ```
122    pub fn time_code_in(&self, dfc: &DiskFormatCode) -> Result<Time, InvalidFieldValue<Time>> {
123        fields::checked_time_code("TCI", &self.tci, dfc)
124    }
125
126    /// Time Code Out (TCO), validated against the ranges in the spec: hours 0-23, minutes
127    /// and seconds 0-59 and frames according to the Disk Format Code.
128    ///
129    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
130    ///
131    /// # Example
132    ///
133    /// ```rust
134    /// use ebustl_parser::Time;
135    ///
136    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
137    /// let dfc = stl.gsi.disk_format_code();
138    /// let time = Time { hours: 10, minutes: 0, seconds: 0, frames: 5 };
139    /// assert_eq!(Ok(time), stl.ttis[0].time_code_out(dfc));
140    /// ```
141    pub fn time_code_out(&self, dfc: &DiskFormatCode) -> Result<Time, InvalidFieldValue<Time>> {
142        fields::checked_time_code("TCO", &self.tco, dfc)
143    }
144
145    /// Vertical Position (VP), the row of the first subtitle row: 1-23 for teletext and 0 up to
146    /// MNR for open subtitles.
147    pub fn vertical_position(&self) -> u8 {
148        self.vp
149    }
150
151    /// Justification Code (JC), validated as 00h-03h.
152    ///
153    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
154    ///
155    /// # Example
156    ///
157    /// ```rust
158    /// use ebustl_parser::Justification;
159    ///
160    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
161    /// assert_eq!(Ok(Justification::Centered), stl.ttis[0].justification());
162    /// ```
163    pub fn justification(&self) -> Result<Justification, InvalidFieldValue<u8>> {
164        match self.jc {
165            0x00 => Ok(Justification::Unchanged),
166            0x01 => Ok(Justification::Left),
167            0x02 => Ok(Justification::Centered),
168            0x03 => Ok(Justification::Right),
169            jc => Err(InvalidFieldValue::new(
170                "JC",
171                format!("{jc:02X}h is not 00h-03h"),
172                jc,
173            )),
174        }
175    }
176
177    /// Comment Flag (CF), validated as 00h or 01h. `true` if the Text Field contains
178    /// comments not intended for transmission.
179    ///
180    /// On error, [`InvalidFieldValue::value`] gives the value as stored in the file.
181    ///
182    /// # Example
183    ///
184    /// ```rust
185    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
186    /// assert_eq!(Ok(false), stl.ttis[0].comment_flag());
187    /// ```
188    pub fn comment_flag(&self) -> Result<bool, InvalidFieldValue<u8>> {
189        match self.cf {
190            0x00 => Ok(false),
191            0x01 => Ok(true),
192            cf => Err(InvalidFieldValue::new(
193                "CF",
194                format!("{cf:02X}h is not 00h or 01h"),
195                cf,
196            )),
197        }
198    }
199}
200
201impl TtiBlock {
202    /// Creates a teletext subtitle in a single TTI block (EBN FFh), with `idx` as Subtitle
203    /// Number.
204    ///
205    /// Rows are separated by `\n` or `\r\n`. Text that does not fit in one block is
206    /// truncated, use [`Stl::add_sub`](crate::Stl::add_sub) to continue it in extension blocks.
207    ///
208    /// # Example
209    ///
210    /// ```rust
211    /// use ebustl_parser::{CharacterCodeTable, Time, TtiBlock, TtiFormat};
212    ///
213    /// let time = |seconds| Time { hours: 10, minutes: 0, seconds, frames: 0 };
214    /// let format = TtiFormat { jc: 2, vp: 20, dh: false };
215    ///
216    /// let tti = TtiBlock::new(1, time(1), time(3), "One\nTwo", format, CharacterCodeTable::Latin);
217    /// assert_eq!("One\r\nTwo", tti.text());
218    /// assert!(tti.is_last_block());
219    /// ```
220    pub fn new(
221        idx: u16,
222        tci: Time,
223        tco: Time,
224        txt: &str,
225        opt: TtiFormat,
226        cct: CharacterCodeTable,
227    ) -> TtiBlock {
228        let presentation = Presentation::Teletext {
229            double_height: opt.dh,
230        };
231        TtiBlock {
232            sgn: 0,
233            sn: idx,
234            ebn: EBN_LAST_BLOCK,
235            cs: CumulativeStatus::NotPartOfASet,
236            tci,
237            tco,
238            vp: opt.vp,
239            jc: opt.jc,
240            cf: 0,
241            tf: text_field::encode_truncated_text_field(txt, presentation, cct),
242            cct, //Needed for Display/Debug without access to GsiBlock
243        }
244    }
245
246    /// Decodes the Text Field of this single block into a string.
247    ///
248    /// Control codes are dropped, except CR/LF which becomes `\r\n`. Use
249    /// [`TtiBlock::text_elements`] to get the control codes too.
250    ///
251    /// A subtitle may span several blocks (extension blocks), use
252    /// [`Stl::subtitles`](crate::Stl::subtitles) and
253    /// [`Subtitle::text`](crate::Subtitle::text) to get the complete text of a subtitle.
254    pub fn text(&self) -> String {
255        text_field::decode(&self.tf, self.cct)
256    }
257
258    /// Decodes the Text Field of this single block into characters and control codes, e.g.
259    /// teletext colors, double height and boxing, or italics for open subtitles.
260    ///
261    /// A subtitle may span several blocks (extension blocks), use
262    /// [`Subtitle::text_elements`](crate::Subtitle::text_elements) to get the elements of the
263    /// complete subtitle.
264    ///
265    /// # Example
266    ///
267    /// ```rust
268    /// use ebustl_parser::{TeletextControl, TextElement};
269    ///
270    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
271    /// let elements = stl.ttis[2].text_elements();
272    /// assert_eq!(TextElement::Teletext(TeletextControl::DoubleHeight), elements[0]);
273    /// ```
274    pub fn text_elements(&self) -> Vec<TextElement> {
275        text_field::decode_elements(&self.tf, self.cct)
276    }
277
278    /// The Text Field up to, but not including, the first unused space code (8Fh).
279    pub(crate) fn text_field_content(&self) -> &[u8] {
280        text_field::content(&self.tf)
281    }
282
283    /// Returns `true` for a block carrying user data (EBN FEh). Its Text Field is not
284    /// subtitle text and must not affect the rest of the subtitle.
285    ///
286    /// # Example
287    ///
288    /// ```rust
289    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
290    /// assert!(!stl.ttis[0].is_user_data());
291    /// ```
292    pub fn is_user_data(&self) -> bool {
293        self.ebn == EBN_USER_DATA
294    }
295
296    /// Returns `true` if the Text Field of this block is part of the subtitle text, i.e. the
297    /// block is an extension block (EBN 00h-EFh) or the last block of a subtitle (EBN FFh).
298    /// User data (FEh) and blocks reserved for future use (F0h-FDh) are not.
299    ///
300    /// # Example
301    ///
302    /// ```rust
303    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
304    /// assert!(stl.ttis.iter().all(|tti| tti.is_text_block()));
305    /// ```
306    pub fn is_text_block(&self) -> bool {
307        self.ebn <= EBN_LAST_EXTENSION || self.ebn == EBN_LAST_BLOCK
308    }
309
310    /// Returns `true` for the last block of a subtitle (EBN FFh).
311    ///
312    /// # Example
313    ///
314    /// ```rust
315    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
316    /// assert!(stl.ttis[0].is_last_block());
317    /// ```
318    pub fn is_last_block(&self) -> bool {
319        self.ebn == EBN_LAST_BLOCK
320    }
321
322    /// Returns `true` if the Comment Flag (CF 01h) marks the Text Field as comments, which
323    /// are not intended for transmission.
324    ///
325    /// # Example
326    ///
327    /// ```rust
328    /// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
329    /// assert!(!stl.ttis[0].is_comment());
330    /// ```
331    pub fn is_comment(&self) -> bool {
332        self.cf == CF_COMMENT
333    }
334
335    #[allow(clippy::vec_init_then_push)]
336    pub(crate) fn serialize(&self) -> Vec<u8> {
337        let mut res = vec![];
338        res.push(self.sgn);
339        res.push((self.sn & 0xff) as u8);
340        res.push((self.sn >> 8) as u8);
341        res.push(self.ebn);
342        res.push(self.cs.serialize());
343        res.extend(self.tci.serialize().iter().cloned());
344        res.extend(self.tco.serialize().iter().cloned());
345        res.push(self.vp);
346        res.push(self.jc);
347        res.push(self.cf);
348        res.extend(self.tf.iter().cloned());
349        res
350    }
351}
352
353/// Highest Extension Block Number used for extension blocks
354const EBN_LAST_EXTENSION: u8 = 0xEF;
355/// Maximum number of text blocks in a subtitle: extension blocks 00h-EFh and the last block
356pub(crate) const MAX_TEXT_BLOCKS: usize = EBN_LAST_EXTENSION as usize + 2;
357/// Extension Block Number of a block carrying user data
358const EBN_USER_DATA: u8 = 0xFE;
359/// Extension Block Number of the last block of a subtitle
360pub(crate) const EBN_LAST_BLOCK: u8 = 0xFF;
361/// Comment Flag value for a Text Field containing comments
362const CF_COMMENT: u8 = 0x01;
363
364impl fmt::Debug for TtiBlock {
365    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366        write!(
367            f,
368            "\n{}-->{} sgn:{} sn:{} ebn:{} cs:{:?} vp:{} jc:{} cf:{} [{}]",
369            self.tci,
370            self.tco,
371            self.sgn,
372            self.sn,
373            self.ebn,
374            self.cs,
375            self.vp,
376            self.jc,
377            self.cf,
378            self.text()
379        )
380    }
381}
382
383impl fmt::Display for TtiBlock {
384    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
385        write!(
386            f,
387            "\n{} {} {} {} {:?} [{}]",
388            self.tci,
389            self.sgn,
390            self.sn,
391            self.ebn,
392            self.cs,
393            self.text()
394        )
395    }
396}