Skip to main content

dvb_subtitle/segments/
region_composition.rs

1//! Region Composition Segment — ETSI EN 300 743 §7.2.3, Table 11 (segment_type 0x11).
2
3use crate::error::{Error, Result};
4use broadcast_common::{Parse, Serialize};
5
6/// The region_composition_segment segment_type.
7pub const SEGMENT_TYPE: u8 = 0x11;
8/// Header: sync_byte(1) + segment_type(1) + page_id(2) + segment_length(2) = 6 bytes.
9pub const HEADER_LEN: usize = 6;
10/// Fixed body: 10 bytes.
11pub const FIXED_LEN: usize = 10;
12/// Each object entry base: object_id(2) + type(2b)+provider(2b)+hpos(12b)+reserved(4b)+vpos(12b) = 6 bytes.
13pub const OBJECT_ENTRY_BASE_LEN: usize = 6;
14/// Extra bytes for character/composite objects: foreground(1) + background(1) = 2 bytes.
15pub const OBJECT_EXTRA_LEN: usize = 2;
16
17/// Region level of compatibility as defined in Table 12.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20#[repr(u8)]
21#[non_exhaustive]
22pub enum RegionLevelOfCompatibility {
23    /// Reserved.
24    Reserved0 = 0x00,
25    /// 2-bit/entry CLUT required.
26    Clut2Bit = 0x01,
27    /// 4-bit/entry CLUT required.
28    Clut4Bit = 0x02,
29    /// 8-bit/entry CLUT required.
30    Clut8Bit = 0x03,
31    /// Reserved range.
32    Reserved(u8),
33}
34
35impl RegionLevelOfCompatibility {
36    /// Human-readable name for this compatibility level.
37    #[must_use]
38    pub fn name(&self) -> &'static str {
39        match self {
40            Self::Reserved0 => "reserved",
41            Self::Clut2Bit => "2-bit CLUT",
42            Self::Clut4Bit => "4-bit CLUT",
43            Self::Clut8Bit => "8-bit CLUT",
44            Self::Reserved(_) => "reserved",
45        }
46    }
47
48    fn to_bits(self) -> u8 {
49        match self {
50            Self::Reserved0 => 0x00,
51            Self::Clut2Bit => 0x01,
52            Self::Clut4Bit => 0x02,
53            Self::Clut8Bit => 0x03,
54            Self::Reserved(v) => v & 0x07,
55        }
56    }
57}
58
59broadcast_common::impl_spec_display!(RegionLevelOfCompatibility, Reserved);
60
61/// Intended region pixel depth as defined in Table 13.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize))]
64#[repr(u8)]
65#[non_exhaustive]
66pub enum RegionDepth {
67    /// Reserved.
68    Reserved0 = 0x00,
69    /// 2-bit.
70    Depth2Bit = 0x01,
71    /// 4-bit.
72    Depth4Bit = 0x02,
73    /// 8-bit.
74    Depth8Bit = 0x03,
75    /// Reserved range.
76    Reserved(u8),
77}
78
79impl RegionDepth {
80    /// Human-readable name for this pixel depth.
81    #[must_use]
82    pub fn name(&self) -> &'static str {
83        match self {
84            Self::Reserved0 => "reserved",
85            Self::Depth2Bit => "2-bit",
86            Self::Depth4Bit => "4-bit",
87            Self::Depth8Bit => "8-bit",
88            Self::Reserved(_) => "reserved",
89        }
90    }
91
92    fn to_bits(self) -> u8 {
93        match self {
94            Self::Reserved0 => 0x00,
95            Self::Depth2Bit => 0x01,
96            Self::Depth4Bit => 0x02,
97            Self::Depth8Bit => 0x03,
98            Self::Reserved(v) => v & 0x07,
99        }
100    }
101}
102
103broadcast_common::impl_spec_display!(RegionDepth, Reserved);
104
105/// Object type as defined in Table 14.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108#[repr(u8)]
109#[non_exhaustive]
110pub enum ObjectType {
111    /// Basic object, bitmap.
112    BasicBitmap = 0x00,
113    /// Basic object, character.
114    BasicCharacter = 0x01,
115    /// Composite object, string of characters.
116    CompositeString = 0x02,
117    /// Reserved.
118    Reserved(u8),
119}
120
121impl ObjectType {
122    /// Human-readable name for this object type.
123    #[must_use]
124    pub fn name(&self) -> &'static str {
125        match self {
126            Self::BasicBitmap => "basic_bitmap",
127            Self::BasicCharacter => "basic_character",
128            Self::CompositeString => "composite_string",
129            Self::Reserved(_) => "reserved",
130        }
131    }
132
133    fn to_bits(self) -> u8 {
134        match self {
135            Self::BasicBitmap => 0x00,
136            Self::BasicCharacter => 0x01,
137            Self::CompositeString => 0x02,
138            Self::Reserved(v) => v & 0x03,
139        }
140    }
141}
142
143broadcast_common::impl_spec_display!(ObjectType, Reserved);
144
145/// Object provider flag as defined in Table 15.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize))]
148#[repr(u8)]
149#[non_exhaustive]
150pub enum ObjectProviderFlag {
151    /// Provided in the subtitling stream.
152    InStream = 0x00,
153    /// Provided by a ROM in the IRD.
154    InRom = 0x01,
155    /// Reserved.
156    Reserved(u8),
157}
158
159impl ObjectProviderFlag {
160    /// Human-readable name for this provision method.
161    #[must_use]
162    pub fn name(&self) -> &'static str {
163        match self {
164            Self::InStream => "in_stream",
165            Self::InRom => "in_rom",
166            Self::Reserved(_) => "reserved",
167        }
168    }
169
170    fn to_bits(self) -> u8 {
171        match self {
172            Self::InStream => 0x00,
173            Self::InRom => 0x01,
174            Self::Reserved(v) => v & 0x03,
175        }
176    }
177}
178
179broadcast_common::impl_spec_display!(ObjectProviderFlag, Reserved);
180
181/// An object entry within a region composition segment.
182#[derive(Debug, Clone, PartialEq, Eq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize))]
184pub struct RegionObjectEntry {
185    /// Object identifier.
186    pub object_id: u16,
187    /// Object type.
188    pub object_type: ObjectType,
189    /// Object provider flag.
190    pub object_provider_flag: ObjectProviderFlag,
191    /// Horizontal position relative to region.
192    pub object_horizontal_position: u16,
193    /// Vertical position relative to region.
194    pub object_vertical_position: u16,
195    /// Foreground pixel code (only for character/composite objects).
196    pub foreground_pixel_code: Option<u8>,
197    /// Background pixel code (only for character/composite objects).
198    pub background_pixel_code: Option<u8>,
199}
200
201impl RegionObjectEntry {
202    fn serialized_len(&self) -> usize {
203        if self.foreground_pixel_code.is_some() {
204            OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN
205        } else {
206            OBJECT_ENTRY_BASE_LEN
207        }
208    }
209
210    fn serialize_into(&self, buf: &mut [u8]) {
211        buf[0..2].copy_from_slice(&self.object_id.to_be_bytes());
212        let hpos = self.object_horizontal_position;
213        let vpos = self.object_vertical_position;
214        buf[2] = (self.object_type.to_bits() << 6)
215            | (self.object_provider_flag.to_bits() << 4)
216            | ((hpos >> 8) as u8 & 0x0F);
217        buf[3] = hpos as u8;
218        buf[4] = ((vpos >> 8) as u8 & 0x0F) << 4; // upper nibble of vpos, lower nibble reserved=0
219        buf[5] = vpos as u8;
220        if let (Some(fg), Some(bg)) = (self.foreground_pixel_code, self.background_pixel_code) {
221            buf[6] = fg;
222            buf[7] = bg;
223        }
224    }
225}
226
227/// Region Composition Segment.
228#[derive(Debug, Clone, PartialEq, Eq)]
229#[cfg_attr(feature = "serde", derive(serde::Serialize))]
230pub struct RegionCompositionSegment {
231    /// The page_id (from generic segment header).
232    pub page_id: u16,
233    /// Region identifier.
234    pub region_id: u8,
235    /// Region version number (modulo 16).
236    pub region_version_number: u8,
237    /// Fill flag.
238    pub region_fill_flag: bool,
239    /// Reserved bits in body byte 1 (bits `[2:0]`).
240    pub reserved_byte1: u8,
241    /// Region width in pixels.
242    pub region_width: u16,
243    /// Region height in pixels.
244    pub region_height: u16,
245    /// Minimum CLUT type required.
246    pub region_level_of_compatibility: RegionLevelOfCompatibility,
247    /// Intended pixel depth.
248    pub region_depth: RegionDepth,
249    /// Reserved bits in body byte 6 (bits `[1:0]`).
250    pub reserved_byte6: u8,
251    /// CLUT family identifier.
252    pub clut_id: u8,
253    /// Background colour for 8-bit CLUT.
254    pub region_8bit_pixel_code: u8,
255    /// Background colour for 4-bit CLUT.
256    pub region_4bit_pixel_code: u8,
257    /// Background colour for 2-bit CLUT.
258    pub region_2bit_pixel_code: u8,
259    /// Reserved bits in body byte 9 (bits `[1:0]`).
260    pub reserved_byte9: u8,
261    /// Object entries.
262    pub objects: alloc::vec::Vec<RegionObjectEntry>,
263}
264
265fn parse_object_entry(bytes: &[u8]) -> Result<(RegionObjectEntry, usize)> {
266    if bytes.len() < OBJECT_ENTRY_BASE_LEN {
267        return Err(Error::BufferTooShort {
268            need: OBJECT_ENTRY_BASE_LEN,
269            have: bytes.len(),
270            what: "region_object_entry",
271        });
272    }
273    let object_id = u16::from_be_bytes([bytes[0], bytes[1]]);
274    let obj_type_val = (bytes[2] >> 6) & 0x03;
275    let obj_provider_val = (bytes[2] >> 4) & 0x03;
276    let obj_hpos = ((u16::from(bytes[2]) & 0x0F) << 8) | u16::from(bytes[3]);
277    let reserved = (bytes[4] >> 4) & 0x0F;
278    // reserved bits tolerated per §7.2.0.2 forward compatibility
279    let _ = reserved;
280    let obj_vpos = ((u16::from(bytes[4]) & 0x0F) << 8) | u16::from(bytes[5]);
281
282    let obj_type = match obj_type_val {
283        0x00 => ObjectType::BasicBitmap,
284        0x01 => ObjectType::BasicCharacter,
285        0x02 => ObjectType::CompositeString,
286        v => ObjectType::Reserved(v),
287    };
288    let obj_provider = match obj_provider_val {
289        0x00 => ObjectProviderFlag::InStream,
290        0x01 => ObjectProviderFlag::InRom,
291        v => ObjectProviderFlag::Reserved(v),
292    };
293
294    let has_extra = obj_type_val == 0x01 || obj_type_val == 0x02;
295    if has_extra {
296        if bytes.len() < OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN {
297            return Err(Error::BufferTooShort {
298                need: OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN,
299                have: bytes.len(),
300                what: "region_object_entry extra",
301            });
302        }
303        Ok((
304            RegionObjectEntry {
305                object_id,
306                object_type: obj_type,
307                object_provider_flag: obj_provider,
308                object_horizontal_position: obj_hpos,
309                object_vertical_position: obj_vpos,
310                foreground_pixel_code: Some(bytes[6]),
311                background_pixel_code: Some(bytes[7]),
312            },
313            OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN,
314        ))
315    } else {
316        Ok((
317            RegionObjectEntry {
318                object_id,
319                object_type: obj_type,
320                object_provider_flag: obj_provider,
321                object_horizontal_position: obj_hpos,
322                object_vertical_position: obj_vpos,
323                foreground_pixel_code: None,
324                background_pixel_code: None,
325            },
326            OBJECT_ENTRY_BASE_LEN,
327        ))
328    }
329}
330
331impl<'a> Parse<'a> for RegionCompositionSegment {
332    type Error = Error;
333
334    fn parse(bytes: &'a [u8]) -> Result<Self> {
335        if bytes.len() < HEADER_LEN + FIXED_LEN {
336            return Err(Error::BufferTooShort {
337                need: HEADER_LEN + FIXED_LEN,
338                have: bytes.len(),
339                what: "region_composition_segment",
340            });
341        }
342        if bytes[1] != SEGMENT_TYPE {
343            return Err(Error::UnknownSegmentType(bytes[1]));
344        }
345        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
346        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
347        let total = HEADER_LEN + segment_length;
348        if bytes.len() < total {
349            return Err(Error::BufferTooShort {
350                need: total,
351                have: bytes.len(),
352                what: "region_composition_segment data",
353            });
354        }
355        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
356        if body.len() < FIXED_LEN {
357            return Err(Error::BufferTooShort {
358                need: FIXED_LEN,
359                have: body.len(),
360                what: "region_composition_segment body",
361            });
362        }
363        let region_id = body[0];
364        let region_version_number = body[1] >> 4;
365        let region_fill_flag = (body[1] & 0x08) != 0;
366        let reserved_byte1 = body[1] & 0x07;
367        let region_width = u16::from_be_bytes([body[2], body[3]]);
368        let region_height = u16::from_be_bytes([body[4], body[5]]);
369        let rloc_val = (body[6] >> 5) & 0x07;
370        let region_level_of_compatibility = match rloc_val {
371            0x00 => RegionLevelOfCompatibility::Reserved0,
372            0x01 => RegionLevelOfCompatibility::Clut2Bit,
373            0x02 => RegionLevelOfCompatibility::Clut4Bit,
374            0x03 => RegionLevelOfCompatibility::Clut8Bit,
375            v => RegionLevelOfCompatibility::Reserved(v),
376        };
377        let rd_val = (body[6] >> 2) & 0x07;
378        let region_depth = match rd_val {
379            0x00 => RegionDepth::Reserved0,
380            0x01 => RegionDepth::Depth2Bit,
381            0x02 => RegionDepth::Depth4Bit,
382            0x03 => RegionDepth::Depth8Bit,
383            v => RegionDepth::Reserved(v),
384        };
385        let reserved_byte6 = body[6] & 0x03;
386        let clut_id = body[7];
387        let region_8bit_pixel_code = body[8];
388        let region_4bit_pixel_code = body[9] >> 4;
389        let region_2bit_pixel_code = (body[9] >> 2) & 0x03;
390        let reserved_byte9 = body[9] & 0x03;
391
392        let obj_data = &body[FIXED_LEN..];
393        let mut objects = alloc::vec::Vec::new();
394        let mut pos: usize = 0;
395        while pos < obj_data.len() {
396            let (entry, entry_len) = parse_object_entry(&obj_data[pos..])?;
397            objects.push(entry);
398            pos += entry_len;
399        }
400
401        Ok(RegionCompositionSegment {
402            page_id,
403            region_id,
404            region_version_number,
405            region_fill_flag,
406            reserved_byte1,
407            region_width,
408            region_height,
409            region_level_of_compatibility,
410            region_depth,
411            reserved_byte6,
412            clut_id,
413            region_8bit_pixel_code,
414            region_4bit_pixel_code,
415            region_2bit_pixel_code,
416            reserved_byte9,
417            objects,
418        })
419    }
420}
421
422impl Serialize for RegionCompositionSegment {
423    type Error = Error;
424
425    fn serialized_len(&self) -> usize {
426        HEADER_LEN
427            + FIXED_LEN
428            + self
429                .objects
430                .iter()
431                .map(|o| o.serialized_len())
432                .sum::<usize>()
433    }
434
435    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
436        let len = self.serialized_len();
437        if buf.len() < len {
438            return Err(Error::BufferTooShort {
439                need: len,
440                have: buf.len(),
441                what: "region_composition_segment serialize",
442            });
443        }
444        buf[0] = 0x0F;
445        buf[1] = SEGMENT_TYPE;
446        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
447        let seg_len = (len - HEADER_LEN) as u16;
448        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
449
450        buf[6] = self.region_id;
451        buf[7] = (self.region_version_number << 4)
452            | (u8::from(self.region_fill_flag) << 3)
453            | (self.reserved_byte1 & 0x07);
454        buf[8..10].copy_from_slice(&self.region_width.to_be_bytes());
455        buf[10..12].copy_from_slice(&self.region_height.to_be_bytes());
456        buf[12] = (self.region_level_of_compatibility.to_bits() << 5)
457            | (self.region_depth.to_bits() << 2)
458            | (self.reserved_byte6 & 0x03);
459        buf[13] = self.clut_id;
460        buf[14] = self.region_8bit_pixel_code;
461        buf[15] = (self.region_4bit_pixel_code << 4)
462            | (self.region_2bit_pixel_code << 2)
463            | (self.reserved_byte9 & 0x03);
464
465        let mut off = HEADER_LEN + FIXED_LEN;
466        for obj in &self.objects {
467            obj.serialize_into(&mut buf[off..]);
468            off += obj.serialized_len();
469        }
470        Ok(len)
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use broadcast_common::{Parse, Serialize};
478
479    #[test]
480    fn round_trip_bitmap_objects() {
481        let bytes = [
482            0x0F, 0x11, 0x00, 0x01, 0x00, 0x16, 0x01, 0x88, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
483            0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x02, 0x10, 0x14, 0x00,
484            0x14, // obj 2: hpos=20, vpos=20, vpos upper nibble=0
485        ];
486        let seg = RegionCompositionSegment::parse(&bytes).unwrap();
487        assert_eq!(seg.region_id, 1);
488        assert!(seg.region_fill_flag);
489        assert_eq!(seg.region_width, 719);
490        assert_eq!(seg.objects.len(), 2);
491        assert_eq!(seg.objects[0].object_id, 1);
492        assert_eq!(seg.objects[0].object_type, ObjectType::BasicBitmap);
493        assert_eq!(seg.objects[0].object_horizontal_position, 10);
494        assert_eq!(seg.objects[1].object_id, 2);
495        assert_eq!(
496            seg.objects[1].object_provider_flag,
497            ObjectProviderFlag::InRom
498        );
499        let out = seg.to_bytes();
500        assert_eq!(out, bytes);
501
502        // Biting test: mutate a field and re-parse
503        let mut seg2 = seg.clone();
504        seg2.region_width = 1280;
505        let out2 = seg2.to_bytes();
506        assert_ne!(out2, bytes);
507        let reparse = RegionCompositionSegment::parse(&out2).unwrap();
508        assert_eq!(reparse.region_width, 1280);
509    }
510
511    #[test]
512    fn round_trip_character_object() {
513        let bytes = [
514            0x0F, 0x11, 0x00, 0x01, 0x00, 0x12, 0x01, 0x80, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
515            0x00, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x00, 0x1E, 0xAA, 0xBB,
516        ];
517        let seg = RegionCompositionSegment::parse(&bytes).unwrap();
518        assert_eq!(seg.objects.len(), 1);
519        assert_eq!(seg.objects[0].object_type, ObjectType::BasicCharacter);
520        assert_eq!(seg.objects[0].foreground_pixel_code, Some(0xAA));
521        assert_eq!(seg.objects[0].background_pixel_code, Some(0xBB));
522        let out = seg.to_bytes();
523        assert_eq!(out, bytes);
524
525        // Biting test
526        let mut seg2 = seg.clone();
527        seg2.objects[0].foreground_pixel_code = Some(0xCC);
528        let out2 = seg2.to_bytes();
529        assert_ne!(out2, bytes);
530        let reparse = RegionCompositionSegment::parse(&out2).unwrap();
531        assert_eq!(reparse.objects[0].foreground_pixel_code, Some(0xCC));
532    }
533}