1use 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
12pub struct PgsParser {
14 display_sets: Vec<DisplaySet>,
16 timestamps_ms: Vec<u32>,
18 indexed_cache: HashMap<(u16, u8), DecodedBitmap>,
20 last_boundary_index: Option<usize>,
22 cached_context: Option<RenderContext>,
24 cached_context_index: Option<usize>,
26 last_render_issue: Option<String>,
28}
29
30struct DecodedBitmap {
32 pub indexed: Vec<u8>,
33 pub width: u16,
34 pub height: u16,
35}
36
37impl PgsParser {
38 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 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 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 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 offset = candidate + 1;
88 }
89 } else {
90 break;
92 }
93 }
94 }
95
96 self.display_sets.len()
97 }
98
99 pub fn count(&self) -> usize {
101 self.display_sets.len()
102 }
103
104 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 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 pub fn get_timestamps(&self) -> Vec<f64> {
126 self.timestamps_ms.iter().map(|&ts| ts as f64).collect()
127 }
128
129 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 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 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 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 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 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 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 let boundary_index = self.find_boundary_index(index);
207 self.ensure_context_for_index(boundary_index, index);
208
209 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 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 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 let mut compositions = Vec::new();
238
239 for comp_obj in &composition.composition_objects {
240 let obj = match context.objects.get(&comp_obj.object_id) {
242 Some(obj) => obj,
243 None => continue,
244 };
245
246 let _window = context.windows.get(&comp_obj.window_id);
248
249 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 pub fn last_render_issue(&self) -> String {
308 self.last_render_issue.clone().unwrap_or_default()
309 }
310
311 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 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
401struct RenderContext {
403 object_parts: HashMap<u16, Vec<ObjectDefinitionSegment>>,
405 objects: HashMap<u16, AssembledObject>,
407 palettes: HashMap<u8, PaletteDefinitionSegment>,
409 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#[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 pub fn get_rgba(&self) -> &[u8] {
486 &self.rgba
487 }
488}
489
490pub 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 pub fn composition_count(&self) -> usize {
507 self.compositions.len()
508 }
509
510 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}