Skip to main content

dvb_subtitle/segments/
page_composition.rs

1//! Page Composition Segment — ETSI EN 300 743 §7.2.2, Table 9 (segment_type 0x10).
2
3use crate::error::{Error, Result};
4use broadcast_common::{Parse, Serialize};
5
6/// The page_composition_segment segment_type.
7pub const SEGMENT_TYPE: u8 = 0x10;
8/// Header: sync_byte(1) + segment_type(1) + page_id(2) + segment_length(2) = 6 bytes.
9pub const HEADER_LEN: usize = 6;
10/// Fixed after header: page_time_out(1) + page_version(4b)+page_state(2b)+reserved(2b) = 2 bytes.
11pub const FIXED_LEN: usize = 2;
12/// Each region entry: region_id(1) + reserved(1) + region_horizontal_address(2) + region_vertical_address(2) = 6 bytes.
13pub const REGION_ENTRY_LEN: usize = 6;
14
15/// Page state as defined in Table 10.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18#[repr(u8)]
19#[non_exhaustive]
20pub enum PageState {
21    /// Normal case — page update.
22    Normal = 0x00,
23    /// Acquisition point — page refresh.
24    Acquisition = 0x01,
25    /// Mode change — new page.
26    ModeChange = 0x02,
27    /// Reserved.
28    Reserved(u8),
29}
30
31impl PageState {
32    /// Human-readable name.
33    #[must_use]
34    pub fn name(&self) -> &'static str {
35        match self {
36            Self::Normal => "normal",
37            Self::Acquisition => "acquisition",
38            Self::ModeChange => "mode_change",
39            Self::Reserved(_) => "reserved",
40        }
41    }
42
43    fn to_bits(self) -> u8 {
44        match self {
45            Self::Normal => 0x00,
46            Self::Acquisition => 0x01,
47            Self::ModeChange => 0x02,
48            Self::Reserved(v) => v & 0x03,
49        }
50    }
51}
52
53broadcast_common::impl_spec_display!(PageState, Reserved);
54
55/// A single region entry within a page composition segment.
56#[derive(Debug, Clone, PartialEq, Eq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58pub struct PageRegionEntry {
59    /// Region identifier.
60    pub region_id: u8,
61    /// Reserved byte in the region entry (must be preserved for round-trip).
62    pub reserved: u8,
63    /// Horizontal address of top-left pixel.
64    pub region_horizontal_address: u16,
65    /// Vertical address of top line.
66    pub region_vertical_address: u16,
67}
68
69impl PageRegionEntry {
70    #[allow(dead_code)]
71    fn serialized_len() -> usize {
72        REGION_ENTRY_LEN
73    }
74
75    fn serialize_into(&self, buf: &mut [u8]) {
76        buf[0] = self.region_id;
77        buf[1] = self.reserved;
78        buf[2..4].copy_from_slice(&self.region_horizontal_address.to_be_bytes());
79        buf[4..6].copy_from_slice(&self.region_vertical_address.to_be_bytes());
80    }
81
82    fn parse(bytes: &[u8]) -> Result<Self> {
83        if bytes.len() < REGION_ENTRY_LEN {
84            return Err(Error::BufferTooShort {
85                need: REGION_ENTRY_LEN,
86                have: bytes.len(),
87                what: "page_region_entry",
88            });
89        }
90        let reserved = bytes[1];
91        Ok(PageRegionEntry {
92            region_id: bytes[0],
93            reserved,
94            region_horizontal_address: u16::from_be_bytes([bytes[2], bytes[3]]),
95            region_vertical_address: u16::from_be_bytes([bytes[4], bytes[5]]),
96        })
97    }
98}
99
100/// Page Composition Segment.
101#[derive(Debug, Clone, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103pub struct PageCompositionSegment {
104    /// The page_id (from generic segment header).
105    pub page_id: u16,
106    /// Time-out period in seconds.
107    pub page_time_out: u8,
108    /// Version number (modulo 16).
109    pub page_version_number: u8,
110    /// Page state.
111    pub page_state: PageState,
112    /// Reserved bits in body byte 1 (bits `[1:0]`).
113    pub reserved: u8,
114    /// Region entries.
115    pub regions: alloc::vec::Vec<PageRegionEntry>,
116    /// Trailing bytes after the region loop (preserved for round-trip).
117    #[cfg_attr(feature = "serde", serde(skip))]
118    pub(crate) suffix: alloc::vec::Vec<u8>,
119}
120
121impl<'a> Parse<'a> for PageCompositionSegment {
122    type Error = Error;
123
124    fn parse(bytes: &'a [u8]) -> Result<Self> {
125        if bytes.len() < HEADER_LEN + FIXED_LEN {
126            return Err(Error::BufferTooShort {
127                need: HEADER_LEN + FIXED_LEN,
128                have: bytes.len(),
129                what: "page_composition_segment",
130            });
131        }
132        if bytes[1] != SEGMENT_TYPE {
133            return Err(Error::UnknownSegmentType(bytes[1]));
134        }
135        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
136        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
137        let total = HEADER_LEN + segment_length;
138        if bytes.len() < total {
139            return Err(Error::BufferTooShort {
140                need: total,
141                have: bytes.len(),
142                what: "page_composition_segment data",
143            });
144        }
145        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
146        if body.len() < FIXED_LEN {
147            return Err(Error::BufferTooShort {
148                need: FIXED_LEN,
149                have: body.len(),
150                what: "page_composition_segment body",
151            });
152        }
153        let page_time_out = body[0];
154        let page_version_number = body[1] >> 4;
155        let page_state_bits = (body[1] >> 2) & 0x03;
156        let reserved = body[1] & 0x03;
157        let page_state = match page_state_bits {
158            0x00 => PageState::Normal,
159            0x01 => PageState::Acquisition,
160            0x02 => PageState::ModeChange,
161            v => PageState::Reserved(v),
162        };
163
164        let region_data = &body[FIXED_LEN..];
165        let region_count = region_data.len() / REGION_ENTRY_LEN;
166        if region_data.len() % REGION_ENTRY_LEN != 0 {
167            return Err(Error::BufferTooShort {
168                need: (region_count + 1) * REGION_ENTRY_LEN,
169                have: region_data.len(),
170                what: "page_composition_segment regions",
171            });
172        }
173        let mut regions = alloc::vec::Vec::with_capacity(region_count);
174        for i in 0..region_count {
175            let entry_bytes = &region_data[i * REGION_ENTRY_LEN..][..REGION_ENTRY_LEN];
176            regions.push(PageRegionEntry::parse(entry_bytes)?);
177        }
178        let suffix =
179            alloc::vec::Vec::from(&region_data[region_count * REGION_ENTRY_LEN..region_data.len()]);
180
181        Ok(PageCompositionSegment {
182            page_id,
183            page_time_out,
184            page_version_number,
185            page_state,
186            reserved,
187            regions,
188            suffix,
189        })
190    }
191}
192
193impl Serialize for PageCompositionSegment {
194    type Error = Error;
195
196    fn serialized_len(&self) -> usize {
197        HEADER_LEN + FIXED_LEN + self.regions.len() * REGION_ENTRY_LEN + self.suffix.len()
198    }
199
200    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
201        let len = self.serialized_len();
202        if buf.len() < len {
203            return Err(Error::BufferTooShort {
204                need: len,
205                have: buf.len(),
206                what: "page_composition_segment serialize",
207            });
208        }
209        buf[0] = 0x0F;
210        buf[1] = SEGMENT_TYPE;
211        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
212        let seg_len = (len - HEADER_LEN) as u16;
213        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
214
215        buf[6] = self.page_time_out;
216        buf[7] = (self.page_version_number << 4)
217            | (self.page_state.to_bits() << 2)
218            | (self.reserved & 0x03);
219
220        for (i, region) in self.regions.iter().enumerate() {
221            let off = HEADER_LEN + FIXED_LEN + i * REGION_ENTRY_LEN;
222            region.serialize_into(&mut buf[off..off + REGION_ENTRY_LEN]);
223        }
224        let suffix_off = HEADER_LEN + FIXED_LEN + self.regions.len() * REGION_ENTRY_LEN;
225        buf[suffix_off..suffix_off + self.suffix.len()].copy_from_slice(&self.suffix);
226        Ok(len)
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use broadcast_common::{Parse, Serialize};
234
235    #[test]
236    fn round_trip() {
237        let bytes = [
238            0x0F, 0x10, 0x00, 0x01, 0x00, 0x0E, 0x0A, 0x04, 0x01, 0x00, 0x00, 0x64, 0x00, 0x32,
239            0x02, 0x00, 0x00, 0xC8, 0x00, 0x96,
240        ];
241        let seg = PageCompositionSegment::parse(&bytes).unwrap();
242        assert_eq!(seg.page_time_out, 10);
243        assert_eq!(seg.page_version_number, 0);
244        assert_eq!(seg.page_state, PageState::Acquisition);
245        assert_eq!(seg.regions.len(), 2);
246        assert_eq!(seg.regions[0].region_id, 1);
247        assert_eq!(seg.regions[0].region_horizontal_address, 100);
248        assert_eq!(seg.regions[0].region_vertical_address, 50);
249        let out = seg.to_bytes();
250        assert_eq!(out, bytes);
251
252        // Biting test: mutate page_time_out changes output
253        let mut seg2 = seg.clone();
254        seg2.page_time_out = 20;
255        let out2 = seg2.to_bytes();
256        assert_ne!(out2, bytes);
257        let reparse = PageCompositionSegment::parse(&out2).unwrap();
258        assert_eq!(reparse.page_time_out, 20);
259    }
260}