Skip to main content

dvb_cc/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 (`dvb-cc/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}
80dvb_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}
161dvb_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 dvb_cc::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            if seq != (prev + 1) & 0x03 {
504                for s in &mut self.services {
505                    s.reset();
506                }
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            C0_BS => {
584                if let Some(w) = service.current() {
585                    w.back_space();
586                }
587                1
588            }
589            C0_FF => {
590                if let Some(w) = service.current() {
591                    w.clear_text();
592                }
593                1
594            }
595            C0_CR => {
596                if let Some(w) = service.current() {
597                    w.carriage_return();
598                }
599                1
600            }
601            C0_HCR => {
602                if let Some(w) = service.current() {
603                    w.horizontal_cr();
604                }
605                1
606            }
607            C0_EXT1 => Self::handle_ext1(service, data),
608            C0_P16 => 3, // P16: command + 2 bytes (16-bit char addressing)
609            // Undefined codes: 0x11–0x17 ⇒ 2 bytes; 0x19–0x1F ⇒ 3 bytes; all
610            // other (undefined 0x00–0x0F) ⇒ 1 byte (§7.1.4).
611            0x11..=0x17 => 2,
612            0x19..=0x1F => 3,
613            _ => 1,
614        }
615    }
616
617    /// EXT1 (0x10) prefix → C2/G2/C3/G3 (§7.1.1). Returns total bytes consumed
618    /// including the EXT1 byte.
619    fn handle_ext1(service: &mut Service, data: &[u8]) -> usize {
620        let Some(&base) = data.get(1) else {
621            return 1;
622        };
623        match base {
624            // C2 (0x00–0x1F): EXT1 + base + 0..=3 data bytes (Table 20).
625            0x00..=0x07 => 2,
626            0x08..=0x0F => 3,
627            0x10..=0x17 => 4,
628            0x18..=0x1F => 5,
629            // G2 (0x20–0x7F): EXT1 + base (two-byte element).
630            0x20..=0x7F => {
631                Self::put(service, Self::g2_char(base));
632                2
633            }
634            // C3 (0x80–0x9F): fixed/variable length (Tables 22/23).
635            0x80..=0x87 => 6,
636            0x88..=0x8F => 7,
637            0x90..=0x9F => {
638                // variable: 1-byte header after the command; N = (data1 & 0x3F)+1.
639                let n = data.get(2).map_or(0, |d| (d & 0x3F) as usize + 1);
640                3 + n
641            }
642            // G3 (0xA0–0xFF): EXT1 + base (two-byte element).
643            _ => {
644                Self::put(service, Self::g3_char(base));
645                2
646            }
647        }
648    }
649
650    /// Handle a C1 caption command (§7.1.5 / §8.10.5). Returns bytes consumed.
651    fn handle_c1(service: &mut Service, data: &[u8]) -> usize {
652        let op = data[0];
653        match op {
654            C1_CW0..=C1_CW7 => {
655                let id = (op - C1_CW0) as usize;
656                if service.windows.get(id).and_then(|w| w.as_ref()).is_some() {
657                    service.current_window = Some(id);
658                }
659                1
660            }
661            C1_CLW => Self::window_map_cmd(service, data, WindowMapOp::Clear),
662            C1_DSW => Self::window_map_cmd(service, data, WindowMapOp::Display),
663            C1_HDW => Self::window_map_cmd(service, data, WindowMapOp::Hide),
664            C1_TGW => Self::window_map_cmd(service, data, WindowMapOp::Toggle),
665            C1_DLW => Self::window_map_cmd(service, data, WindowMapOp::Delete),
666            C1_DLY => 2, // DLY: command + tenths-of-seconds
667            C1_DLC => 1, // DLC: no parameters
668            C1_RST => {
669                service.reset();
670                1
671            }
672            C1_SPA => Self::set_pen_attributes(service, data),
673            C1_SPC => Self::set_pen_color(service, data),
674            C1_SPL => Self::set_pen_location(service, data),
675            C1_SWA => Self::set_window_attributes(service, data),
676            C1_DF0..=C1_DF7 => Self::define_window(service, data),
677            // 0x93–0x96 reserved 1-byte window commands (§7.1.5.1).
678            _ => 1,
679        }
680    }
681
682    fn window_map_cmd(service: &mut Service, data: &[u8], op: WindowMapOp) -> usize {
683        let Some(&map) = data.get(1) else {
684            return 1;
685        };
686        for id in 0..NUM_WINDOWS {
687            if map & (1 << id) == 0 {
688                continue;
689            }
690            match op {
691                WindowMapOp::Clear => {
692                    if let Some(w) = service.windows[id].as_mut() {
693                        w.clear_text();
694                    }
695                }
696                WindowMapOp::Display => {
697                    if let Some(w) = service.windows[id].as_mut() {
698                        w.state = WindowState::Visible;
699                    }
700                }
701                WindowMapOp::Hide => {
702                    if let Some(w) = service.windows[id].as_mut() {
703                        w.state = WindowState::Hidden;
704                    }
705                }
706                WindowMapOp::Toggle => {
707                    if let Some(w) = service.windows[id].as_mut() {
708                        w.state = match w.state {
709                            WindowState::Visible => WindowState::Hidden,
710                            WindowState::Hidden => WindowState::Visible,
711                        };
712                    }
713                }
714                WindowMapOp::Delete => {
715                    service.windows[id] = None;
716                    if service.current_window == Some(id) {
717                        service.current_window = None;
718                    }
719                }
720            }
721        }
722        2
723    }
724
725    /// DefineWindow DF0–DF7 (§8.10.5.2): 6 parameter bytes.
726    fn define_window(service: &mut Service, data: &[u8]) -> usize {
727        const TOTAL: usize = 7;
728        if data.len() < TOTAL {
729            return data.len().max(1);
730        }
731        let id = (data[0] - C1_DF0) as usize;
732        let p1 = data[1];
733        let p2 = data[2];
734        let p3 = data[3];
735        let p4 = data[4];
736        let p5 = data[5];
737        let p6 = data[6];
738
739        let creating = service.windows[id].is_none();
740        let w = service.windows[id].get_or_insert_with(Window::new);
741
742        w.priority = p1 & 0x07;
743        w.column_lock = (p1 >> 3) & 0x01 != 0;
744        w.row_lock = (p1 >> 4) & 0x01 != 0;
745        w.state = if (p1 >> 5) & 0x01 != 0 {
746            WindowState::Visible
747        } else {
748            WindowState::Hidden
749        };
750        w.relative_position = (p2 >> 7) & 0x01 != 0;
751        w.anchor_vertical = p2 & 0x7F;
752        w.anchor_horizontal = p3;
753        w.anchor_point = AnchorPoint::from_bits((p4 >> 4) & 0x0F);
754        w.row_count = (p4 & 0x0F) + 1;
755        w.column_count = (p5 & 0x3F) + 1;
756        w.window_style = (p6 >> 3) & 0x07;
757        w.pen_style = p6 & 0x07;
758
759        if creating {
760            // On create: apply preset window/pen styles, fill, pen at (0,0).
761            apply_window_style(
762                w,
763                if w.window_style == 0 {
764                    1
765                } else {
766                    w.window_style
767                },
768            );
769            apply_pen_style(w, if w.pen_style == 0 { 1 } else { w.pen_style });
770            w.ensure_grid();
771            w.clear_text();
772        } else {
773            // On update: a non-zero style preset is re-applied; pen unaffected.
774            if w.window_style != 0 {
775                apply_window_style(w, w.window_style);
776            }
777            if w.pen_style != 0 {
778                apply_pen_style(w, w.pen_style);
779            }
780            w.ensure_grid();
781        }
782        service.current_window = Some(id);
783        TOTAL
784    }
785
786    /// SetWindowAttributes SWA (§8.10.5.8): 4 parameter bytes.
787    fn set_window_attributes(service: &mut Service, data: &[u8]) -> usize {
788        const TOTAL: usize = 5;
789        if data.len() < TOTAL {
790            return data.len().max(1);
791        }
792        let p1 = data[1];
793        let p2 = data[2];
794        let p3 = data[3];
795        let p4 = data[4];
796        if let Some(w) = service.current() {
797            w.fill_opacity = Opacity::from_bits((p1 >> 6) & 0x03);
798            w.fill_color = Color::new((p1 >> 4) & 0x03, (p1 >> 2) & 0x03, p1 & 0x03);
799            let bt_lo = (p2 >> 6) & 0x03;
800            w.border_color = Color::new((p2 >> 4) & 0x03, (p2 >> 2) & 0x03, p2 & 0x03);
801            let bt_hi = (p3 >> 7) & 0x01;
802            w.border_type = EdgeType::from_bits((bt_hi << 2) | bt_lo);
803            w.word_wrap = (p3 >> 6) & 0x01 != 0;
804            w.print_direction = PrintDirection::from_bits((p3 >> 4) & 0x03);
805            w.scroll_direction = ScrollDirection::from_bits((p3 >> 2) & 0x03);
806            w.justify = Justify::from_bits(p3 & 0x03);
807            // p4: effect speed / direction / display effect — not rendered here.
808            let _ = p4;
809        }
810        TOTAL
811    }
812
813    /// SetPenAttributes SPA (§8.10.5.9): 2 parameter bytes.
814    fn set_pen_attributes(service: &mut Service, data: &[u8]) -> usize {
815        const TOTAL: usize = 3;
816        if data.len() < TOTAL {
817            return data.len().max(1);
818        }
819        let p1 = data[1];
820        let p2 = data[2];
821        if let Some(w) = service.current() {
822            w.pen_offset = PenOffset::from_bits((p1 >> 2) & 0x03);
823            w.pen_size = PenSize::from_bits(p1 & 0x03);
824            w.italics = (p2 >> 7) & 0x01 != 0;
825            w.underline = (p2 >> 6) & 0x01 != 0;
826            w.edge_type = EdgeType::from_bits((p2 >> 3) & 0x07);
827            w.font_style = FontStyle::from_bits(p2 & 0x07);
828        }
829        TOTAL
830    }
831
832    /// SetPenColor SPC (§8.10.5.10): 3 parameter bytes.
833    fn set_pen_color(service: &mut Service, data: &[u8]) -> usize {
834        const TOTAL: usize = 4;
835        if data.len() < TOTAL {
836            return data.len().max(1);
837        }
838        let p1 = data[1];
839        let p2 = data[2];
840        let p3 = data[3];
841        if let Some(w) = service.current() {
842            w.fg_opacity = Opacity::from_bits((p1 >> 6) & 0x03);
843            w.fg_color = Color::new((p1 >> 4) & 0x03, (p1 >> 2) & 0x03, p1 & 0x03);
844            w.bg_opacity = Opacity::from_bits((p2 >> 6) & 0x03);
845            w.bg_color = Color::new((p2 >> 4) & 0x03, (p2 >> 2) & 0x03, p2 & 0x03);
846            // p3 = edge colour
847            w.border_color = Color::new((p3 >> 4) & 0x03, (p3 >> 2) & 0x03, p3 & 0x03);
848        }
849        TOTAL
850    }
851
852    /// SetPenLocation SPL (§8.10.5.11): 2 parameter bytes.
853    fn set_pen_location(service: &mut Service, data: &[u8]) -> usize {
854        const TOTAL: usize = 3;
855        if data.len() < TOTAL {
856            return data.len().max(1);
857        }
858        let row = (data[1] & 0x0F) as usize;
859        let col = (data[2] & 0x3F) as usize;
860        if let Some(w) = service.current() {
861            w.set_pen_location(row, col);
862        }
863        TOTAL
864    }
865
866    fn put(service: &mut Service, ch: char) {
867        if let Some(w) = service.current() {
868            w.put_char(ch);
869        }
870    }
871
872    /// G0 byte → glyph (§7.1.6): ASCII printable, 0x7F = musical note ♪.
873    fn g0_char(b: u8) -> char {
874        if b == G0_MUSIC_NOTE {
875            '\u{266A}'
876        } else {
877            char::from(b)
878        }
879    }
880
881    /// G2 byte → glyph (§7.1.8 / Table 17), with substitution for the rest.
882    fn g2_char(b: u8) -> char {
883        match b {
884            0x20 | 0x21 => ' ', // TSP / NBTSP — transparent space
885            0x25 => '\u{2026}', // …
886            0x2A => '\u{0160}', // Š
887            0x2C => '\u{0152}', // Œ
888            0x30 => '\u{25A0}', // ■ solid block
889            0x31 => '\u{2018}', // ‘
890            0x32 => '\u{2019}', // ’
891            0x33 => '\u{201C}', // "
892            0x34 => '\u{201D}', // "
893            0x35 => '\u{2022}', // • bullet
894            0x39 => '\u{2122}', // ™
895            0x3A => '\u{0161}', // š
896            0x3C => '\u{0153}', // œ
897            0x3D => '\u{2120}', // ℠
898            0x3F => '\u{0178}', // Ÿ
899            0x76 => '\u{215B}', // ⅛
900            0x77 => '\u{215C}', // ⅜
901            0x78 => '\u{215D}', // ⅝
902            0x79 => '\u{215E}', // ⅞
903            _ => '_',           // unsupported G2 ⇒ underscore (Table 28 floor)
904        }
905    }
906
907    /// G3 byte → glyph (§7.1.9): 0xA0 = [CC] icon; the rest substitute `_`.
908    fn g3_char(b: u8) -> char {
909        if b == 0xA0 {
910            '\u{1F4FA}' // 📺 stand-in for the [CC] icon
911        } else {
912            '_'
913        }
914    }
915
916    /// Read the windows of a service (`1`–`6`). Returns an empty array view for
917    /// an out-of-range service number.
918    #[must_use]
919    pub fn windows(&self, service_number: usize) -> &[Option<Window>; NUM_WINDOWS] {
920        const EMPTY: [Option<Window>; NUM_WINDOWS] =
921            [None, None, None, None, None, None, None, None];
922        if service_number == 0 || service_number > NUM_SERVICES {
923            return &EMPTY;
924        }
925        &self.services[service_number - 1].windows
926    }
927
928    /// All decoded text for a service (`1`–`6`), visible-window text joined with
929    /// `\n` in window-priority order (0 = highest first), then by window ID.
930    #[must_use]
931    pub fn service_text(&self, service_number: usize) -> String {
932        if service_number == 0 || service_number > NUM_SERVICES {
933            return String::new();
934        }
935        let svc = &self.services[service_number - 1];
936        let mut idxs: Vec<usize> = (0..NUM_WINDOWS)
937            .filter(|&i| {
938                svc.windows[i]
939                    .as_ref()
940                    .is_some_and(|w| w.state == WindowState::Visible)
941            })
942            .collect();
943        idxs.sort_by_key(|&i| {
944            svc.windows[i]
945                .as_ref()
946                .map_or((u8::MAX, i), |w| (w.priority, i))
947        });
948        let mut out = String::new();
949        for i in idxs {
950            if let Some(w) = svc.windows[i].as_ref() {
951                let t = w.text();
952                if t.is_empty() {
953                    continue;
954                }
955                if !out.is_empty() {
956                    out.push('\n');
957                }
958                out.push_str(&t);
959            }
960        }
961        out
962    }
963}
964
965/// The window-map command kinds that share the CLW/DSW/HDW/TGW/DLW bitmap byte.
966#[derive(Clone, Copy)]
967enum WindowMapOp {
968    Clear,
969    Display,
970    Hide,
971    Toggle,
972    Delete,
973}
974
975/// Apply a predefined window style 1–7 (Table 26).
976fn apply_window_style(w: &mut Window, id: u8) {
977    // All presets: print dir L→R (except 7), scroll BOTTOM→TOP (except 7),
978    // border NONE, display effect SNAP. justify + wordwrap + fill vary.
979    w.border_type = EdgeType::None;
980    match id {
981        1 => style(w, Justify::Left, false, Some(Color::BLACK), Opacity::Solid),
982        2 => style(w, Justify::Left, false, None, Opacity::Transparent),
983        3 => style(
984            w,
985            Justify::Center,
986            false,
987            Some(Color::BLACK),
988            Opacity::Solid,
989        ),
990        4 => style(w, Justify::Left, true, Some(Color::BLACK), Opacity::Solid),
991        5 => style(w, Justify::Left, true, None, Opacity::Transparent),
992        6 => style(w, Justify::Center, true, Some(Color::BLACK), Opacity::Solid),
993        7 => {
994            w.justify = Justify::Left;
995            w.word_wrap = false;
996            w.print_direction = PrintDirection::TopToBottom;
997            w.scroll_direction = ScrollDirection::RightToLeft;
998            w.fill_color = Color::BLACK;
999            w.fill_opacity = Opacity::Solid;
1000        }
1001        _ => {}
1002    }
1003}
1004
1005fn style(w: &mut Window, j: Justify, ww: bool, fill: Option<Color>, op: Opacity) {
1006    w.justify = j;
1007    w.word_wrap = ww;
1008    w.print_direction = PrintDirection::LeftToRight;
1009    w.scroll_direction = ScrollDirection::BottomToTop;
1010    w.fill_opacity = op;
1011    if let Some(c) = fill {
1012        w.fill_color = c;
1013    }
1014}
1015
1016/// Apply a predefined pen style 1–7 (Table 27).
1017fn apply_pen_style(w: &mut Window, id: u8) {
1018    w.pen_size = PenSize::Standard;
1019    w.pen_offset = PenOffset::Normal;
1020    w.italics = false;
1021    w.underline = false;
1022    w.fg_color = Color::WHITE;
1023    w.fg_opacity = Opacity::Solid;
1024    match id {
1025        1 => pen(
1026            w,
1027            FontStyle::Default,
1028            EdgeType::None,
1029            Color::BLACK,
1030            Opacity::Solid,
1031        ),
1032        2 => pen(
1033            w,
1034            FontStyle::MonospacedSerif,
1035            EdgeType::None,
1036            Color::BLACK,
1037            Opacity::Solid,
1038        ),
1039        3 => pen(
1040            w,
1041            FontStyle::ProportionalSerif,
1042            EdgeType::None,
1043            Color::BLACK,
1044            Opacity::Solid,
1045        ),
1046        4 => pen(
1047            w,
1048            FontStyle::MonospacedSansSerif,
1049            EdgeType::None,
1050            Color::BLACK,
1051            Opacity::Solid,
1052        ),
1053        5 => pen(
1054            w,
1055            FontStyle::ProportionalSansSerif,
1056            EdgeType::None,
1057            Color::BLACK,
1058            Opacity::Solid,
1059        ),
1060        6 => pen(
1061            w,
1062            FontStyle::MonospacedSansSerif,
1063            EdgeType::Uniform,
1064            Color::BLACK,
1065            Opacity::Transparent,
1066        ),
1067        7 => pen(
1068            w,
1069            FontStyle::ProportionalSansSerif,
1070            EdgeType::Uniform,
1071            Color::BLACK,
1072            Opacity::Transparent,
1073        ),
1074        _ => {}
1075    }
1076}
1077
1078fn pen(w: &mut Window, font: FontStyle, edge: EdgeType, bg: Color, bg_op: Opacity) {
1079    w.font_style = font;
1080    w.edge_type = edge;
1081    w.bg_color = bg;
1082    w.bg_opacity = bg_op;
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087    use super::*;
1088
1089    /// Build a single-service CCP carrying `cmds` as service `svc`'s block.
1090    fn ccp(svc: u8, cmds: &[u8]) -> Vec<u8> {
1091        let mut sb = alloc::vec![(svc << 5) | (cmds.len() as u8)];
1092        sb.extend_from_slice(cmds);
1093        // size_code = number of byte-pairs including the header byte.
1094        let size_code = (sb.len().div_ceil(2) + 1) as u8 & 0x3F;
1095        let mut packet = alloc::vec![size_code];
1096        packet.extend_from_slice(&sb);
1097        packet
1098    }
1099
1100    /// CTA-708-E DefineWindow worked example (`cea708-decode.md`, p.66–67):
1101    /// `0x9A 38 4A D1 8B 0F 11` → window id=2, visible=YES, rl=YES, cl=YES,
1102    /// priority=0, rp=0, av=74, ah=209, ap=8, rc=11 (→12 rows), cc=15 (→16 cols),
1103    /// ws=2, ps=1.
1104    #[test]
1105    fn define_window_worked_example() {
1106        let mut dec = Cea708Decoder::new();
1107        let packet = ccp(1, &[0x9A, 0x38, 0x4A, 0xD1, 0x8B, 0x0F, 0x11]);
1108        dec.push_packet(&packet);
1109        let w = dec.windows(1)[2].as_ref().expect("window 2 defined");
1110        assert_eq!(w.state, WindowState::Visible);
1111        assert!(w.row_lock);
1112        assert!(w.column_lock);
1113        assert_eq!(w.priority, 0);
1114        assert!(!w.relative_position);
1115        assert_eq!(w.anchor_vertical, 74);
1116        assert_eq!(w.anchor_horizontal, 209);
1117        assert_eq!(w.anchor_point, AnchorPoint::BottomRight);
1118        assert_eq!(w.row_count, 12);
1119        assert_eq!(w.column_count, 16);
1120        assert_eq!(w.window_style, 2);
1121        assert_eq!(w.pen_style, 1);
1122    }
1123
1124    /// SWA worked example (`cea708-decode.md`, p.76):
1125    /// `0x97,0x64,0x53,0x88,0x22` → border type = 5 (SHADOW_RIGHT).
1126    #[test]
1127    fn swa_border_type_split() {
1128        let mut dec = Cea708Decoder::new();
1129        // define a window first (so there is a current window), then SWA.
1130        let packet = ccp(
1131            1,
1132            &[
1133                0x98, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, // DF0 visible, defaults
1134                0x97, 0x64, 0x53, 0x88, 0x22, // SWA
1135            ],
1136        );
1137        dec.push_packet(&packet);
1138        let w = dec.windows(1)[0].as_ref().expect("window 0");
1139        assert_eq!(w.border_type, EdgeType::RightDropShadow);
1140    }
1141
1142    /// A simple caption: define a window (visible), write "Hi", read service text.
1143    #[test]
1144    fn decode_text() {
1145        let mut dec = Cea708Decoder::new();
1146        let packet = ccp(
1147            1,
1148            &[
1149                0x98, 0x20, 0x00, 0x00, 0x02, 0x0F, 0x00, // DF0 visible, 3 rows × 16
1150                b'H', b'i',
1151            ],
1152        );
1153        dec.push_packet(&packet);
1154        assert_eq!(dec.service_text(1), "Hi");
1155    }
1156
1157    /// ≥2 services + multi-window exercised in one packet.
1158    #[test]
1159    fn two_services_multi_window() {
1160        let mut dec = Cea708Decoder::new();
1161        // Service 1: define window 0 visible, write "S1".
1162        let s1_block = [0x98, 0x20, 0x00, 0x00, 0x00, 0x0F, 0x00, b'S', b'1'];
1163        // Service 2: define window 1 visible, write "S2".
1164        let s2_block = [0x99, 0x20, 0x00, 0x00, 0x00, 0x0F, 0x00, b'S', b'2'];
1165        let mut data = Vec::new();
1166        data.push((1 << 5) | (s1_block.len() as u8));
1167        data.extend_from_slice(&s1_block);
1168        data.push((2 << 5) | (s2_block.len() as u8));
1169        data.extend_from_slice(&s2_block);
1170        let size_code = (data.len().div_ceil(2) + 1) as u8 & 0x3F;
1171        let mut packet = alloc::vec![size_code];
1172        packet.extend_from_slice(&data);
1173        dec.push_packet(&packet);
1174        assert_eq!(dec.service_text(1), "S1");
1175        assert_eq!(dec.service_text(2), "S2");
1176        assert!(dec.windows(1)[0].is_some());
1177        assert!(dec.windows(2)[1].is_some());
1178    }
1179
1180    #[test]
1181    fn carriage_return_rolls_up() {
1182        let mut w = Window::new();
1183        w.row_count = 2;
1184        w.column_count = 10;
1185        w.ensure_grid();
1186        w.put_char('A');
1187        w.carriage_return();
1188        w.put_char('B');
1189        w.carriage_return(); // now at bottom; should roll up
1190        w.put_char('C');
1191        assert_eq!(w.text(), "B\nC");
1192    }
1193
1194    #[test]
1195    fn g0_music_note() {
1196        assert_eq!(Cea708Decoder::g0_char(0x7F), '\u{266A}');
1197        assert_eq!(Cea708Decoder::g0_char(b'A'), 'A');
1198    }
1199
1200    #[test]
1201    fn no_panic_on_arbitrary_input() {
1202        // Feed adversarial / truncated / malformed bytes; must never panic.
1203        let inputs: &[&[u8]] = &[
1204            &[],
1205            &[0x00],
1206            &[0xFF],
1207            &[0x01, 0x98],                               // DefineWindow truncated
1208            &[0x3F, 0x80, 0x90, 0x91, 0x92, 0x97, 0x98], // size_code huge, partial cmds
1209            &[0x20, 0xEE, (7 << 5) | 1],                 // extended service escape truncated
1210            &[0x10, 0x9A],                               // C0 EXT1 → C3 variable, truncated
1211            &[0x18, 0x00],                               // P16 truncated
1212        ];
1213        for inp in inputs {
1214            let mut dec = Cea708Decoder::new();
1215            dec.push_packet(inp);
1216        }
1217        // a long pseudo-random stream
1218        let mut dec = Cea708Decoder::new();
1219        let mut x: u32 = 0x1234_5678;
1220        let mut buf = Vec::new();
1221        for _ in 0..4096 {
1222            x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1223            buf.push((x >> 16) as u8);
1224        }
1225        dec.push_packet(&buf);
1226        // also drive it via triplets
1227        let mut dec2 = Cea708Decoder::new();
1228        let triplets: Vec<CcTriplet> = buf
1229            .chunks(2)
1230            .map(|c| CcTriplet {
1231                cc_valid: true,
1232                cc_type: CcType::Dtvcc708Data,
1233                cc_data_1: c[0],
1234                cc_data_2: *c.get(1).unwrap_or(&0),
1235            })
1236            .collect();
1237        dec2.push_triplets(&triplets);
1238    }
1239}