Skip to main content

dvb_subtitle/segments/
disparity_signalling.rs

1//! Disparity Signalling Segment — ETSI EN 300 743 §7.2.7, Table 29 (segment_type 0x15).
2//!
3//! Supports plano-stereoscopic 3DTV subtitling by allowing disparity values
4//! to be ascribed to regions or subregions.
5
6use crate::error::{Error, Result};
7use broadcast_common::{Parse, Serialize};
8
9/// The disparity_signalling_segment segment_type.
10pub const SEGMENT_TYPE: u8 = 0x15;
11/// Header: 6 bytes.
12pub const HEADER_LEN: usize = 6;
13/// Fixed body: dss_version_number(4b)+page_flag(1b)+reserved(3b) + page_default_disparity_shift(1) = 2 bytes.
14pub const FIXED_LEN: usize = 2;
15
16/// A disparity shift update sequence as defined in Table 30.
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize))]
19pub struct DisparityShiftUpdateSequence {
20    /// Interval duration in 90kHz STC units (24-bit).
21    pub interval_duration: u32,
22    /// Interval count + disparity shift integer pairs. `division_period_count`
23    /// (Table 30) is exactly this list's length — "the number of ... time
24    /// intervals within the following ... 'for' loop" — so it is derived on
25    /// serialize rather than stored (#1108/DS-W3: a stored count could
26    /// diverge from the data it's supposed to describe).
27    pub intervals: alloc::vec::Vec<DisparityShiftInterval>,
28}
29
30/// An interval within a disparity shift update sequence.
31#[derive(Debug, Clone, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize))]
33pub struct DisparityShiftInterval {
34    /// Interval count multiplier.
35    pub interval_count: u8,
36    /// Disparity shift integer part (signed).
37    pub disparity_shift_integer: i8,
38}
39
40impl DisparityShiftUpdateSequence {
41    fn serialized_len(&self) -> usize {
42        1 + 3 + 1 + self.intervals.len() * 2
43    }
44
45    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
46        let body_len = 3 + 1 + self.intervals.len() * 2;
47        buf[0] = broadcast_common::len::fit_u8(body_len, "disparity_shift_update_sequence_length")?;
48        buf[1] = (self.interval_duration >> 16) as u8;
49        buf[2] = (self.interval_duration >> 8) as u8;
50        buf[3] = self.interval_duration as u8;
51        buf[4] = broadcast_common::len::fit_u8(self.intervals.len(), "division_period_count")?;
52        for (i, interval) in self.intervals.iter().enumerate() {
53            let off = 5 + i * 2;
54            buf[off] = interval.interval_count;
55            buf[off + 1] = interval.disparity_shift_integer as u8;
56        }
57        Ok(())
58    }
59
60    fn parse(bytes: &[u8]) -> Result<(Self, usize)> {
61        if bytes.len() < 5 {
62            return Err(Error::BufferTooShort {
63                need: 5,
64                have: bytes.len(),
65                what: "disparity_shift_update_sequence",
66            });
67        }
68        let seq_len = bytes[0] as usize;
69        if bytes.len() < 1 + seq_len {
70            return Err(Error::BufferTooShort {
71                need: 1 + seq_len,
72                have: bytes.len(),
73                what: "disparity_shift_update_sequence data",
74            });
75        }
76        let dur = ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32);
77        let count = bytes[4];
78        let total = 1 + seq_len;
79        let mut intervals = alloc::vec::Vec::new();
80        let mut ipos = 5;
81        for _ in 0..count {
82            if ipos + 2 > total {
83                return Err(Error::BufferTooShort {
84                    need: ipos + 2,
85                    have: total,
86                    what: "disparity_shift_update_sequence interval",
87                });
88            }
89            intervals.push(DisparityShiftInterval {
90                interval_count: bytes[ipos],
91                disparity_shift_integer: bytes[ipos + 1] as i8,
92            });
93            ipos += 2;
94        }
95        Ok((
96            DisparityShiftUpdateSequence {
97                interval_duration: dur,
98                intervals,
99            },
100            total,
101        ))
102    }
103}
104
105/// A subregion within a region for disparity signalling.
106#[derive(Debug, Clone, PartialEq, Eq)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108pub struct Subregion {
109    /// Horizontal position relative to page (only if multiple subregions).
110    pub subregion_horizontal_position: Option<u16>,
111    /// Width in pixels (only if multiple subregions).
112    pub subregion_width: Option<u16>,
113    /// Disparity shift integer part (signed).
114    pub subregion_disparity_shift_integer: i8,
115    /// Disparity shift fractional part (unsigned, in 1/16 pixel).
116    pub subregion_disparity_shift_fractional: u8,
117    /// Reserved bits in the subregion shift/frac byte (bits `[3:0]`).
118    pub reserved: u8,
119    /// Update sequence if flag is set.
120    pub update_sequence: Option<DisparityShiftUpdateSequence>,
121}
122
123impl Subregion {
124    fn serialized_len(&self) -> usize {
125        let base = if self.subregion_horizontal_position.is_some() {
126            4 + 2
127        } else {
128            2
129        };
130        base + self
131            .update_sequence
132            .as_ref()
133            .map_or(0, |u| u.serialized_len())
134    }
135
136    /// `has_pos` comes from the *region*, not this subregion (Table 29: the
137    /// position fields are present for every subregion in a region, or none
138    /// of them, per `number_of_subregions_minus_1`) — see DisparityRegion::
139    /// serialize_into. Errors if this subregion's stored `Option`s disagree
140    /// with that (#1108/DS-W3): writing this subregion's own state instead
141    /// could silently misframe the following subregions/regions.
142    fn serialize_into(&self, buf: &mut [u8], has_pos: bool) -> Result<()> {
143        let stored_has_pos =
144            self.subregion_horizontal_position.is_some() && self.subregion_width.is_some();
145        if stored_has_pos != has_pos {
146            return Err(Error::SubregionPositionMismatch);
147        }
148        let mut off = 0;
149        if let (Some(h), Some(w)) = (self.subregion_horizontal_position, self.subregion_width) {
150            buf[0..2].copy_from_slice(&h.to_be_bytes());
151            buf[2..4].copy_from_slice(&w.to_be_bytes());
152            off = 4;
153        }
154        buf[off] = self.subregion_disparity_shift_integer as u8;
155        buf[off + 1] = (self.subregion_disparity_shift_fractional << 4) | (self.reserved & 0x0F);
156        off += 2;
157        if let Some(ref seq) = self.update_sequence {
158            seq.serialize_into(&mut buf[off..])?;
159        }
160        Ok(())
161    }
162}
163
164/// A region entry in the disparity signalling segment.
165#[derive(Debug, Clone, PartialEq, Eq)]
166#[cfg_attr(feature = "serde", derive(serde::Serialize))]
167pub struct DisparityRegion {
168    /// Region identifier.
169    pub region_id: u8,
170    /// Whether an update sequence follows for this region's subregions.
171    pub update_sequence_region_flag: bool,
172    /// Reserved bits in the region flags byte (bits `[6:2]`).
173    pub reserved_flags: u8,
174    /// Subregions within this region.
175    pub subregions: alloc::vec::Vec<Subregion>,
176}
177
178impl DisparityRegion {
179    fn serialized_len(&self) -> usize {
180        2 + self
181            .subregions
182            .iter()
183            .map(|s| s.serialized_len())
184            .sum::<usize>()
185    }
186
187    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
188        let count = self.subregions.len();
189        // number_of_subregions_minus_1 is 2 bits (Table 29): only 1..=4
190        // subregions are representable. Pre-fix, `as u8` + `& 0x03` silently
191        // wrapped an out-of-range count instead of rejecting it (#1108/DS-W3).
192        if count == 0 || count > 4 {
193            return Err(Error::InvalidSubregionCount(count));
194        }
195        let num = (count - 1) as u8;
196        buf[0] = self.region_id;
197        buf[1] = (u8::from(self.update_sequence_region_flag) << 7)
198            | (self.reserved_flags & 0x7C)
199            | (num & 0x03);
200        // Position fields are present for every subregion, or none, per
201        // whether the region itself has more than one (Table 29's
202        // `if (number_of_subregions_minus_1 > 0)` guards the whole `for` body).
203        let has_pos = count > 1;
204        let mut off = 2;
205        for sub in &self.subregions {
206            sub.serialize_into(&mut buf[off..], has_pos)?;
207            off += sub.serialized_len();
208        }
209        Ok(())
210    }
211}
212
213/// Disparity Signalling Segment.
214#[derive(Debug, Clone, PartialEq, Eq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize))]
216pub struct DisparitySignallingSegment {
217    /// The page_id from the segment header.
218    pub page_id: u16,
219    /// DSS version number (modulo 16).
220    pub dss_version_number: u8,
221    /// Whether a page-level update sequence follows.
222    pub update_sequence_page_flag: bool,
223    /// Reserved bits in body byte 0 (bits `[2:0]`).
224    pub reserved: u8,
225    /// Default disparity shift for the whole page (signed).
226    pub page_default_disparity_shift: i8,
227    /// Page-level update sequence if flag is set.
228    pub page_update_sequence: Option<DisparityShiftUpdateSequence>,
229    /// Region entries.
230    pub regions: alloc::vec::Vec<DisparityRegion>,
231}
232
233impl<'a> Parse<'a> for DisparitySignallingSegment {
234    type Error = Error;
235
236    fn parse(bytes: &'a [u8]) -> Result<Self> {
237        if bytes.len() < HEADER_LEN + FIXED_LEN {
238            return Err(Error::BufferTooShort {
239                need: HEADER_LEN + FIXED_LEN,
240                have: bytes.len(),
241                what: "disparity_signalling_segment",
242            });
243        }
244        if bytes[0] != crate::pes_data_field::SYNC_BYTE {
245            return Err(Error::BadSyncByte(bytes[0]));
246        }
247        if bytes[1] != SEGMENT_TYPE {
248            return Err(Error::UnknownSegmentType(bytes[1]));
249        }
250        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
251        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
252        let total = HEADER_LEN + segment_length;
253        if bytes.len() < total {
254            return Err(Error::BufferTooShort {
255                need: total,
256                have: bytes.len(),
257                what: "disparity_signalling_segment data",
258            });
259        }
260        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
261        if body.len() < FIXED_LEN {
262            return Err(Error::BufferTooShort {
263                need: FIXED_LEN,
264                have: body.len(),
265                what: "disparity_signalling_segment body",
266            });
267        }
268        let dss_version_number = body[0] >> 4;
269        let update_sequence_page_flag = (body[0] & 0x08) != 0;
270        let reserved = body[0] & 0x07;
271        let page_default_disparity_shift = body[1] as i8;
272
273        let mut pos = FIXED_LEN;
274        let page_update_sequence = if update_sequence_page_flag {
275            let (seq, seq_len) = DisparityShiftUpdateSequence::parse(&body[pos..])?;
276            pos += seq_len;
277            Some(seq)
278        } else {
279            None
280        };
281
282        let mut regions = alloc::vec::Vec::new();
283        while pos < body.len() {
284            // Table 29's region loop is `while (processed_length <
285            // segment_length)` with no stuffing provision, so 1 leftover
286            // byte means the segment is malformed, not that it should be
287            // silently dropped (#1108/DS-W6: this used to just `break`).
288            if pos + 2 > body.len() {
289                return Err(Error::BufferTooShort {
290                    need: pos + 2,
291                    have: body.len(),
292                    what: "disparity_signalling_segment region header",
293                });
294            }
295            let region_id = body[pos];
296            pos += 1;
297            let update_flag = (body[pos] & 0x80) != 0;
298            let reserved_flags = body[pos] & 0x7C;
299            let num_subregions_minus_1 = body[pos] & 0x03;
300            pos += 1;
301
302            let mut subregions = alloc::vec::Vec::new();
303            for _n in 0..=num_subregions_minus_1 {
304                let has_pos = num_subregions_minus_1 > 0;
305                let entry_len = if has_pos { 4 + 2 } else { 2 };
306                if pos + entry_len > body.len() {
307                    return Err(Error::BufferTooShort {
308                        need: pos + entry_len,
309                        have: body.len(),
310                        what: "subregion entry",
311                    });
312                }
313
314                let (hpos, width) = if has_pos {
315                    let h = u16::from_be_bytes([body[pos], body[pos + 1]]);
316                    let w = u16::from_be_bytes([body[pos + 2], body[pos + 3]]);
317                    pos += 4;
318                    (Some(h), Some(w))
319                } else {
320                    (None, None)
321                };
322
323                let shift_int = body[pos] as i8;
324                let shift_frac = body[pos + 1] >> 4;
325                let reserved = body[pos + 1] & 0x0F;
326                pos += 2;
327
328                let sub_update = if update_flag {
329                    let (seq, seq_len) = DisparityShiftUpdateSequence::parse(&body[pos..])?;
330                    pos += seq_len;
331                    Some(seq)
332                } else {
333                    None
334                };
335
336                subregions.push(Subregion {
337                    subregion_horizontal_position: hpos,
338                    subregion_width: width,
339                    subregion_disparity_shift_integer: shift_int,
340                    subregion_disparity_shift_fractional: shift_frac,
341                    reserved,
342                    update_sequence: sub_update,
343                });
344            }
345
346            regions.push(DisparityRegion {
347                region_id,
348                update_sequence_region_flag: update_flag,
349                reserved_flags,
350                subregions,
351            });
352        }
353
354        Ok(DisparitySignallingSegment {
355            page_id,
356            dss_version_number,
357            update_sequence_page_flag,
358            reserved,
359            page_default_disparity_shift,
360            page_update_sequence,
361            regions,
362        })
363    }
364}
365
366impl Serialize for DisparitySignallingSegment {
367    type Error = Error;
368
369    fn serialized_len(&self) -> usize {
370        HEADER_LEN
371            + FIXED_LEN
372            + self
373                .page_update_sequence
374                .as_ref()
375                .map_or(0, |s| s.serialized_len())
376            + self
377                .regions
378                .iter()
379                .map(|r| r.serialized_len())
380                .sum::<usize>()
381    }
382
383    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
384        let len = self.serialized_len();
385        if buf.len() < len {
386            return Err(Error::BufferTooShort {
387                need: len,
388                have: buf.len(),
389                what: "disparity_signalling_segment serialize",
390            });
391        }
392        buf[0] = 0x0F;
393        buf[1] = SEGMENT_TYPE;
394        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
395        let seg_len = crate::segments::check_segment_length(len - HEADER_LEN)?;
396        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
397
398        buf[6] = (self.dss_version_number << 4)
399            | (u8::from(self.update_sequence_page_flag) << 3)
400            | (self.reserved & 0x07);
401        buf[7] = self.page_default_disparity_shift as u8;
402
403        let mut off = HEADER_LEN + FIXED_LEN;
404        if let Some(ref seq) = self.page_update_sequence {
405            seq.serialize_into(&mut buf[off..])?;
406            off += seq.serialized_len();
407        }
408        for region in &self.regions {
409            region.serialize_into(&mut buf[off..])?;
410            off += region.serialized_len();
411        }
412        Ok(len)
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use broadcast_common::{Parse, Serialize};
420
421    #[test]
422    fn round_trip_simple() {
423        let bytes = [
424            0x0F, 0x15, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
425        ];
426        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
427        assert_eq!(seg.dss_version_number, 0);
428        assert!(!seg.update_sequence_page_flag);
429        assert_eq!(seg.regions.len(), 1);
430        assert_eq!(seg.regions[0].region_id, 1);
431        assert_eq!(seg.regions[0].subregions.len(), 1);
432        let out = seg.to_bytes();
433        assert_eq!(out, bytes);
434
435        // Biting test
436        let mut seg2 = seg.clone();
437        seg2.regions[0].region_id = 5;
438        let out2 = seg2.to_bytes();
439        assert_ne!(out2, bytes);
440        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
441        assert_eq!(reparse.regions[0].region_id, 5);
442    }
443
444    /// DS-W2 (#1108): sync_byte wasn't checked by any typed segment parser.
445    #[test]
446    fn rejects_bad_sync_byte() {
447        let bytes = [
448            0x00, 0x15, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
449        ];
450        assert!(matches!(
451            DisparitySignallingSegment::parse(&bytes),
452            Err(Error::BadSyncByte(0x00))
453        ));
454    }
455
456    /// DS-W6 (#1108): Table 29's region loop has no stuffing provision, so a
457    /// single leftover byte after the last complete region is now rejected,
458    /// not silently dropped via a bare `break`.
459    #[test]
460    fn rejects_trailing_partial_region() {
461        let bytes = [
462            0x0F, 0x15, 0x00, 0x01, 0x00, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF,
463        ];
464        assert!(matches!(
465            DisparitySignallingSegment::parse(&bytes),
466            Err(Error::BufferTooShort { .. })
467        ));
468    }
469
470    #[test]
471    fn round_trip_with_page_update_seq() {
472        let bytes = [
473            0x0F, 0x15, 0x00, 0x01, 0x00, 0x0B, 0x88, 0x05, 0x08, 0x00, 0x00, 0x0A, 0x02, 0x01,
474            0x10, 0x03, 0x20,
475        ];
476        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
477        assert_eq!(seg.dss_version_number, 8);
478        assert!(seg.update_sequence_page_flag);
479        assert_eq!(seg.regions.len(), 0);
480        let out = seg.to_bytes();
481        assert_eq!(out, bytes);
482
483        // Biting test
484        let mut seg2 = seg.clone();
485        seg2.page_default_disparity_shift = 10;
486        let out2 = seg2.to_bytes();
487        assert_ne!(out2, bytes);
488        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
489        assert_eq!(reparse.page_default_disparity_shift, 10);
490    }
491
492    #[test]
493    fn round_trip_with_update_sequence() {
494        let bytes = [
495            0x0F, 0x15, 0x00, 0x01, 0x00, 0x16, 0x88, 0x05, 0x08, 0x00, 0x00, 0x0A, 0x02, 0x01,
496            0x10, 0x03, 0x20, 0x01, 0x80, 0x10, 0x00, 0x06, 0x00, 0x00, 0x14, 0x01, 0x01, 0x08,
497        ];
498        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
499        assert_eq!(seg.dss_version_number, 8);
500        assert!(seg.update_sequence_page_flag);
501        assert_eq!(seg.page_default_disparity_shift, 5);
502        let seq = seg.page_update_sequence.as_ref().unwrap();
503        assert_eq!(seq.intervals.len(), 2);
504        assert_eq!(seg.regions.len(), 1);
505        assert_eq!(seg.regions[0].region_id, 1);
506        assert!(seg.regions[0].update_sequence_region_flag);
507        assert_eq!(seg.regions[0].subregions.len(), 1);
508        let out = seg.to_bytes();
509        assert_eq!(out, bytes);
510
511        // Biting test
512        let mut seg2 = seg.clone();
513        seg2.dss_version_number = 3;
514        let out2 = seg2.to_bytes();
515        assert_ne!(out2, bytes);
516        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
517        assert_eq!(reparse.dss_version_number, 3);
518    }
519
520    /// DS-W3 (#1108): `division_period_count` is now derived from
521    /// `intervals.len()`, so appending an interval must grow the on-wire
522    /// count byte and re-parse with the new interval intact.
523    #[test]
524    fn division_period_count_tracks_intervals_len() {
525        let bytes = [
526            0x0F, 0x15, 0x00, 0x01, 0x00, 0x0B, 0x88, 0x05, 0x08, 0x00, 0x00, 0x0A, 0x02, 0x01,
527            0x10, 0x03, 0x20,
528        ];
529        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
530        let mut seg2 = seg.clone();
531        seg2.page_update_sequence
532            .as_mut()
533            .unwrap()
534            .intervals
535            .push(DisparityShiftInterval {
536                interval_count: 5,
537                disparity_shift_integer: -1,
538            });
539        let out2 = seg2.to_bytes();
540        assert_ne!(out2, bytes);
541        // division_period_count is body[4] of the update sequence, which
542        // starts right after the 8-byte fixed header (offset HEADER_LEN+FIXED_LEN).
543        let seq_off = HEADER_LEN + FIXED_LEN;
544        assert_eq!(
545            out2[seq_off + 4],
546            3,
547            "division_period_count must track intervals.len()"
548        );
549        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
550        assert_eq!(reparse.page_update_sequence.unwrap().intervals.len(), 3);
551    }
552
553    /// DS-W3 (#1108): `number_of_subregions_minus_1` is 2 bits (Table 29), so
554    /// 0 or more than 4 subregions must be rejected, not silently wrapped by
555    /// `as u8 & 0x03`.
556    #[test]
557    fn rejects_out_of_range_subregion_count() {
558        let region = DisparityRegion {
559            region_id: 1,
560            update_sequence_region_flag: false,
561            reserved_flags: 0,
562            subregions: alloc::vec::Vec::new(),
563        };
564        let mut buf = [0u8; 16];
565        assert!(matches!(
566            region.serialize_into(&mut buf),
567            Err(Error::InvalidSubregionCount(0))
568        ));
569
570        let sub = Subregion {
571            subregion_horizontal_position: None,
572            subregion_width: None,
573            subregion_disparity_shift_integer: 0,
574            subregion_disparity_shift_fractional: 0,
575            reserved: 0,
576            update_sequence: None,
577        };
578        let region5 = DisparityRegion {
579            region_id: 1,
580            update_sequence_region_flag: false,
581            reserved_flags: 0,
582            subregions: alloc::vec![sub; 5],
583        };
584        let mut buf = [0u8; 64];
585        assert!(matches!(
586            region5.serialize_into(&mut buf),
587            Err(Error::InvalidSubregionCount(5))
588        ));
589    }
590
591    /// DS-W3 (#1108): a region with 2+ subregions requires the position
592    /// fields present on ALL of them (Table 29); one subregion disagreeing
593    /// with the others must be rejected, not silently misframed.
594    #[test]
595    fn rejects_inconsistent_subregion_position() {
596        let with_pos = Subregion {
597            subregion_horizontal_position: Some(10),
598            subregion_width: Some(20),
599            subregion_disparity_shift_integer: 0,
600            subregion_disparity_shift_fractional: 0,
601            reserved: 0,
602            update_sequence: None,
603        };
604        let without_pos = Subregion {
605            subregion_horizontal_position: None,
606            subregion_width: None,
607            subregion_disparity_shift_integer: 0,
608            subregion_disparity_shift_fractional: 0,
609            reserved: 0,
610            update_sequence: None,
611        };
612        let region = DisparityRegion {
613            region_id: 1,
614            update_sequence_region_flag: false,
615            reserved_flags: 0,
616            subregions: alloc::vec![with_pos, without_pos],
617        };
618        let mut buf = [0u8; 32];
619        assert!(matches!(
620            region.serialize_into(&mut buf),
621            Err(Error::SubregionPositionMismatch)
622        ));
623    }
624}