use crate::cc_data::{CcTriplet, CcType};
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Cea608Color {
#[default]
White,
Green,
Blue,
Cyan,
Red,
Yellow,
Magenta,
}
impl Cea608Color {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::White => "white",
Self::Green => "green",
Self::Blue => "blue",
Self::Cyan => "cyan",
Self::Red => "red",
Self::Yellow => "yellow",
Self::Magenta => "magenta",
}
}
#[must_use]
pub(super) fn from_idx(idx: u8) -> Self {
match idx & 0x07 {
0 => Self::White,
1 => Self::Green,
2 => Self::Blue,
3 => Self::Cyan,
4 => Self::Red,
5 => Self::Yellow,
6 => Self::Magenta,
_ => Self::White,
}
}
}
dvb_common::impl_spec_display!(Cea608Color);
const SCREEN_ROWS: usize = 15;
const SCREEN_COLS: usize = 32;
const MC_RCL: u8 = 0x20;
const MC_BS: u8 = 0x21;
const MC_DER: u8 = 0x24;
const MC_RU2: u8 = 0x25;
const MC_RU3: u8 = 0x26;
const MC_RU4: u8 = 0x27;
const MC_FON: u8 = 0x28;
const MC_RDC: u8 = 0x29;
const MC_TR: u8 = 0x2A;
const MC_RTD: u8 = 0x2B;
const MC_EDM: u8 = 0x2C;
const MC_CR: u8 = 0x2D;
const MC_ENM: u8 = 0x2E;
const MC_EOC: u8 = 0x2F;
const TAB_FIRST_C1: u8 = 0x17;
const TAB_FIRST_C2: u8 = 0x1F;
const TAB1: u8 = 0x21;
const TAB3: u8 = 0x23;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Cea608Mode {
#[default]
None,
PopOn,
RollUp(u8),
PaintOn,
Text,
}
impl Cea608Mode {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::None => "none",
Self::PopOn => "pop_on",
Self::RollUp(_) => "roll_up",
Self::PaintOn => "paint_on",
Self::Text => "text",
}
}
}
dvb_common::impl_spec_display!(Cea608Mode);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Cea608Channel {
Cc1,
Cc2,
Cc3,
Cc4,
}
impl Cea608Channel {
#[must_use]
fn index(self) -> usize {
match self {
Self::Cc1 => 0,
Self::Cc2 => 1,
Self::Cc3 => 2,
Self::Cc4 => 3,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Cc1 => "cc1",
Self::Cc2 => "cc2",
Self::Cc3 => "cc3",
Self::Cc4 => "cc4",
}
}
}
dvb_common::impl_spec_display!(Cea608Channel);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Cea608StyledChar {
pub ch: char,
pub underline: bool,
pub italics: bool,
pub color: Cea608Color,
}
impl Default for Cea608StyledChar {
fn default() -> Self {
Cea608StyledChar {
ch: ' ',
underline: false,
italics: false,
color: Cea608Color::White,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Cea608Row {
cells: Vec<(usize, Cea608StyledChar)>,
}
impl Cea608Row {
fn set(&mut self, col: usize, c: Cea608StyledChar) {
if let Some(slot) = self.cells.iter_mut().find(|(i, _)| *i == col) {
slot.1 = c;
} else {
self.cells.push((col, c));
}
}
fn clear_from(&mut self, col: usize) {
self.cells.retain(|(i, _)| *i < col);
}
fn remove(&mut self, col: usize) {
self.cells.retain(|(i, _)| *i != col);
}
#[must_use]
pub fn text(&self) -> String {
let mut sorted = self.cells.clone();
sorted.sort_by_key(|(i, _)| *i);
let mut out = String::new();
let mut last = None;
for (i, c) in sorted {
if let Some(prev) = last {
for _ in (prev + 1)..i {
out.push(' ');
}
}
out.push(c.ch);
last = Some(i);
}
out
}
#[must_use]
pub fn styled_cells(&self) -> Vec<(usize, Cea608StyledChar)> {
let mut sorted = self.cells.clone();
sorted.sort_by_key(|(i, _)| *i);
sorted
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Cea608Screen {
rows: [Cea608Row; SCREEN_ROWS],
}
impl Cea608Screen {
#[must_use]
pub fn text(&self) -> String {
let mut out = String::new();
for row in &self.rows {
let line = row.text();
let trimmed = line.trim_end();
if trimmed.is_empty() {
continue;
}
if !out.is_empty() {
out.push('\n');
}
out.push_str(trimmed);
}
out
}
#[must_use]
pub fn rows(&self) -> &[Cea608Row; SCREEN_ROWS] {
&self.rows
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Pen {
underline: bool,
italics: bool,
color: Cea608Color,
}
impl Default for Pen {
fn default() -> Self {
Pen {
underline: false,
italics: false,
color: Cea608Color::White,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct ChannelState {
displayed: Cea608Screen,
nondisplayed: Cea608Screen,
mode: Cea608Mode,
rollup_rows: u8,
cursor_row: usize,
cursor_col: usize,
pen: Pen,
}
impl ChannelState {
fn active_mut(&mut self) -> &mut Cea608Screen {
match self.mode {
Cea608Mode::PopOn => &mut self.nondisplayed,
_ => &mut self.displayed,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cea608Decoder {
channels: [ChannelState; 4],
last_control: [Option<(u8, u8)>; 2],
xds_active: bool,
}
impl Default for Cea608Decoder {
fn default() -> Self {
Self::new()
}
}
impl Cea608Decoder {
#[must_use]
pub fn new() -> Self {
Cea608Decoder {
channels: Default::default(),
last_control: [None, None],
xds_active: false,
}
}
pub fn push_triplets<'a, I>(&mut self, triplets: I)
where
I: IntoIterator<Item = &'a CcTriplet>,
{
for t in triplets {
if !t.cc_valid {
continue;
}
let field2 = match t.cc_type {
CcType::Ntsc608Field1 => false,
CcType::Ntsc608Field2 => true,
_ => continue,
};
self.process_pair(field2, t.cc_data_1, t.cc_data_2);
}
}
pub fn push_pair(&mut self, field2: bool, b1: u8, b2: u8) {
self.process_pair(field2, b1, b2);
}
fn process_pair(&mut self, field2: bool, raw1: u8, raw2: u8) {
let b1 = raw1 & 0x7F;
let b2 = raw2 & 0x7F;
let field_idx = usize::from(field2);
if b1 == 0x00 && b2 == 0x00 {
return;
}
if field2 && (0x01..=0x0F).contains(&b1) {
self.xds_active = b1 != 0x0F;
return;
}
if field2 && self.xds_active {
return;
}
if (0x10..=0x1F).contains(&b1) {
if self.last_control[field_idx] == Some((b1, b2)) {
self.last_control[field_idx] = None; return;
}
self.last_control[field_idx] = Some((b1, b2));
self.xds_active = false;
self.handle_control(field2, b1, b2);
return;
}
self.last_control[field_idx] = None;
self.xds_active = false;
let ch = self.default_channel(field2);
if b1 >= 0x20 {
self.put_char(ch, Self::standard_char(b1));
}
if b2 >= 0x20 {
self.put_char(ch, Self::standard_char(b2));
}
}
fn handle_control(&mut self, field2: bool, b1: u8, b2: u8) {
let c2 = b1 >= 0x18;
let base1 = if c2 { b1 - 0x08 } else { b1 }; let ch = self.channel_for(field2, c2);
if base1 == 0x14 && (0x20..=0x2F).contains(&b2) {
self.misc_control(ch, b2);
return;
}
if (b1 == TAB_FIRST_C1 || b1 == TAB_FIRST_C2) && (TAB1..=TAB3).contains(&b2) {
let n = (b2 - TAB1 + 1) as usize;
let st = &mut self.channels[ch.index()];
st.cursor_col = (st.cursor_col + n).min(SCREEN_COLS - 1);
return;
}
if base1 == 0x11 && (0x20..=0x2F).contains(&b2) {
self.mid_row(ch, b2);
return;
}
if base1 == 0x11 && (0x30..=0x3F).contains(&b2) {
self.put_char(ch, Self::special_char(b2));
return;
}
if base1 == 0x12 && (0x20..=0x3F).contains(&b2) {
self.put_extended(ch, Self::extended_char_block1(b2));
return;
}
if base1 == 0x13 && (0x20..=0x3F).contains(&b2) {
self.put_extended(ch, Self::extended_char_block2(b2));
return;
}
if (0x40..=0x7F).contains(&b2) {
self.pac(field2, ch, b1, b2);
}
}
fn misc_control(&mut self, ch: Cea608Channel, code: u8) {
let st = &mut self.channels[ch.index()];
match code {
MC_RCL => {
st.mode = Cea608Mode::PopOn;
st.nondisplayed = Cea608Screen::default();
st.cursor_row = 0;
st.cursor_col = 0;
}
MC_RU2 | MC_RU3 | MC_RU4 => {
let rows = match code {
MC_RU2 => 2,
MC_RU3 => 3,
_ => 4,
};
st.mode = Cea608Mode::RollUp(rows);
st.rollup_rows = rows;
st.cursor_row = SCREEN_ROWS - 1;
st.cursor_col = 0;
st.pen = Pen::default();
}
MC_RDC => {
st.mode = Cea608Mode::PaintOn;
}
MC_CR => {
self.carriage_return(ch);
}
MC_EDM => {
st.displayed = Cea608Screen::default();
}
MC_ENM => {
st.nondisplayed = Cea608Screen::default();
}
MC_EOC => {
core::mem::swap(&mut st.displayed, &mut st.nondisplayed);
}
MC_BS if st.cursor_col > 0 => {
st.cursor_col -= 1;
let row = st.cursor_row.min(SCREEN_ROWS - 1);
let col = st.cursor_col;
st.active_mut().rows[row].remove(col);
}
MC_DER => {
let row = st.cursor_row.min(SCREEN_ROWS - 1);
let col = st.cursor_col;
st.active_mut().rows[row].clear_from(col);
}
MC_TR => {
st.mode = Cea608Mode::Text;
st.displayed = Cea608Screen::default();
st.cursor_row = 0;
st.cursor_col = 0;
}
MC_RTD => {
st.mode = Cea608Mode::Text;
}
MC_FON => {}
_ => {}
}
}
fn carriage_return(&mut self, ch: Cea608Channel) {
let st = &mut self.channels[ch.index()];
if let Cea608Mode::RollUp(rows) = st.mode {
let base = st.cursor_row.min(SCREEN_ROWS - 1);
let rows = rows as usize;
let top = base.saturating_sub(rows - 1);
for r in top..base {
st.displayed.rows[r] = st.displayed.rows[r + 1].clone();
}
st.displayed.rows[base] = Cea608Row::default();
st.cursor_col = 0;
} else {
if st.cursor_row + 1 < SCREEN_ROWS {
st.cursor_row += 1;
}
st.cursor_col = 0;
}
}
fn mid_row(&mut self, ch: Cea608Channel, b2: u8) {
let idx = (b2 - 0x20) as usize; let underline = idx & 0x01 != 0;
let color_idx = (idx >> 1) as u8;
let (color, italics) = if color_idx <= 6 {
(Cea608Color::from_idx(color_idx), false)
} else {
(Cea608Color::White, true) };
let st = &mut self.channels[ch.index()];
st.pen = Pen {
underline,
italics,
color,
};
self.put_char(ch, ' ');
}
fn pac(&mut self, _field2: bool, ch: Cea608Channel, b1: u8, b2: u8) {
let f = if b1 >= 0x18 { b1 - 0x08 } else { b1 };
let row_pair = match f {
0x11 => 1, 0x12 => 3, 0x15 => 5, 0x16 => 7, 0x17 => 9, 0x10 => 11, 0x13 => 12, 0x14 => 14, _ => 1,
};
let second_of_pair = b2 >= 0x60;
let row = if f == 0x10 {
11 } else if second_of_pair {
row_pair + 1
} else {
row_pair
};
let row = row.clamp(1, SCREEN_ROWS); let attr = b2 & 0x1F; let underline = attr & 0x01 != 0;
let (color, italics, indent) = if attr >= 0x10 {
let indent = ((attr - 0x10) >> 1) as usize * 4;
(Cea608Color::White, false, indent)
} else {
let color_idx = attr >> 1;
let (c, it) = if color_idx <= 6 {
(Cea608Color::from_idx(color_idx), false)
} else {
(Cea608Color::White, true)
};
(c, it, 0)
};
let st = &mut self.channels[ch.index()];
st.cursor_row = (row - 1).min(SCREEN_ROWS - 1);
st.cursor_col = indent.min(SCREEN_COLS - 1);
st.pen = Pen {
underline,
italics,
color,
};
}
fn put_char(&mut self, ch: Cea608Channel, c: char) {
let st = &mut self.channels[ch.index()];
let row = st.cursor_row.min(SCREEN_ROWS - 1);
let col = st.cursor_col;
if col >= SCREEN_COLS {
return;
}
let pen = st.pen;
let styled = Cea608StyledChar {
ch: c,
underline: pen.underline,
italics: pen.italics,
color: pen.color,
};
st.active_mut().rows[row].set(col, styled);
st.cursor_col = (col + 1).min(SCREEN_COLS);
}
fn put_extended(&mut self, ch: Cea608Channel, c: char) {
{
let st = &mut self.channels[ch.index()];
if st.cursor_col > 0 {
st.cursor_col -= 1;
let row = st.cursor_row.min(SCREEN_ROWS - 1);
let col = st.cursor_col;
st.active_mut().rows[row].remove(col);
}
}
self.put_char(ch, c);
}
fn channel_for(&self, field2: bool, c2: bool) -> Cea608Channel {
match (field2, c2) {
(false, false) => Cea608Channel::Cc1,
(false, true) => Cea608Channel::Cc2,
(true, false) => Cea608Channel::Cc3,
(true, true) => Cea608Channel::Cc4,
}
}
fn default_channel(&self, field2: bool) -> Cea608Channel {
if field2 {
Cea608Channel::Cc3
} else {
Cea608Channel::Cc1
}
}
#[must_use]
pub fn screen(&self, channel: Cea608Channel) -> &Cea608Screen {
&self.channels[channel.index()].displayed
}
#[must_use]
pub fn mode(&self, channel: Cea608Channel) -> Cea608Mode {
self.channels[channel.index()].mode
}
#[must_use]
pub fn channel_text(&self, channel: Cea608Channel) -> String {
self.channels[channel.index()].displayed.text()
}
fn standard_char(b: u8) -> char {
match b {
0x2A => '\u{00E1}', 0x5C => '\u{00E9}', 0x5E => '\u{00ED}', 0x5F => '\u{00F3}', 0x60 => '\u{00FA}', 0x7B => '\u{00E7}', 0x7C => '\u{00F7}', 0x7D => '\u{00D1}', 0x7E => '\u{00F1}', 0x7F => '\u{25A0}', _ => char::from(b), }
}
fn special_char(b2: u8) -> char {
match b2 {
0x30 => '\u{00AE}', 0x31 => '\u{00B0}', 0x32 => '\u{00BD}', 0x33 => '\u{00BF}', 0x34 => '\u{2122}', 0x35 => '\u{00A2}', 0x36 => '\u{00A3}', 0x37 => '\u{266A}', 0x38 => '\u{00E0}', 0x39 => ' ', 0x3A => '\u{00E8}', 0x3B => '\u{00E2}', 0x3C => '\u{00EA}', 0x3D => '\u{00EE}', 0x3E => '\u{00F4}', 0x3F => '\u{00FB}', _ => '?',
}
}
fn extended_char_block1(b2: u8) -> char {
match b2 {
0x20 => '\u{00C1}', 0x21 => '\u{00C9}', 0x22 => '\u{00D3}', 0x23 => '\u{00DA}', 0x24 => '\u{00DC}', 0x25 => '\u{00FC}', 0x26 => '\u{2018}', 0x27 => '\u{00A1}', 0x28 => '*',
0x29 => '\'',
0x2A => '\u{2014}', 0x2B => '\u{00A9}', 0x2C => '\u{2120}', 0x2D => '\u{2022}', 0x2E => '\u{201C}', 0x2F => '\u{201D}', 0x30 => '\u{00C0}', 0x31 => '\u{00C2}', 0x32 => '\u{00C7}', 0x33 => '\u{00C8}', 0x34 => '\u{00CA}', 0x35 => '\u{00CB}', 0x36 => '\u{00EB}', 0x37 => '\u{00CE}', 0x38 => '\u{00CF}', 0x39 => '\u{00EF}', 0x3A => '\u{00D4}', 0x3B => '\u{00D9}', 0x3C => '\u{00F9}', 0x3D => '\u{00DB}', 0x3E => '\u{00AB}', 0x3F => '\u{00BB}', _ => '?',
}
}
fn extended_char_block2(b2: u8) -> char {
match b2 {
0x20 => '\u{00C3}', 0x21 => '\u{00E3}', 0x22 => '\u{00CD}', 0x23 => '\u{00CC}', 0x24 => '\u{00EC}', 0x25 => '\u{00D2}', 0x26 => '\u{00F2}', 0x27 => '\u{00D5}', 0x28 => '\u{00F5}', 0x29 => '{',
0x2A => '}',
0x2B => '\\',
0x2C => '^',
0x2D => '_',
0x2E => '|',
0x2F => '~',
0x30 => '\u{00C4}', 0x31 => '\u{00E4}', 0x32 => '\u{00D6}', 0x33 => '\u{00F6}', 0x34 => '\u{00DF}', 0x35 => '\u{00A5}', 0x36 => '\u{00A4}', 0x37 => '|',
0x38 => '\u{00C5}', 0x39 => '\u{00E5}', 0x3A => '\u{00D8}', 0x3B => '\u{00F8}', 0x3C => '\u{231C}', 0x3D => '\u{231D}', 0x3E => '\u{231E}', 0x3F => '\u{231F}', _ => '?',
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn par(v: u8) -> u8 {
let ones = (v & 0x7F).count_ones();
if ones % 2 == 0 {
v | 0x80
} else {
v & 0x7F
}
}
#[test]
fn pop_on_caption() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x14), par(0x20)); dec.push_pair(false, par(0x14), par(0x70)); dec.push_pair(false, par(b'H'), par(b'I'));
dec.push_pair(false, par(0x14), par(0x2F)); assert_eq!(dec.channel_text(Cea608Channel::Cc1), "HI");
assert_eq!(dec.mode(Cea608Channel::Cc1), Cea608Mode::PopOn);
}
#[test]
fn control_doubling() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x14), par(0x29)); dec.push_pair(false, par(0x14), par(0x29)); assert_eq!(dec.mode(Cea608Channel::Cc1), Cea608Mode::PaintOn);
dec.push_pair(false, par(b'X'), par(0x00));
dec.push_pair(false, par(0x14), par(0x2C)); }
#[test]
fn roll_up_two_rows() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x14), par(0x25)); dec.push_pair(false, par(b'A'), par(b'B'));
dec.push_pair(false, par(0x14), par(0x2D)); dec.push_pair(false, par(b'C'), par(b'D'));
let text = dec.channel_text(Cea608Channel::Cc1);
assert!(text.contains("AB"), "got {text:?}");
assert!(text.contains("CD"), "got {text:?}");
let ab = text.find("AB").unwrap();
let cd = text.find("CD").unwrap();
assert!(ab < cd, "AB should be above CD: {text:?}");
}
#[test]
fn mid_row_colour() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x14), par(0x20)); dec.push_pair(false, par(0x14), par(0x70)); dec.push_pair(false, par(b'A'), par(0x00));
dec.push_pair(false, par(0x11), par(0x22)); dec.push_pair(false, par(b'B'), par(0x00));
dec.push_pair(false, par(0x14), par(0x2F)); let screen = dec.screen(Cea608Channel::Cc1);
let mut found_green_b = false;
for row in screen.rows() {
for (_, c) in row.styled_cells() {
if c.ch == 'B' {
assert_eq!(c.color, Cea608Color::Green);
found_green_b = true;
}
}
}
assert!(found_green_b, "expected a green 'B'");
}
#[test]
fn special_char_music_note() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x14), par(0x29)); dec.push_pair(false, par(0x11), par(0x37)); assert!(dec.channel_text(Cea608Channel::Cc1).contains('\u{266A}'));
}
#[test]
fn extended_char_backspace() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x14), par(0x29)); dec.push_pair(false, par(b'u'), par(0x00)); dec.push_pair(false, par(0x12), par(0x25)); let t = dec.channel_text(Cea608Channel::Cc1);
assert_eq!(t, "\u{00FC}"); }
#[test]
fn channel_cc2() {
let mut dec = Cea608Decoder::new();
dec.push_pair(false, par(0x1C), par(0x29)); assert_eq!(dec.mode(Cea608Channel::Cc2), Cea608Mode::PaintOn);
assert_eq!(dec.mode(Cea608Channel::Cc1), Cea608Mode::None);
}
#[test]
fn field2_cc3() {
let mut dec = Cea608Decoder::new();
dec.push_pair(true, par(0x14), par(0x29)); assert_eq!(dec.mode(Cea608Channel::Cc3), Cea608Mode::PaintOn);
}
#[test]
fn xds_skipped() {
let mut dec = Cea608Decoder::new();
dec.push_pair(true, par(0x01), par(0x02)); dec.push_pair(true, par(0x20), par(0x21)); dec.push_pair(true, par(0x0F), par(0x40)); assert_eq!(dec.channel_text(Cea608Channel::Cc3), "");
}
#[test]
fn standard_char_accents() {
assert_eq!(Cea608Decoder::standard_char(0x2A), '\u{00E1}'); assert_eq!(Cea608Decoder::standard_char(b'A'), 'A');
assert_eq!(Cea608Decoder::standard_char(0x7F), '\u{25A0}'); }
#[test]
fn no_panic_on_arbitrary_input() {
let mut dec = Cea608Decoder::new();
let mut x: u32 = 0xDEAD_BEEF;
for _ in 0..8192 {
x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
let b1 = (x >> 8) as u8;
let b2 = (x >> 16) as u8;
let f2 = (x & 1) != 0;
dec.push_pair(f2, b1, b2);
}
let triplets = [
CcTriplet {
cc_valid: true,
cc_type: CcType::Ntsc608Field1,
cc_data_1: 0x14,
cc_data_2: 0x2D,
},
CcTriplet {
cc_valid: false,
cc_type: CcType::Ntsc608Field2,
cc_data_1: 0xFF,
cc_data_2: 0xFF,
},
CcTriplet {
cc_valid: true,
cc_type: CcType::Ntsc608Field2,
cc_data_1: 0x01,
cc_data_2: 0x00,
},
];
dec.push_triplets(&triplets);
}
}