1use 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
10pub(crate) const TF_LENGTH: usize = 112;
12pub(crate) const TF_CR_LF: u8 = 0x8A;
14pub(crate) const TF_UNUSED_SPACE: u8 = 0x8F;
16
17const TELETEXT_END_BOX: u8 = 0x0A;
19const TELETEXT_START_BOX: u8 = 0x0B;
20const TELETEXT_DOUBLE_HEIGHT: u8 = 0x0D;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum TextElement {
48 Text(String),
50 NewLine,
52 Teletext(TeletextControl),
55 Open(OpenControl),
57 Reserved(u8),
59}
60
61impl TextElement {
62 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum TeletextControl {
81 AlphaColor(Color),
83 Flash,
85 Steady,
87 EndBox,
89 StartBox,
91 NormalHeight,
93 DoubleHeight,
95 DoubleWidth,
97 DoubleSize,
99 MosaicColor(Color),
101 Conceal,
103 ContiguousMosaic,
105 SeparatedMosaic,
107 Reserved,
109 BlackBackground,
111 NewBackground,
113 HoldMosaic,
115 ReleaseMosaic,
117}
118
119impl TeletextControl {
120 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum Color {
149 Black,
151 Red,
153 Green,
155 Yellow,
157 Blue,
159 Magenta,
161 Cyan,
163 White,
165}
166
167impl Color {
168 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum OpenControl {
186 ItalicsOn,
188 ItalicsOff,
190 UnderlineOn,
192 UnderlineOff,
194 BoxingOn,
196 BoxingOff,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub(crate) enum Presentation {
203 Teletext { double_height: bool },
205 Open,
207}
208
209impl Presentation {
210 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 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 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
246pub(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
276pub(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 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
296fn 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
317fn 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
333pub(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
342pub(crate) fn decode_elements(tf: &[u8], cct: CharacterCodeTable) -> Vec<TextElement> {
346 let mut elements = Vec::new();
347 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 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
372pub(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
388fn 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 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 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}