1use crate::cc_data::{CcTriplet, CcType};
20use alloc::string::String;
21use alloc::vec::Vec;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29#[non_exhaustive]
30pub enum Cea608Color {
31 #[default]
33 White,
34 Green,
36 Blue,
38 Cyan,
40 Red,
42 Yellow,
44 Magenta,
46}
47
48impl Cea608Color {
49 #[must_use]
51 pub fn name(&self) -> &'static str {
52 match self {
53 Self::White => "white",
54 Self::Green => "green",
55 Self::Blue => "blue",
56 Self::Cyan => "cyan",
57 Self::Red => "red",
58 Self::Yellow => "yellow",
59 Self::Magenta => "magenta",
60 }
61 }
62
63 #[must_use]
65 pub(super) fn from_idx(idx: u8) -> Self {
66 match idx & 0x07 {
67 0 => Self::White,
68 1 => Self::Green,
69 2 => Self::Blue,
70 3 => Self::Cyan,
71 4 => Self::Red,
72 5 => Self::Yellow,
73 6 => Self::Magenta,
74 _ => Self::White,
75 }
76 }
77}
78dvb_common::impl_spec_display!(Cea608Color);
79
80const SCREEN_ROWS: usize = 15;
82const SCREEN_COLS: usize = 32;
84
85const MC_RCL: u8 = 0x20;
87const MC_BS: u8 = 0x21;
88const MC_DER: u8 = 0x24;
89const MC_RU2: u8 = 0x25;
90const MC_RU3: u8 = 0x26;
91const MC_RU4: u8 = 0x27;
92const MC_FON: u8 = 0x28;
93const MC_RDC: u8 = 0x29;
94const MC_TR: u8 = 0x2A;
95const MC_RTD: u8 = 0x2B;
96const MC_EDM: u8 = 0x2C;
97const MC_CR: u8 = 0x2D;
98const MC_ENM: u8 = 0x2E;
99const MC_EOC: u8 = 0x2F;
100
101const TAB_FIRST_C1: u8 = 0x17;
103const TAB_FIRST_C2: u8 = 0x1F;
104const TAB1: u8 = 0x21;
105const TAB3: u8 = 0x23;
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize))]
110#[non_exhaustive]
111pub enum Cea608Mode {
112 #[default]
114 None,
115 PopOn,
117 RollUp(u8),
119 PaintOn,
121 Text,
123}
124
125impl Cea608Mode {
126 #[must_use]
128 pub fn name(&self) -> &'static str {
129 match self {
130 Self::None => "none",
131 Self::PopOn => "pop_on",
132 Self::RollUp(_) => "roll_up",
133 Self::PaintOn => "paint_on",
134 Self::Text => "text",
135 }
136 }
137}
138dvb_common::impl_spec_display!(Cea608Mode);
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize))]
144#[non_exhaustive]
145pub enum Cea608Channel {
146 Cc1,
148 Cc2,
150 Cc3,
152 Cc4,
154}
155
156impl Cea608Channel {
157 #[must_use]
159 fn index(self) -> usize {
160 match self {
161 Self::Cc1 => 0,
162 Self::Cc2 => 1,
163 Self::Cc3 => 2,
164 Self::Cc4 => 3,
165 }
166 }
167 #[must_use]
169 pub fn name(&self) -> &'static str {
170 match self {
171 Self::Cc1 => "cc1",
172 Self::Cc2 => "cc2",
173 Self::Cc3 => "cc3",
174 Self::Cc4 => "cc4",
175 }
176 }
177}
178dvb_common::impl_spec_display!(Cea608Channel);
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize))]
183pub struct Cea608StyledChar {
184 pub ch: char,
186 pub underline: bool,
188 pub italics: bool,
190 pub color: Cea608Color,
192}
193
194impl Default for Cea608StyledChar {
195 fn default() -> Self {
196 Cea608StyledChar {
197 ch: ' ',
198 underline: false,
199 italics: false,
200 color: Cea608Color::White,
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Default)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize))]
208pub struct Cea608Row {
209 cells: Vec<(usize, Cea608StyledChar)>,
211}
212
213impl Cea608Row {
214 fn set(&mut self, col: usize, c: Cea608StyledChar) {
215 if let Some(slot) = self.cells.iter_mut().find(|(i, _)| *i == col) {
216 slot.1 = c;
217 } else {
218 self.cells.push((col, c));
219 }
220 }
221 fn clear_from(&mut self, col: usize) {
222 self.cells.retain(|(i, _)| *i < col);
223 }
224 fn remove(&mut self, col: usize) {
225 self.cells.retain(|(i, _)| *i != col);
226 }
227 #[must_use]
229 pub fn text(&self) -> String {
230 let mut sorted = self.cells.clone();
231 sorted.sort_by_key(|(i, _)| *i);
232 let mut out = String::new();
233 let mut last = None;
234 for (i, c) in sorted {
235 if let Some(prev) = last {
236 for _ in (prev + 1)..i {
237 out.push(' ');
238 }
239 }
240 out.push(c.ch);
241 last = Some(i);
242 }
243 out
244 }
245 #[must_use]
247 pub fn styled_cells(&self) -> Vec<(usize, Cea608StyledChar)> {
248 let mut sorted = self.cells.clone();
249 sorted.sort_by_key(|(i, _)| *i);
250 sorted
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Default)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize))]
257pub struct Cea608Screen {
258 rows: [Cea608Row; SCREEN_ROWS],
259}
260
261impl Cea608Screen {
262 #[must_use]
264 pub fn text(&self) -> String {
265 let mut out = String::new();
266 for row in &self.rows {
267 let line = row.text();
268 let trimmed = line.trim_end();
269 if trimmed.is_empty() {
270 continue;
271 }
272 if !out.is_empty() {
273 out.push('\n');
274 }
275 out.push_str(trimmed);
276 }
277 out
278 }
279 #[must_use]
281 pub fn rows(&self) -> &[Cea608Row; SCREEN_ROWS] {
282 &self.rows
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288struct Pen {
289 underline: bool,
290 italics: bool,
291 color: Cea608Color,
292}
293
294impl Default for Pen {
295 fn default() -> Self {
296 Pen {
297 underline: false,
298 italics: false,
299 color: Cea608Color::White,
300 }
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Default)]
306struct ChannelState {
307 displayed: Cea608Screen,
308 nondisplayed: Cea608Screen,
309 mode: Cea608Mode,
310 rollup_rows: u8,
311 cursor_row: usize,
312 cursor_col: usize,
313 pen: Pen,
314}
315
316impl ChannelState {
317 fn active_mut(&mut self) -> &mut Cea608Screen {
320 match self.mode {
321 Cea608Mode::PopOn => &mut self.nondisplayed,
322 _ => &mut self.displayed,
323 }
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct Cea608Decoder {
346 channels: [ChannelState; 4],
347 last_control: [Option<(u8, u8)>; 2],
349 xds_active: bool,
350}
351
352impl Default for Cea608Decoder {
353 fn default() -> Self {
354 Self::new()
355 }
356}
357
358impl Cea608Decoder {
359 #[must_use]
361 pub fn new() -> Self {
362 Cea608Decoder {
363 channels: Default::default(),
364 last_control: [None, None],
365 xds_active: false,
366 }
367 }
368
369 pub fn push_triplets<'a, I>(&mut self, triplets: I)
371 where
372 I: IntoIterator<Item = &'a CcTriplet>,
373 {
374 for t in triplets {
375 if !t.cc_valid {
376 continue;
377 }
378 let field2 = match t.cc_type {
379 CcType::Ntsc608Field1 => false,
380 CcType::Ntsc608Field2 => true,
381 _ => continue,
382 };
383 self.process_pair(field2, t.cc_data_1, t.cc_data_2);
384 }
385 }
386
387 pub fn push_pair(&mut self, field2: bool, b1: u8, b2: u8) {
390 self.process_pair(field2, b1, b2);
391 }
392
393 fn process_pair(&mut self, field2: bool, raw1: u8, raw2: u8) {
394 let b1 = raw1 & 0x7F;
395 let b2 = raw2 & 0x7F;
396 let field_idx = usize::from(field2);
397
398 if b1 == 0x00 && b2 == 0x00 {
400 return;
401 }
402
403 if field2 && (0x01..=0x0F).contains(&b1) {
407 self.xds_active = b1 != 0x0F;
410 return;
411 }
412 if field2 && self.xds_active {
415 return;
416 }
417
418 if (0x10..=0x1F).contains(&b1) {
420 if self.last_control[field_idx] == Some((b1, b2)) {
422 self.last_control[field_idx] = None; return;
424 }
425 self.last_control[field_idx] = Some((b1, b2));
426 self.xds_active = false;
427 self.handle_control(field2, b1, b2);
428 return;
429 }
430
431 self.last_control[field_idx] = None;
433 self.xds_active = false;
434 let ch = self.default_channel(field2);
437 if b1 >= 0x20 {
438 self.put_char(ch, Self::standard_char(b1));
439 }
440 if b2 >= 0x20 {
441 self.put_char(ch, Self::standard_char(b2));
442 }
443 }
444
445 fn handle_control(&mut self, field2: bool, b1: u8, b2: u8) {
447 let c2 = b1 >= 0x18;
451 let base1 = if c2 { b1 - 0x08 } else { b1 }; let ch = self.channel_for(field2, c2);
453
454 if base1 == 0x14 && (0x20..=0x2F).contains(&b2) {
457 self.misc_control(ch, b2);
458 return;
459 }
460 if (b1 == TAB_FIRST_C1 || b1 == TAB_FIRST_C2) && (TAB1..=TAB3).contains(&b2) {
462 let n = (b2 - TAB1 + 1) as usize;
463 let st = &mut self.channels[ch.index()];
464 st.cursor_col = (st.cursor_col + n).min(SCREEN_COLS - 1);
465 return;
466 }
467 if base1 == 0x11 && (0x20..=0x2F).contains(&b2) {
469 self.mid_row(ch, b2);
470 return;
471 }
472 if base1 == 0x11 && (0x30..=0x3F).contains(&b2) {
474 self.put_char(ch, Self::special_char(b2));
475 return;
476 }
477 if base1 == 0x12 && (0x20..=0x3F).contains(&b2) {
479 self.put_extended(ch, Self::extended_char_block1(b2));
480 return;
481 }
482 if base1 == 0x13 && (0x20..=0x3F).contains(&b2) {
484 self.put_extended(ch, Self::extended_char_block2(b2));
485 return;
486 }
487 if (0x40..=0x7F).contains(&b2) {
489 self.pac(field2, ch, b1, b2);
490 }
491 }
494
495 fn misc_control(&mut self, ch: Cea608Channel, code: u8) {
497 let st = &mut self.channels[ch.index()];
498 match code {
499 MC_RCL => {
500 st.mode = Cea608Mode::PopOn;
501 st.nondisplayed = Cea608Screen::default();
502 st.cursor_row = 0;
503 st.cursor_col = 0;
504 }
505 MC_RU2 | MC_RU3 | MC_RU4 => {
506 let rows = match code {
507 MC_RU2 => 2,
508 MC_RU3 => 3,
509 _ => 4,
510 };
511 st.mode = Cea608Mode::RollUp(rows);
512 st.rollup_rows = rows;
513 st.cursor_row = SCREEN_ROWS - 1;
515 st.cursor_col = 0;
516 st.pen = Pen::default();
517 }
518 MC_RDC => {
519 st.mode = Cea608Mode::PaintOn;
520 }
521 MC_CR => {
522 self.carriage_return(ch);
523 }
524 MC_EDM => {
525 st.displayed = Cea608Screen::default();
526 }
527 MC_ENM => {
528 st.nondisplayed = Cea608Screen::default();
529 }
530 MC_EOC => {
531 core::mem::swap(&mut st.displayed, &mut st.nondisplayed);
533 }
534 MC_BS if st.cursor_col > 0 => {
535 st.cursor_col -= 1;
536 let row = st.cursor_row.min(SCREEN_ROWS - 1);
537 let col = st.cursor_col;
538 st.active_mut().rows[row].remove(col);
539 }
540 MC_DER => {
541 let row = st.cursor_row.min(SCREEN_ROWS - 1);
542 let col = st.cursor_col;
543 st.active_mut().rows[row].clear_from(col);
544 }
545 MC_TR => {
546 st.mode = Cea608Mode::Text;
547 st.displayed = Cea608Screen::default();
548 st.cursor_row = 0;
549 st.cursor_col = 0;
550 }
551 MC_RTD => {
552 st.mode = Cea608Mode::Text;
553 }
554 MC_FON => {}
555 _ => {}
556 }
557 }
558
559 fn carriage_return(&mut self, ch: Cea608Channel) {
561 let st = &mut self.channels[ch.index()];
562 if let Cea608Mode::RollUp(rows) = st.mode {
563 let base = st.cursor_row.min(SCREEN_ROWS - 1);
564 let rows = rows as usize;
565 let top = base.saturating_sub(rows - 1);
566 for r in top..base {
568 st.displayed.rows[r] = st.displayed.rows[r + 1].clone();
569 }
570 st.displayed.rows[base] = Cea608Row::default();
571 st.cursor_col = 0;
572 } else {
573 if st.cursor_row + 1 < SCREEN_ROWS {
575 st.cursor_row += 1;
576 }
577 st.cursor_col = 0;
578 }
579 }
580
581 fn mid_row(&mut self, ch: Cea608Channel, b2: u8) {
583 let idx = (b2 - 0x20) as usize; let underline = idx & 0x01 != 0;
585 let color_idx = (idx >> 1) as u8;
586 let (color, italics) = if color_idx <= 6 {
587 (Cea608Color::from_idx(color_idx), false)
588 } else {
589 (Cea608Color::White, true) };
591 let st = &mut self.channels[ch.index()];
592 st.pen = Pen {
593 underline,
594 italics,
595 color,
596 };
597 self.put_char(ch, ' ');
599 }
600
601 fn pac(&mut self, _field2: bool, ch: Cea608Channel, b1: u8, b2: u8) {
603 let f = if b1 >= 0x18 { b1 - 0x08 } else { b1 };
605 let row_pair = match f {
607 0x11 => 1, 0x12 => 3, 0x15 => 5, 0x16 => 7, 0x17 => 9, 0x10 => 11, 0x13 => 12, 0x14 => 14, _ => 1,
616 };
617 let second_of_pair = b2 >= 0x60;
620 let row = if f == 0x10 {
621 11 } else if second_of_pair {
623 row_pair + 1
624 } else {
625 row_pair
626 };
627 let row = row.clamp(1, SCREEN_ROWS); let attr = b2 & 0x1F; let underline = attr & 0x01 != 0;
630
631 let (color, italics, indent) = if attr >= 0x10 {
632 let indent = ((attr - 0x10) >> 1) as usize * 4;
634 (Cea608Color::White, false, indent)
635 } else {
636 let color_idx = attr >> 1;
638 let (c, it) = if color_idx <= 6 {
639 (Cea608Color::from_idx(color_idx), false)
640 } else {
641 (Cea608Color::White, true)
642 };
643 (c, it, 0)
644 };
645
646 let st = &mut self.channels[ch.index()];
647 st.cursor_row = (row - 1).min(SCREEN_ROWS - 1);
648 st.cursor_col = indent.min(SCREEN_COLS - 1);
649 st.pen = Pen {
650 underline,
651 italics,
652 color,
653 };
654 }
655
656 fn put_char(&mut self, ch: Cea608Channel, c: char) {
658 let st = &mut self.channels[ch.index()];
659 let row = st.cursor_row.min(SCREEN_ROWS - 1);
660 let col = st.cursor_col;
661 if col >= SCREEN_COLS {
662 return;
663 }
664 let pen = st.pen;
665 let styled = Cea608StyledChar {
666 ch: c,
667 underline: pen.underline,
668 italics: pen.italics,
669 color: pen.color,
670 };
671 st.active_mut().rows[row].set(col, styled);
672 st.cursor_col = (col + 1).min(SCREEN_COLS);
673 }
674
675 fn put_extended(&mut self, ch: Cea608Channel, c: char) {
678 {
679 let st = &mut self.channels[ch.index()];
680 if st.cursor_col > 0 {
681 st.cursor_col -= 1;
682 let row = st.cursor_row.min(SCREEN_ROWS - 1);
683 let col = st.cursor_col;
684 st.active_mut().rows[row].remove(col);
685 }
686 }
687 self.put_char(ch, c);
688 }
689
690 fn channel_for(&self, field2: bool, c2: bool) -> Cea608Channel {
692 match (field2, c2) {
693 (false, false) => Cea608Channel::Cc1,
694 (false, true) => Cea608Channel::Cc2,
695 (true, false) => Cea608Channel::Cc3,
696 (true, true) => Cea608Channel::Cc4,
697 }
698 }
699
700 fn default_channel(&self, field2: bool) -> Cea608Channel {
702 if field2 {
703 Cea608Channel::Cc3
704 } else {
705 Cea608Channel::Cc1
706 }
707 }
708
709 #[must_use]
711 pub fn screen(&self, channel: Cea608Channel) -> &Cea608Screen {
712 &self.channels[channel.index()].displayed
713 }
714
715 #[must_use]
717 pub fn mode(&self, channel: Cea608Channel) -> Cea608Mode {
718 self.channels[channel.index()].mode
719 }
720
721 #[must_use]
723 pub fn channel_text(&self, channel: Cea608Channel) -> String {
724 self.channels[channel.index()].displayed.text()
725 }
726
727 fn standard_char(b: u8) -> char {
731 match b {
732 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), }
744 }
745
746 fn special_char(b2: u8) -> char {
748 match b2 {
749 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}', _ => '?',
766 }
767 }
768
769 fn extended_char_block1(b2: u8) -> char {
771 match b2 {
772 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 => '*',
783 0x29 => '\'',
784 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}', _ => '?',
808 }
809 }
810
811 fn extended_char_block2(b2: u8) -> char {
813 match b2 {
814 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 => '{',
825 0x2A => '}',
826 0x2B => '\\',
827 0x2C => '^',
828 0x2D => '_',
829 0x2E => '|',
830 0x2F => '~',
831 0x30 => '\u{00C4}', 0x31 => '\u{00E4}', 0x32 => '\u{00D6}', 0x33 => '\u{00F6}', 0x34 => '\u{00DF}', 0x35 => '\u{00A5}', 0x36 => '\u{00A4}', 0x37 => '|',
840 0x38 => '\u{00C5}', 0x39 => '\u{00E5}', 0x3A => '\u{00D8}', 0x3B => '\u{00F8}', 0x3C => '\u{231C}', 0x3D => '\u{231D}', 0x3E => '\u{231E}', 0x3F => '\u{231F}', _ => '?',
850 }
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 fn par(v: u8) -> u8 {
860 let ones = (v & 0x7F).count_ones();
861 if ones % 2 == 0 {
862 v | 0x80
863 } else {
864 v & 0x7F
865 }
866 }
867
868 #[test]
870 fn pop_on_caption() {
871 let mut dec = Cea608Decoder::new();
872 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'));
875 dec.push_pair(false, par(0x14), par(0x2F)); assert_eq!(dec.channel_text(Cea608Channel::Cc1), "HI");
877 assert_eq!(dec.mode(Cea608Channel::Cc1), Cea608Mode::PopOn);
878 }
879
880 #[test]
882 fn control_doubling() {
883 let mut dec = Cea608Decoder::new();
884 dec.push_pair(false, par(0x14), par(0x29)); dec.push_pair(false, par(0x14), par(0x29)); assert_eq!(dec.mode(Cea608Channel::Cc1), Cea608Mode::PaintOn);
887 dec.push_pair(false, par(b'X'), par(0x00));
889 dec.push_pair(false, par(0x14), par(0x2C)); }
891
892 #[test]
894 fn roll_up_two_rows() {
895 let mut dec = Cea608Decoder::new();
896 dec.push_pair(false, par(0x14), par(0x25)); dec.push_pair(false, par(b'A'), par(b'B'));
898 dec.push_pair(false, par(0x14), par(0x2D)); dec.push_pair(false, par(b'C'), par(b'D'));
900 let text = dec.channel_text(Cea608Channel::Cc1);
901 assert!(text.contains("AB"), "got {text:?}");
902 assert!(text.contains("CD"), "got {text:?}");
903 let ab = text.find("AB").unwrap();
905 let cd = text.find("CD").unwrap();
906 assert!(ab < cd, "AB should be above CD: {text:?}");
907 }
908
909 #[test]
911 fn mid_row_colour() {
912 let mut dec = Cea608Decoder::new();
913 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));
916 dec.push_pair(false, par(0x11), par(0x22)); dec.push_pair(false, par(b'B'), par(0x00));
918 dec.push_pair(false, par(0x14), par(0x2F)); let screen = dec.screen(Cea608Channel::Cc1);
921 let mut found_green_b = false;
922 for row in screen.rows() {
923 for (_, c) in row.styled_cells() {
924 if c.ch == 'B' {
925 assert_eq!(c.color, Cea608Color::Green);
926 found_green_b = true;
927 }
928 }
929 }
930 assert!(found_green_b, "expected a green 'B'");
931 }
932
933 #[test]
935 fn special_char_music_note() {
936 let mut dec = Cea608Decoder::new();
937 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}'));
940 }
941
942 #[test]
944 fn extended_char_backspace() {
945 let mut dec = Cea608Decoder::new();
946 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);
950 assert_eq!(t, "\u{00FC}"); }
952
953 #[test]
955 fn channel_cc2() {
956 let mut dec = Cea608Decoder::new();
957 dec.push_pair(false, par(0x1C), par(0x29)); assert_eq!(dec.mode(Cea608Channel::Cc2), Cea608Mode::PaintOn);
959 assert_eq!(dec.mode(Cea608Channel::Cc1), Cea608Mode::None);
960 }
961
962 #[test]
964 fn field2_cc3() {
965 let mut dec = Cea608Decoder::new();
966 dec.push_pair(true, par(0x14), par(0x29)); assert_eq!(dec.mode(Cea608Channel::Cc3), Cea608Mode::PaintOn);
968 }
969
970 #[test]
972 fn xds_skipped() {
973 let mut dec = Cea608Decoder::new();
974 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), "");
978 }
979
980 #[test]
981 fn standard_char_accents() {
982 assert_eq!(Cea608Decoder::standard_char(0x2A), '\u{00E1}'); assert_eq!(Cea608Decoder::standard_char(b'A'), 'A');
984 assert_eq!(Cea608Decoder::standard_char(0x7F), '\u{25A0}'); }
986
987 #[test]
988 fn no_panic_on_arbitrary_input() {
989 let mut dec = Cea608Decoder::new();
990 let mut x: u32 = 0xDEAD_BEEF;
991 for _ in 0..8192 {
992 x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
993 let b1 = (x >> 8) as u8;
994 let b2 = (x >> 16) as u8;
995 let f2 = (x & 1) != 0;
996 dec.push_pair(f2, b1, b2);
997 }
998 let triplets = [
1000 CcTriplet {
1001 cc_valid: true,
1002 cc_type: CcType::Ntsc608Field1,
1003 cc_data_1: 0x14,
1004 cc_data_2: 0x2D,
1005 },
1006 CcTriplet {
1007 cc_valid: false,
1008 cc_type: CcType::Ntsc608Field2,
1009 cc_data_1: 0xFF,
1010 cc_data_2: 0xFF,
1011 },
1012 CcTriplet {
1013 cc_valid: true,
1014 cc_type: CcType::Ntsc608Field2,
1015 cc_data_1: 0x01,
1016 cc_data_2: 0x00,
1017 },
1018 ];
1019 dec.push_triplets(&triplets);
1020 }
1021}