Skip to main content

ebustl_parser/
text_field.rs

1//! The Text Fields (TF) of TTI blocks: the types of the decoded content, and the encoding and
2//! decoding of subtitle text with control codes (Tech 3264 section 5).
3
4use textcode::{
5    Iso8859_5, Iso8859_6, Iso8859_7, Iso8859_8, decode as decode_iso8859, encode as encode_iso8859,
6};
7
8use crate::{CharacterCodeTable, latin};
9
10/// Length of the Text Field of a TTI block
11pub(crate) const TF_LENGTH: usize = 112;
12/// Text Field code for CR/LF
13pub(crate) const TF_CR_LF: u8 = 0x8A;
14/// Text Field code for unused space, also terminating the text
15pub(crate) const TF_UNUSED_SPACE: u8 = 0x8F;
16
17// Teletext control codes, Appendix 2
18const TELETEXT_END_BOX: u8 = 0x0A;
19const TELETEXT_START_BOX: u8 = 0x0B;
20const TELETEXT_DOUBLE_HEIGHT: u8 = 0x0D;
21
22/// A part of the decoded Text Field of a subtitle.
23///
24/// Characters between control codes are combined into a single [`TextElement::Text`]. The
25/// unused space code (8Fh) ends the text and is not included.
26///
27/// # Example
28///
29/// ```rust
30/// use ebustl_parser::{Color, TeletextControl, TextElement};
31///
32/// let stl = ebustl_parser::parse_stl_from_file("stls/test.stl").unwrap();
33/// assert_eq!(
34///     vec![
35///         TextElement::Teletext(TeletextControl::DoubleHeight),
36///         TextElement::Teletext(TeletextControl::AlphaColor(Color::Cyan)),
37///         TextElement::Teletext(TeletextControl::StartBox),
38///         TextElement::Teletext(TeletextControl::StartBox),
39///         TextElement::Text("au pied de la statue de la Liberté.".to_string()),
40///         TextElement::Teletext(TeletextControl::EndBox),
41///         TextElement::NewLine,
42///     ],
43///     stl.ttis[2].text_elements()
44/// );
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum TextElement {
48    /// Characters, decoded with the Character Code Table
49    Text(String),
50    /// CR/LF (8Ah), starting the next row
51    NewLine,
52    /// A teletext control code (00h-1Fh), used for teletext subtitles. Teletext control codes
53    /// occupy a character position on screen.
54    Teletext(TeletextControl),
55    /// A control code for open subtitles (80h-85h)
56    Open(OpenControl),
57    /// A code reserved for future use (86h-89h, 8Bh-8Eh and 90h-9Fh)
58    Reserved(u8),
59}
60
61impl TextElement {
62    /// The element for a control code, i.e. 00h-1Fh or 80h-9Fh except unused space (8Fh).
63    pub(crate) fn from_control_code(code: u8) -> TextElement {
64        match code {
65            0x00..=0x1F => TextElement::Teletext(TeletextControl::from_code(code)),
66            0x80 => TextElement::Open(OpenControl::ItalicsOn),
67            0x81 => TextElement::Open(OpenControl::ItalicsOff),
68            0x82 => TextElement::Open(OpenControl::UnderlineOn),
69            0x83 => TextElement::Open(OpenControl::UnderlineOff),
70            0x84 => TextElement::Open(OpenControl::BoxingOn),
71            0x85 => TextElement::Open(OpenControl::BoxingOff),
72            0x8A => TextElement::NewLine,
73            code => TextElement::Reserved(code),
74        }
75    }
76}
77
78/// A teletext control code (00h-1Fh), Tech 3264 Appendix 2, reproduced from EBU Tech 3240.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum TeletextControl {
81    /// Alphanumeric characters in the color (00h-07h)
82    AlphaColor(Color),
83    /// Flash (08h)
84    Flash,
85    /// Steady (09h)
86    Steady,
87    /// End box (0Ah)
88    EndBox,
89    /// Start box (0Bh)
90    StartBox,
91    /// Normal height (0Ch)
92    NormalHeight,
93    /// Double height (0Dh)
94    DoubleHeight,
95    /// Double width (0Eh)
96    DoubleWidth,
97    /// Double size (0Fh)
98    DoubleSize,
99    /// Mosaic graphics in the color (10h-17h)
100    MosaicColor(Color),
101    /// Conceal (18h)
102    Conceal,
103    /// Contiguous mosaic (19h)
104    ContiguousMosaic,
105    /// Separated mosaic (1Ah)
106    SeparatedMosaic,
107    /// Reserved (1Bh)
108    Reserved,
109    /// Black background (1Ch)
110    BlackBackground,
111    /// New background (1Dh), the background takes the current foreground color
112    NewBackground,
113    /// Hold mosaic (1Eh)
114    HoldMosaic,
115    /// Release mosaic (1Fh)
116    ReleaseMosaic,
117}
118
119impl TeletextControl {
120    /// The control for a code 00h-1Fh.
121    fn from_code(code: u8) -> TeletextControl {
122        match code {
123            0x00..=0x07 => TeletextControl::AlphaColor(Color::from_code(code)),
124            0x08 => TeletextControl::Flash,
125            0x09 => TeletextControl::Steady,
126            0x0A => TeletextControl::EndBox,
127            0x0B => TeletextControl::StartBox,
128            0x0C => TeletextControl::NormalHeight,
129            0x0D => TeletextControl::DoubleHeight,
130            0x0E => TeletextControl::DoubleWidth,
131            0x0F => TeletextControl::DoubleSize,
132            0x10..=0x17 => TeletextControl::MosaicColor(Color::from_code(code)),
133            0x18 => TeletextControl::Conceal,
134            0x19 => TeletextControl::ContiguousMosaic,
135            0x1A => TeletextControl::SeparatedMosaic,
136            0x1B => TeletextControl::Reserved,
137            0x1C => TeletextControl::BlackBackground,
138            0x1D => TeletextControl::NewBackground,
139            0x1E => TeletextControl::HoldMosaic,
140            _ => TeletextControl::ReleaseMosaic,
141        }
142    }
143}
144
145/// A teletext color, used by [`TeletextControl::AlphaColor`] and
146/// [`TeletextControl::MosaicColor`].
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum Color {
149    /// Black (0)
150    Black,
151    /// Red (1)
152    Red,
153    /// Green (2)
154    Green,
155    /// Yellow (3)
156    Yellow,
157    /// Blue (4)
158    Blue,
159    /// Magenta (5)
160    Magenta,
161    /// Cyan (6)
162    Cyan,
163    /// White (7)
164    White,
165}
166
167impl Color {
168    /// The color in the lowest three bits of a teletext color code.
169    fn from_code(code: u8) -> Color {
170        match code & 0x07 {
171            0 => Color::Black,
172            1 => Color::Red,
173            2 => Color::Green,
174            3 => Color::Yellow,
175            4 => Color::Blue,
176            5 => Color::Magenta,
177            6 => Color::Cyan,
178            _ => Color::White,
179        }
180    }
181}
182
183/// A control code for open (in-vision) subtitles (80h-85h).
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum OpenControl {
186    /// Italics on (80h)
187    ItalicsOn,
188    /// Italics off (81h)
189    ItalicsOff,
190    /// Underline on (82h)
191    UnderlineOn,
192    /// Underline off (83h)
193    UnderlineOff,
194    /// Boxing on (84h)
195    BoxingOn,
196    /// Boxing off (85h)
197    BoxingOff,
198}
199
200/// How the rows of a subtitle are presented
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub(crate) enum Presentation {
203    /// Teletext subtitles, each row boxed and optionally in double height
204    Teletext { double_height: bool },
205    /// Open (in-vision) subtitles, which must not use teletext control codes
206    Open,
207}
208
209impl Presentation {
210    /// Control codes starting each row
211    fn row_start(self) -> &'static [u8] {
212        match self {
213            Presentation::Teletext {
214                double_height: true,
215            } => &[
216                TELETEXT_DOUBLE_HEIGHT,
217                TELETEXT_START_BOX,
218                TELETEXT_START_BOX,
219            ],
220            Presentation::Teletext {
221                double_height: false,
222            } => &[TELETEXT_START_BOX, TELETEXT_START_BOX],
223            Presentation::Open => &[],
224        }
225    }
226
227    /// Control codes ending each row
228    fn row_end(self) -> &'static [u8] {
229        match self {
230            Presentation::Teletext { .. } => &[TELETEXT_END_BOX, TELETEXT_END_BOX],
231            Presentation::Open => &[],
232        }
233    }
234
235    /// Codes between two rows. A double height row occupies two teletext rows.
236    fn row_separator(self) -> &'static [u8] {
237        match self {
238            Presentation::Teletext {
239                double_height: true,
240            } => &[TF_CR_LF, TF_CR_LF],
241            _ => &[TF_CR_LF],
242        }
243    }
244}
245
246/// Encodes `text` into Text Fields, one per TTI block, padded with unused space (8Fh).
247///
248/// Rows are separated by `\n` or `\r\n`, other control characters are dropped. A character is
249/// never split between two Text Fields, and the last Text Field always ends with unused space
250/// as the spec requires.
251pub(crate) fn encode_text_fields(
252    text: &str,
253    presentation: Presentation,
254    cct: CharacterCodeTable,
255) -> Vec<Vec<u8>> {
256    let terminator = [TF_UNUSED_SPACE];
257    let units = text_units(text, presentation, cct);
258
259    let mut fields = Vec::new();
260    let mut field = Vec::with_capacity(TF_LENGTH);
261    for unit in units
262        .iter()
263        .map(Vec::as_slice)
264        .chain([terminator.as_slice()])
265    {
266        if field.len() + unit.len() > TF_LENGTH {
267            fields.push(pad(field));
268            field = Vec::with_capacity(TF_LENGTH);
269        }
270        field.extend_from_slice(unit);
271    }
272    fields.push(pad(field));
273    fields
274}
275
276/// Encodes `text` into a single Text Field like [`encode_text_fields`], truncating text that
277/// does not fit.
278pub(crate) fn encode_truncated_text_field(
279    text: &str,
280    presentation: Presentation,
281    cct: CharacterCodeTable,
282) -> Vec<u8> {
283    let units = text_units(text, presentation, cct);
284    // Keep at least one byte for the terminating unused space
285    let mut len = 0;
286    let fitting = units
287        .iter()
288        .take_while(|unit| {
289            len += unit.len();
290            len < TF_LENGTH
291        })
292        .count();
293    pad(units[..fitting].concat())
294}
295
296/// Encodes the text as units, the bytes of a character or control code, which must be kept
297/// together in one Text Field.
298fn text_units(text: &str, presentation: Presentation, cct: CharacterCodeTable) -> Vec<Vec<u8>> {
299    let control_codes = |codes: &[u8]| codes.iter().map(|&code| vec![code]).collect::<Vec<_>>();
300
301    let mut units = Vec::new();
302    for (i, row) in text.lines().enumerate() {
303        if i > 0 {
304            units.extend(control_codes(presentation.row_separator()));
305        }
306        units.extend(control_codes(presentation.row_start()));
307        units.extend(
308            row.chars()
309                .filter(|c| !c.is_control())
310                .map(|c| encode_characters(c.encode_utf8(&mut [0; 4]), cct)),
311        );
312        units.extend(control_codes(presentation.row_end()));
313    }
314    units
315}
316
317/// Encodes text using the given character code table.
318fn encode_characters(text: &str, cct: CharacterCodeTable) -> Vec<u8> {
319    match cct {
320        CharacterCodeTable::Latin => latin::encode(text),
321        CharacterCodeTable::LatinCyrillic => encode_iso8859::<Iso8859_5>(text),
322        CharacterCodeTable::LatinArabic => encode_iso8859::<Iso8859_6>(text),
323        CharacterCodeTable::LatinGreek => encode_iso8859::<Iso8859_7>(text),
324        CharacterCodeTable::LatinHebrew => encode_iso8859::<Iso8859_8>(text),
325    }
326}
327
328fn pad(mut field: Vec<u8>) -> Vec<u8> {
329    field.resize(TF_LENGTH, TF_UNUSED_SPACE);
330    field
331}
332
333/// The Text Field up to, but not including, the first unused space code (8Fh).
334pub(crate) fn content(tf: &[u8]) -> &[u8] {
335    let end = tf
336        .iter()
337        .position(|&c| c == TF_UNUSED_SPACE)
338        .unwrap_or(tf.len());
339    &tf[..end]
340}
341
342/// Decodes a Text Field, or several concatenated ones, into characters and control codes.
343///
344/// Decoding stops at the first unused space code (8Fh).
345pub(crate) fn decode_elements(tf: &[u8], cct: CharacterCodeTable) -> Vec<TextElement> {
346    let mut elements = Vec::new();
347    // Start of the current run of character codes
348    let mut run_start = 0;
349    for (i, &code) in tf.iter().enumerate() {
350        let is_control_code = matches!(code, 0x00..=0x1F | 0x80..=0x9F);
351        if !is_control_code {
352            continue;
353        }
354        push_text(&mut elements, &tf[run_start..i], cct);
355        if code == TF_UNUSED_SPACE {
356            return elements;
357        }
358        elements.push(TextElement::from_control_code(code));
359        run_start = i + 1;
360    }
361    // The text may fill the field without a terminating control code
362    push_text(&mut elements, &tf[run_start..], cct);
363    elements
364}
365
366fn push_text(elements: &mut Vec<TextElement>, data: &[u8], cct: CharacterCodeTable) {
367    if !data.is_empty() {
368        elements.push(TextElement::Text(decode_characters(data, cct)));
369    }
370}
371
372/// Decodes a Text Field, or several concatenated ones, into a string.
373///
374/// Control codes are dropped, except CR/LF which becomes `\r\n`. Decoding stops at the first
375/// unused space code (8Fh).
376pub(crate) fn decode(tf: &[u8], cct: CharacterCodeTable) -> String {
377    let mut result = String::new();
378    for element in decode_elements(tf, cct) {
379        match element {
380            TextElement::Text(text) => result.push_str(&text),
381            TextElement::NewLine => result.push_str("\r\n"),
382            _ => {}
383        }
384    }
385    result
386}
387
388/// Decodes character codes (no control codes) using the given character code table.
389fn decode_characters(data: &[u8], cct: CharacterCodeTable) -> String {
390    match cct {
391        CharacterCodeTable::Latin => latin::decode(data),
392        CharacterCodeTable::LatinCyrillic => decode_iso8859::<Iso8859_5>(data),
393        CharacterCodeTable::LatinArabic => decode_iso8859::<Iso8859_6>(data),
394        CharacterCodeTable::LatinGreek => decode_iso8859::<Iso8859_7>(data),
395        CharacterCodeTable::LatinHebrew => decode_iso8859::<Iso8859_8>(data),
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    const TELETEXT: Presentation = Presentation::Teletext {
404        double_height: false,
405    };
406    const TELETEXT_DH: Presentation = Presentation::Teletext {
407        double_height: true,
408    };
409    const LATIN: CharacterCodeTable = CharacterCodeTable::Latin;
410
411    /// The single Text Field of `text`, without the padding
412    fn encoded(text: &str, presentation: Presentation) -> Vec<u8> {
413        let fields = encode_text_fields(text, presentation, LATIN);
414        assert_eq!(1, fields.len());
415        without_padding(&fields[0])
416    }
417
418    fn without_padding(field: &[u8]) -> Vec<u8> {
419        assert_eq!(TF_LENGTH, field.len());
420        let end = field.iter().position(|&c| c == TF_UNUSED_SPACE).unwrap();
421        assert!(field[end..].iter().all(|&c| c == TF_UNUSED_SPACE));
422        field[..end].to_vec()
423    }
424
425    #[test]
426    fn teletext_row_is_boxed() {
427        assert_eq!(b"\x0B\x0BHi\x0A\x0A".to_vec(), encoded("Hi", TELETEXT));
428    }
429
430    #[test]
431    fn teletext_double_height_row() {
432        assert_eq!(
433            b"\x0D\x0B\x0BHi\x0A\x0A".to_vec(),
434            encoded("Hi", TELETEXT_DH)
435        );
436    }
437
438    #[test]
439    fn teletext_rows_are_separated_by_cr_lf() {
440        assert_eq!(
441            b"\x0B\x0BOne\x0A\x0A\x8A\x0B\x0BTwo\x0A\x0A".to_vec(),
442            encoded("One\nTwo", TELETEXT)
443        );
444    }
445
446    #[test]
447    fn teletext_double_height_rows_are_separated_by_two_cr_lf() {
448        assert_eq!(
449            b"\x0D\x0B\x0BOne\x0A\x0A\x8A\x8A\x0D\x0B\x0BTwo\x0A\x0A".to_vec(),
450            encoded("One\nTwo", TELETEXT_DH)
451        );
452    }
453
454    #[test]
455    fn open_subtitle_has_no_teletext_codes() {
456        assert_eq!(
457            b"One\x8ATwo".to_vec(),
458            encoded("One\nTwo", Presentation::Open)
459        );
460    }
461
462    #[test]
463    fn crlf_and_trailing_newline() {
464        assert_eq!(
465            b"One\x8ATwo".to_vec(),
466            encoded("One\r\nTwo\n", Presentation::Open)
467        );
468    }
469
470    #[test]
471    fn control_characters_are_dropped() {
472        assert_eq!(
473            b"OneTwo".to_vec(),
474            encoded("One\tTwo\r", Presentation::Open)
475        );
476    }
477
478    #[test]
479    fn characters_are_encoded_with_the_table() {
480        assert_eq!(b"\xC2e\xA4".to_vec(), encoded("é$", Presentation::Open));
481        let fields = encode_text_fields("Ж", Presentation::Open, CharacterCodeTable::LatinCyrillic);
482        assert_eq!(vec![0xB6], without_padding(&fields[0]));
483    }
484
485    #[test]
486    fn empty_text_is_unused_space() {
487        assert_eq!(Vec::<u8>::new(), encoded("", TELETEXT));
488    }
489
490    #[test]
491    fn text_filling_one_field_leaves_room_for_unused_space() {
492        let fields = encode_text_fields(&"a".repeat(TF_LENGTH - 1), Presentation::Open, LATIN);
493        assert_eq!(1, fields.len());
494        assert_eq!(TF_UNUSED_SPACE, fields[0][TF_LENGTH - 1]);
495    }
496
497    #[test]
498    fn full_field_is_followed_by_field_of_unused_space() {
499        let fields = encode_text_fields(&"a".repeat(TF_LENGTH), Presentation::Open, LATIN);
500        assert_eq!(2, fields.len());
501        assert_eq!(vec![b'a'; TF_LENGTH], fields[0]);
502        assert_eq!(vec![TF_UNUSED_SPACE; TF_LENGTH], fields[1]);
503    }
504
505    #[test]
506    fn long_text_continues_in_next_fields() {
507        let text = "0123456789".repeat(30);
508        let fields = encode_text_fields(&text, Presentation::Open, LATIN);
509
510        assert_eq!(3, fields.len());
511        assert_eq!(text.as_bytes()[..TF_LENGTH], fields[0][..]);
512        assert_eq!(text.as_bytes()[TF_LENGTH..2 * TF_LENGTH], fields[1][..]);
513        assert_eq!(
514            text.as_bytes()[2 * TF_LENGTH..].to_vec(),
515            without_padding(&fields[2])
516        );
517    }
518
519    #[test]
520    fn accented_character_is_not_split_between_fields() {
521        // 'é' is two bytes (C2h 65h) and would straddle the end of the first field
522        let text = format!("{}é", "a".repeat(TF_LENGTH - 1));
523        let fields = encode_text_fields(&text, Presentation::Open, LATIN);
524
525        assert_eq!(2, fields.len());
526        assert_eq!(vec![b'a'; TF_LENGTH - 1], without_padding(&fields[0]));
527        assert_eq!(b"\xC2e".to_vec(), without_padding(&fields[1]));
528    }
529
530    #[test]
531    fn truncated_field_ends_with_unused_space() {
532        let field = encode_truncated_text_field(&"a".repeat(200), TELETEXT, LATIN);
533
534        let mut expected = b"\x0B\x0B".to_vec();
535        expected.extend(vec![b'a'; TF_LENGTH - 3]);
536        assert_eq!(expected, without_padding(&field));
537    }
538
539    #[test]
540    fn truncated_field_does_not_split_accented_character() {
541        let text = format!("{}é", "a".repeat(TF_LENGTH - 2));
542        let field = encode_truncated_text_field(&text, Presentation::Open, LATIN);
543
544        assert_eq!(vec![b'a'; TF_LENGTH - 2], without_padding(&field));
545    }
546
547    #[test]
548    fn short_text_is_not_truncated() {
549        let field = encode_truncated_text_field("One\nTwo", TELETEXT, LATIN);
550        assert_eq!(encoded("One\nTwo", TELETEXT), without_padding(&field));
551    }
552
553    #[test]
554    fn content_ends_at_unused_space() {
555        assert_eq!(b"One", content(b"One\x8FTwo\x8F"));
556        assert_eq!(b"", content(b"\x8F\x8F"));
557        assert_eq!(b"Full", content(b"Full"));
558    }
559
560    #[test]
561    fn decode_drops_control_codes_except_cr_lf() {
562        let tf = b"\x0D\x0B\x0BOne\x0A\x0A\x8A\x80Two\x81\x8F";
563        assert_eq!("One\r\nTwo", decode(tf, LATIN));
564    }
565
566    #[test]
567    fn decode_stops_at_unused_space() {
568        assert_eq!("One", decode(b"One\x8FTwo", LATIN));
569    }
570
571    #[test]
572    fn decode_text_without_terminating_control_code() {
573        assert_eq!("a".repeat(TF_LENGTH), decode(&[b'a'; TF_LENGTH], LATIN));
574    }
575
576    #[test]
577    fn decode_uses_the_table() {
578        assert_eq!("é$", decode(b"\xC2e\xA4", LATIN));
579        assert_eq!("Ж", decode(&[0xB6], CharacterCodeTable::LatinCyrillic));
580    }
581
582    #[test]
583    fn encode_decode_roundtrip() {
584        let text = "Première ligne à $5\nDeuxième ligne";
585        for presentation in [TELETEXT, Presentation::Open] {
586            let fields = encode_text_fields(text, presentation, LATIN);
587            assert_eq!(
588                "Première ligne à $5\r\nDeuxième ligne",
589                decode(&fields[0], LATIN)
590            );
591        }
592        let fields = encode_text_fields(text, TELETEXT_DH, LATIN);
593        assert_eq!(
594            "Première ligne à $5\r\n\r\nDeuxième ligne",
595            decode(&fields[0], LATIN)
596        );
597    }
598
599    #[test]
600    fn decode_elements_with_control_codes() {
601        let tf = b"\x0D\x06\x0B\x0BHi\x0A\x0A\x8A\x80\xC2e\x81\x86\x8Fignored";
602        assert_eq!(
603            vec![
604                TextElement::Teletext(TeletextControl::DoubleHeight),
605                TextElement::Teletext(TeletextControl::AlphaColor(Color::Cyan)),
606                TextElement::Teletext(TeletextControl::StartBox),
607                TextElement::Teletext(TeletextControl::StartBox),
608                TextElement::Text("Hi".to_string()),
609                TextElement::Teletext(TeletextControl::EndBox),
610                TextElement::Teletext(TeletextControl::EndBox),
611                TextElement::NewLine,
612                TextElement::Open(OpenControl::ItalicsOn),
613                TextElement::Text("é".to_string()),
614                TextElement::Open(OpenControl::ItalicsOff),
615                TextElement::Reserved(0x86),
616            ],
617            decode_elements(tf, LATIN)
618        );
619    }
620
621    #[test]
622    fn decode_elements_of_empty_field() {
623        assert_eq!(
624            Vec::<TextElement>::new(),
625            decode_elements(&[TF_UNUSED_SPACE; TF_LENGTH], LATIN)
626        );
627    }
628
629    #[test]
630    fn decode_elements_of_text_filling_the_field() {
631        assert_eq!(
632            vec![TextElement::Text("a".repeat(TF_LENGTH))],
633            decode_elements(&[b'a'; TF_LENGTH], LATIN)
634        );
635    }
636
637    #[test]
638    fn decode_agrees_with_decode_elements() {
639        let tf = b"\x0D\x0B\x0BOne\x0A\x0A\x8A\x8A\x80Two\x81\x8F";
640        assert_eq!("One\r\n\r\nTwo", decode(tf, LATIN));
641    }
642
643    #[test]
644    fn teletext_control_codes() {
645        use TeletextControl::*;
646        let expected = [
647            AlphaColor(Color::Black),
648            AlphaColor(Color::Red),
649            AlphaColor(Color::Green),
650            AlphaColor(Color::Yellow),
651            AlphaColor(Color::Blue),
652            AlphaColor(Color::Magenta),
653            AlphaColor(Color::Cyan),
654            AlphaColor(Color::White),
655            Flash,
656            Steady,
657            EndBox,
658            StartBox,
659            NormalHeight,
660            DoubleHeight,
661            DoubleWidth,
662            DoubleSize,
663            MosaicColor(Color::Black),
664            MosaicColor(Color::Red),
665            MosaicColor(Color::Green),
666            MosaicColor(Color::Yellow),
667            MosaicColor(Color::Blue),
668            MosaicColor(Color::Magenta),
669            MosaicColor(Color::Cyan),
670            MosaicColor(Color::White),
671            Conceal,
672            ContiguousMosaic,
673            SeparatedMosaic,
674            Reserved,
675            BlackBackground,
676            NewBackground,
677            HoldMosaic,
678            ReleaseMosaic,
679        ];
680        for (code, control) in (0x00..=0x1F).zip(expected) {
681            assert_eq!(
682                TextElement::Teletext(control),
683                TextElement::from_control_code(code),
684                "code {code:02X}h"
685            );
686        }
687    }
688
689    #[test]
690    fn open_subtitle_control_codes() {
691        use OpenControl::*;
692        let expected = [
693            ItalicsOn,
694            ItalicsOff,
695            UnderlineOn,
696            UnderlineOff,
697            BoxingOn,
698            BoxingOff,
699        ];
700        for (code, control) in (0x80..=0x85).zip(expected) {
701            assert_eq!(
702                TextElement::Open(control),
703                TextElement::from_control_code(code),
704                "code {code:02X}h"
705            );
706        }
707    }
708
709    #[test]
710    fn cr_lf_and_reserved_codes() {
711        assert_eq!(TextElement::NewLine, TextElement::from_control_code(0x8A));
712        for code in (0x86..=0x89).chain(0x8B..=0x8E).chain(0x90..=0x9F) {
713            assert_eq!(
714                TextElement::Reserved(code),
715                TextElement::from_control_code(code)
716            );
717        }
718    }
719}