use crate::style::{Alignment, Color, Position};
use crate::{Subtitle, SubtitleError, SubtitleResult, SubtitleStyle};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608Mode {
PopOn,
RollUp2,
RollUp3,
RollUp4,
PaintOn,
}
pub struct Cea608Decoder {
mode: Cea608Mode,
buffer: String,
display: String,
row: u8,
column: u8,
style: Cea608Style,
pending_pts_ms: i64,
last_control: Option<(u8, u8)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608Style {
White,
Green,
Blue,
Cyan,
Red,
Yellow,
Magenta,
Italic,
}
impl Cea608Style {
#[must_use]
pub const fn color(&self) -> Color {
match self {
Self::White | Self::Italic => Color::white(),
Self::Green => Color::rgb(0, 255, 0),
Self::Blue => Color::rgb(0, 0, 255),
Self::Cyan => Color::rgb(0, 255, 255),
Self::Red => Color::rgb(255, 0, 0),
Self::Yellow => Color::rgb(255, 255, 0),
Self::Magenta => Color::rgb(255, 0, 255),
}
}
#[must_use]
fn from_style_nibble(nibble: u8) -> Self {
match nibble & 0x0E {
0x00 => Self::White,
0x02 => Self::Green,
0x04 => Self::Blue,
0x06 => Self::Cyan,
0x08 => Self::Red,
0x0A => Self::Yellow,
0x0C => Self::Magenta,
0x0E => Self::Italic,
_ => Self::White,
}
}
}
const SPECIAL_CHARS: [&str; 32] = [
"®", "°", "½", "¿", "™", "¢", "£", "♪", "à", " ", "è", "â", "ê", "î", "ô", "û", "Á", "É", "Ó", "Ú", "Ü", "ü", "'", "¡", "*", "'", "—", "©", "℠", "•", "\"", "\"", ];
impl Default for Cea608Decoder {
fn default() -> Self {
Self::new()
}
}
impl Cea608Decoder {
#[must_use]
pub fn new() -> Self {
Self {
mode: Cea608Mode::PopOn,
buffer: String::new(),
display: String::new(),
row: 15,
column: 0,
style: Cea608Style::White,
pending_pts_ms: 0,
last_control: None,
}
}
pub fn decode_pair_with_pts(
&mut self,
byte1: u8,
byte2: u8,
pts_ms: i64,
) -> SubtitleResult<Option<Subtitle>> {
let b1 = byte1 & 0x7F;
let b2 = byte2 & 0x7F;
if (0x10..=0x1F).contains(&b1) || b1 == 0x00 {
if let Some(prev) = self.last_control {
if prev == (b1, b2) {
self.last_control = None;
return Ok(None);
}
}
self.last_control = Some((b1, b2));
} else {
self.last_control = None;
}
self.pending_pts_ms = pts_ms;
if b1 == 0x00 && b2 == 0x00 {
return Ok(None);
}
if (0x10..=0x1F).contains(&b1) {
return self.decode_control(b1, b2, pts_ms);
}
if (0x20..=0x7F).contains(&b1) {
self.add_char(b1 as char);
}
if (0x20..=0x7F).contains(&b2) {
self.add_char(b2 as char);
}
Ok(None)
}
pub fn decode_pair(&mut self, byte1: u8, byte2: u8) -> SubtitleResult<Option<Subtitle>> {
self.decode_pair_with_pts(byte1, byte2, 0)
}
fn decode_control(&mut self, b1: u8, b2: u8, pts_ms: i64) -> SubtitleResult<Option<Subtitle>> {
let b1_base = b1 & 0x07;
if (b1 == 0x11 || b1 == 0x19) && (0x20..=0x3F).contains(&b2) {
return self.decode_special(b1, b2);
}
if (b1 == 0x12 || b1 == 0x1A || b1 == 0x13 || b1 == 0x1B) && (0x20..=0x3F).contains(&b2) {
let _ = match b1 {
0x12 | 0x1A => self.decode_extended_set1(b2),
0x13 | 0x1B => self.decode_extended_set2(b2),
_ => Ok(()),
};
return Ok(None);
}
if (b1 & 0x70) != 0x10 {
return Ok(None);
}
if (0x40..=0x7F).contains(&b2) {
self.decode_pac(b1, b2);
return Ok(None);
}
if (b1 == 0x11 || b1 == 0x19) && (0x20..=0x2F).contains(&b2) {
self.style = Cea608Style::from_style_nibble(b2);
return Ok(None);
}
if b1 == 0x14 || b1 == 0x1C {
return self.decode_misc_control(b2, pts_ms);
}
if (b1 == 0x17 || b1 == 0x1F) && (0x21..=0x23).contains(&b2) {
let tab = b2 - 0x20;
self.column = self.column.saturating_add(tab);
return Ok(None);
}
Ok(None)
}
fn decode_misc_control(&mut self, b2: u8, pts_ms: i64) -> SubtitleResult<Option<Subtitle>> {
match b2 {
0x20 => {
self.mode = Cea608Mode::PopOn;
}
0x21 => {
self.buffer.pop();
if self.column > 0 {
self.column -= 1;
}
}
0x24 => {
}
0x25 => {
self.mode = Cea608Mode::RollUp2;
self.buffer.clear();
}
0x26 => {
self.mode = Cea608Mode::RollUp3;
self.buffer.clear();
}
0x27 => {
self.mode = Cea608Mode::RollUp4;
self.buffer.clear();
}
0x28 => {}
0x29 => {
self.mode = Cea608Mode::PaintOn;
}
0x2A => {
self.buffer.clear();
}
0x2B => {}
0x2C => {
self.display.clear();
}
0x2D => {
let result = self.carriage_return(pts_ms);
return Ok(result);
}
0x2E => {
self.buffer.clear();
}
0x2F => {
let result = self.end_of_caption(pts_ms);
return Ok(result);
}
_ => {}
}
Ok(None)
}
fn decode_pac(&mut self, b1: u8, b2: u8) {
let row_bits = ((b1 & 0x07) << 1) | ((b2 & 0x20) >> 5);
self.row = Self::pac_row(row_bits);
self.column = 0;
if (b2 & 0x10) != 0 {
self.style = Cea608Style::White;
} else {
self.style = Cea608Style::from_style_nibble(b2);
}
}
const fn pac_row(bits: u8) -> u8 {
match bits & 0x0F {
0x00 => 11,
0x01 => 1,
0x02 => 3,
0x03 => 12,
0x04 => 14,
0x05 => 5,
0x06 => 7,
0x07 => 9,
0x08 => 11,
0x09 => 1,
0x0A => 3,
0x0B => 12,
0x0C => 14,
0x0D => 5,
0x0E => 7,
0x0F => 9,
_ => 15,
}
}
fn decode_special(&mut self, _b1: u8, b2: u8) -> SubtitleResult<Option<Subtitle>> {
let idx = (b2 as usize).saturating_sub(0x20);
if let Some(ch) = SPECIAL_CHARS.get(idx) {
self.buffer.push_str(ch);
self.column = self.column.saturating_add(1);
}
Ok(None)
}
fn decode_extended_set1(&mut self, b2: u8) -> SubtitleResult<()> {
self.buffer.pop();
let ch = Self::extended_char_set1(b2);
self.buffer.push(ch);
Ok(())
}
fn decode_extended_set2(&mut self, b2: u8) -> SubtitleResult<()> {
self.buffer.pop();
let ch = Self::extended_char_set2(b2);
self.buffer.push(ch);
Ok(())
}
fn extended_char_set1(b2: u8) -> char {
match b2 {
0x20 => 'Á',
0x21 => 'É',
0x22 => 'Ó',
0x23 => 'Ú',
0x24 => 'Ü',
0x25 => 'ü',
0x26 => '\'',
0x27 => '¡',
0x28 => '*',
0x29 => '\'',
0x2A => '—',
0x2B => '©',
0x2C => '℠',
0x2D => '•',
0x2E => '"',
0x2F => '"',
0x30 => 'Â',
0x31 => 'â',
0x32 => 'Ä',
0x33 => 'ä',
0x34 => 'À',
0x35 => 'à',
0x36 => 'Å',
0x37 => 'å',
0x38 => 'Ç',
0x39 => 'ç',
0x3A => 'È',
0x3B => 'è',
0x3C => 'Ê',
0x3D => 'ê',
0x3E => 'Ë',
0x3F => 'ë',
_ => ' ',
}
}
fn extended_char_set2(b2: u8) -> char {
match b2 {
0x20 => 'Î',
0x21 => 'î',
0x22 => 'Ï',
0x23 => 'ï',
0x24 => 'Ô',
0x25 => 'ô',
0x26 => 'Ö',
0x27 => 'ö',
0x28 => 'Û',
0x29 => 'û',
0x2A => 'Ù',
0x2B => 'ù',
0x2C => 'Ÿ',
0x2D => 'ÿ',
0x2E => 'Ñ',
0x2F => 'ñ',
0x30 => '|',
0x31 => 'Ä',
0x32 => 'ä',
0x33 => 'Ö',
0x34 => 'ö',
0x35 => 'ß',
0x36 => '¥',
0x37 => '¤',
0x38 => '|',
0x39 => 'Å',
0x3A => 'å',
0x3B => 'Ø',
0x3C => 'ø',
0x3D => '⌐',
0x3E => '¬',
0x3F => '+',
_ => ' ',
}
}
fn add_char(&mut self, c: char) {
match self.mode {
Cea608Mode::PopOn => {
self.buffer.push(c);
}
Cea608Mode::RollUp2
| Cea608Mode::RollUp3
| Cea608Mode::RollUp4
| Cea608Mode::PaintOn => {
self.display.push(c);
}
}
self.column = self.column.saturating_add(1);
}
fn end_of_caption(&mut self, pts_ms: i64) -> Option<Subtitle> {
if self.buffer.is_empty() {
return None;
}
std::mem::swap(&mut self.buffer, &mut self.display);
self.buffer.clear();
self.build_subtitle(&self.display.clone(), pts_ms, pts_ms + 3000)
}
fn carriage_return(&mut self, pts_ms: i64) -> Option<Subtitle> {
let text = self.display.trim().to_string();
if text.is_empty() {
return None;
}
self.display.clear();
self.column = 0;
self.build_subtitle(&text, pts_ms, pts_ms + 3000)
}
fn build_subtitle(&self, text: &str, start_ms: i64, end_ms: i64) -> Option<Subtitle> {
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
let mut subtitle = Subtitle::new(start_ms, end_ms, trimmed.to_string());
let mut style = SubtitleStyle::default();
style.primary_color = self.style.color();
style.position = Position::bottom_center();
subtitle.style = Some(style);
Some(subtitle)
}
pub fn reset(&mut self) {
self.buffer.clear();
self.display.clear();
self.row = 15;
self.column = 0;
self.style = Cea608Style::White;
self.pending_pts_ms = 0;
self.last_control = None;
}
#[must_use]
pub fn display(&self) -> &str {
&self.display
}
}
#[derive(Clone, Debug, Default)]
struct Cea708Window {
text: String,
visible: bool,
row_count: u8,
column_count: u8,
}
pub struct Cea708Decoder {
current_window: u8,
windows: [Cea708Window; 8],
}
impl Default for Cea708Decoder {
fn default() -> Self {
Self::new()
}
}
impl Cea708Decoder {
#[must_use]
pub fn new() -> Self {
Self {
current_window: 0,
windows: Default::default(),
}
}
pub fn decode_service_block(&mut self, data: &[u8]) -> SubtitleResult<Option<Vec<Subtitle>>> {
let mut results: Vec<Subtitle> = Vec::new();
let mut i = 0;
while i < data.len() {
let cmd = data[i];
i += 1;
match cmd {
0x00 => {} 0x03 => {
let win = &self.windows[self.current_window as usize];
if !win.text.is_empty() {
let subtitle = Subtitle::new(0, 3000, win.text.trim().to_string());
results.push(subtitle);
}
}
0x08 => {
self.windows[self.current_window as usize].text.pop();
}
0x0C => {
self.windows[self.current_window as usize].text.clear();
}
0x0D => {
self.windows[self.current_window as usize].text.push('\n');
}
0x0E => {
}
0x80..=0x87 => {
self.current_window = cmd - 0x80;
}
0x88 => {
if i < data.len() {
let mask = data[i];
i += 1;
for bit in 0..8u8 {
if (mask >> bit) & 1 != 0 {
self.windows[bit as usize].text.clear();
}
}
}
}
0x89 => {
if i < data.len() {
let mask = data[i];
i += 1;
for bit in 0..8u8 {
if (mask >> bit) & 1 != 0 {
self.windows[bit as usize].visible = true;
}
}
}
}
0x8A => {
if i < data.len() {
let mask = data[i];
i += 1;
for bit in 0..8u8 {
if (mask >> bit) & 1 != 0 {
self.windows[bit as usize].visible = false;
}
}
}
}
0x8B => {
if i < data.len() {
let mask = data[i];
i += 1;
for bit in 0..8u8 {
if (mask >> bit) & 1 != 0 {
let w = &mut self.windows[bit as usize];
w.visible = !w.visible;
}
}
}
}
0x8C => {
if i < data.len() {
let mask = data[i];
i += 1;
for bit in 0..8u8 {
if (mask >> bit) & 1 != 0 {
self.windows[bit as usize] = Cea708Window::default();
}
}
}
}
0x8D => {
if i < data.len() {
i += 1;
}
}
0x8E => {
}
0x8F => {
self.reset();
}
0x90..=0x9F => {
let param_count: usize = match cmd {
0x90 => 2, 0x91 => 2, 0x92 => 2, 0x97 => 4, 0x98..=0x9F => 6, _ => 0,
};
for _ in 0..param_count {
if i < data.len() {
i += 1;
}
}
}
0x20..=0x7F => {
let c = cmd as char;
self.windows[self.current_window as usize].text.push(c);
}
0xA0..=0xFF => {
let c = char::from(cmd);
self.windows[self.current_window as usize].text.push(c);
}
_ => {}
}
}
if results.is_empty() {
Ok(None)
} else {
Ok(Some(results))
}
}
pub fn reset(&mut self) {
self.current_window = 0;
for window in &mut self.windows {
*window = Cea708Window::default();
}
}
#[must_use]
pub fn window_text(&self, window: u8) -> &str {
self.windows
.get(window as usize)
.map(|w| w.text.as_str())
.unwrap_or("")
}
}
pub fn extract_cea608_from_user_data(user_data: &[u8]) -> SubtitleResult<Vec<(u8, u8)>> {
let mut pairs = Vec::new();
let mut i = 0;
while i + 2 < user_data.len() {
let marker = user_data[i];
if marker == 0xFC || marker == 0xFD || marker == 0xCC {
if i + 2 < user_data.len() {
let byte1 = user_data[i + 1];
let byte2 = user_data[i + 2];
pairs.push((byte1, byte2));
}
i += 3;
} else {
i += 1;
}
}
Ok(pairs)
}
pub fn extract_cea708_from_user_data(user_data: &[u8]) -> SubtitleResult<Vec<u8>> {
if user_data.len() < 8 {
return Ok(user_data.to_vec());
}
let mut output = Vec::new();
let mut i = 0;
while i + 2 < user_data.len() {
let marker = user_data[i];
let cc_valid = (marker & 0x04) != 0;
let cc_type = marker & 0x03;
if cc_valid && (cc_type == 0x02 || cc_type == 0x03) {
output.push(user_data[i + 1]);
output.push(user_data[i + 2]);
}
i += 3;
}
if output.is_empty() {
Ok(user_data.to_vec())
} else {
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cea608_pop_on_basic() {
let mut dec = Cea608Decoder::new();
dec.decode_pair(b'H', b'i').expect("decode");
dec.decode_pair(b'!', 0x80).expect("decode");
let sub = dec.decode_pair(0x14, 0x2F).expect("decode");
assert!(sub.is_some(), "End of Caption should emit subtitle");
let s = sub.expect("subtitle");
assert!(
s.text.contains("Hi!"),
"Expected 'Hi!' in text, got: {}",
s.text
);
}
#[test]
fn test_cea608_roll_up_carriage_return() {
let mut dec = Cea608Decoder::new();
dec.decode_pair(0x14, 0x25).expect("decode");
dec.decode_pair(b'O', b'K').expect("decode");
let sub = dec.decode_pair(0x14, 0x2D).expect("decode");
assert!(
sub.is_some(),
"Carriage return in roll-up should emit subtitle"
);
}
#[test]
fn test_cea608_erase_displayed_memory() {
let mut dec = Cea608Decoder::new();
dec.decode_pair(b'A', b'B').expect("decode");
dec.decode_pair(0x14, 0x2F).expect("decode"); assert!(!dec.display().is_empty());
dec.decode_pair(0x14, 0x2C).expect("decode"); assert!(dec.display().is_empty());
}
#[test]
fn test_cea608_special_char_copyright() {
let mut dec = Cea608Decoder::new();
dec.decode_pair(0x11, 0x3B).expect("decode");
dec.decode_pair(0x14, 0x2F).expect("decode");
assert!(dec.display().contains('©'), "Expected © in display");
}
#[test]
fn test_cea608_duplicate_control_code_ignored() {
let mut dec = Cea608Decoder::new();
dec.decode_pair(b'X', 0x00).expect("decode");
dec.decode_pair(0x14, 0x2F).expect("decode");
let result = dec.decode_pair(0x14, 0x2F).expect("decode");
let _ = result;
}
#[test]
fn test_cea608_backspace() {
let mut dec = Cea608Decoder::new();
dec.decode_pair(b'A', b'B').expect("decode");
dec.decode_pair(0x14, 0x21).expect("decode"); dec.decode_pair(0x14, 0x2F).expect("decode");
assert!(dec.display().contains('A'));
assert!(!dec.display().contains('B'));
}
#[test]
fn test_cea708_basic_text() {
let mut dec = Cea708Decoder::new();
let data = b"\x80Hello\x03";
let result = dec.decode_service_block(data).expect("decode");
assert!(result.is_some());
let subs = result.expect("subs");
assert!(!subs.is_empty());
assert!(subs[0].text.contains("Hello"));
}
#[test]
fn test_cea708_clear_window() {
let mut dec = Cea708Decoder::new();
let data = b"\x80Hello\x88\xFF";
dec.decode_service_block(data).expect("decode");
assert!(dec.window_text(0).is_empty(), "Window 0 should be cleared");
}
#[test]
fn test_extract_cea608_from_user_data() {
let data = vec![0xFC, 0x48, 0x69]; let pairs = extract_cea608_from_user_data(&data).expect("extract");
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0], (0x48, 0x69));
}
#[test]
fn test_extract_cea708_from_user_data_dtvcc() {
let data = vec![0x07, 0xAB, 0xCD];
let result = extract_cea708_from_user_data(&data).expect("extract");
assert!(result.contains(&0xAB));
assert!(result.contains(&0xCD));
}
}