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