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