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}