use crate::{SubtitleError, SubtitleResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608Channel {
CC1,
CC2,
CC3,
CC4,
}
impl Cea608Channel {
#[must_use]
pub const fn base_code(&self) -> u8 {
match self {
Self::CC1 | Self::CC3 => 0x14,
Self::CC2 | Self::CC4 => 0x1C,
}
}
#[must_use]
pub const fn is_field2(&self) -> bool {
matches!(self, Self::CC3 | Self::CC4)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608Mode {
PopOn,
RollUp2,
RollUp3,
RollUp4,
PaintOn,
Text,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608Color {
White,
Green,
Blue,
Cyan,
Red,
Yellow,
Magenta,
}
impl Cea608Color {
#[must_use]
pub const fn style_code(&self) -> u8 {
match self {
Self::White => 0,
Self::Green => 1,
Self::Blue => 2,
Self::Cyan => 3,
Self::Red => 4,
Self::Yellow => 5,
Self::Magenta => 6,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Cea608Attributes {
pub italic: bool,
pub underline: bool,
pub flash: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct Cea608Pac {
pub row: u8,
pub column: u8,
pub color: Cea608Color,
pub attributes: Cea608Attributes,
}
impl Cea608Pac {
#[must_use]
pub fn encode(&self, channel: Cea608Channel) -> (u8, u8) {
let row_code = match self.row {
1 => 0x11,
2 => 0x11,
3 => 0x12,
4 => 0x12,
5 => 0x15,
6 => 0x15,
7 => 0x16,
8 => 0x16,
9 => 0x17,
10 => 0x17,
11 => 0x13,
12 => 0x13,
13 => 0x14,
14 => 0x14,
15 => 0x15,
_ => 0x14, };
let row_offset = if self.row % 2 == 0 { 0x20 } else { 0x00 };
let mut byte1 = row_code;
if channel.is_field2() {
byte1 |= 0x08;
}
let mut byte2 = row_offset;
if self.column > 0 {
byte2 |= 0x10 | ((self.column / 4) & 0x0F);
} else {
byte2 |= self.color.style_code() << 1;
if self.attributes.underline {
byte2 |= 0x01;
}
}
(byte1, byte2)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cea608MidRowCode {
pub color: Option<Cea608Color>,
pub italic: bool,
pub underline: bool,
}
impl Cea608MidRowCode {
#[must_use]
pub fn encode(&self, channel: Cea608Channel) -> (u8, u8) {
let byte1 = if channel.is_field2() { 0x19 } else { 0x11 };
let mut byte2 = 0x20;
if let Some(color) = self.color {
byte2 |= color.style_code() << 1;
}
if self.underline {
byte2 |= 0x01;
}
(byte1, byte2)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608Command {
ResumeCaption,
Backspace,
DeleteToEndOfRow,
RollUpCaptions2,
RollUpCaptions3,
RollUpCaptions4,
FlashOn,
ResumeDirect,
TextRestart,
ResumeText,
EraseDisplayedMemory,
CarriageReturn,
EraseNonDisplayed,
EndOfCaption,
TabOffset(u8),
}
impl Cea608Command {
#[must_use]
pub fn encode(&self, channel: Cea608Channel) -> (u8, u8) {
let base = channel.base_code();
let byte2 = match self {
Self::ResumeCaption => 0x20,
Self::Backspace => 0x21,
Self::DeleteToEndOfRow => 0x24,
Self::RollUpCaptions2 => 0x25,
Self::RollUpCaptions3 => 0x26,
Self::RollUpCaptions4 => 0x27,
Self::FlashOn => 0x28,
Self::ResumeDirect => 0x29,
Self::TextRestart => 0x2A,
Self::ResumeText => 0x2B,
Self::EraseDisplayedMemory => 0x2C,
Self::CarriageReturn => 0x2D,
Self::EraseNonDisplayed => 0x2E,
Self::EndOfCaption => 0x2F,
Self::TabOffset(offset) => 0x21 + (*offset).min(3),
};
(base, byte2)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608SpecialChar {
RegisteredMark,
Degree,
OneHalf,
InvertedQuestion,
Trademark,
Cents,
PoundSterling,
MusicNote,
AGrave,
TransparentSpace,
EGrave,
ACircumflex,
ECircumflex,
ICircumflex,
OCircumflex,
UCircumflex,
}
impl Cea608SpecialChar {
#[must_use]
pub fn encode(&self, channel: Cea608Channel) -> (u8, u8) {
let byte1 = if channel.is_field2() { 0x1B } else { 0x13 };
let byte2 = match self {
Self::RegisteredMark => 0x30,
Self::Degree => 0x31,
Self::OneHalf => 0x32,
Self::InvertedQuestion => 0x33,
Self::Trademark => 0x34,
Self::Cents => 0x35,
Self::PoundSterling => 0x36,
Self::MusicNote => 0x37,
Self::AGrave => 0x38,
Self::TransparentSpace => 0x39,
Self::EGrave => 0x3A,
Self::ACircumflex => 0x3B,
Self::ECircumflex => 0x3C,
Self::ICircumflex => 0x3D,
Self::OCircumflex => 0x3E,
Self::UCircumflex => 0x3F,
};
(byte1, byte2)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea608ExtendedChar {
AAcute,
EAcute,
OAcute,
UAcute,
UDiaeresis,
UDiaeresisLower,
Apostrophe,
InvertedExclamation,
Asterisk,
LeftQuote,
EmDash,
Copyright,
ServiceMark,
Bullet,
LeftDoubleQuote,
RightDoubleQuote,
}
impl Cea608ExtendedChar {
#[must_use]
pub fn encode(&self, channel: Cea608Channel) -> (u8, u8) {
let byte1 = if channel.is_field2() { 0x1B } else { 0x12 };
let byte2 = match self {
Self::AAcute => 0x20,
Self::EAcute => 0x21,
Self::OAcute => 0x22,
Self::UAcute => 0x23,
Self::UDiaeresis => 0x24,
Self::UDiaeresisLower => 0x25,
Self::Apostrophe => 0x26,
Self::InvertedExclamation => 0x27,
Self::Asterisk => 0x28,
Self::LeftQuote => 0x29,
Self::EmDash => 0x2A,
Self::Copyright => 0x2B,
Self::ServiceMark => 0x2C,
Self::Bullet => 0x2D,
Self::LeftDoubleQuote => 0x2E,
Self::RightDoubleQuote => 0x2F,
};
(byte1, byte2)
}
}
pub struct Cea608Encoder {
channel: Cea608Channel,
mode: Cea608Mode,
current_row: u8,
current_column: u8,
current_color: Cea608Color,
current_attributes: Cea608Attributes,
output_buffer: Vec<(u8, u8)>,
frame_count: u32,
}
impl Cea608Encoder {
#[must_use]
pub fn new(channel: Cea608Channel) -> Self {
Self {
channel,
mode: Cea608Mode::PopOn,
current_row: 15,
current_column: 0,
current_color: Cea608Color::White,
current_attributes: Cea608Attributes::default(),
output_buffer: Vec::new(),
frame_count: 0,
}
}
pub fn set_mode(&mut self, mode: Cea608Mode) {
self.mode = mode;
let command = match mode {
Cea608Mode::PopOn => Cea608Command::ResumeCaption,
Cea608Mode::RollUp2 => Cea608Command::RollUpCaptions2,
Cea608Mode::RollUp3 => Cea608Command::RollUpCaptions3,
Cea608Mode::RollUp4 => Cea608Command::RollUpCaptions4,
Cea608Mode::PaintOn => Cea608Command::ResumeDirect,
Cea608Mode::Text => Cea608Command::ResumeText,
};
self.send_command(command);
}
pub fn send_command(&mut self, command: Cea608Command) {
let (b1, b2) = command.encode(self.channel);
self.output_buffer.push((b1, b2));
self.output_buffer.push((b1, b2));
}
pub fn set_position(&mut self, row: u8, column: u8) {
self.current_row = row.clamp(1, 15);
self.current_column = column.clamp(0, 31);
let pac = Cea608Pac {
row: self.current_row,
column: self.current_column,
color: self.current_color,
attributes: self.current_attributes,
};
let (b1, b2) = pac.encode(self.channel);
self.output_buffer.push((b1, b2));
}
pub fn set_style(&mut self, color: Cea608Color, italic: bool, underline: bool) {
self.current_color = color;
self.current_attributes.italic = italic;
self.current_attributes.underline = underline;
let mid_row = Cea608MidRowCode {
color: Some(color),
italic,
underline,
};
let (b1, b2) = mid_row.encode(self.channel);
self.output_buffer.push((b1, b2));
}
pub fn add_text(&mut self, text: &str) -> SubtitleResult<()> {
for c in text.chars() {
self.add_char(c)?;
}
Ok(())
}
pub fn add_char(&mut self, c: char) -> SubtitleResult<()> {
match c {
' '..='~' => {
let b = c as u8;
self.output_buffer.push((b & 0x7F, 0x80)); self.current_column += 1;
}
'®' => {
let (b1, b2) = Cea608SpecialChar::RegisteredMark.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'°' => {
let (b1, b2) = Cea608SpecialChar::Degree.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'½' => {
let (b1, b2) = Cea608SpecialChar::OneHalf.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'¿' => {
let (b1, b2) = Cea608SpecialChar::InvertedQuestion.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'™' => {
let (b1, b2) = Cea608SpecialChar::Trademark.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'¢' => {
let (b1, b2) = Cea608SpecialChar::Cents.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'£' => {
let (b1, b2) = Cea608SpecialChar::PoundSterling.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'♪' => {
let (b1, b2) = Cea608SpecialChar::MusicNote.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'À' => {
let (b1, b2) = Cea608SpecialChar::AGrave.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'È' => {
let (b1, b2) = Cea608SpecialChar::EGrave.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'Á' => {
let (b1, b2) = Cea608ExtendedChar::AAcute.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'É' => {
let (b1, b2) = Cea608ExtendedChar::EAcute.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'Ó' => {
let (b1, b2) = Cea608ExtendedChar::OAcute.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'Ú' => {
let (b1, b2) = Cea608ExtendedChar::UAcute.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'Ü' => {
let (b1, b2) = Cea608ExtendedChar::UDiaeresis.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'ü' => {
let (b1, b2) = Cea608ExtendedChar::UDiaeresisLower.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'¡' => {
let (b1, b2) = Cea608ExtendedChar::InvertedExclamation.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'©' => {
let (b1, b2) = Cea608ExtendedChar::Copyright.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'•' => {
let (b1, b2) = Cea608ExtendedChar::Bullet.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'—' => {
let (b1, b2) = Cea608ExtendedChar::EmDash.encode(self.channel);
self.output_buffer.push((b1, b2));
}
'\n' => {
if matches!(
self.mode,
Cea608Mode::RollUp2 | Cea608Mode::RollUp3 | Cea608Mode::RollUp4
) {
self.send_command(Cea608Command::CarriageReturn);
}
}
_ => {
return Err(SubtitleError::ParseError(format!(
"Unsupported character in CEA-608: {c}"
)));
}
}
Ok(())
}
pub fn clear_display(&mut self) {
self.send_command(Cea608Command::EraseDisplayedMemory);
}
pub fn clear_buffer(&mut self) {
self.send_command(Cea608Command::EraseNonDisplayed);
}
pub fn end_caption(&mut self) {
self.send_command(Cea608Command::EndOfCaption);
}
#[must_use]
pub fn add_parity(byte: u8) -> u8 {
let mut b = byte & 0x7F;
let mut parity = 0u8;
let mut temp = b;
while temp > 0 {
parity ^= temp & 1;
temp >>= 1;
}
if parity == 0 {
b |= 0x80;
}
b
}
#[must_use]
pub fn take_output(&mut self) -> Vec<(u8, u8)> {
let mut output = Vec::new();
std::mem::swap(&mut output, &mut self.output_buffer);
output
.into_iter()
.map(|(b1, b2)| {
if b2 == 0x80 {
(Self::add_parity(b1), 0x80)
} else {
(Self::add_parity(b1), Self::add_parity(b2))
}
})
.collect()
}
#[must_use]
pub fn get_frame_output(&mut self, frame_rate: f64) -> Option<(u8, u8)> {
if self.output_buffer.is_empty() {
return Some((0x80, 0x80)); }
self.frame_count += 1;
if !self.output_buffer.is_empty() {
let (b1, b2) = self.output_buffer.remove(0);
Some((Self::add_parity(b1), Self::add_parity(b2)))
} else {
Some((0x80, 0x80))
}
}
pub fn reset(&mut self) {
self.current_row = 15;
self.current_column = 0;
self.current_color = Cea608Color::White;
self.current_attributes = Cea608Attributes::default();
self.output_buffer.clear();
self.frame_count = 0;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cea708ServiceNumber(u8);
impl Cea708ServiceNumber {
pub fn new(number: u8) -> SubtitleResult<Self> {
if number == 0 || number > 63 {
return Err(SubtitleError::InvalidFormat(format!(
"Invalid CEA-708 service number: {number}"
)));
}
Ok(Self(number))
}
#[must_use]
pub const fn value(&self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cea708WindowId(u8);
impl Cea708WindowId {
pub fn new(id: u8) -> SubtitleResult<Self> {
if id > 7 {
return Err(SubtitleError::InvalidFormat(format!(
"Invalid CEA-708 window ID: {id}"
)));
}
Ok(Self(id))
}
#[must_use]
pub const fn value(&self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea708PenSize {
Small,
Standard,
Large,
}
impl Cea708PenSize {
#[must_use]
pub const fn code(&self) -> u8 {
match self {
Self::Small => 0,
Self::Standard => 1,
Self::Large => 2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea708FontStyle {
Default,
MonospacedSerif,
ProportionalSerif,
MonospacedSansSerif,
ProportionalSansSerif,
Casual,
Cursive,
SmallCaps,
}
impl Cea708FontStyle {
#[must_use]
pub const fn code(&self) -> u8 {
match self {
Self::Default => 0,
Self::MonospacedSerif => 1,
Self::ProportionalSerif => 2,
Self::MonospacedSansSerif => 3,
Self::ProportionalSansSerif => 4,
Self::Casual => 5,
Self::Cursive => 6,
Self::SmallCaps => 7,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cea708PenAttributes {
pub pen_size: Cea708PenSize,
pub font_style: Cea708FontStyle,
pub text_offset: u8,
pub italic: bool,
pub underline: bool,
pub edge_type: u8,
}
impl Default for Cea708PenAttributes {
fn default() -> Self {
Self {
pen_size: Cea708PenSize::Standard,
font_style: Cea708FontStyle::Default,
text_offset: 0,
italic: false,
underline: false,
edge_type: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cea708Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Cea708Color {
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self {
r: r & 0x03,
g: g & 0x03,
b: b & 0x03,
}
}
#[must_use]
pub const fn white() -> Self {
Self::new(3, 3, 3)
}
#[must_use]
pub const fn black() -> Self {
Self::new(0, 0, 0)
}
#[must_use]
pub const fn red() -> Self {
Self::new(3, 0, 0)
}
#[must_use]
pub const fn green() -> Self {
Self::new(0, 3, 0)
}
#[must_use]
pub const fn blue() -> Self {
Self::new(0, 0, 3)
}
#[must_use]
pub const fn yellow() -> Self {
Self::new(3, 3, 0)
}
#[must_use]
pub const fn magenta() -> Self {
Self::new(3, 0, 3)
}
#[must_use]
pub const fn cyan() -> Self {
Self::new(0, 3, 3)
}
#[must_use]
pub const fn encode(&self) -> u8 {
(self.r << 4) | (self.g << 2) | self.b
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cea708Opacity {
Solid,
Flash,
Translucent,
Transparent,
}
impl Cea708Opacity {
#[must_use]
pub const fn code(&self) -> u8 {
match self {
Self::Solid => 0,
Self::Flash => 1,
Self::Translucent => 2,
Self::Transparent => 3,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cea708PenColor {
pub foreground: Cea708Color,
pub foreground_opacity: Cea708Opacity,
pub background: Cea708Color,
pub background_opacity: Cea708Opacity,
pub edge: Cea708Color,
}
impl Default for Cea708PenColor {
fn default() -> Self {
Self {
foreground: Cea708Color::white(),
foreground_opacity: Cea708Opacity::Solid,
background: Cea708Color::black(),
background_opacity: Cea708Opacity::Solid,
edge: Cea708Color::black(),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cea708WindowAnchor {
pub anchor_point: u8,
pub vertical: u8,
pub horizontal: u8,
}
impl Default for Cea708WindowAnchor {
fn default() -> Self {
Self {
anchor_point: 0,
vertical: 80,
horizontal: 50,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cea708WindowAttributes {
pub priority: u8,
pub relative_positioning: bool,
pub anchor: Cea708WindowAnchor,
pub row_count: u8,
pub column_count: u8,
pub row_lock: bool,
pub column_lock: bool,
pub visible: bool,
}
impl Default for Cea708WindowAttributes {
fn default() -> Self {
Self {
priority: 0,
relative_positioning: false,
anchor: Cea708WindowAnchor::default(),
row_count: 4,
column_count: 32,
row_lock: false,
column_lock: false,
visible: true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum Cea708Command {
SetCurrentWindow(u8),
ClearWindows(u8),
DisplayWindows(u8),
HideWindows(u8),
ToggleWindows(u8),
DeleteWindows(u8),
Delay(u8),
DelayCancel,
Reset,
SetPenAttributes,
SetPenColor,
SetPenLocation(u8, u8),
SetWindowAttributes,
DefineWindow(u8),
}
impl Cea708Command {
#[must_use]
pub fn encode(&self) -> Vec<u8> {
match self {
Self::SetCurrentWindow(win) => vec![0x80 | (win & 0x07)],
Self::ClearWindows(bitmap) => vec![0x88, *bitmap],
Self::DisplayWindows(bitmap) => vec![0x89, *bitmap],
Self::HideWindows(bitmap) => vec![0x8A, *bitmap],
Self::ToggleWindows(bitmap) => vec![0x8B, *bitmap],
Self::DeleteWindows(bitmap) => vec![0x8C, *bitmap],
Self::Delay(tenths) => vec![0x8D, *tenths],
Self::DelayCancel => vec![0x8E],
Self::Reset => vec![0x8F],
Self::SetPenAttributes => vec![0x90],
Self::SetPenColor => vec![0x91],
Self::SetPenLocation(row, col) => vec![0x92, *row, *col],
Self::SetWindowAttributes => vec![0x97],
Self::DefineWindow(win) => vec![0x98 | (win & 0x07)],
}
}
}
pub struct Cea708Encoder {
service_number: Cea708ServiceNumber,
current_window: Cea708WindowId,
windows: [Option<Cea708WindowAttributes>; 8],
pen_attributes: Cea708PenAttributes,
pen_color: Cea708PenColor,
output_buffer: Vec<u8>,
sequence_number: u8,
}
impl Cea708Encoder {
#[must_use]
pub fn new(service_number: Cea708ServiceNumber) -> Self {
Self {
service_number,
current_window: Cea708WindowId(0),
windows: [None; 8],
pen_attributes: Cea708PenAttributes::default(),
pen_color: Cea708PenColor::default(),
output_buffer: Vec::new(),
sequence_number: 0,
}
}
pub fn define_window(
&mut self,
window_id: Cea708WindowId,
attributes: Cea708WindowAttributes,
) -> SubtitleResult<()> {
let id = window_id.value() as usize;
if id >= 8 {
return Err(SubtitleError::InvalidFormat(
"Window ID must be 0-7".to_string(),
));
}
self.output_buffer.push(0x98 | (window_id.value() & 0x07));
let mut attr = 0u8;
if attributes.visible {
attr |= 0x20;
}
if attributes.row_lock {
attr |= 0x10;
}
if attributes.column_lock {
attr |= 0x08;
}
attr |= attributes.priority & 0x07;
self.output_buffer.push(attr);
self.output_buffer.push(attributes.anchor.vertical);
self.output_buffer.push(attributes.anchor.horizontal);
self.output_buffer.push(attributes.anchor.anchor_point);
self.output_buffer.push(attributes.row_count.clamp(1, 15));
self.output_buffer
.push(attributes.column_count.clamp(1, 42));
self.windows[id] = Some(attributes);
Ok(())
}
pub fn set_current_window(&mut self, window_id: Cea708WindowId) {
self.current_window = window_id;
self.output_buffer.push(0x80 | (window_id.value() & 0x07));
}
pub fn clear_windows(&mut self, window_bitmap: u8) {
self.output_buffer.push(0x88);
self.output_buffer.push(window_bitmap);
}
pub fn display_windows(&mut self, window_bitmap: u8) {
self.output_buffer.push(0x89);
self.output_buffer.push(window_bitmap);
}
pub fn hide_windows(&mut self, window_bitmap: u8) {
self.output_buffer.push(0x8A);
self.output_buffer.push(window_bitmap);
}
pub fn delete_windows(&mut self, window_bitmap: u8) {
self.output_buffer.push(0x8C);
self.output_buffer.push(window_bitmap);
for i in 0..8 {
if (window_bitmap & (1 << i)) != 0 {
self.windows[i] = None;
}
}
}
pub fn set_pen_attributes(&mut self, attributes: Cea708PenAttributes) {
self.pen_attributes = attributes;
self.output_buffer.push(0x90);
let mut byte1 = attributes.pen_size.code() << 6;
byte1 |= (attributes.text_offset & 0x03) << 4;
byte1 |= attributes.font_style.code() & 0x07;
self.output_buffer.push(byte1);
let mut byte2 = (attributes.edge_type & 0x07) << 5;
if attributes.underline {
byte2 |= 0x01;
}
if attributes.italic {
byte2 |= 0x02;
}
self.output_buffer.push(byte2);
}
pub fn set_pen_color(&mut self, color: Cea708PenColor) {
self.pen_color = color;
self.output_buffer.push(0x91);
self.output_buffer
.push((color.foreground_opacity.code() << 6) | color.foreground.encode());
self.output_buffer
.push((color.background_opacity.code() << 6) | color.background.encode());
self.output_buffer.push(color.edge.encode());
}
pub fn set_pen_location(&mut self, row: u8, column: u8) {
self.output_buffer.push(0x92);
self.output_buffer.push(row.clamp(0, 14));
self.output_buffer.push(column.clamp(0, 41));
}
pub fn add_text(&mut self, text: &str) -> SubtitleResult<()> {
for c in text.chars() {
self.add_char(c)?;
}
Ok(())
}
pub fn add_char(&mut self, c: char) -> SubtitleResult<()> {
let code = c as u32;
if code <= 0x1F {
return Ok(());
}
if code <= 0x7F {
self.output_buffer.push(code as u8);
} else if code <= 0xFF {
self.output_buffer.push(0x10); self.output_buffer.push(code as u8);
} else if code <= 0xFFFF {
self.output_buffer.push(0x10);
self.output_buffer.push(0x00); self.output_buffer.push((code >> 8) as u8);
self.output_buffer.push((code & 0xFF) as u8);
} else {
return Err(SubtitleError::ParseError(format!(
"Character not supported in CEA-708: {c}"
)));
}
Ok(())
}
pub fn reset(&mut self) {
self.output_buffer.push(0x8F);
for window in &mut self.windows {
*window = None;
}
self.pen_attributes = Cea708PenAttributes::default();
self.pen_color = Cea708PenColor::default();
}
#[must_use]
pub fn build_service_block(&mut self) -> Vec<u8> {
let mut block = Vec::new();
let service_number = self.service_number.value();
let block_size = (self.output_buffer.len() + 1).min(31) as u8;
block.push((block_size << 5) | service_number);
let data_len = block_size.saturating_sub(1) as usize;
if data_len > 0 {
let drain_len = data_len.min(self.output_buffer.len());
block.extend_from_slice(&self.output_buffer[..drain_len]);
self.output_buffer.drain(..drain_len);
}
while block.len() < block_size as usize {
block.push(0x00); }
block
}
#[must_use]
pub fn build_cdp(&mut self, framerate_code: u8, timecode: Option<u32>) -> Vec<u8> {
let mut cdp = Vec::new();
cdp.push(0x96);
let length_pos = cdp.len();
cdp.push(0x00);
cdp.push(0x40 | (framerate_code & 0x0F));
cdp.push(0x43);
self.sequence_number = (self.sequence_number + 1) & 0x03;
cdp.push(self.sequence_number << 6);
if let Some(tc) = timecode {
cdp.push(0x71); cdp.push(0x04); cdp.push((tc >> 24) as u8);
cdp.push((tc >> 16) as u8);
cdp.push((tc >> 8) as u8);
cdp.push(tc as u8);
}
cdp.push(0x72); cdp.push(0x02); cdp.push(0x01); cdp.push(self.service_number.value());
cdp.push(0x73);
let service_block = self.build_service_block();
let cc_count = service_block.len().div_ceil(3) as u8;
cdp.push(0xE0 | cc_count);
for chunk in service_block.chunks(2) {
cdp.push(0xFC); cdp.push(*chunk.first().unwrap_or(&0x00));
cdp.push(*chunk.get(1).unwrap_or(&0x00));
}
cdp.push(0x74); cdp.push(self.sequence_number << 6);
let mut checksum = 0u8;
for &byte in &cdp {
checksum = checksum.wrapping_add(byte);
}
cdp.push(checksum.wrapping_neg());
cdp[length_pos] = (cdp.len() - 2) as u8;
cdp
}
#[must_use]
pub fn take_buffer(&mut self) -> Vec<u8> {
std::mem::take(&mut self.output_buffer)
}
#[must_use]
pub fn has_data(&self) -> bool {
!self.output_buffer.is_empty()
}
}
#[must_use]
pub fn get_framerate_code(fps: f64) -> u8 {
if (fps - 23.976).abs() < 0.01 {
0x01 } else if (fps - 24.0).abs() < 0.01 {
0x02 } else if (fps - 25.0).abs() < 0.01 {
0x03 } else if (fps - 29.97).abs() < 0.01 {
0x04 } else if (fps - 30.0).abs() < 0.01 {
0x05 } else if (fps - 50.0).abs() < 0.01 {
0x06 } else if (fps - 59.94).abs() < 0.01 {
0x07 } else if (fps - 60.0).abs() < 0.01 {
0x08 } else {
0x04 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cea608_basic_text() {
let mut encoder = Cea608Encoder::new(Cea608Channel::CC1);
encoder.set_mode(Cea608Mode::PopOn);
encoder.set_position(15, 0);
encoder.add_text("Hello").expect("should succeed in test");
encoder.end_caption();
let output = encoder.take_output();
assert!(!output.is_empty());
}
#[test]
fn test_cea708_service_block() {
let service = Cea708ServiceNumber::new(1).expect("should succeed in test");
let mut encoder = Cea708Encoder::new(service);
let window = Cea708WindowId::new(0).expect("should succeed in test");
encoder.set_current_window(window);
encoder.add_text("Test").expect("should succeed in test");
let block = encoder.build_service_block();
assert!(!block.is_empty());
}
#[test]
fn test_parity_calculation() {
let byte = 0x20;
let with_parity = Cea608Encoder::add_parity(byte);
let bit_count = (with_parity & 0x7F).count_ones() + ((with_parity >> 7) & 1) as u32;
assert!(
bit_count % 2 == 1,
"Parity should be odd, got {} 1-bits",
bit_count
);
}
#[test]
fn test_framerate_codes() {
assert_eq!(get_framerate_code(29.97), 0x04);
assert_eq!(get_framerate_code(30.0), 0x05);
assert_eq!(get_framerate_code(23.976), 0x01);
}
}