Skip to main content

cc_data/decode/
cea708.rs

1//! CEA-708 (DTVCC) caption decode — ANSI/CTA-708-E S-2023 §5–§8 + 47 CFR §79.102.
2//!
3//! Decode pipeline (`cc-data/docs/decode/cea708-decode.md`):
4//! `cc_data` byte pairs → Caption Channel Packets (§5) → Service Blocks (§6) →
5//! the C0/C1/G0/G1/G2/G3 command interpreter (§7/§8) driving the window + pen
6//! model. Up to six services (47 CFR §79.102 (c)) are tracked; each service has
7//! eight windows (DF0–DF7) and a current pen. Decoded window text is exposed.
8//!
9//! Decoder is panic-free on arbitrary input: short / over-length packets, bad
10//! service blocks and truncated commands are ignored.
11
12use crate::cc_data::{CcTriplet, CcType};
13use crate::decode::screen::{
14    Color, EdgeType, FontStyle, Justify, Opacity, PenOffset, PenSize, PrintDirection,
15    ScrollDirection,
16};
17use alloc::string::String;
18use alloc::vec::Vec;
19
20/// Anchor point of a CEA-708 window — which corner / edge / centre the anchor
21/// coordinates refer to (CTA-708-E §8.4.6, 4-bit `ap` field, values 0–8).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize))]
24#[non_exhaustive]
25pub enum AnchorPoint {
26    /// Top-left corner (ap = 0).
27    #[default]
28    TopLeft,
29    /// Top centre (ap = 1).
30    TopCenter,
31    /// Top-right corner (ap = 2).
32    TopRight,
33    /// Middle-left edge (ap = 3).
34    MiddleLeft,
35    /// Middle centre (ap = 4).
36    MiddleCenter,
37    /// Middle-right edge (ap = 5).
38    MiddleRight,
39    /// Bottom-left corner (ap = 6).
40    BottomLeft,
41    /// Bottom centre (ap = 7).
42    BottomCenter,
43    /// Bottom-right corner (ap = 8).
44    BottomRight,
45}
46
47impl AnchorPoint {
48    /// From the 4-bit `ap` wire value (§8.4.6; values 9–15 fold to `TopLeft`).
49    #[must_use]
50    pub fn from_bits(v: u8) -> Self {
51        match v & 0x0F {
52            0 => Self::TopLeft,
53            1 => Self::TopCenter,
54            2 => Self::TopRight,
55            3 => Self::MiddleLeft,
56            4 => Self::MiddleCenter,
57            5 => Self::MiddleRight,
58            6 => Self::BottomLeft,
59            7 => Self::BottomCenter,
60            8 => Self::BottomRight,
61            _ => Self::TopLeft,
62        }
63    }
64    /// Label per the project's `name()` convention.
65    #[must_use]
66    pub fn name(&self) -> &'static str {
67        match self {
68            Self::TopLeft => "top_left",
69            Self::TopCenter => "top_center",
70            Self::TopRight => "top_right",
71            Self::MiddleLeft => "middle_left",
72            Self::MiddleCenter => "middle_center",
73            Self::MiddleRight => "middle_right",
74            Self::BottomLeft => "bottom_left",
75            Self::BottomCenter => "bottom_center",
76            Self::BottomRight => "bottom_right",
77        }
78    }
79}
80broadcast_common::impl_spec_display!(AnchorPoint);
81
82// ── Service / window counts (§6.1, §8) ──────────────────────────────────────
83/// Number of standard services tracked (47 CFR §79.102 (c): Caption Service #1–#6).
84const NUM_SERVICES: usize = 6;
85/// Windows per service (DF0–DF7).
86const NUM_WINDOWS: usize = 8;
87/// Maximum rows in a window (rc field, virtual rows − 1, max 11 → 12 rows).
88const MAX_WINDOW_ROWS: usize = 12;
89/// Maximum columns in a window (cc field, virtual cols − 1, max 41 → 42 cols).
90const MAX_WINDOW_COLS: usize = 42;
91
92// ── Packet layer (§5) ───────────────────────────────────────────────────────
93/// `packet_size_code == 0` ⇒ 127 data bytes (§5.1).
94const PACKET_SIZE_ZERO_DATA: usize = 127;
95
96// ── Service block (§6.2) ──────────────────────────────────────────────────────
97/// `service_number == 7` is the extended-service escape (§6.2.2).
98const EXTENDED_SERVICE_ESCAPE: u8 = 7;
99
100// ── C0 control codes (§7.1.4, Table 13) ───────────────────────────────────────
101const C0_NUL: u8 = 0x00;
102const C0_ETX: u8 = 0x03;
103const C0_BS: u8 = 0x08;
104const C0_FF: u8 = 0x0C;
105const C0_CR: u8 = 0x0D;
106const C0_HCR: u8 = 0x0E;
107const C0_EXT1: u8 = 0x10;
108const C0_P16: u8 = 0x18;
109
110// ── C1 caption command opcodes (§7.1.5, Table 14) ─────────────────────────────
111const C1_CW0: u8 = 0x80; // CW0..CW7 = 0x80..=0x87
112const C1_CW7: u8 = 0x87;
113const C1_CLW: u8 = 0x88;
114const C1_DSW: u8 = 0x89;
115const C1_HDW: u8 = 0x8A;
116const C1_TGW: u8 = 0x8B;
117const C1_DLW: u8 = 0x8C;
118const C1_DLY: u8 = 0x8D;
119const C1_DLC: u8 = 0x8E;
120const C1_RST: u8 = 0x8F;
121const C1_SPA: u8 = 0x90;
122const C1_SPC: u8 = 0x91;
123const C1_SPL: u8 = 0x92;
124const C1_SWA: u8 = 0x97;
125const C1_DF0: u8 = 0x98; // DF0..DF7 = 0x98..=0x9F
126const C1_DF7: u8 = 0x9F;
127
128// ── Code-space range boundaries (§7.1, Table 11) ──────────────────────────────
129const C0_END: u8 = 0x1F;
130const G0_START: u8 = 0x20;
131const G0_END: u8 = 0x7F;
132const C1_START: u8 = 0x80;
133const C1_END: u8 = 0x9F;
134const G1_START: u8 = 0xA0;
135
136// ── G0 substitution (§7.1.6): 0x7F is the musical note, not DEL ───────────────
137const G0_MUSIC_NOTE: u8 = 0x7F;
138
139/// State of a window's display (§8.10.5 DisplayWindows / HideWindows / Toggle).
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize))]
142#[non_exhaustive]
143pub enum WindowState {
144    /// Window defined but not yet shown (default after DefineWindow).
145    #[default]
146    Hidden,
147    /// Window is being displayed.
148    Visible,
149}
150
151impl WindowState {
152    /// Label per the project's `name()` convention.
153    #[must_use]
154    pub fn name(&self) -> &'static str {
155        match self {
156            Self::Hidden => "hidden",
157            Self::Visible => "visible",
158        }
159    }
160}
161broadcast_common::impl_spec_display!(WindowState);
162
163/// A decoded CEA-708 caption window (§8.4 window model).
164///
165/// Holds the window attributes set by `DefineWindow` / `SetWindowAttributes`,
166/// the pen attributes set by `SetPenAttributes` / `SetPenColor`, and the painted
167/// text grid. Only created when a `DefineWindow` for its ID is received.
168#[derive(Debug, Clone, PartialEq, Eq)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize))]
170pub struct Window {
171    /// Whether the window is currently displayed.
172    pub state: WindowState,
173    /// Window priority, 0 (highest) – 7.
174    pub priority: u8,
175    /// Anchor point (which corner/edge/centre the anchor coordinates refer to).
176    pub anchor_point: AnchorPoint,
177    /// Anchor vertical coordinate.
178    pub anchor_vertical: u8,
179    /// Anchor horizontal coordinate.
180    pub anchor_horizontal: u8,
181    /// `true` if the anchor coordinates are relative (percent).
182    pub relative_position: bool,
183    /// Row count (virtual rows; = rc + 1).
184    pub row_count: u8,
185    /// Column count (virtual cols; = cc + 1).
186    pub column_count: u8,
187    /// Row lock.
188    pub row_lock: bool,
189    /// Column lock.
190    pub column_lock: bool,
191    /// Window-style preset ID (0–7) requested in the last DefineWindow.
192    pub window_style: u8,
193    /// Pen-style preset ID (0–7) requested in the last DefineWindow.
194    pub pen_style: u8,
195    /// Justification.
196    pub justify: Justify,
197    /// Print direction.
198    pub print_direction: PrintDirection,
199    /// Scroll direction.
200    pub scroll_direction: ScrollDirection,
201    /// Word wrap.
202    pub word_wrap: bool,
203    /// Window fill colour.
204    pub fill_color: Color,
205    /// Window fill opacity.
206    pub fill_opacity: Opacity,
207    /// Border colour.
208    pub border_color: Color,
209    /// Border type (none / raised / depressed / uniform / shadow).
210    pub border_type: EdgeType,
211    /// Pen size.
212    pub pen_size: PenSize,
213    /// Pen offset (subscript/normal/superscript).
214    pub pen_offset: PenOffset,
215    /// Font style.
216    pub font_style: FontStyle,
217    /// Pen italics.
218    pub italics: bool,
219    /// Pen underline.
220    pub underline: bool,
221    /// Pen edge type.
222    pub edge_type: EdgeType,
223    /// Pen foreground colour.
224    pub fg_color: Color,
225    /// Pen foreground opacity.
226    pub fg_opacity: Opacity,
227    /// Pen background colour.
228    pub bg_color: Color,
229    /// Pen background opacity.
230    pub bg_opacity: Opacity,
231    /// Text grid, `row_count` rows of `column_count` chars (rows are `String`s).
232    rows: Vec<String>,
233    /// Current pen row.
234    pen_row: usize,
235    /// Current pen column.
236    pen_col: usize,
237}
238
239impl Window {
240    fn new() -> Self {
241        Window {
242            state: WindowState::Hidden,
243            priority: 0,
244            anchor_point: AnchorPoint::TopLeft,
245            anchor_vertical: 0,
246            anchor_horizontal: 0,
247            relative_position: false,
248            row_count: 1,
249            column_count: 1,
250            row_lock: false,
251            column_lock: false,
252            window_style: 0,
253            pen_style: 0,
254            justify: Justify::Left,
255            print_direction: PrintDirection::LeftToRight,
256            scroll_direction: ScrollDirection::BottomToTop,
257            word_wrap: false,
258            fill_color: Color::BLACK,
259            fill_opacity: Opacity::Solid,
260            border_color: Color::BLACK,
261            border_type: EdgeType::None,
262            pen_size: PenSize::Standard,
263            pen_offset: PenOffset::Normal,
264            font_style: FontStyle::Default,
265            italics: false,
266            underline: false,
267            edge_type: EdgeType::None,
268            fg_color: Color::WHITE,
269            fg_opacity: Opacity::Solid,
270            bg_color: Color::BLACK,
271            bg_opacity: Opacity::Solid,
272            rows: Vec::new(),
273            pen_row: 0,
274            pen_col: 0,
275        }
276    }
277
278    fn ensure_grid(&mut self) {
279        let rows = (self.row_count as usize).clamp(1, MAX_WINDOW_ROWS);
280        if self.rows.len() != rows {
281            self.rows = alloc::vec![String::new(); rows];
282        }
283    }
284
285    fn clear_text(&mut self) {
286        for r in &mut self.rows {
287            r.clear();
288        }
289        self.pen_row = 0;
290        self.pen_col = 0;
291    }
292
293    fn cols(&self) -> usize {
294        (self.column_count as usize).clamp(1, MAX_WINDOW_COLS)
295    }
296
297    /// Append a character at the current pen position, advancing the pen.
298    fn put_char(&mut self, ch: char) {
299        self.ensure_grid();
300        let cols = self.cols();
301        if self.pen_row >= self.rows.len() {
302            return;
303        }
304        // pad row out to pen_col with spaces
305        let row = &mut self.rows[self.pen_row];
306        while row.chars().count() < self.pen_col {
307            row.push(' ');
308        }
309        if self.pen_col < cols {
310            row.push(ch);
311            self.pen_col += 1;
312        }
313    }
314
315    /// Back Space (C0 BS).
316    fn back_space(&mut self) {
317        if self.pen_col > 0 {
318            self.pen_col -= 1;
319            if self.pen_row < self.rows.len() {
320                let row = &mut self.rows[self.pen_row];
321                let mut chars: Vec<char> = row.chars().collect();
322                if self.pen_col < chars.len() {
323                    chars.truncate(self.pen_col);
324                    *row = chars.into_iter().collect();
325                }
326            }
327        }
328    }
329
330    /// Carriage Return (C0 CR): start of next row; roll up if past the bottom.
331    fn carriage_return(&mut self) {
332        self.ensure_grid();
333        self.pen_col = 0;
334        if self.pen_row + 1 < self.rows.len() {
335            self.pen_row += 1;
336        } else if !self.rows.is_empty() {
337            // roll up: drop the top row, append a blank at the bottom
338            self.rows.remove(0);
339            self.rows.push(String::new());
340            self.pen_row = self.rows.len() - 1;
341        }
342    }
343
344    /// Horizontal Carriage Return (C0 HCR): start of current row, erase the row.
345    fn horizontal_cr(&mut self) {
346        self.ensure_grid();
347        if self.pen_row < self.rows.len() {
348            self.rows[self.pen_row].clear();
349        }
350        self.pen_col = 0;
351    }
352
353    fn set_pen_location(&mut self, row: usize, col: usize) {
354        self.ensure_grid();
355        self.pen_row = row.min(self.rows.len().saturating_sub(1));
356        self.pen_col = col.min(self.cols());
357    }
358
359    /// The window's visible text, rows joined with `\n`, trailing blank rows
360    /// trimmed and per-row trailing spaces removed.
361    #[must_use]
362    pub fn text(&self) -> String {
363        // Trim trailing per-row spaces; keep interior blank rows as newlines but
364        // drop trailing blank rows.
365        let mut lines: Vec<&str> = self.rows.iter().map(|r| r.trim_end()).collect();
366        while lines.last().is_some_and(|l| l.is_empty()) {
367            lines.pop();
368        }
369        lines.join("\n")
370    }
371}
372
373/// One DTVCC service (§6.1): up to eight windows + a current-window pointer.
374#[derive(Debug, Clone, PartialEq, Eq, Default)]
375#[cfg_attr(feature = "serde", derive(serde::Serialize))]
376struct Service {
377    windows: [Option<Window>; NUM_WINDOWS],
378    /// Current window ID (0–7), or `None` when unknown.
379    current_window: Option<usize>,
380}
381
382impl Service {
383    fn reset(&mut self) {
384        *self = Service::default();
385    }
386
387    fn current(&mut self) -> Option<&mut Window> {
388        let id = self.current_window?;
389        self.windows.get_mut(id)?.as_mut()
390    }
391}
392
393/// CEA-708 (DTVCC) caption decoder.
394///
395/// Feed it [`CcTriplet`]s (or raw `cc_data` byte pairs) from the DTVCC stream
396/// (`cc_type` 2/3); read decoded window text per service via
397/// [`service_text`](Cea708Decoder::service_text) / [`windows`](Cea708Decoder::windows).
398///
399/// ```
400/// use cc_data::decode::Cea708Decoder;
401/// let mut dec = Cea708Decoder::new();
402/// // A CCP (header + service-1 block) carrying the DefineWindow worked example
403/// // for window 2: 0x9A 38 4A D1 8B 0F 11.
404/// dec.push_packet(&[0x05, 0x27, 0x9A, 0x38, 0x4A, 0xD1, 0x8B, 0x0F, 0x11]);
405/// let w = &dec.windows(1)[2];
406/// assert!(w.is_some());
407/// ```
408#[derive(Debug, Clone, PartialEq, Eq)]
409#[cfg_attr(feature = "serde", derive(serde::Serialize))]
410pub struct Cea708Decoder {
411    services: [Service; NUM_SERVICES],
412    /// Accumulated CCP data for the in-progress packet.
413    packet: Vec<u8>,
414    /// Last sequence number seen (for discontinuity detection).
415    last_seq: Option<u8>,
416}
417
418impl Default for Cea708Decoder {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424impl Cea708Decoder {
425    /// A new decoder with no services defined.
426    #[must_use]
427    pub fn new() -> Self {
428        Cea708Decoder {
429            services: Default::default(),
430            packet: Vec::new(),
431            last_seq: None,
432        }
433    }
434
435    /// Reset every service (§8.9.5 packet-loss recovery / RST).
436    pub fn reset(&mut self) {
437        for s in &mut self.services {
438            s.reset();
439        }
440        self.packet.clear();
441        self.last_seq = None;
442    }
443
444    /// Feed the decoder the 708 (DTVCC) triplets of a [`crate::CcData`].
445    ///
446    /// A `cc_type == Dtvcc708Start` triplet begins a new Caption Channel Packet;
447    /// `Dtvcc708Data` triplets continue it. Invalid triplets are skipped.
448    pub fn push_triplets<'a, I>(&mut self, triplets: I)
449    where
450        I: IntoIterator<Item = &'a CcTriplet>,
451    {
452        for t in triplets {
453            if !t.cc_valid {
454                continue;
455            }
456            match t.cc_type {
457                CcType::Dtvcc708Start => {
458                    // a new CCP starts; flush any complete prior packet
459                    self.flush_packet();
460                    self.packet.clear();
461                    self.packet.push(t.cc_data_1);
462                    self.packet.push(t.cc_data_2);
463                }
464                CcType::Dtvcc708Data => {
465                    self.packet.push(t.cc_data_1);
466                    self.packet.push(t.cc_data_2);
467                }
468                _ => {}
469            }
470        }
471        self.flush_packet();
472    }
473
474    /// Feed one complete Caption Channel Packet (the CCP header byte followed by
475    /// its data bytes). Useful for testing / when packets are pre-assembled.
476    pub fn push_packet(&mut self, ccp: &[u8]) {
477        self.decode_packet(ccp);
478    }
479
480    /// Flush the accumulated packet buffer if it forms a complete CCP.
481    fn flush_packet(&mut self) {
482        if self.packet.is_empty() {
483            return;
484        }
485        let packet = core::mem::take(&mut self.packet);
486        self.decode_packet(&packet);
487    }
488
489    /// Decode a Caption Channel Packet (§5): header byte + service blocks.
490    fn decode_packet(&mut self, ccp: &[u8]) {
491        let Some((&header, rest)) = ccp.split_first() else {
492            return;
493        };
494        let seq = (header >> 6) & 0x03;
495        let size_code = header & 0x3F;
496        let data_size = if size_code == 0 {
497            PACKET_SIZE_ZERO_DATA
498        } else {
499            (size_code as usize) * 2 - 1
500        };
501        // discontinuity check (§5.1): non-consecutive seq ⇒ reset every service
502        if let Some(prev) = self.last_seq
503            && seq != (prev + 1) & 0x03
504        {
505            for s in &mut self.services {
506                s.reset();
507            }
508        }
509        self.last_seq = Some(seq);
510        let end = data_size.min(rest.len());
511        self.decode_service_blocks(&rest[..end]);
512    }
513
514    /// Walk the service blocks of a CCP (§6.2).
515    fn decode_service_blocks(&mut self, mut data: &[u8]) {
516        loop {
517            let Some((&header, rest)) = data.split_first() else {
518                return;
519            };
520            // Null Service Block Header (§6.2.3): all-zero ⇒ no more blocks.
521            if header == 0 {
522                return;
523            }
524            let mut service_number = u16::from((header >> 5) & 0x07);
525            let block_size = (header & 0x1F) as usize;
526            let mut body = rest;
527            if service_number == u16::from(EXTENDED_SERVICE_ESCAPE) && block_size != 0 {
528                // Extended Service Block Header (§6.2.2): 2nd byte low 6 bits.
529                let Some((&ext, after)) = rest.split_first() else {
530                    return;
531                };
532                service_number = u16::from(ext & 0x3F);
533                body = after;
534            }
535            if block_size > body.len() {
536                // truncated block — process what we have, then stop
537                self.dispatch_service(service_number, body);
538                return;
539            }
540            let (block, next) = body.split_at(block_size);
541            self.dispatch_service(service_number, block);
542            data = next;
543        }
544    }
545
546    fn dispatch_service(&mut self, service_number: u16, block: &[u8]) {
547        // We track standard services 1–6 (47 CFR §79.102 (c)).
548        if service_number == 0 || service_number as usize > NUM_SERVICES {
549            return;
550        }
551        let idx = service_number as usize - 1;
552        Self::interpret(&mut self.services[idx], block);
553    }
554
555    /// The C0/C1/G0/G1/G2/G3 command interpreter (§7/§8) for one service block.
556    fn interpret(service: &mut Service, block: &[u8]) {
557        let mut i = 0usize;
558        while i < block.len() {
559            let b = block[i];
560            let consumed = match b {
561                0x00..=C0_END => Self::handle_c0(service, &block[i..]),
562                G0_START..=G0_END => {
563                    Self::put(service, Self::g0_char(b));
564                    1
565                }
566                C1_START..=C1_END => Self::handle_c1(service, &block[i..]),
567                G1_START..=0xFF => {
568                    // G1 = ISO 8859-1 Latin-1: byte value is the code point.
569                    Self::put(service, char::from(b));
570                    1
571                }
572            };
573            i += consumed.max(1);
574        }
575    }
576
577    /// Handle a C0 control code (§7.1.4). Returns bytes consumed (≥1).
578    fn handle_c0(service: &mut Service, data: &[u8]) -> usize {
579        let b = data[0];
580        match b {
581            C0_NUL => 1,
582            C0_ETX => 1,
583            // Each of BS/FF/CR/HCR is a no-op when there's no current window
584            // (falls through to the trailing `_ => 1`, same as the guard-true
585            // arms below — every path through this match returns 1).
586            C0_BS if let Some(w) = service.current() => {
587                w.back_space();
588                1
589            }
590            C0_FF if let Some(w) = service.current() => {
591                w.clear_text();
592                1
593            }
594            C0_CR if let Some(w) = service.current() => {
595                w.carriage_return();
596                1
597            }
598            C0_HCR if let Some(w) = service.current() => {
599                w.horizontal_cr();
600                1
601            }
602            C0_EXT1 => Self::handle_ext1(service, data),
603            C0_P16 => 3, // P16: command + 2 bytes (16-bit char addressing)
604            // Undefined codes: 0x11–0x17 ⇒ 2 bytes; 0x19–0x1F ⇒ 3 bytes; all
605            // other (undefined 0x00–0x0F) ⇒ 1 byte (§7.1.4).
606            0x11..=0x17 => 2,
607            0x19..=0x1F => 3,
608            _ => 1,
609        }
610    }
611
612    /// EXT1 (0x10) prefix → C2/G2/C3/G3 (§7.1.1). Returns total bytes consumed
613    /// including the EXT1 byte.
614    fn handle_ext1(service: &mut Service, data: &[u8]) -> usize {
615        let Some(&base) = data.get(1) else {
616            return 1;
617        };
618        match base {
619            // C2 (0x00–0x1F): EXT1 + base + 0..=3 data bytes (Table 20).
620            0x00..=0x07 => 2,
621            0x08..=0x0F => 3,
622            0x10..=0x17 => 4,
623            0x18..=0x1F => 5,
624            // G2 (0x20–0x7F): EXT1 + base (two-byte element).
625            0x20..=0x7F => {
626                Self::put(service, Self::g2_char(base));
627                2
628            }
629            // C3 (0x80–0x9F): fixed/variable length (Tables 22/23).
630            0x80..=0x87 => 6,
631            0x88..=0x8F => 7,
632            0x90..=0x9F => {
633                // variable: 1-byte header after the command; N = (data1 & 0x3F)+1.
634                let n = data.get(2).map_or(0, |d| (d & 0x3F) as usize + 1);
635                3 + n
636            }
637            // G3 (0xA0–0xFF): EXT1 + base (two-byte element).
638            _ => {
639                Self::put(service, Self::g3_char(base));
640                2
641            }
642        }
643    }
644
645    /// Handle a C1 caption command (§7.1.5 / §8.10.5). Returns bytes consumed.
646    fn handle_c1(service: &mut Service, data: &[u8]) -> usize {
647        let op = data[0];
648        match op {
649            C1_CW0..=C1_CW7 => {
650                let id = (op - C1_CW0) as usize;
651                if service.windows.get(id).and_then(|w| w.as_ref()).is_some() {
652                    service.current_window = Some(id);
653                }
654                1
655            }
656            C1_CLW => Self::window_map_cmd(service, data, WindowMapOp::Clear),
657            C1_DSW => Self::window_map_cmd(service, data, WindowMapOp::Display),
658            C1_HDW => Self::window_map_cmd(service, data, WindowMapOp::Hide),
659            C1_TGW => Self::window_map_cmd(service, data, WindowMapOp::Toggle),
660            C1_DLW => Self::window_map_cmd(service, data, WindowMapOp::Delete),
661            C1_DLY => 2, // DLY: command + tenths-of-seconds
662            C1_DLC => 1, // DLC: no parameters
663            C1_RST => {
664                service.reset();
665                1
666            }
667            C1_SPA => Self::set_pen_attributes(service, data),
668            C1_SPC => Self::set_pen_color(service, data),
669            C1_SPL => Self::set_pen_location(service, data),
670            C1_SWA => Self::set_window_attributes(service, data),
671            C1_DF0..=C1_DF7 => Self::define_window(service, data),
672            // 0x93–0x96 reserved 1-byte window commands (§7.1.5.1).
673            _ => 1,
674        }
675    }
676
677    fn window_map_cmd(service: &mut Service, data: &[u8], op: WindowMapOp) -> usize {
678        let Some(&map) = data.get(1) else {
679            return 1;
680        };
681        for id in 0..NUM_WINDOWS {
682            if map & (1 << id) == 0 {
683                continue;
684            }
685            match op {
686                WindowMapOp::Clear if let Some(w) = service.windows[id].as_mut() => {
687                    w.clear_text();
688                }
689                WindowMapOp::Clear => {}
690                WindowMapOp::Display if let Some(w) = service.windows[id].as_mut() => {
691                    w.state = WindowState::Visible;
692                }
693                WindowMapOp::Display => {}
694                WindowMapOp::Hide if let Some(w) = service.windows[id].as_mut() => {
695                    w.state = WindowState::Hidden;
696                }
697                WindowMapOp::Hide => {}
698                WindowMapOp::Toggle if let Some(w) = service.windows[id].as_mut() => {
699                    w.state = match w.state {
700                        WindowState::Visible => WindowState::Hidden,
701                        WindowState::Hidden => WindowState::Visible,
702                    };
703                }
704                WindowMapOp::Toggle => {}
705                WindowMapOp::Delete => {
706                    service.windows[id] = None;
707                    if service.current_window == Some(id) {
708                        service.current_window = None;
709                    }
710                }
711            }
712        }
713        2
714    }
715
716    /// DefineWindow DF0–DF7 (§8.10.5.2): 6 parameter bytes.
717    fn define_window(service: &mut Service, data: &[u8]) -> usize {
718        const TOTAL: usize = 7;
719        if data.len() < TOTAL {
720            return data.len().max(1);
721        }
722        let id = (data[0] - C1_DF0) as usize;
723        let p1 = data[1];
724        let p2 = data[2];
725        let p3 = data[3];
726        let p4 = data[4];
727        let p5 = data[5];
728        let p6 = data[6];
729
730        let creating = service.windows[id].is_none();
731        let w = service.windows[id].get_or_insert_with(Window::new);
732
733        w.priority = p1 & 0x07;
734        w.column_lock = (p1 >> 3) & 0x01 != 0;
735        w.row_lock = (p1 >> 4) & 0x01 != 0;
736        w.state = if (p1 >> 5) & 0x01 != 0 {
737            WindowState::Visible
738        } else {
739            WindowState::Hidden
740        };
741        w.relative_position = (p2 >> 7) & 0x01 != 0;
742        w.anchor_vertical = p2 & 0x7F;
743        w.anchor_horizontal = p3;
744        w.anchor_point = AnchorPoint::from_bits((p4 >> 4) & 0x0F);
745        w.row_count = (p4 & 0x0F) + 1;
746        w.column_count = (p5 & 0x3F) + 1;
747        w.window_style = (p6 >> 3) & 0x07;
748        w.pen_style = p6 & 0x07;
749
750        if creating {
751            // On create: apply preset window/pen styles, fill, pen at (0,0).
752            apply_window_style(
753                w,
754                if w.window_style == 0 {
755                    1
756                } else {
757                    w.window_style
758                },
759            );
760            apply_pen_style(w, if w.pen_style == 0 { 1 } else { w.pen_style });
761            w.ensure_grid();
762            w.clear_text();
763        } else {
764            // On update: a non-zero style preset is re-applied; pen unaffected.
765            if w.window_style != 0 {
766                apply_window_style(w, w.window_style);
767            }
768            if w.pen_style != 0 {
769                apply_pen_style(w, w.pen_style);
770            }
771            w.ensure_grid();
772        }
773        service.current_window = Some(id);
774        TOTAL
775    }
776
777    /// SetWindowAttributes SWA (§8.10.5.8): 4 parameter bytes.
778    fn set_window_attributes(service: &mut Service, data: &[u8]) -> usize {
779        const TOTAL: usize = 5;
780        if data.len() < TOTAL {
781            return data.len().max(1);
782        }
783        let p1 = data[1];
784        let p2 = data[2];
785        let p3 = data[3];
786        let p4 = data[4];
787        if let Some(w) = service.current() {
788            w.fill_opacity = Opacity::from_bits((p1 >> 6) & 0x03);
789            w.fill_color = Color::new((p1 >> 4) & 0x03, (p1 >> 2) & 0x03, p1 & 0x03);
790            let bt_lo = (p2 >> 6) & 0x03;
791            w.border_color = Color::new((p2 >> 4) & 0x03, (p2 >> 2) & 0x03, p2 & 0x03);
792            let bt_hi = (p3 >> 7) & 0x01;
793            w.border_type = EdgeType::from_bits((bt_hi << 2) | bt_lo);
794            w.word_wrap = (p3 >> 6) & 0x01 != 0;
795            w.print_direction = PrintDirection::from_bits((p3 >> 4) & 0x03);
796            w.scroll_direction = ScrollDirection::from_bits((p3 >> 2) & 0x03);
797            w.justify = Justify::from_bits(p3 & 0x03);
798            // p4: effect speed / direction / display effect — not rendered here.
799            let _ = p4;
800        }
801        TOTAL
802    }
803
804    /// SetPenAttributes SPA (§8.10.5.9): 2 parameter bytes.
805    fn set_pen_attributes(service: &mut Service, data: &[u8]) -> usize {
806        const TOTAL: usize = 3;
807        if data.len() < TOTAL {
808            return data.len().max(1);
809        }
810        let p1 = data[1];
811        let p2 = data[2];
812        if let Some(w) = service.current() {
813            w.pen_offset = PenOffset::from_bits((p1 >> 2) & 0x03);
814            w.pen_size = PenSize::from_bits(p1 & 0x03);
815            w.italics = (p2 >> 7) & 0x01 != 0;
816            w.underline = (p2 >> 6) & 0x01 != 0;
817            w.edge_type = EdgeType::from_bits((p2 >> 3) & 0x07);
818            w.font_style = FontStyle::from_bits(p2 & 0x07);
819        }
820        TOTAL
821    }
822
823    /// SetPenColor SPC (§8.10.5.10): 3 parameter bytes.
824    fn set_pen_color(service: &mut Service, data: &[u8]) -> usize {
825        const TOTAL: usize = 4;
826        if data.len() < TOTAL {
827            return data.len().max(1);
828        }
829        let p1 = data[1];
830        let p2 = data[2];
831        let p3 = data[3];
832        if let Some(w) = service.current() {
833            w.fg_opacity = Opacity::from_bits((p1 >> 6) & 0x03);
834            w.fg_color = Color::new((p1 >> 4) & 0x03, (p1 >> 2) & 0x03, p1 & 0x03);
835            w.bg_opacity = Opacity::from_bits((p2 >> 6) & 0x03);
836            w.bg_color = Color::new((p2 >> 4) & 0x03, (p2 >> 2) & 0x03, p2 & 0x03);
837            // p3 = edge colour
838            w.border_color = Color::new((p3 >> 4) & 0x03, (p3 >> 2) & 0x03, p3 & 0x03);
839        }
840        TOTAL
841    }
842
843    /// SetPenLocation SPL (§8.10.5.11): 2 parameter bytes.
844    fn set_pen_location(service: &mut Service, data: &[u8]) -> usize {
845        const TOTAL: usize = 3;
846        if data.len() < TOTAL {
847            return data.len().max(1);
848        }
849        let row = (data[1] & 0x0F) as usize;
850        let col = (data[2] & 0x3F) as usize;
851        if let Some(w) = service.current() {
852            w.set_pen_location(row, col);
853        }
854        TOTAL
855    }
856
857    fn put(service: &mut Service, ch: char) {
858        if let Some(w) = service.current() {
859            w.put_char(ch);
860        }
861    }
862
863    /// G0 byte → glyph (§7.1.6): ASCII printable, 0x7F = musical note ♪.
864    fn g0_char(b: u8) -> char {
865        if b == G0_MUSIC_NOTE {
866            '\u{266A}'
867        } else {
868            char::from(b)
869        }
870    }
871
872    /// G2 byte → glyph (§7.1.8 / Table 17), with substitution for the rest.
873    fn g2_char(b: u8) -> char {
874        match b {
875            0x20 | 0x21 => ' ', // TSP / NBTSP — transparent space
876            0x25 => '\u{2026}', // …
877            0x2A => '\u{0160}', // Š
878            0x2C => '\u{0152}', // Œ
879            0x30 => '\u{25A0}', // ■ solid block
880            0x31 => '\u{2018}', // ‘
881            0x32 => '\u{2019}', // ’
882            0x33 => '\u{201C}', // "
883            0x34 => '\u{201D}', // "
884            0x35 => '\u{2022}', // • bullet
885            0x39 => '\u{2122}', // ™
886            0x3A => '\u{0161}', // š
887            0x3C => '\u{0153}', // œ
888            0x3D => '\u{2120}', // ℠
889            0x3F => '\u{0178}', // Ÿ
890            0x76 => '\u{215B}', // ⅛
891            0x77 => '\u{215C}', // ⅜
892            0x78 => '\u{215D}', // ⅝
893            0x79 => '\u{215E}', // ⅞
894            _ => '_',           // unsupported G2 ⇒ underscore (Table 28 floor)
895        }
896    }
897
898    /// G3 byte → glyph (§7.1.9): 0xA0 = [CC] icon; the rest substitute `_`.
899    fn g3_char(b: u8) -> char {
900        if b == 0xA0 {
901            '\u{1F4FA}' // 📺 stand-in for the [CC] icon
902        } else {
903            '_'
904        }
905    }
906
907    /// Read the windows of a service (`1`–`6`). Returns an empty array view for
908    /// an out-of-range service number.
909    #[must_use]
910    pub fn windows(&self, service_number: usize) -> &[Option<Window>; NUM_WINDOWS] {
911        const EMPTY: [Option<Window>; NUM_WINDOWS] =
912            [None, None, None, None, None, None, None, None];
913        if service_number == 0 || service_number > NUM_SERVICES {
914            return &EMPTY;
915        }
916        &self.services[service_number - 1].windows
917    }
918
919    /// All decoded text for a service (`1`–`6`), visible-window text joined with
920    /// `\n` in window-priority order (0 = highest first), then by window ID.
921    #[must_use]
922    pub fn service_text(&self, service_number: usize) -> String {
923        if service_number == 0 || service_number > NUM_SERVICES {
924            return String::new();
925        }
926        let svc = &self.services[service_number - 1];
927        let mut idxs: Vec<usize> = (0..NUM_WINDOWS)
928            .filter(|&i| {
929                svc.windows[i]
930                    .as_ref()
931                    .is_some_and(|w| w.state == WindowState::Visible)
932            })
933            .collect();
934        idxs.sort_by_key(|&i| {
935            svc.windows[i]
936                .as_ref()
937                .map_or((u8::MAX, i), |w| (w.priority, i))
938        });
939        let mut out = String::new();
940        for i in idxs {
941            if let Some(w) = svc.windows[i].as_ref() {
942                let t = w.text();
943                if t.is_empty() {
944                    continue;
945                }
946                if !out.is_empty() {
947                    out.push('\n');
948                }
949                out.push_str(&t);
950            }
951        }
952        out
953    }
954}
955
956/// The window-map command kinds that share the CLW/DSW/HDW/TGW/DLW bitmap byte.
957#[derive(Clone, Copy)]
958enum WindowMapOp {
959    Clear,
960    Display,
961    Hide,
962    Toggle,
963    Delete,
964}
965
966/// Apply a predefined window style 1–7 (Table 26).
967fn apply_window_style(w: &mut Window, id: u8) {
968    // All presets: print dir L→R (except 7), scroll BOTTOM→TOP (except 7),
969    // border NONE, display effect SNAP. justify + wordwrap + fill vary.
970    w.border_type = EdgeType::None;
971    match id {
972        1 => style(w, Justify::Left, false, Some(Color::BLACK), Opacity::Solid),
973        2 => style(w, Justify::Left, false, None, Opacity::Transparent),
974        3 => style(
975            w,
976            Justify::Center,
977            false,
978            Some(Color::BLACK),
979            Opacity::Solid,
980        ),
981        4 => style(w, Justify::Left, true, Some(Color::BLACK), Opacity::Solid),
982        5 => style(w, Justify::Left, true, None, Opacity::Transparent),
983        6 => style(w, Justify::Center, true, Some(Color::BLACK), Opacity::Solid),
984        7 => {
985            w.justify = Justify::Left;
986            w.word_wrap = false;
987            w.print_direction = PrintDirection::TopToBottom;
988            w.scroll_direction = ScrollDirection::RightToLeft;
989            w.fill_color = Color::BLACK;
990            w.fill_opacity = Opacity::Solid;
991        }
992        _ => {}
993    }
994}
995
996fn style(w: &mut Window, j: Justify, ww: bool, fill: Option<Color>, op: Opacity) {
997    w.justify = j;
998    w.word_wrap = ww;
999    w.print_direction = PrintDirection::LeftToRight;
1000    w.scroll_direction = ScrollDirection::BottomToTop;
1001    w.fill_opacity = op;
1002    if let Some(c) = fill {
1003        w.fill_color = c;
1004    }
1005}
1006
1007/// Apply a predefined pen style 1–7 (Table 27).
1008fn apply_pen_style(w: &mut Window, id: u8) {
1009    w.pen_size = PenSize::Standard;
1010    w.pen_offset = PenOffset::Normal;
1011    w.italics = false;
1012    w.underline = false;
1013    w.fg_color = Color::WHITE;
1014    w.fg_opacity = Opacity::Solid;
1015    match id {
1016        1 => pen(
1017            w,
1018            FontStyle::Default,
1019            EdgeType::None,
1020            Color::BLACK,
1021            Opacity::Solid,
1022        ),
1023        2 => pen(
1024            w,
1025            FontStyle::MonospacedSerif,
1026            EdgeType::None,
1027            Color::BLACK,
1028            Opacity::Solid,
1029        ),
1030        3 => pen(
1031            w,
1032            FontStyle::ProportionalSerif,
1033            EdgeType::None,
1034            Color::BLACK,
1035            Opacity::Solid,
1036        ),
1037        4 => pen(
1038            w,
1039            FontStyle::MonospacedSansSerif,
1040            EdgeType::None,
1041            Color::BLACK,
1042            Opacity::Solid,
1043        ),
1044        5 => pen(
1045            w,
1046            FontStyle::ProportionalSansSerif,
1047            EdgeType::None,
1048            Color::BLACK,
1049            Opacity::Solid,
1050        ),
1051        6 => pen(
1052            w,
1053            FontStyle::MonospacedSansSerif,
1054            EdgeType::Uniform,
1055            Color::BLACK,
1056            Opacity::Transparent,
1057        ),
1058        7 => pen(
1059            w,
1060            FontStyle::ProportionalSansSerif,
1061            EdgeType::Uniform,
1062            Color::BLACK,
1063            Opacity::Transparent,
1064        ),
1065        _ => {}
1066    }
1067}
1068
1069fn pen(w: &mut Window, font: FontStyle, edge: EdgeType, bg: Color, bg_op: Opacity) {
1070    w.font_style = font;
1071    w.edge_type = edge;
1072    w.bg_color = bg;
1073    w.bg_opacity = bg_op;
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078    use super::*;
1079
1080    /// Build a single-service CCP carrying `cmds` as service `svc`'s block.
1081    fn ccp(svc: u8, cmds: &[u8]) -> Vec<u8> {
1082        let mut sb = alloc::vec![(svc << 5) | (cmds.len() as u8)];
1083        sb.extend_from_slice(cmds);
1084        // size_code = number of byte-pairs including the header byte.
1085        let size_code = (sb.len().div_ceil(2) + 1) as u8 & 0x3F;
1086        let mut packet = alloc::vec![size_code];
1087        packet.extend_from_slice(&sb);
1088        packet
1089    }
1090
1091    /// CTA-708-E DefineWindow worked example (`cea708-decode.md`, p.66–67):
1092    /// `0x9A 38 4A D1 8B 0F 11` → window id=2, visible=YES, rl=YES, cl=YES,
1093    /// priority=0, rp=0, av=74, ah=209, ap=8, rc=11 (→12 rows), cc=15 (→16 cols),
1094    /// ws=2, ps=1.
1095    #[test]
1096    fn define_window_worked_example() {
1097        let mut dec = Cea708Decoder::new();
1098        let packet = ccp(1, &[0x9A, 0x38, 0x4A, 0xD1, 0x8B, 0x0F, 0x11]);
1099        dec.push_packet(&packet);
1100        let w = dec.windows(1)[2].as_ref().expect("window 2 defined");
1101        assert_eq!(w.state, WindowState::Visible);
1102        assert!(w.row_lock);
1103        assert!(w.column_lock);
1104        assert_eq!(w.priority, 0);
1105        assert!(!w.relative_position);
1106        assert_eq!(w.anchor_vertical, 74);
1107        assert_eq!(w.anchor_horizontal, 209);
1108        assert_eq!(w.anchor_point, AnchorPoint::BottomRight);
1109        assert_eq!(w.row_count, 12);
1110        assert_eq!(w.column_count, 16);
1111        assert_eq!(w.window_style, 2);
1112        assert_eq!(w.pen_style, 1);
1113    }
1114
1115    /// SWA worked example (`cea708-decode.md`, p.76):
1116    /// `0x97,0x64,0x53,0x88,0x22` → border type = 5 (SHADOW_RIGHT).
1117    #[test]
1118    fn swa_border_type_split() {
1119        let mut dec = Cea708Decoder::new();
1120        // define a window first (so there is a current window), then SWA.
1121        let packet = ccp(
1122            1,
1123            &[
1124                0x98, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, // DF0 visible, defaults
1125                0x97, 0x64, 0x53, 0x88, 0x22, // SWA
1126            ],
1127        );
1128        dec.push_packet(&packet);
1129        let w = dec.windows(1)[0].as_ref().expect("window 0");
1130        assert_eq!(w.border_type, EdgeType::RightDropShadow);
1131    }
1132
1133    /// A simple caption: define a window (visible), write "Hi", read service text.
1134    #[test]
1135    fn decode_text() {
1136        let mut dec = Cea708Decoder::new();
1137        let packet = ccp(
1138            1,
1139            &[
1140                0x98, 0x20, 0x00, 0x00, 0x02, 0x0F, 0x00, // DF0 visible, 3 rows × 16
1141                b'H', b'i',
1142            ],
1143        );
1144        dec.push_packet(&packet);
1145        assert_eq!(dec.service_text(1), "Hi");
1146    }
1147
1148    /// ≥2 services + multi-window exercised in one packet.
1149    #[test]
1150    fn two_services_multi_window() {
1151        let mut dec = Cea708Decoder::new();
1152        // Service 1: define window 0 visible, write "S1".
1153        let s1_block = [0x98, 0x20, 0x00, 0x00, 0x00, 0x0F, 0x00, b'S', b'1'];
1154        // Service 2: define window 1 visible, write "S2".
1155        let s2_block = [0x99, 0x20, 0x00, 0x00, 0x00, 0x0F, 0x00, b'S', b'2'];
1156        let mut data = Vec::new();
1157        data.push((1 << 5) | (s1_block.len() as u8));
1158        data.extend_from_slice(&s1_block);
1159        data.push((2 << 5) | (s2_block.len() as u8));
1160        data.extend_from_slice(&s2_block);
1161        let size_code = (data.len().div_ceil(2) + 1) as u8 & 0x3F;
1162        let mut packet = alloc::vec![size_code];
1163        packet.extend_from_slice(&data);
1164        dec.push_packet(&packet);
1165        assert_eq!(dec.service_text(1), "S1");
1166        assert_eq!(dec.service_text(2), "S2");
1167        assert!(dec.windows(1)[0].is_some());
1168        assert!(dec.windows(2)[1].is_some());
1169    }
1170
1171    #[test]
1172    fn carriage_return_rolls_up() {
1173        let mut w = Window::new();
1174        w.row_count = 2;
1175        w.column_count = 10;
1176        w.ensure_grid();
1177        w.put_char('A');
1178        w.carriage_return();
1179        w.put_char('B');
1180        w.carriage_return(); // now at bottom; should roll up
1181        w.put_char('C');
1182        assert_eq!(w.text(), "B\nC");
1183    }
1184
1185    #[test]
1186    fn g0_music_note() {
1187        assert_eq!(Cea708Decoder::g0_char(0x7F), '\u{266A}');
1188        assert_eq!(Cea708Decoder::g0_char(b'A'), 'A');
1189    }
1190
1191    #[test]
1192    fn no_panic_on_arbitrary_input() {
1193        // Feed adversarial / truncated / malformed bytes; must never panic.
1194        let inputs: &[&[u8]] = &[
1195            &[],
1196            &[0x00],
1197            &[0xFF],
1198            &[0x01, 0x98],                               // DefineWindow truncated
1199            &[0x3F, 0x80, 0x90, 0x91, 0x92, 0x97, 0x98], // size_code huge, partial cmds
1200            &[0x20, 0xEE, (7 << 5) | 1],                 // extended service escape truncated
1201            &[0x10, 0x9A],                               // C0 EXT1 → C3 variable, truncated
1202            &[0x18, 0x00],                               // P16 truncated
1203        ];
1204        for inp in inputs {
1205            let mut dec = Cea708Decoder::new();
1206            dec.push_packet(inp);
1207        }
1208        // a long pseudo-random stream
1209        let mut dec = Cea708Decoder::new();
1210        let mut x: u32 = 0x1234_5678;
1211        let mut buf = Vec::new();
1212        for _ in 0..4096 {
1213            x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1214            buf.push((x >> 16) as u8);
1215        }
1216        dec.push_packet(&buf);
1217        // also drive it via triplets
1218        let mut dec2 = Cea708Decoder::new();
1219        let triplets: Vec<CcTriplet> = buf
1220            .chunks(2)
1221            .map(|c| CcTriplet {
1222                cc_valid: true,
1223                cc_type: CcType::Dtvcc708Data,
1224                cc_data_1: c[0],
1225                cc_data_2: *c.get(1).unwrap_or(&0),
1226            })
1227            .collect();
1228        dec2.push_triplets(&triplets);
1229    }
1230}