Skip to main content

libbitsub_core/pgs/
parser.rs

1//! PGS file parser and subtitle data management.
2
3use memchr::memchr;
4use std::collections::HashMap;
5
6use super::{
7    AssembledObject, DisplaySet, MAX_PGS_BITMAP_PIXELS, ObjectDefinitionSegment,
8    PaletteDefinitionSegment, WindowDefinition, apply_palette_rgba_bytes, decode_rle_to_indexed,
9};
10use crate::utils::binary_search_timestamp;
11
12/// PGS subtitle parser and renderer.
13pub struct PgsParser {
14    /// All parsed display sets
15    display_sets: Vec<DisplaySet>,
16    /// Timestamps in milliseconds for quick lookup
17    timestamps_ms: Vec<u32>,
18    /// Cache for decoded indexed pixels (before palette application)
19    indexed_cache: HashMap<(u16, u8), DecodedBitmap>,
20    /// Last rendered boundary index (for cache invalidation)
21    last_boundary_index: Option<usize>,
22    /// Incrementally maintained rendering context for the active epoch.
23    cached_context: Option<RenderContext>,
24    /// Highest display-set index applied to the cached context.
25    cached_context_index: Option<usize>,
26    /// Last non-fatal render issue for diagnostics.
27    last_render_issue: Option<String>,
28}
29
30/// Cached decoded bitmap (indexed pixels, before palette)
31struct DecodedBitmap {
32    pub indexed: Vec<u8>,
33    pub width: u16,
34    pub height: u16,
35}
36
37impl PgsParser {
38    /// Create a new PGS parser.
39    pub fn new() -> Self {
40        Self {
41            display_sets: Vec::new(),
42            timestamps_ms: Vec::new(),
43            indexed_cache: HashMap::new(),
44            last_boundary_index: None,
45            cached_context: None,
46            cached_context_index: None,
47            last_render_issue: None,
48        }
49    }
50
51    /// Parse a PGS file from binary data.
52    /// Returns the number of display sets parsed.
53    pub fn parse(&mut self, data: &[u8]) -> usize {
54        self.display_sets.clear();
55        self.timestamps_ms.clear();
56        self.indexed_cache.clear();
57        self.last_boundary_index = None;
58        self.cached_context = None;
59        self.cached_context_index = None;
60        self.last_render_issue = None;
61
62        let len = data.len();
63
64        // Pre-allocate based on typical PGS file structure
65        // Roughly 1 display set per 2-5KB, use conservative estimate
66        let estimated_count = (len / 3000).max(16);
67        self.display_sets.reserve(estimated_count);
68        self.timestamps_ms.reserve(estimated_count);
69
70        let mut offset = 0;
71
72        while offset < len {
73            if let Some((display_set, consumed)) = DisplaySet::parse(&data[offset..], true) {
74                self.timestamps_ms.push(display_set.pts_ms());
75                self.display_sets.push(display_set);
76                offset += consumed;
77            } else {
78                // Try to recover by scanning for next magic number using SIMD-accelerated search
79                // "PG" (0x50 0x47)
80                offset += 1;
81                if let Some(pos) = memchr(0x50, &data[offset..]) {
82                    let candidate = offset + pos;
83                    if candidate + 1 < len && data[candidate + 1] == 0x47 {
84                        offset = candidate;
85                    } else {
86                        // Found 0x50 but not followed by 0x47, continue searching
87                        offset = candidate + 1;
88                    }
89                } else {
90                    // No more 0x50 bytes found, done
91                    break;
92                }
93            }
94        }
95
96        self.display_sets.len()
97    }
98
99    /// Get the number of display sets.
100    pub fn count(&self) -> usize {
101        self.display_sets.len()
102    }
103
104    /// Get the presentation width for this subtitle track.
105    pub fn screen_width(&self) -> u16 {
106        self.display_sets
107            .iter()
108            .find_map(|ds| ds.composition.as_ref().map(|composition| composition.width))
109            .unwrap_or(0)
110    }
111
112    /// Get the presentation height for this subtitle track.
113    pub fn screen_height(&self) -> u16 {
114        self.display_sets
115            .iter()
116            .find_map(|ds| {
117                ds.composition
118                    .as_ref()
119                    .map(|composition| composition.height)
120            })
121            .unwrap_or(0)
122    }
123
124    /// Get all timestamps in milliseconds.
125    pub fn get_timestamps(&self) -> Vec<f64> {
126        self.timestamps_ms.iter().map(|&ts| ts as f64).collect()
127    }
128
129    /// Find the display set index for a given timestamp in milliseconds.
130    pub fn find_index_at_timestamp(&self, time_ms: f64) -> i32 {
131        if self.timestamps_ms.is_empty() {
132            return -1;
133        }
134
135        let time_ms_u32 = time_ms as u32;
136        let index = binary_search_timestamp(&self.timestamps_ms, time_ms_u32);
137        let start_time = self.timestamps_ms[index];
138
139        if time_ms_u32 < start_time {
140            return -1;
141        }
142
143        index as i32
144    }
145
146    /// Get the cue start time in milliseconds.
147    pub fn get_cue_start_time(&self, index: usize) -> f64 {
148        self.timestamps_ms
149            .get(index)
150            .copied()
151            .map_or(-1.0, |ts| ts as f64)
152    }
153
154    /// Get the cue end time in milliseconds.
155    pub fn get_cue_end_time(&self, index: usize) -> f64 {
156        let Some(&start_time) = self.timestamps_ms.get(index) else {
157            return -1.0;
158        };
159
160        let end_time = self
161            .timestamps_ms
162            .get(index + 1)
163            .copied()
164            .unwrap_or_else(|| start_time.saturating_add(5000));
165
166        end_time as f64
167    }
168
169    /// Get the number of composition objects in a cue.
170    pub fn get_cue_composition_count(&self, index: usize) -> u32 {
171        self.display_sets
172            .get(index)
173            .and_then(|ds| ds.composition.as_ref())
174            .map_or(0, |composition| {
175                composition.composition_objects.len() as u32
176            })
177    }
178
179    /// Get the cue palette ID.
180    pub fn get_cue_palette_id(&self, index: usize) -> i32 {
181        self.display_sets
182            .get(index)
183            .and_then(|ds| ds.composition.as_ref())
184            .map_or(-1, |composition| composition.palette_id as i32)
185    }
186
187    /// Get the cue composition state.
188    pub fn get_cue_composition_state(&self, index: usize) -> i32 {
189        self.display_sets
190            .get(index)
191            .and_then(|ds| ds.composition.as_ref())
192            .map_or(-1, |composition| composition.composition_state as i32)
193    }
194
195    /// Render subtitle at the given index and return RGBA data.
196    /// Returns null if index is invalid or no subtitle data.
197    pub fn render_at_index(&mut self, index: usize) -> Option<SubtitleFrame> {
198        self.last_render_issue = None;
199
200        if index >= self.display_sets.len() {
201            self.last_render_issue = Some("INDEX_OUT_OF_RANGE".to_string());
202            return None;
203        }
204
205        // Find boundary (epoch start or acquisition point) for context building
206        let boundary_index = self.find_boundary_index(index);
207        self.ensure_context_for_index(boundary_index, index);
208
209        // Get current display set
210        let ds = &self.display_sets[index];
211        let Some(composition) = ds.composition.as_ref() else {
212            self.last_render_issue = Some("MISSING_COMPOSITION".to_string());
213            return None;
214        };
215
216        // Empty composition_objects means clear the screen
217        if composition.composition_objects.is_empty() {
218            self.last_render_issue = Some("EMPTY_CUE".to_string());
219            return None;
220        }
221
222        let width = composition.width;
223        let height = composition.height;
224
225        let Some(context) = self.cached_context.as_ref() else {
226            self.last_render_issue = Some("RENDER_CONTEXT_UNAVAILABLE".to_string());
227            return None;
228        };
229
230        // Find the palette to use
231        let Some(palette) = context.palettes.get(&composition.palette_id) else {
232            self.last_render_issue = Some("MISSING_PALETTE".to_string());
233            return None;
234        };
235
236        // Render all composition objects
237        let mut compositions = Vec::new();
238
239        for comp_obj in &composition.composition_objects {
240            // Get assembled object
241            let obj = match context.objects.get(&comp_obj.object_id) {
242                Some(obj) => obj,
243                None => continue,
244            };
245
246            // Window lookup is optional - don't fail if not found
247            let _window = context.windows.get(&comp_obj.window_id);
248
249            // Decode or get cached indexed pixels
250            let cache_key = (obj.id, obj.version);
251            let decoded = if let Some(cached) = self.indexed_cache.get(&cache_key) {
252                cached
253            } else {
254                let pixel_count = match Self::bitmap_pixel_count(obj.width, obj.height) {
255                    Some(pixel_count) => pixel_count,
256                    None => continue,
257                };
258
259                let mut indexed = vec![0u8; pixel_count];
260                decode_rle_to_indexed(&obj.data, &mut indexed);
261
262                self.indexed_cache.insert(
263                    cache_key,
264                    DecodedBitmap {
265                        indexed,
266                        width: obj.width,
267                        height: obj.height,
268                    },
269                );
270                self.indexed_cache.get(&cache_key).unwrap()
271            };
272
273            let pixel_count = match Self::bitmap_pixel_count(decoded.width, decoded.height) {
274                Some(pixel_count) => pixel_count,
275                None => continue,
276            };
277
278            let rgba_len = match pixel_count.checked_mul(4) {
279                Some(rgba_len) => rgba_len,
280                None => continue,
281            };
282
283            let mut rgba = vec![0u8; rgba_len];
284            apply_palette_rgba_bytes(&decoded.indexed, &palette.rgba, &mut rgba);
285
286            compositions.push(SubtitleComposition {
287                x: comp_obj.x,
288                y: comp_obj.y,
289                width: decoded.width,
290                height: decoded.height,
291                rgba,
292            });
293        }
294
295        if compositions.is_empty() {
296            self.last_render_issue = Some("EMPTY_RENDER".to_string());
297        }
298
299        Some(SubtitleFrame {
300            width,
301            height,
302            compositions,
303        })
304    }
305
306    /// Get the last non-fatal render issue for diagnostics.
307    pub fn last_render_issue(&self) -> String {
308        self.last_render_issue.clone().unwrap_or_default()
309    }
310
311    /// Clear the internal cache.
312    pub fn clear_cache(&mut self) {
313        self.indexed_cache.clear();
314        self.last_boundary_index = None;
315        self.cached_context = None;
316        self.cached_context_index = None;
317        self.last_render_issue = None;
318    }
319
320    fn ensure_context_for_index(&mut self, boundary_index: usize, target_index: usize) {
321        let needs_rebuild = self.last_boundary_index != Some(boundary_index)
322            || self.cached_context.is_none()
323            || self
324                .cached_context_index
325                .is_none_or(|cached_index| target_index < cached_index);
326
327        if needs_rebuild {
328            self.indexed_cache.clear();
329            self.last_boundary_index = Some(boundary_index);
330
331            let mut context = RenderContext::new();
332            self.apply_display_sets(&mut context, boundary_index, target_index);
333            self.cached_context = Some(context);
334            self.cached_context_index = Some(target_index);
335            return;
336        }
337
338        let Some(cached_index) = self.cached_context_index else {
339            return;
340        };
341
342        if cached_index >= target_index {
343            return;
344        }
345
346        let mut context = self
347            .cached_context
348            .take()
349            .unwrap_or_else(RenderContext::new);
350        self.apply_display_sets(&mut context, cached_index + 1, target_index);
351        self.cached_context = Some(context);
352        self.cached_context_index = Some(target_index);
353    }
354
355    /// Find the boundary index (epoch start or acquisition point) before the given index.
356    fn find_boundary_index(&self, index: usize) -> usize {
357        for i in (0..=index).rev() {
358            if let Some(comp) = &self.display_sets[i].composition
359                && (comp.is_epoch_start() || comp.is_acquisition_point())
360            {
361                return i;
362            }
363        }
364        0
365    }
366
367    fn apply_display_sets(
368        &self,
369        context: &mut RenderContext,
370        start_index: usize,
371        end_index: usize,
372    ) {
373        for i in start_index..=end_index {
374            context.apply_display_set(&self.display_sets[i]);
375        }
376    }
377
378    fn bitmap_pixel_count(width: u16, height: u16) -> Option<usize> {
379        let width = width as usize;
380        let height = height as usize;
381
382        if width == 0 || height == 0 {
383            return None;
384        }
385
386        let pixel_count = width.checked_mul(height)?;
387        if pixel_count > MAX_PGS_BITMAP_PIXELS {
388            return None;
389        }
390
391        Some(pixel_count)
392    }
393}
394
395impl Default for PgsParser {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401/// Rendering context built from display sets.
402struct RenderContext {
403    /// Object parts by ID (before assembly)
404    object_parts: HashMap<u16, Vec<ObjectDefinitionSegment>>,
405    /// Assembled objects by ID
406    objects: HashMap<u16, AssembledObject>,
407    /// Palettes by ID
408    palettes: HashMap<u8, PaletteDefinitionSegment>,
409    /// Windows by ID
410    windows: HashMap<u8, WindowDefinition>,
411}
412
413impl RenderContext {
414    fn new() -> Self {
415        Self {
416            object_parts: HashMap::new(),
417            objects: HashMap::new(),
418            palettes: HashMap::new(),
419            windows: HashMap::new(),
420        }
421    }
422
423    fn apply_display_set(&mut self, ds: &DisplaySet) {
424        let mut updated_object_ids = Vec::new();
425
426        for obj in &ds.objects {
427            if obj.is_first_in_sequence() {
428                self.object_parts.insert(obj.id, vec![obj.clone()]);
429                updated_object_ids.push(obj.id);
430            } else if let Some(parts) = self.object_parts.get_mut(&obj.id) {
431                parts.push(obj.clone());
432                if !updated_object_ids.contains(&obj.id) {
433                    updated_object_ids.push(obj.id);
434                }
435            }
436        }
437
438        for object_id in updated_object_ids {
439            if let Some(parts) = self.object_parts.get(&object_id) {
440                if let Some(assembled) = AssembledObject::from_segments(parts) {
441                    self.objects.insert(object_id, assembled);
442                } else {
443                    self.objects.remove(&object_id);
444                }
445            }
446        }
447
448        for palette in &ds.palettes {
449            self.palettes.insert(palette.id, palette.clone());
450        }
451
452        for wds in &ds.windows {
453            for window in &wds.windows {
454                self.windows.insert(window.id, *window);
455            }
456        }
457    }
458}
459
460/// A single subtitle composition element.
461#[derive(Clone)]
462pub struct SubtitleComposition {
463    pub x: u16,
464    pub y: u16,
465    pub width: u16,
466    pub height: u16,
467    pub rgba: Vec<u8>,
468}
469
470impl SubtitleComposition {
471    pub fn x(&self) -> u16 {
472        self.x
473    }
474    pub fn y(&self) -> u16 {
475        self.y
476    }
477    pub fn width(&self) -> u16 {
478        self.width
479    }
480    pub fn height(&self) -> u16 {
481        self.height
482    }
483
484    /// Get RGBA pixel data.
485    pub fn get_rgba(&self) -> &[u8] {
486        &self.rgba
487    }
488}
489
490/// A complete subtitle frame with all compositions.
491pub struct SubtitleFrame {
492    pub width: u16,
493    pub height: u16,
494    pub compositions: Vec<SubtitleComposition>,
495}
496
497impl SubtitleFrame {
498    pub fn width(&self) -> u16 {
499        self.width
500    }
501    pub fn height(&self) -> u16 {
502        self.height
503    }
504
505    /// Get the number of compositions.
506    pub fn composition_count(&self) -> usize {
507        self.compositions.len()
508    }
509
510    /// Get a composition by index.
511    pub fn get_composition(&self, index: usize) -> Option<SubtitleComposition> {
512        self.compositions.get(index).cloned()
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use crate::pgs::{CompositionObject, PresentationCompositionSegment};
520
521    #[test]
522    fn find_index_at_timestamp_returns_none_before_first_pts() {
523        let mut parser = PgsParser::new();
524        parser.timestamps_ms = vec![1200, 2400, 3600];
525
526        assert_eq!(parser.find_index_at_timestamp(0.0), -1);
527        assert_eq!(parser.find_index_at_timestamp(1199.0), -1);
528        assert_eq!(parser.find_index_at_timestamp(1200.0), 0);
529        assert_eq!(parser.find_index_at_timestamp(2500.0), 1);
530    }
531
532    #[test]
533    fn test_render_at_index_skips_oversized_objects() {
534        let mut parser = PgsParser {
535            display_sets: vec![DisplaySet {
536                pts: 0,
537                dts: 0,
538                composition: Some(PresentationCompositionSegment {
539                    width: 1920,
540                    height: 1080,
541                    frame_rate: 0,
542                    composition_number: 0,
543                    composition_state: 0,
544                    palette_update_flag: 0,
545                    palette_id: 0,
546                    composition_objects: vec![CompositionObject {
547                        object_id: 1,
548                        window_id: 0,
549                        cropped_flag: 0,
550                        x: 0,
551                        y: 0,
552                        crop_x: 0,
553                        crop_y: 0,
554                        crop_width: 0,
555                        crop_height: 0,
556                    }],
557                }),
558                palettes: vec![PaletteDefinitionSegment {
559                    id: 0,
560                    version: 0,
561                    rgba: vec![0u32; 256],
562                }],
563                objects: vec![ObjectDefinitionSegment {
564                    id: 1,
565                    version: 0,
566                    sequence_flag: 0xC0,
567                    data_length: 1,
568                    width: 5000,
569                    height: 5000,
570                    data: vec![1],
571                }],
572                windows: Vec::new(),
573            }],
574            timestamps_ms: vec![0],
575            indexed_cache: HashMap::new(),
576            last_boundary_index: None,
577            cached_context: None,
578            cached_context_index: None,
579            last_render_issue: None,
580        };
581
582        let frame = parser.render_at_index(0).expect("frame should exist");
583
584        assert_eq!(frame.composition_count(), 0);
585    }
586}