use textcode::{
Iso8859_5, Iso8859_6, Iso8859_7, Iso8859_8, decode as decode_iso8859, encode as encode_iso8859,
};
use crate::{CharacterCodeTable, latin};
pub(crate) const TF_LENGTH: usize = 112;
pub(crate) const TF_CR_LF: u8 = 0x8A;
pub(crate) const TF_UNUSED_SPACE: u8 = 0x8F;
const TELETEXT_END_BOX: u8 = 0x0A;
const TELETEXT_START_BOX: u8 = 0x0B;
const TELETEXT_DOUBLE_HEIGHT: u8 = 0x0D;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextElement {
Text(String),
NewLine,
Teletext(TeletextControl),
Open(OpenControl),
Reserved(u8),
}
impl TextElement {
pub(crate) fn from_control_code(code: u8) -> TextElement {
match code {
0x00..=0x1F => TextElement::Teletext(TeletextControl::from_code(code)),
0x80 => TextElement::Open(OpenControl::ItalicsOn),
0x81 => TextElement::Open(OpenControl::ItalicsOff),
0x82 => TextElement::Open(OpenControl::UnderlineOn),
0x83 => TextElement::Open(OpenControl::UnderlineOff),
0x84 => TextElement::Open(OpenControl::BoxingOn),
0x85 => TextElement::Open(OpenControl::BoxingOff),
0x8A => TextElement::NewLine,
code => TextElement::Reserved(code),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TeletextControl {
AlphaColor(Color),
Flash,
Steady,
EndBox,
StartBox,
NormalHeight,
DoubleHeight,
DoubleWidth,
DoubleSize,
MosaicColor(Color),
Conceal,
ContiguousMosaic,
SeparatedMosaic,
Reserved,
BlackBackground,
NewBackground,
HoldMosaic,
ReleaseMosaic,
}
impl TeletextControl {
fn from_code(code: u8) -> TeletextControl {
match code {
0x00..=0x07 => TeletextControl::AlphaColor(Color::from_code(code)),
0x08 => TeletextControl::Flash,
0x09 => TeletextControl::Steady,
0x0A => TeletextControl::EndBox,
0x0B => TeletextControl::StartBox,
0x0C => TeletextControl::NormalHeight,
0x0D => TeletextControl::DoubleHeight,
0x0E => TeletextControl::DoubleWidth,
0x0F => TeletextControl::DoubleSize,
0x10..=0x17 => TeletextControl::MosaicColor(Color::from_code(code)),
0x18 => TeletextControl::Conceal,
0x19 => TeletextControl::ContiguousMosaic,
0x1A => TeletextControl::SeparatedMosaic,
0x1B => TeletextControl::Reserved,
0x1C => TeletextControl::BlackBackground,
0x1D => TeletextControl::NewBackground,
0x1E => TeletextControl::HoldMosaic,
_ => TeletextControl::ReleaseMosaic,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl Color {
fn from_code(code: u8) -> Color {
match code & 0x07 {
0 => Color::Black,
1 => Color::Red,
2 => Color::Green,
3 => Color::Yellow,
4 => Color::Blue,
5 => Color::Magenta,
6 => Color::Cyan,
_ => Color::White,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenControl {
ItalicsOn,
ItalicsOff,
UnderlineOn,
UnderlineOff,
BoxingOn,
BoxingOff,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Presentation {
Teletext { double_height: bool },
Open,
}
impl Presentation {
fn row_start(self) -> &'static [u8] {
match self {
Presentation::Teletext {
double_height: true,
} => &[
TELETEXT_DOUBLE_HEIGHT,
TELETEXT_START_BOX,
TELETEXT_START_BOX,
],
Presentation::Teletext {
double_height: false,
} => &[TELETEXT_START_BOX, TELETEXT_START_BOX],
Presentation::Open => &[],
}
}
fn row_end(self) -> &'static [u8] {
match self {
Presentation::Teletext { .. } => &[TELETEXT_END_BOX, TELETEXT_END_BOX],
Presentation::Open => &[],
}
}
fn row_separator(self) -> &'static [u8] {
match self {
Presentation::Teletext {
double_height: true,
} => &[TF_CR_LF, TF_CR_LF],
_ => &[TF_CR_LF],
}
}
}
pub(crate) fn encode_text_fields(
text: &str,
presentation: Presentation,
cct: CharacterCodeTable,
) -> Vec<Vec<u8>> {
let terminator = [TF_UNUSED_SPACE];
let units = text_units(text, presentation, cct);
let mut fields = Vec::new();
let mut field = Vec::with_capacity(TF_LENGTH);
for unit in units
.iter()
.map(Vec::as_slice)
.chain([terminator.as_slice()])
{
if field.len() + unit.len() > TF_LENGTH {
fields.push(pad(field));
field = Vec::with_capacity(TF_LENGTH);
}
field.extend_from_slice(unit);
}
fields.push(pad(field));
fields
}
pub(crate) fn encode_truncated_text_field(
text: &str,
presentation: Presentation,
cct: CharacterCodeTable,
) -> Vec<u8> {
let units = text_units(text, presentation, cct);
let mut len = 0;
let fitting = units
.iter()
.take_while(|unit| {
len += unit.len();
len < TF_LENGTH
})
.count();
pad(units[..fitting].concat())
}
fn text_units(text: &str, presentation: Presentation, cct: CharacterCodeTable) -> Vec<Vec<u8>> {
let control_codes = |codes: &[u8]| codes.iter().map(|&code| vec![code]).collect::<Vec<_>>();
let mut units = Vec::new();
for (i, row) in text.lines().enumerate() {
if i > 0 {
units.extend(control_codes(presentation.row_separator()));
}
units.extend(control_codes(presentation.row_start()));
units.extend(
row.chars()
.filter(|c| !c.is_control())
.map(|c| encode_characters(c.encode_utf8(&mut [0; 4]), cct)),
);
units.extend(control_codes(presentation.row_end()));
}
units
}
fn encode_characters(text: &str, cct: CharacterCodeTable) -> Vec<u8> {
match cct {
CharacterCodeTable::Latin => latin::encode(text),
CharacterCodeTable::LatinCyrillic => encode_iso8859::<Iso8859_5>(text),
CharacterCodeTable::LatinArabic => encode_iso8859::<Iso8859_6>(text),
CharacterCodeTable::LatinGreek => encode_iso8859::<Iso8859_7>(text),
CharacterCodeTable::LatinHebrew => encode_iso8859::<Iso8859_8>(text),
}
}
fn pad(mut field: Vec<u8>) -> Vec<u8> {
field.resize(TF_LENGTH, TF_UNUSED_SPACE);
field
}
pub(crate) fn content(tf: &[u8]) -> &[u8] {
let end = tf
.iter()
.position(|&c| c == TF_UNUSED_SPACE)
.unwrap_or(tf.len());
&tf[..end]
}
pub(crate) fn decode_elements(tf: &[u8], cct: CharacterCodeTable) -> Vec<TextElement> {
let mut elements = Vec::new();
let mut run_start = 0;
for (i, &code) in tf.iter().enumerate() {
let is_control_code = matches!(code, 0x00..=0x1F | 0x80..=0x9F);
if !is_control_code {
continue;
}
push_text(&mut elements, &tf[run_start..i], cct);
if code == TF_UNUSED_SPACE {
return elements;
}
elements.push(TextElement::from_control_code(code));
run_start = i + 1;
}
push_text(&mut elements, &tf[run_start..], cct);
elements
}
fn push_text(elements: &mut Vec<TextElement>, data: &[u8], cct: CharacterCodeTable) {
if !data.is_empty() {
elements.push(TextElement::Text(decode_characters(data, cct)));
}
}
pub(crate) fn decode(tf: &[u8], cct: CharacterCodeTable) -> String {
let mut result = String::new();
for element in decode_elements(tf, cct) {
match element {
TextElement::Text(text) => result.push_str(&text),
TextElement::NewLine => result.push_str("\r\n"),
_ => {}
}
}
result
}
fn decode_characters(data: &[u8], cct: CharacterCodeTable) -> String {
match cct {
CharacterCodeTable::Latin => latin::decode(data),
CharacterCodeTable::LatinCyrillic => decode_iso8859::<Iso8859_5>(data),
CharacterCodeTable::LatinArabic => decode_iso8859::<Iso8859_6>(data),
CharacterCodeTable::LatinGreek => decode_iso8859::<Iso8859_7>(data),
CharacterCodeTable::LatinHebrew => decode_iso8859::<Iso8859_8>(data),
}
}
#[cfg(test)]
mod tests {
use super::*;
const TELETEXT: Presentation = Presentation::Teletext {
double_height: false,
};
const TELETEXT_DH: Presentation = Presentation::Teletext {
double_height: true,
};
const LATIN: CharacterCodeTable = CharacterCodeTable::Latin;
fn encoded(text: &str, presentation: Presentation) -> Vec<u8> {
let fields = encode_text_fields(text, presentation, LATIN);
assert_eq!(1, fields.len());
without_padding(&fields[0])
}
fn without_padding(field: &[u8]) -> Vec<u8> {
assert_eq!(TF_LENGTH, field.len());
let end = field.iter().position(|&c| c == TF_UNUSED_SPACE).unwrap();
assert!(field[end..].iter().all(|&c| c == TF_UNUSED_SPACE));
field[..end].to_vec()
}
#[test]
fn teletext_row_is_boxed() {
assert_eq!(b"\x0B\x0BHi\x0A\x0A".to_vec(), encoded("Hi", TELETEXT));
}
#[test]
fn teletext_double_height_row() {
assert_eq!(
b"\x0D\x0B\x0BHi\x0A\x0A".to_vec(),
encoded("Hi", TELETEXT_DH)
);
}
#[test]
fn teletext_rows_are_separated_by_cr_lf() {
assert_eq!(
b"\x0B\x0BOne\x0A\x0A\x8A\x0B\x0BTwo\x0A\x0A".to_vec(),
encoded("One\nTwo", TELETEXT)
);
}
#[test]
fn teletext_double_height_rows_are_separated_by_two_cr_lf() {
assert_eq!(
b"\x0D\x0B\x0BOne\x0A\x0A\x8A\x8A\x0D\x0B\x0BTwo\x0A\x0A".to_vec(),
encoded("One\nTwo", TELETEXT_DH)
);
}
#[test]
fn open_subtitle_has_no_teletext_codes() {
assert_eq!(
b"One\x8ATwo".to_vec(),
encoded("One\nTwo", Presentation::Open)
);
}
#[test]
fn crlf_and_trailing_newline() {
assert_eq!(
b"One\x8ATwo".to_vec(),
encoded("One\r\nTwo\n", Presentation::Open)
);
}
#[test]
fn control_characters_are_dropped() {
assert_eq!(
b"OneTwo".to_vec(),
encoded("One\tTwo\r", Presentation::Open)
);
}
#[test]
fn characters_are_encoded_with_the_table() {
assert_eq!(b"\xC2e\xA4".to_vec(), encoded("é$", Presentation::Open));
let fields = encode_text_fields("Ж", Presentation::Open, CharacterCodeTable::LatinCyrillic);
assert_eq!(vec![0xB6], without_padding(&fields[0]));
}
#[test]
fn empty_text_is_unused_space() {
assert_eq!(Vec::<u8>::new(), encoded("", TELETEXT));
}
#[test]
fn text_filling_one_field_leaves_room_for_unused_space() {
let fields = encode_text_fields(&"a".repeat(TF_LENGTH - 1), Presentation::Open, LATIN);
assert_eq!(1, fields.len());
assert_eq!(TF_UNUSED_SPACE, fields[0][TF_LENGTH - 1]);
}
#[test]
fn full_field_is_followed_by_field_of_unused_space() {
let fields = encode_text_fields(&"a".repeat(TF_LENGTH), Presentation::Open, LATIN);
assert_eq!(2, fields.len());
assert_eq!(vec![b'a'; TF_LENGTH], fields[0]);
assert_eq!(vec![TF_UNUSED_SPACE; TF_LENGTH], fields[1]);
}
#[test]
fn long_text_continues_in_next_fields() {
let text = "0123456789".repeat(30);
let fields = encode_text_fields(&text, Presentation::Open, LATIN);
assert_eq!(3, fields.len());
assert_eq!(text.as_bytes()[..TF_LENGTH], fields[0][..]);
assert_eq!(text.as_bytes()[TF_LENGTH..2 * TF_LENGTH], fields[1][..]);
assert_eq!(
text.as_bytes()[2 * TF_LENGTH..].to_vec(),
without_padding(&fields[2])
);
}
#[test]
fn accented_character_is_not_split_between_fields() {
let text = format!("{}é", "a".repeat(TF_LENGTH - 1));
let fields = encode_text_fields(&text, Presentation::Open, LATIN);
assert_eq!(2, fields.len());
assert_eq!(vec![b'a'; TF_LENGTH - 1], without_padding(&fields[0]));
assert_eq!(b"\xC2e".to_vec(), without_padding(&fields[1]));
}
#[test]
fn truncated_field_ends_with_unused_space() {
let field = encode_truncated_text_field(&"a".repeat(200), TELETEXT, LATIN);
let mut expected = b"\x0B\x0B".to_vec();
expected.extend(vec![b'a'; TF_LENGTH - 3]);
assert_eq!(expected, without_padding(&field));
}
#[test]
fn truncated_field_does_not_split_accented_character() {
let text = format!("{}é", "a".repeat(TF_LENGTH - 2));
let field = encode_truncated_text_field(&text, Presentation::Open, LATIN);
assert_eq!(vec![b'a'; TF_LENGTH - 2], without_padding(&field));
}
#[test]
fn short_text_is_not_truncated() {
let field = encode_truncated_text_field("One\nTwo", TELETEXT, LATIN);
assert_eq!(encoded("One\nTwo", TELETEXT), without_padding(&field));
}
#[test]
fn content_ends_at_unused_space() {
assert_eq!(b"One", content(b"One\x8FTwo\x8F"));
assert_eq!(b"", content(b"\x8F\x8F"));
assert_eq!(b"Full", content(b"Full"));
}
#[test]
fn decode_drops_control_codes_except_cr_lf() {
let tf = b"\x0D\x0B\x0BOne\x0A\x0A\x8A\x80Two\x81\x8F";
assert_eq!("One\r\nTwo", decode(tf, LATIN));
}
#[test]
fn decode_stops_at_unused_space() {
assert_eq!("One", decode(b"One\x8FTwo", LATIN));
}
#[test]
fn decode_text_without_terminating_control_code() {
assert_eq!("a".repeat(TF_LENGTH), decode(&[b'a'; TF_LENGTH], LATIN));
}
#[test]
fn decode_uses_the_table() {
assert_eq!("é$", decode(b"\xC2e\xA4", LATIN));
assert_eq!("Ж", decode(&[0xB6], CharacterCodeTable::LatinCyrillic));
}
#[test]
fn encode_decode_roundtrip() {
let text = "Première ligne à $5\nDeuxième ligne";
for presentation in [TELETEXT, Presentation::Open] {
let fields = encode_text_fields(text, presentation, LATIN);
assert_eq!(
"Première ligne à $5\r\nDeuxième ligne",
decode(&fields[0], LATIN)
);
}
let fields = encode_text_fields(text, TELETEXT_DH, LATIN);
assert_eq!(
"Première ligne à $5\r\n\r\nDeuxième ligne",
decode(&fields[0], LATIN)
);
}
#[test]
fn decode_elements_with_control_codes() {
let tf = b"\x0D\x06\x0B\x0BHi\x0A\x0A\x8A\x80\xC2e\x81\x86\x8Fignored";
assert_eq!(
vec![
TextElement::Teletext(TeletextControl::DoubleHeight),
TextElement::Teletext(TeletextControl::AlphaColor(Color::Cyan)),
TextElement::Teletext(TeletextControl::StartBox),
TextElement::Teletext(TeletextControl::StartBox),
TextElement::Text("Hi".to_string()),
TextElement::Teletext(TeletextControl::EndBox),
TextElement::Teletext(TeletextControl::EndBox),
TextElement::NewLine,
TextElement::Open(OpenControl::ItalicsOn),
TextElement::Text("é".to_string()),
TextElement::Open(OpenControl::ItalicsOff),
TextElement::Reserved(0x86),
],
decode_elements(tf, LATIN)
);
}
#[test]
fn decode_elements_of_empty_field() {
assert_eq!(
Vec::<TextElement>::new(),
decode_elements(&[TF_UNUSED_SPACE; TF_LENGTH], LATIN)
);
}
#[test]
fn decode_elements_of_text_filling_the_field() {
assert_eq!(
vec![TextElement::Text("a".repeat(TF_LENGTH))],
decode_elements(&[b'a'; TF_LENGTH], LATIN)
);
}
#[test]
fn decode_agrees_with_decode_elements() {
let tf = b"\x0D\x0B\x0BOne\x0A\x0A\x8A\x8A\x80Two\x81\x8F";
assert_eq!("One\r\n\r\nTwo", decode(tf, LATIN));
}
#[test]
fn teletext_control_codes() {
use TeletextControl::*;
let expected = [
AlphaColor(Color::Black),
AlphaColor(Color::Red),
AlphaColor(Color::Green),
AlphaColor(Color::Yellow),
AlphaColor(Color::Blue),
AlphaColor(Color::Magenta),
AlphaColor(Color::Cyan),
AlphaColor(Color::White),
Flash,
Steady,
EndBox,
StartBox,
NormalHeight,
DoubleHeight,
DoubleWidth,
DoubleSize,
MosaicColor(Color::Black),
MosaicColor(Color::Red),
MosaicColor(Color::Green),
MosaicColor(Color::Yellow),
MosaicColor(Color::Blue),
MosaicColor(Color::Magenta),
MosaicColor(Color::Cyan),
MosaicColor(Color::White),
Conceal,
ContiguousMosaic,
SeparatedMosaic,
Reserved,
BlackBackground,
NewBackground,
HoldMosaic,
ReleaseMosaic,
];
for (code, control) in (0x00..=0x1F).zip(expected) {
assert_eq!(
TextElement::Teletext(control),
TextElement::from_control_code(code),
"code {code:02X}h"
);
}
}
#[test]
fn open_subtitle_control_codes() {
use OpenControl::*;
let expected = [
ItalicsOn,
ItalicsOff,
UnderlineOn,
UnderlineOff,
BoxingOn,
BoxingOff,
];
for (code, control) in (0x80..=0x85).zip(expected) {
assert_eq!(
TextElement::Open(control),
TextElement::from_control_code(code),
"code {code:02X}h"
);
}
}
#[test]
fn cr_lf_and_reserved_codes() {
assert_eq!(TextElement::NewLine, TextElement::from_control_code(0x8A));
for code in (0x86..=0x89).chain(0x8B..=0x8E).chain(0x90..=0x9F) {
assert_eq!(
TextElement::Reserved(code),
TextElement::from_control_code(code)
);
}
}
}