use crate::cc_data::{CcTriplet, CcType};
use crate::decode::screen::{
Color, EdgeType, FontStyle, Justify, Opacity, PenOffset, PenSize, PrintDirection,
ScrollDirection,
};
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 AnchorPoint {
#[default]
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
MiddleCenter,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl AnchorPoint {
#[must_use]
pub fn from_bits(v: u8) -> Self {
match v & 0x0F {
0 => Self::TopLeft,
1 => Self::TopCenter,
2 => Self::TopRight,
3 => Self::MiddleLeft,
4 => Self::MiddleCenter,
5 => Self::MiddleRight,
6 => Self::BottomLeft,
7 => Self::BottomCenter,
8 => Self::BottomRight,
_ => Self::TopLeft,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::TopLeft => "top_left",
Self::TopCenter => "top_center",
Self::TopRight => "top_right",
Self::MiddleLeft => "middle_left",
Self::MiddleCenter => "middle_center",
Self::MiddleRight => "middle_right",
Self::BottomLeft => "bottom_left",
Self::BottomCenter => "bottom_center",
Self::BottomRight => "bottom_right",
}
}
}
dvb_common::impl_spec_display!(AnchorPoint);
const NUM_SERVICES: usize = 6;
const NUM_WINDOWS: usize = 8;
const MAX_WINDOW_ROWS: usize = 12;
const MAX_WINDOW_COLS: usize = 42;
const PACKET_SIZE_ZERO_DATA: usize = 127;
const EXTENDED_SERVICE_ESCAPE: u8 = 7;
const C0_NUL: u8 = 0x00;
const C0_ETX: u8 = 0x03;
const C0_BS: u8 = 0x08;
const C0_FF: u8 = 0x0C;
const C0_CR: u8 = 0x0D;
const C0_HCR: u8 = 0x0E;
const C0_EXT1: u8 = 0x10;
const C0_P16: u8 = 0x18;
const C1_CW0: u8 = 0x80; const C1_CW7: u8 = 0x87;
const C1_CLW: u8 = 0x88;
const C1_DSW: u8 = 0x89;
const C1_HDW: u8 = 0x8A;
const C1_TGW: u8 = 0x8B;
const C1_DLW: u8 = 0x8C;
const C1_DLY: u8 = 0x8D;
const C1_DLC: u8 = 0x8E;
const C1_RST: u8 = 0x8F;
const C1_SPA: u8 = 0x90;
const C1_SPC: u8 = 0x91;
const C1_SPL: u8 = 0x92;
const C1_SWA: u8 = 0x97;
const C1_DF0: u8 = 0x98; const C1_DF7: u8 = 0x9F;
const C0_END: u8 = 0x1F;
const G0_START: u8 = 0x20;
const G0_END: u8 = 0x7F;
const C1_START: u8 = 0x80;
const C1_END: u8 = 0x9F;
const G1_START: u8 = 0xA0;
const G0_MUSIC_NOTE: u8 = 0x7F;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum WindowState {
#[default]
Hidden,
Visible,
}
impl WindowState {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Hidden => "hidden",
Self::Visible => "visible",
}
}
}
dvb_common::impl_spec_display!(WindowState);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Window {
pub state: WindowState,
pub priority: u8,
pub anchor_point: AnchorPoint,
pub anchor_vertical: u8,
pub anchor_horizontal: u8,
pub relative_position: bool,
pub row_count: u8,
pub column_count: u8,
pub row_lock: bool,
pub column_lock: bool,
pub window_style: u8,
pub pen_style: u8,
pub justify: Justify,
pub print_direction: PrintDirection,
pub scroll_direction: ScrollDirection,
pub word_wrap: bool,
pub fill_color: Color,
pub fill_opacity: Opacity,
pub border_color: Color,
pub border_type: EdgeType,
pub pen_size: PenSize,
pub pen_offset: PenOffset,
pub font_style: FontStyle,
pub italics: bool,
pub underline: bool,
pub edge_type: EdgeType,
pub fg_color: Color,
pub fg_opacity: Opacity,
pub bg_color: Color,
pub bg_opacity: Opacity,
rows: Vec<String>,
pen_row: usize,
pen_col: usize,
}
impl Window {
fn new() -> Self {
Window {
state: WindowState::Hidden,
priority: 0,
anchor_point: AnchorPoint::TopLeft,
anchor_vertical: 0,
anchor_horizontal: 0,
relative_position: false,
row_count: 1,
column_count: 1,
row_lock: false,
column_lock: false,
window_style: 0,
pen_style: 0,
justify: Justify::Left,
print_direction: PrintDirection::LeftToRight,
scroll_direction: ScrollDirection::BottomToTop,
word_wrap: false,
fill_color: Color::BLACK,
fill_opacity: Opacity::Solid,
border_color: Color::BLACK,
border_type: EdgeType::None,
pen_size: PenSize::Standard,
pen_offset: PenOffset::Normal,
font_style: FontStyle::Default,
italics: false,
underline: false,
edge_type: EdgeType::None,
fg_color: Color::WHITE,
fg_opacity: Opacity::Solid,
bg_color: Color::BLACK,
bg_opacity: Opacity::Solid,
rows: Vec::new(),
pen_row: 0,
pen_col: 0,
}
}
fn ensure_grid(&mut self) {
let rows = (self.row_count as usize).clamp(1, MAX_WINDOW_ROWS);
if self.rows.len() != rows {
self.rows = alloc::vec![String::new(); rows];
}
}
fn clear_text(&mut self) {
for r in &mut self.rows {
r.clear();
}
self.pen_row = 0;
self.pen_col = 0;
}
fn cols(&self) -> usize {
(self.column_count as usize).clamp(1, MAX_WINDOW_COLS)
}
fn put_char(&mut self, ch: char) {
self.ensure_grid();
let cols = self.cols();
if self.pen_row >= self.rows.len() {
return;
}
let row = &mut self.rows[self.pen_row];
while row.chars().count() < self.pen_col {
row.push(' ');
}
if self.pen_col < cols {
row.push(ch);
self.pen_col += 1;
}
}
fn back_space(&mut self) {
if self.pen_col > 0 {
self.pen_col -= 1;
if self.pen_row < self.rows.len() {
let row = &mut self.rows[self.pen_row];
let mut chars: Vec<char> = row.chars().collect();
if self.pen_col < chars.len() {
chars.truncate(self.pen_col);
*row = chars.into_iter().collect();
}
}
}
}
fn carriage_return(&mut self) {
self.ensure_grid();
self.pen_col = 0;
if self.pen_row + 1 < self.rows.len() {
self.pen_row += 1;
} else if !self.rows.is_empty() {
self.rows.remove(0);
self.rows.push(String::new());
self.pen_row = self.rows.len() - 1;
}
}
fn horizontal_cr(&mut self) {
self.ensure_grid();
if self.pen_row < self.rows.len() {
self.rows[self.pen_row].clear();
}
self.pen_col = 0;
}
fn set_pen_location(&mut self, row: usize, col: usize) {
self.ensure_grid();
self.pen_row = row.min(self.rows.len().saturating_sub(1));
self.pen_col = col.min(self.cols());
}
#[must_use]
pub fn text(&self) -> String {
let mut lines: Vec<&str> = self.rows.iter().map(|r| r.trim_end()).collect();
while lines.last().is_some_and(|l| l.is_empty()) {
lines.pop();
}
lines.join("\n")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Service {
windows: [Option<Window>; NUM_WINDOWS],
current_window: Option<usize>,
}
impl Service {
fn reset(&mut self) {
*self = Service::default();
}
fn current(&mut self) -> Option<&mut Window> {
let id = self.current_window?;
self.windows.get_mut(id)?.as_mut()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Cea708Decoder {
services: [Service; NUM_SERVICES],
packet: Vec<u8>,
last_seq: Option<u8>,
}
impl Default for Cea708Decoder {
fn default() -> Self {
Self::new()
}
}
impl Cea708Decoder {
#[must_use]
pub fn new() -> Self {
Cea708Decoder {
services: Default::default(),
packet: Vec::new(),
last_seq: None,
}
}
pub fn reset(&mut self) {
for s in &mut self.services {
s.reset();
}
self.packet.clear();
self.last_seq = None;
}
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;
}
match t.cc_type {
CcType::Dtvcc708Start => {
self.flush_packet();
self.packet.clear();
self.packet.push(t.cc_data_1);
self.packet.push(t.cc_data_2);
}
CcType::Dtvcc708Data => {
self.packet.push(t.cc_data_1);
self.packet.push(t.cc_data_2);
}
_ => {}
}
}
self.flush_packet();
}
pub fn push_packet(&mut self, ccp: &[u8]) {
self.decode_packet(ccp);
}
fn flush_packet(&mut self) {
if self.packet.is_empty() {
return;
}
let packet = core::mem::take(&mut self.packet);
self.decode_packet(&packet);
}
fn decode_packet(&mut self, ccp: &[u8]) {
let Some((&header, rest)) = ccp.split_first() else {
return;
};
let seq = (header >> 6) & 0x03;
let size_code = header & 0x3F;
let data_size = if size_code == 0 {
PACKET_SIZE_ZERO_DATA
} else {
(size_code as usize) * 2 - 1
};
if let Some(prev) = self.last_seq {
if seq != (prev + 1) & 0x03 {
for s in &mut self.services {
s.reset();
}
}
}
self.last_seq = Some(seq);
let end = data_size.min(rest.len());
self.decode_service_blocks(&rest[..end]);
}
fn decode_service_blocks(&mut self, mut data: &[u8]) {
loop {
let Some((&header, rest)) = data.split_first() else {
return;
};
if header == 0 {
return;
}
let mut service_number = u16::from((header >> 5) & 0x07);
let block_size = (header & 0x1F) as usize;
let mut body = rest;
if service_number == u16::from(EXTENDED_SERVICE_ESCAPE) && block_size != 0 {
let Some((&ext, after)) = rest.split_first() else {
return;
};
service_number = u16::from(ext & 0x3F);
body = after;
}
if block_size > body.len() {
self.dispatch_service(service_number, body);
return;
}
let (block, next) = body.split_at(block_size);
self.dispatch_service(service_number, block);
data = next;
}
}
fn dispatch_service(&mut self, service_number: u16, block: &[u8]) {
if service_number == 0 || service_number as usize > NUM_SERVICES {
return;
}
let idx = service_number as usize - 1;
Self::interpret(&mut self.services[idx], block);
}
fn interpret(service: &mut Service, block: &[u8]) {
let mut i = 0usize;
while i < block.len() {
let b = block[i];
let consumed = match b {
0x00..=C0_END => Self::handle_c0(service, &block[i..]),
G0_START..=G0_END => {
Self::put(service, Self::g0_char(b));
1
}
C1_START..=C1_END => Self::handle_c1(service, &block[i..]),
G1_START..=0xFF => {
Self::put(service, char::from(b));
1
}
};
i += consumed.max(1);
}
}
fn handle_c0(service: &mut Service, data: &[u8]) -> usize {
let b = data[0];
match b {
C0_NUL => 1,
C0_ETX => 1,
C0_BS => {
if let Some(w) = service.current() {
w.back_space();
}
1
}
C0_FF => {
if let Some(w) = service.current() {
w.clear_text();
}
1
}
C0_CR => {
if let Some(w) = service.current() {
w.carriage_return();
}
1
}
C0_HCR => {
if let Some(w) = service.current() {
w.horizontal_cr();
}
1
}
C0_EXT1 => Self::handle_ext1(service, data),
C0_P16 => 3, 0x11..=0x17 => 2,
0x19..=0x1F => 3,
_ => 1,
}
}
fn handle_ext1(service: &mut Service, data: &[u8]) -> usize {
let Some(&base) = data.get(1) else {
return 1;
};
match base {
0x00..=0x07 => 2,
0x08..=0x0F => 3,
0x10..=0x17 => 4,
0x18..=0x1F => 5,
0x20..=0x7F => {
Self::put(service, Self::g2_char(base));
2
}
0x80..=0x87 => 6,
0x88..=0x8F => 7,
0x90..=0x9F => {
let n = data.get(2).map_or(0, |d| (d & 0x3F) as usize + 1);
3 + n
}
_ => {
Self::put(service, Self::g3_char(base));
2
}
}
}
fn handle_c1(service: &mut Service, data: &[u8]) -> usize {
let op = data[0];
match op {
C1_CW0..=C1_CW7 => {
let id = (op - C1_CW0) as usize;
if service.windows.get(id).and_then(|w| w.as_ref()).is_some() {
service.current_window = Some(id);
}
1
}
C1_CLW => Self::window_map_cmd(service, data, WindowMapOp::Clear),
C1_DSW => Self::window_map_cmd(service, data, WindowMapOp::Display),
C1_HDW => Self::window_map_cmd(service, data, WindowMapOp::Hide),
C1_TGW => Self::window_map_cmd(service, data, WindowMapOp::Toggle),
C1_DLW => Self::window_map_cmd(service, data, WindowMapOp::Delete),
C1_DLY => 2, C1_DLC => 1, C1_RST => {
service.reset();
1
}
C1_SPA => Self::set_pen_attributes(service, data),
C1_SPC => Self::set_pen_color(service, data),
C1_SPL => Self::set_pen_location(service, data),
C1_SWA => Self::set_window_attributes(service, data),
C1_DF0..=C1_DF7 => Self::define_window(service, data),
_ => 1,
}
}
fn window_map_cmd(service: &mut Service, data: &[u8], op: WindowMapOp) -> usize {
let Some(&map) = data.get(1) else {
return 1;
};
for id in 0..NUM_WINDOWS {
if map & (1 << id) == 0 {
continue;
}
match op {
WindowMapOp::Clear => {
if let Some(w) = service.windows[id].as_mut() {
w.clear_text();
}
}
WindowMapOp::Display => {
if let Some(w) = service.windows[id].as_mut() {
w.state = WindowState::Visible;
}
}
WindowMapOp::Hide => {
if let Some(w) = service.windows[id].as_mut() {
w.state = WindowState::Hidden;
}
}
WindowMapOp::Toggle => {
if let Some(w) = service.windows[id].as_mut() {
w.state = match w.state {
WindowState::Visible => WindowState::Hidden,
WindowState::Hidden => WindowState::Visible,
};
}
}
WindowMapOp::Delete => {
service.windows[id] = None;
if service.current_window == Some(id) {
service.current_window = None;
}
}
}
}
2
}
fn define_window(service: &mut Service, data: &[u8]) -> usize {
const TOTAL: usize = 7;
if data.len() < TOTAL {
return data.len().max(1);
}
let id = (data[0] - C1_DF0) as usize;
let p1 = data[1];
let p2 = data[2];
let p3 = data[3];
let p4 = data[4];
let p5 = data[5];
let p6 = data[6];
let creating = service.windows[id].is_none();
let w = service.windows[id].get_or_insert_with(Window::new);
w.priority = p1 & 0x07;
w.column_lock = (p1 >> 3) & 0x01 != 0;
w.row_lock = (p1 >> 4) & 0x01 != 0;
w.state = if (p1 >> 5) & 0x01 != 0 {
WindowState::Visible
} else {
WindowState::Hidden
};
w.relative_position = (p2 >> 7) & 0x01 != 0;
w.anchor_vertical = p2 & 0x7F;
w.anchor_horizontal = p3;
w.anchor_point = AnchorPoint::from_bits((p4 >> 4) & 0x0F);
w.row_count = (p4 & 0x0F) + 1;
w.column_count = (p5 & 0x3F) + 1;
w.window_style = (p6 >> 3) & 0x07;
w.pen_style = p6 & 0x07;
if creating {
apply_window_style(
w,
if w.window_style == 0 {
1
} else {
w.window_style
},
);
apply_pen_style(w, if w.pen_style == 0 { 1 } else { w.pen_style });
w.ensure_grid();
w.clear_text();
} else {
if w.window_style != 0 {
apply_window_style(w, w.window_style);
}
if w.pen_style != 0 {
apply_pen_style(w, w.pen_style);
}
w.ensure_grid();
}
service.current_window = Some(id);
TOTAL
}
fn set_window_attributes(service: &mut Service, data: &[u8]) -> usize {
const TOTAL: usize = 5;
if data.len() < TOTAL {
return data.len().max(1);
}
let p1 = data[1];
let p2 = data[2];
let p3 = data[3];
let p4 = data[4];
if let Some(w) = service.current() {
w.fill_opacity = Opacity::from_bits((p1 >> 6) & 0x03);
w.fill_color = Color::new((p1 >> 4) & 0x03, (p1 >> 2) & 0x03, p1 & 0x03);
let bt_lo = (p2 >> 6) & 0x03;
w.border_color = Color::new((p2 >> 4) & 0x03, (p2 >> 2) & 0x03, p2 & 0x03);
let bt_hi = (p3 >> 7) & 0x01;
w.border_type = EdgeType::from_bits((bt_hi << 2) | bt_lo);
w.word_wrap = (p3 >> 6) & 0x01 != 0;
w.print_direction = PrintDirection::from_bits((p3 >> 4) & 0x03);
w.scroll_direction = ScrollDirection::from_bits((p3 >> 2) & 0x03);
w.justify = Justify::from_bits(p3 & 0x03);
let _ = p4;
}
TOTAL
}
fn set_pen_attributes(service: &mut Service, data: &[u8]) -> usize {
const TOTAL: usize = 3;
if data.len() < TOTAL {
return data.len().max(1);
}
let p1 = data[1];
let p2 = data[2];
if let Some(w) = service.current() {
w.pen_offset = PenOffset::from_bits((p1 >> 2) & 0x03);
w.pen_size = PenSize::from_bits(p1 & 0x03);
w.italics = (p2 >> 7) & 0x01 != 0;
w.underline = (p2 >> 6) & 0x01 != 0;
w.edge_type = EdgeType::from_bits((p2 >> 3) & 0x07);
w.font_style = FontStyle::from_bits(p2 & 0x07);
}
TOTAL
}
fn set_pen_color(service: &mut Service, data: &[u8]) -> usize {
const TOTAL: usize = 4;
if data.len() < TOTAL {
return data.len().max(1);
}
let p1 = data[1];
let p2 = data[2];
let p3 = data[3];
if let Some(w) = service.current() {
w.fg_opacity = Opacity::from_bits((p1 >> 6) & 0x03);
w.fg_color = Color::new((p1 >> 4) & 0x03, (p1 >> 2) & 0x03, p1 & 0x03);
w.bg_opacity = Opacity::from_bits((p2 >> 6) & 0x03);
w.bg_color = Color::new((p2 >> 4) & 0x03, (p2 >> 2) & 0x03, p2 & 0x03);
w.border_color = Color::new((p3 >> 4) & 0x03, (p3 >> 2) & 0x03, p3 & 0x03);
}
TOTAL
}
fn set_pen_location(service: &mut Service, data: &[u8]) -> usize {
const TOTAL: usize = 3;
if data.len() < TOTAL {
return data.len().max(1);
}
let row = (data[1] & 0x0F) as usize;
let col = (data[2] & 0x3F) as usize;
if let Some(w) = service.current() {
w.set_pen_location(row, col);
}
TOTAL
}
fn put(service: &mut Service, ch: char) {
if let Some(w) = service.current() {
w.put_char(ch);
}
}
fn g0_char(b: u8) -> char {
if b == G0_MUSIC_NOTE {
'\u{266A}'
} else {
char::from(b)
}
}
fn g2_char(b: u8) -> char {
match b {
0x20 | 0x21 => ' ', 0x25 => '\u{2026}', 0x2A => '\u{0160}', 0x2C => '\u{0152}', 0x30 => '\u{25A0}', 0x31 => '\u{2018}', 0x32 => '\u{2019}', 0x33 => '\u{201C}', 0x34 => '\u{201D}', 0x35 => '\u{2022}', 0x39 => '\u{2122}', 0x3A => '\u{0161}', 0x3C => '\u{0153}', 0x3D => '\u{2120}', 0x3F => '\u{0178}', 0x76 => '\u{215B}', 0x77 => '\u{215C}', 0x78 => '\u{215D}', 0x79 => '\u{215E}', _ => '_', }
}
fn g3_char(b: u8) -> char {
if b == 0xA0 {
'\u{1F4FA}' } else {
'_'
}
}
#[must_use]
pub fn windows(&self, service_number: usize) -> &[Option<Window>; NUM_WINDOWS] {
const EMPTY: [Option<Window>; NUM_WINDOWS] =
[None, None, None, None, None, None, None, None];
if service_number == 0 || service_number > NUM_SERVICES {
return &EMPTY;
}
&self.services[service_number - 1].windows
}
#[must_use]
pub fn service_text(&self, service_number: usize) -> String {
if service_number == 0 || service_number > NUM_SERVICES {
return String::new();
}
let svc = &self.services[service_number - 1];
let mut idxs: Vec<usize> = (0..NUM_WINDOWS)
.filter(|&i| {
svc.windows[i]
.as_ref()
.is_some_and(|w| w.state == WindowState::Visible)
})
.collect();
idxs.sort_by_key(|&i| {
svc.windows[i]
.as_ref()
.map_or((u8::MAX, i), |w| (w.priority, i))
});
let mut out = String::new();
for i in idxs {
if let Some(w) = svc.windows[i].as_ref() {
let t = w.text();
if t.is_empty() {
continue;
}
if !out.is_empty() {
out.push('\n');
}
out.push_str(&t);
}
}
out
}
}
#[derive(Clone, Copy)]
enum WindowMapOp {
Clear,
Display,
Hide,
Toggle,
Delete,
}
fn apply_window_style(w: &mut Window, id: u8) {
w.border_type = EdgeType::None;
match id {
1 => style(w, Justify::Left, false, Some(Color::BLACK), Opacity::Solid),
2 => style(w, Justify::Left, false, None, Opacity::Transparent),
3 => style(
w,
Justify::Center,
false,
Some(Color::BLACK),
Opacity::Solid,
),
4 => style(w, Justify::Left, true, Some(Color::BLACK), Opacity::Solid),
5 => style(w, Justify::Left, true, None, Opacity::Transparent),
6 => style(w, Justify::Center, true, Some(Color::BLACK), Opacity::Solid),
7 => {
w.justify = Justify::Left;
w.word_wrap = false;
w.print_direction = PrintDirection::TopToBottom;
w.scroll_direction = ScrollDirection::RightToLeft;
w.fill_color = Color::BLACK;
w.fill_opacity = Opacity::Solid;
}
_ => {}
}
}
fn style(w: &mut Window, j: Justify, ww: bool, fill: Option<Color>, op: Opacity) {
w.justify = j;
w.word_wrap = ww;
w.print_direction = PrintDirection::LeftToRight;
w.scroll_direction = ScrollDirection::BottomToTop;
w.fill_opacity = op;
if let Some(c) = fill {
w.fill_color = c;
}
}
fn apply_pen_style(w: &mut Window, id: u8) {
w.pen_size = PenSize::Standard;
w.pen_offset = PenOffset::Normal;
w.italics = false;
w.underline = false;
w.fg_color = Color::WHITE;
w.fg_opacity = Opacity::Solid;
match id {
1 => pen(
w,
FontStyle::Default,
EdgeType::None,
Color::BLACK,
Opacity::Solid,
),
2 => pen(
w,
FontStyle::MonospacedSerif,
EdgeType::None,
Color::BLACK,
Opacity::Solid,
),
3 => pen(
w,
FontStyle::ProportionalSerif,
EdgeType::None,
Color::BLACK,
Opacity::Solid,
),
4 => pen(
w,
FontStyle::MonospacedSansSerif,
EdgeType::None,
Color::BLACK,
Opacity::Solid,
),
5 => pen(
w,
FontStyle::ProportionalSansSerif,
EdgeType::None,
Color::BLACK,
Opacity::Solid,
),
6 => pen(
w,
FontStyle::MonospacedSansSerif,
EdgeType::Uniform,
Color::BLACK,
Opacity::Transparent,
),
7 => pen(
w,
FontStyle::ProportionalSansSerif,
EdgeType::Uniform,
Color::BLACK,
Opacity::Transparent,
),
_ => {}
}
}
fn pen(w: &mut Window, font: FontStyle, edge: EdgeType, bg: Color, bg_op: Opacity) {
w.font_style = font;
w.edge_type = edge;
w.bg_color = bg;
w.bg_opacity = bg_op;
}
#[cfg(test)]
mod tests {
use super::*;
fn ccp(svc: u8, cmds: &[u8]) -> Vec<u8> {
let mut sb = alloc::vec![(svc << 5) | (cmds.len() as u8)];
sb.extend_from_slice(cmds);
let size_code = (sb.len().div_ceil(2) + 1) as u8 & 0x3F;
let mut packet = alloc::vec![size_code];
packet.extend_from_slice(&sb);
packet
}
#[test]
fn define_window_worked_example() {
let mut dec = Cea708Decoder::new();
let packet = ccp(1, &[0x9A, 0x38, 0x4A, 0xD1, 0x8B, 0x0F, 0x11]);
dec.push_packet(&packet);
let w = dec.windows(1)[2].as_ref().expect("window 2 defined");
assert_eq!(w.state, WindowState::Visible);
assert!(w.row_lock);
assert!(w.column_lock);
assert_eq!(w.priority, 0);
assert!(!w.relative_position);
assert_eq!(w.anchor_vertical, 74);
assert_eq!(w.anchor_horizontal, 209);
assert_eq!(w.anchor_point, AnchorPoint::BottomRight);
assert_eq!(w.row_count, 12);
assert_eq!(w.column_count, 16);
assert_eq!(w.window_style, 2);
assert_eq!(w.pen_style, 1);
}
#[test]
fn swa_border_type_split() {
let mut dec = Cea708Decoder::new();
let packet = ccp(
1,
&[
0x98, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x97, 0x64, 0x53, 0x88, 0x22, ],
);
dec.push_packet(&packet);
let w = dec.windows(1)[0].as_ref().expect("window 0");
assert_eq!(w.border_type, EdgeType::RightDropShadow);
}
#[test]
fn decode_text() {
let mut dec = Cea708Decoder::new();
let packet = ccp(
1,
&[
0x98, 0x20, 0x00, 0x00, 0x02, 0x0F, 0x00, b'H', b'i',
],
);
dec.push_packet(&packet);
assert_eq!(dec.service_text(1), "Hi");
}
#[test]
fn two_services_multi_window() {
let mut dec = Cea708Decoder::new();
let s1_block = [0x98, 0x20, 0x00, 0x00, 0x00, 0x0F, 0x00, b'S', b'1'];
let s2_block = [0x99, 0x20, 0x00, 0x00, 0x00, 0x0F, 0x00, b'S', b'2'];
let mut data = Vec::new();
data.push((1 << 5) | (s1_block.len() as u8));
data.extend_from_slice(&s1_block);
data.push((2 << 5) | (s2_block.len() as u8));
data.extend_from_slice(&s2_block);
let size_code = (data.len().div_ceil(2) + 1) as u8 & 0x3F;
let mut packet = alloc::vec![size_code];
packet.extend_from_slice(&data);
dec.push_packet(&packet);
assert_eq!(dec.service_text(1), "S1");
assert_eq!(dec.service_text(2), "S2");
assert!(dec.windows(1)[0].is_some());
assert!(dec.windows(2)[1].is_some());
}
#[test]
fn carriage_return_rolls_up() {
let mut w = Window::new();
w.row_count = 2;
w.column_count = 10;
w.ensure_grid();
w.put_char('A');
w.carriage_return();
w.put_char('B');
w.carriage_return(); w.put_char('C');
assert_eq!(w.text(), "B\nC");
}
#[test]
fn g0_music_note() {
assert_eq!(Cea708Decoder::g0_char(0x7F), '\u{266A}');
assert_eq!(Cea708Decoder::g0_char(b'A'), 'A');
}
#[test]
fn no_panic_on_arbitrary_input() {
let inputs: &[&[u8]] = &[
&[],
&[0x00],
&[0xFF],
&[0x01, 0x98], &[0x3F, 0x80, 0x90, 0x91, 0x92, 0x97, 0x98], &[0x20, 0xEE, (7 << 5) | 1], &[0x10, 0x9A], &[0x18, 0x00], ];
for inp in inputs {
let mut dec = Cea708Decoder::new();
dec.push_packet(inp);
}
let mut dec = Cea708Decoder::new();
let mut x: u32 = 0x1234_5678;
let mut buf = Vec::new();
for _ in 0..4096 {
x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
buf.push((x >> 16) as u8);
}
dec.push_packet(&buf);
let mut dec2 = Cea708Decoder::new();
let triplets: Vec<CcTriplet> = buf
.chunks(2)
.map(|c| CcTriplet {
cc_valid: true,
cc_type: CcType::Dtvcc708Data,
cc_data_1: c[0],
cc_data_2: *c.get(1).unwrap_or(&0),
})
.collect();
dec2.push_triplets(&triplets);
}
}