Skip to main content

dvb_subtitle/segments/
alternative_clut.rs

1//! Alternative CLUT Segment โ€” ETSI EN 300 743 ยง7.2.8, Table 31 (segment_type 0x16).
2//!
3//! Permits a CLUT to be defined in colour systems other than ITU-R BT.601.
4
5use crate::error::{Error, Result};
6use broadcast_common::len::{need_in, need_out};
7use broadcast_common::{Parse, Serialize};
8
9/// The alternative_CLUT_segment segment_type.
10pub const SEGMENT_TYPE: u8 = 0x16;
11/// Header: 6 bytes.
12pub const HEADER_LEN: usize = 6;
13/// Fixed body: CLUT_id(1) + CLUT_version_number(4b)+reserved(4b) + CLUT_parameters(2) = 4 bytes.
14pub const FIXED_LEN: usize = 4;
15/// Entry for 8-bit output: 4 bytes.
16pub const ENTRY_8BIT_LEN: usize = 4;
17/// Entry for 10-bit output: 5 bytes.
18pub const ENTRY_10BIT_LEN: usize = 5;
19
20/// CLUT parameters as defined in Table 32.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23pub struct ClutParameters {
24    /// Maximum number of CLUT entries (0 = 256).
25    pub entry_max_number: u8,
26    /// Colour component type (0 = YCbCr).
27    pub colour_component_type: u8,
28    /// Output bit depth (0 = 8-bit, 1 = 10-bit).
29    pub output_bit_depth: u8,
30    /// Reserved bit in CLUT_parameters byte 0 (bit 0).
31    pub reserved: u8,
32    /// SDR/HDR dynamic range and colour gamut as per Table 34.
33    pub dynamic_range_and_colour_gamut: u8,
34}
35
36impl ClutParameters {
37    /// Parse CLUT_parameters from 2 raw bytes.
38    pub fn parse(bytes: &[u8]) -> Result<Self> {
39        need_in(bytes, 2, "CLUT_parameters")?;
40        let entry_max_number = bytes[0] >> 6;
41        let colour_component_type = (bytes[0] >> 4) & 0x03;
42        let output_bit_depth = (bytes[0] >> 1) & 0x07;
43        let reserved = bytes[0] & 0x01;
44        Ok(ClutParameters {
45            entry_max_number,
46            colour_component_type,
47            output_bit_depth,
48            reserved,
49            dynamic_range_and_colour_gamut: bytes[1],
50        })
51    }
52
53    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
54        buf[0] =
55            (broadcast_common::len::fit_mask(self.entry_max_number, 0x03, "entry_max_number")?
56                << 6)
57                | (broadcast_common::len::fit_mask(
58                    self.colour_component_type,
59                    0x03,
60                    "colour_component_type",
61                )? << 4)
62                | (broadcast_common::len::fit_mask(
63                    self.output_bit_depth,
64                    0x07,
65                    "output_bit_depth",
66                )? << 1)
67                | broadcast_common::len::fit_mask(self.reserved, 0x01, "reserved")?;
68        buf[1] = self.dynamic_range_and_colour_gamut;
69        Ok(())
70    }
71}
72
73/// Output bit depth as defined in Table 33.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize))]
76#[repr(u8)]
77#[non_exhaustive]
78pub enum OutputBitDepth {
79    /// 8-bit per component.
80    Bit8 = 0x00,
81    /// 10-bit per component.
82    Bit10 = 0x01,
83    /// Reserved.
84    Reserved(u8),
85}
86
87impl OutputBitDepth {
88    /// Human-readable name for this bit depth.
89    #[must_use]
90    pub fn name(&self) -> &'static str {
91        match self {
92            Self::Bit8 => "8-bit",
93            Self::Bit10 => "10-bit",
94            Self::Reserved(_) => "reserved",
95        }
96    }
97}
98
99broadcast_common::impl_spec_display!(OutputBitDepth, Reserved);
100
101/// Dynamic range and colour gamut as defined in Table 34.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize))]
104#[repr(u8)]
105#[non_exhaustive]
106pub enum DynamicRangeColourGamut {
107    /// SDR; ITU-R BT.709.
108    SdrBt709 = 0x00,
109    /// SDR; ITU-R BT.2020-2.
110    SdrBt2020 = 0x01,
111    /// HDR; ITU-R BT.2100-1 PQ.
112    HdrBt2100Pq = 0x02,
113    /// HDR; ITU-R BT.2100-1 HLG.
114    HdrBt2100Hlg = 0x03,
115    /// Reserved.
116    Reserved(u8),
117}
118
119impl DynamicRangeColourGamut {
120    /// Human-readable name for this dynamic range and colour gamut.
121    #[must_use]
122    pub fn name(&self) -> &'static str {
123        match self {
124            Self::SdrBt709 => "SDR_BT.709",
125            Self::SdrBt2020 => "SDR_BT.2020",
126            Self::HdrBt2100Pq => "HDR_BT.2100_PQ",
127            Self::HdrBt2100Hlg => "HDR_BT.2100_HLG",
128            Self::Reserved(_) => "reserved",
129        }
130    }
131}
132
133broadcast_common::impl_spec_display!(DynamicRangeColourGamut, Reserved);
134
135/// A single alternative CLUT entry.
136#[derive(Debug, Clone, PartialEq, Eq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize))]
138pub struct AlternativeClutEntry {
139    /// Luma value (8 or 10 bit).
140    pub luma_value: u16,
141    /// Chroma1 (Cb) value (8 or 10 bit).
142    pub chroma1_value: u16,
143    /// Chroma2 (Cr) value (8 or 10 bit).
144    pub chroma2_value: u16,
145    /// Transparency value (8 or 10 bit).
146    pub t_value: u16,
147}
148
149impl AlternativeClutEntry {
150    fn serialized_len(output_bit_depth: u8) -> usize {
151        match output_bit_depth {
152            0 => ENTRY_8BIT_LEN,
153            1 => ENTRY_10BIT_LEN,
154            _ => ENTRY_8BIT_LEN,
155        }
156    }
157
158    fn serialize_into(&self, buf: &mut [u8], output_bit_depth: u8) -> Result<()> {
159        if output_bit_depth == 1 {
160            // 10-bit entry: each of the four values is 10 bits wide.
161            let v0 = broadcast_common::len::fit_mask(self.luma_value, 0x03FF, "luma_value")?;
162            let v1 = broadcast_common::len::fit_mask(self.chroma1_value, 0x03FF, "chroma1_value")?;
163            let v2 = broadcast_common::len::fit_mask(self.chroma2_value, 0x03FF, "chroma2_value")?;
164            let v3 = broadcast_common::len::fit_mask(self.t_value, 0x03FF, "t_value")?;
165            buf[0] = (v0 >> 2) as u8;
166            buf[1] = (((v0 & 0x03) << 6) | ((v1 & 0x3FC) >> 4)) as u8;
167            buf[2] = (((v1 & 0x0F) << 4) | ((v2 & 0x3F0) >> 6)) as u8;
168            buf[3] = (((v2 & 0x3F) << 2) | ((v3 & 0x300) >> 8)) as u8;
169            buf[4] = (v3 & 0xFF) as u8;
170        } else {
171            // 8-bit entry: each value is 8 bits wide.
172            buf[0] = broadcast_common::len::fit_mask(self.luma_value, 0x00FF, "luma_value")? as u8;
173            buf[1] =
174                broadcast_common::len::fit_mask(self.chroma1_value, 0x00FF, "chroma1_value")? as u8;
175            buf[2] =
176                broadcast_common::len::fit_mask(self.chroma2_value, 0x00FF, "chroma2_value")? as u8;
177            buf[3] = broadcast_common::len::fit_mask(self.t_value, 0x00FF, "t_value")? as u8;
178        }
179        Ok(())
180    }
181
182    fn parse(bytes: &[u8], output_bit_depth: u8) -> Result<Self> {
183        let entry_len = Self::serialized_len(output_bit_depth);
184        need_in(bytes, entry_len, "alternative_CLUT_entry")?;
185        let (luma, c1, c2, t) = if output_bit_depth == 1 {
186            let v0 = ((bytes[0] as u16) << 2) | ((bytes[1] as u16) >> 6);
187            let v1 = (((bytes[1] as u16) & 0x3F) << 4) | ((bytes[2] as u16) >> 4);
188            let v2 = (((bytes[2] as u16) & 0x0F) << 6) | ((bytes[3] as u16) >> 2);
189            let v3 = (((bytes[3] as u16) & 0x03) << 8) | (bytes[4] as u16);
190            (v0, v1, v2, v3)
191        } else {
192            (
193                bytes[0] as u16,
194                bytes[1] as u16,
195                bytes[2] as u16,
196                bytes[3] as u16,
197            )
198        };
199        Ok(AlternativeClutEntry {
200            luma_value: luma,
201            chroma1_value: c1,
202            chroma2_value: c2,
203            t_value: t,
204        })
205    }
206}
207
208/// Alternative CLUT Segment.
209#[derive(Debug, Clone, PartialEq, Eq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize))]
211pub struct AlternativeClutSegment {
212    /// The page_id from the segment header.
213    pub page_id: u16,
214    /// CLUT family identifier.
215    pub clut_id: u8,
216    /// CLUT version number (modulo 16).
217    pub clut_version_number: u8,
218    /// Reserved bits in body byte 1 (bits `[3:0]`).
219    pub reserved: u8,
220    /// CLUT parameters.
221    pub clut_parameters: ClutParameters,
222    /// CLUT entries.
223    pub entries: alloc::vec::Vec<AlternativeClutEntry>,
224}
225
226impl<'a> Parse<'a> for AlternativeClutSegment {
227    type Error = Error;
228
229    fn parse(bytes: &'a [u8]) -> Result<Self> {
230        need_in(bytes, HEADER_LEN + FIXED_LEN, "alternative_CLUT_segment")?;
231        if bytes[0] != crate::pes_data_field::SYNC_BYTE {
232            return Err(Error::InvalidSync(broadcast_common::error::InvalidSync {
233                what: "sync_byte",
234                expected: u32::from(crate::pes_data_field::SYNC_BYTE),
235                found: u32::from(bytes[0]),
236            }));
237        }
238        if bytes[1] != SEGMENT_TYPE {
239            return Err(Error::UnknownSegmentType(bytes[1]));
240        }
241        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
242        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
243        let total = HEADER_LEN + segment_length;
244        need_in(bytes, total, "alternative_CLUT_segment data")?;
245        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
246        need_in(body, FIXED_LEN, "alternative_CLUT_segment body")?;
247        let clut_id = body[0];
248        let clut_version_number = body[1] >> 4;
249        let reserved = body[1] & 0x0F;
250        let clut_parameters = ClutParameters::parse(&body[2..4])?;
251        let output_bit_depth = clut_parameters.output_bit_depth;
252        let entry_len = AlternativeClutEntry::serialized_len(output_bit_depth);
253
254        let entry_data = &body[FIXED_LEN..];
255        // Table 31's entry loop is `while (processed_length < segment_length)`
256        // over whole fixed-size entries โ€” a remainder means the segment is
257        // malformed (#1108/DS-W6: pre-fix, integer division silently dropped
258        // it instead).
259        if !entry_data.len().is_multiple_of(entry_len) {
260            return Err(Error::TrailingEntryBytes {
261                what: "alternative_CLUT_segment entries",
262                entry_len,
263                extra: entry_data.len() % entry_len,
264            });
265        }
266        let num_entries = entry_data.len() / entry_len;
267        let mut entries = alloc::vec::Vec::with_capacity(num_entries);
268        for i in 0..num_entries {
269            let off = i * entry_len;
270            entries.push(AlternativeClutEntry::parse(
271                &entry_data[off..off + entry_len],
272                output_bit_depth,
273            )?);
274        }
275
276        Ok(AlternativeClutSegment {
277            page_id,
278            clut_id,
279            clut_version_number,
280            reserved,
281            clut_parameters,
282            entries,
283        })
284    }
285}
286
287impl Serialize for AlternativeClutSegment {
288    type Error = Error;
289
290    fn serialized_len(&self) -> usize {
291        let entry_len = AlternativeClutEntry::serialized_len(self.clut_parameters.output_bit_depth);
292        HEADER_LEN + FIXED_LEN + self.entries.len() * entry_len
293    }
294
295    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
296        let len = self.serialized_len();
297        need_out(buf, len)?;
298        buf[0] = 0x0F;
299        buf[1] = SEGMENT_TYPE;
300        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
301        let seg_len = crate::segments::check_segment_length(len - HEADER_LEN)?;
302        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
303
304        buf[6] = self.clut_id;
305        buf[7] = (broadcast_common::len::fit_mask(
306            self.clut_version_number,
307            0x0F,
308            "clut_version_number",
309        )? << 4)
310            | broadcast_common::len::fit_mask(self.reserved, 0x0F, "reserved")?;
311        self.clut_parameters.serialize_into(&mut buf[8..10])?;
312
313        let entry_len = AlternativeClutEntry::serialized_len(self.clut_parameters.output_bit_depth);
314        for (i, entry) in self.entries.iter().enumerate() {
315            let off = HEADER_LEN + FIXED_LEN + i * entry_len;
316            entry.serialize_into(
317                &mut buf[off..off + entry_len],
318                self.clut_parameters.output_bit_depth,
319            )?;
320        }
321        Ok(len)
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use broadcast_common::{Parse, Serialize};
329
330    #[test]
331    fn round_trip_8bit() {
332        let bytes = [
333            0x0F, 0x16, 0x00, 0x01, 0x00, 0x08, 0x03, 0x10, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80,
334        ];
335        let seg = AlternativeClutSegment::parse(&bytes).unwrap();
336        assert_eq!(seg.clut_id, 3);
337        assert_eq!(seg.entries.len(), 1);
338        assert_eq!(seg.entries[0].luma_value, 128);
339        let out = seg.try_to_bytes().unwrap();
340        assert_eq!(out, bytes);
341
342        // Biting test
343        let mut seg2 = seg.clone();
344        seg2.entries[0].luma_value = 200;
345        let out2 = seg2.try_to_bytes().unwrap();
346        assert_ne!(out2, bytes);
347        let reparse = AlternativeClutSegment::parse(&out2).unwrap();
348        assert_eq!(reparse.entries[0].luma_value, 200);
349    }
350
351    #[test]
352    fn round_trip_10bit() {
353        let bytes = [
354            0x0F, 0x16, 0x00, 0x01, 0x00, 0x09, 0x03, 0x10, 0x02, 0x02, 0x80, 0x00, 0x00, 0x00,
355            0x00,
356        ];
357        let seg = AlternativeClutSegment::parse(&bytes).unwrap();
358        assert_eq!(seg.clut_parameters.output_bit_depth, 1);
359        assert_eq!(seg.entries.len(), 1);
360        let out = seg.try_to_bytes().unwrap();
361        assert_eq!(out, bytes);
362
363        // Biting test
364        let mut seg2 = seg.clone();
365        seg2.clut_id = 7;
366        let out2 = seg2.try_to_bytes().unwrap();
367        assert_ne!(out2, bytes);
368        let reparse = AlternativeClutSegment::parse(&out2).unwrap();
369        assert_eq!(reparse.clut_id, 7);
370    }
371
372    /// DS-W2 (#1108): sync_byte wasn't checked by any typed segment parser.
373    #[test]
374    fn rejects_bad_sync_byte() {
375        let bytes = [
376            0x00, 0x16, 0x00, 0x01, 0x00, 0x08, 0x03, 0x10, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80,
377        ];
378        assert!(matches!(
379            AlternativeClutSegment::parse(&bytes),
380            Err(Error::InvalidSync(broadcast_common::error::InvalidSync {
381                found: 0x00,
382                ..
383            }))
384        ));
385    }
386
387    /// DS-W6 (#1108): Table 31's entry loop has no stuffing provision, so a
388    /// trailing remainder that isn't a whole entry is now rejected, not
389    /// silently dropped by integer-dividing `entry_data.len() / entry_len`.
390    #[test]
391    fn rejects_trailing_partial_entry() {
392        let bytes = [
393            0x0F, 0x16, 0x00, 0x01, 0x00, 0x0A, 0x03, 0x10, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80,
394            0xAA, 0xBB,
395        ];
396        assert!(matches!(
397            AlternativeClutSegment::parse(&bytes),
398            Err(Error::TrailingEntryBytes {
399                entry_len: 4,
400                extra: 2,
401                ..
402            })
403        ));
404    }
405}