1#![allow(
2 dead_code,
3 clippy::missing_safety_doc,
4 clippy::vec_box,
5 non_camel_case_types,
6 non_snake_case,
7 unsafe_op_in_unsafe_fn
8)]
9
10use std::{
11 ffi::{CStr, CString, c_char, c_double, c_int, c_void},
12 fs, mem, ptr, slice,
13};
14
15#[cfg(not(target_arch = "wasm32"))]
16use libc::{free, malloc};
17use rassa_core::{ImagePlane, Margins, Point, RendererConfig, RgbaColor, Size, ass};
18use rassa_fonts::{
19 AttachedFontProvider, DefaultFontFileProvider, FontAttachment as ProviderFontAttachment,
20 FontProvider, FontconfigProvider, MergedFontProvider, NullFontProvider,
21};
22use rassa_parse::{
23 ParsedAttachment, ParsedEvent, ParsedStyle, ParsedTrack, parse_script_bytes,
24 parse_script_bytes_with_codepage,
25};
26use rassa_render::RenderEngine;
27
28pub struct ASS_Library {
29 fonts_dir: Option<String>,
30 extract_fonts: bool,
31 style_overrides: Vec<String>,
32 message_cb: *mut c_void,
33 message_data: *mut c_void,
34 fonts: Vec<FontAttachment>,
35}
36
37pub struct ASS_Renderer {
38 frame_width: c_int,
39 frame_height: c_int,
40 storage_width: c_int,
41 storage_height: c_int,
42 margins: [c_int; 4],
43 use_margins: bool,
44 pixel_aspect: c_double,
45 shaping: c_int,
46 font_scale: c_double,
47 hinting: c_int,
48 line_spacing: c_double,
49 line_position: c_double,
50 default_font: Option<String>,
51 default_family: Option<String>,
52 default_provider: c_int,
53 fontconfig_config: Option<String>,
54 fontconfig_update: bool,
55 selective_override_bits: c_int,
56 selective_override_style: Option<OwnedStyleOverride>,
57 cache_limits: (c_int, c_int),
58 font_provider_cache: Option<CachedFontProvider>,
59 frame_cache_signature: Option<RenderedFrameCacheSignature>,
60 last_timestamp: Option<i64>,
61 last_active_count: usize,
62 rendered_images: Option<OwnedImageList>,
63}
64
65#[repr(C)]
66pub struct ASS_RenderPriv {
67 _private: [u8; 0],
68}
69
70#[repr(C)]
71pub struct ASS_ParserPriv {
72 _private: [u8; 0],
73}
74
75#[repr(C)]
76pub struct ASS_Style {
77 pub Name: *mut c_char,
78 pub FontName: *mut c_char,
79 pub FontSize: c_double,
80 pub PrimaryColour: u32,
81 pub SecondaryColour: u32,
82 pub OutlineColour: u32,
83 pub BackColour: u32,
84 pub Bold: c_int,
85 pub Italic: c_int,
86 pub Underline: c_int,
87 pub StrikeOut: c_int,
88 pub ScaleX: c_double,
89 pub ScaleY: c_double,
90 pub Spacing: c_double,
91 pub Angle: c_double,
92 pub BorderStyle: c_int,
93 pub Outline: c_double,
94 pub Shadow: c_double,
95 pub Alignment: c_int,
96 pub MarginL: c_int,
97 pub MarginR: c_int,
98 pub MarginV: c_int,
99 pub Encoding: c_int,
100 pub treat_fontname_as_pattern: c_int,
101 pub Blur: c_double,
102 pub Justify: c_int,
103}
104
105#[repr(C)]
106pub struct ASS_Event {
107 pub Start: i64,
108 pub Duration: i64,
109 pub ReadOrder: c_int,
110 pub Layer: c_int,
111 pub Style: c_int,
112 pub Name: *mut c_char,
113 pub MarginL: c_int,
114 pub MarginR: c_int,
115 pub MarginV: c_int,
116 pub Effect: *mut c_char,
117 pub Text: *mut c_char,
118 pub render_priv: *mut ASS_RenderPriv,
119}
120
121#[repr(C)]
122pub struct ASS_Image {
123 pub w: c_int,
124 pub h: c_int,
125 pub stride: c_int,
126 pub bitmap: *mut u8,
127 pub color: u32,
128 pub dst_x: c_int,
129 pub dst_y: c_int,
130 pub next: *mut ASS_Image,
131 pub type_: c_int,
132}
133
134#[repr(C)]
135pub struct ASS_Track {
136 pub n_styles: c_int,
137 pub max_styles: c_int,
138 pub n_events: c_int,
139 pub max_events: c_int,
140 pub styles: *mut ASS_Style,
141 pub events: *mut ASS_Event,
142 pub style_format: *mut c_char,
143 pub event_format: *mut c_char,
144 pub track_type: c_int,
145 pub PlayResX: c_int,
146 pub PlayResY: c_int,
147 pub Timer: c_double,
148 pub WrapStyle: c_int,
149 pub ScaledBorderAndShadow: c_int,
150 pub Kerning: c_int,
151 pub Language: *mut c_char,
152 pub YCbCrMatrix: c_int,
153 pub default_style: c_int,
154 pub name: *mut c_char,
155 pub library: *mut ASS_Library,
156 pub parser_priv: *mut ASS_ParserPriv,
157 pub LayoutResX: c_int,
158 pub LayoutResY: c_int,
159}
160
161impl Default for ASS_Style {
162 fn default() -> Self {
163 Self {
164 Name: ptr::null_mut(),
165 FontName: ptr::null_mut(),
166 FontSize: 20.0,
167 PrimaryColour: 0x0000_00ff,
168 SecondaryColour: 0x0000_ffff,
169 OutlineColour: 0,
170 BackColour: 0,
171 Bold: 0,
172 Italic: 0,
173 Underline: 0,
174 StrikeOut: 0,
175 ScaleX: 1.0,
176 ScaleY: 1.0,
177 Spacing: 0.0,
178 Angle: 0.0,
179 BorderStyle: 1,
180 Outline: 2.0,
181 Shadow: 2.0,
182 Alignment: ass::VALIGN_SUB | ass::HALIGN_CENTER,
183 MarginL: 10,
184 MarginR: 10,
185 MarginV: 10,
186 Encoding: 1,
187 treat_fontname_as_pattern: 0,
188 Blur: 0.0,
189 Justify: ass::ASS_JUSTIFY_AUTO,
190 }
191 }
192}
193
194impl Default for ASS_Event {
195 fn default() -> Self {
196 Self {
197 Start: 0,
198 Duration: 0,
199 ReadOrder: 0,
200 Layer: 0,
201 Style: 0,
202 Name: ptr::null_mut(),
203 MarginL: 0,
204 MarginR: 0,
205 MarginV: 0,
206 Effect: ptr::null_mut(),
207 Text: ptr::null_mut(),
208 render_priv: ptr::null_mut(),
209 }
210 }
211}
212
213#[derive(Default)]
214struct TrackState {
215 features: [bool; 4],
216 check_readorder: bool,
217 prune_delay: Option<i64>,
218 rendered: bool,
219 cache_generation: u64,
220 parsed_cache_signature: Option<ParsedTrackCacheSignature>,
221 parsed_cache: Option<ParsedTrack>,
222}
223
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225struct ParsedTrackCacheSignature {
226 n_styles: c_int,
227 styles: usize,
228 n_events: c_int,
229 events: usize,
230 style_format: usize,
231 event_format: usize,
232 track_type: c_int,
233 play_res_x: c_int,
234 play_res_y: c_int,
235 timer_bits: u64,
236 wrap_style: c_int,
237 scaled_border_and_shadow: c_int,
238 kerning: c_int,
239 language: usize,
240 ycbcr_matrix: c_int,
241 default_style: c_int,
242 layout_res_x: c_int,
243 layout_res_y: c_int,
244}
245
246#[derive(Clone, Debug, Default)]
247struct FontAttachment {
248 name: String,
249 data: Vec<u8>,
250}
251
252#[derive(Clone, Debug, Default, PartialEq)]
253struct OwnedStyleOverride {
254 style: ParsedStyle,
255}
256
257struct CachedFontProvider {
258 signature: FontProviderCacheSignature,
259 provider: Box<dyn FontProvider>,
260}
261
262#[derive(Clone, Debug, PartialEq, Eq)]
263struct FontProviderCacheSignature {
264 library: usize,
265 library_fonts_len: usize,
266 library_fonts_data: Vec<(usize, usize)>,
267 default_font: Option<String>,
268 default_family: Option<String>,
269 default_provider: c_int,
270 fontconfig_config: Option<String>,
271 fontconfig_update: bool,
272}
273
274#[derive(Clone, Debug, PartialEq)]
275struct RenderedFrameCacheSignature {
276 track: usize,
277 track_generation: u64,
278 parsed_track: ParsedTrackCacheSignature,
279 renderer_config: RendererConfig,
280 font_provider: FontProviderCacheSignature,
281 selective_override_bits: c_int,
282 selective_override_style: Option<OwnedStyleOverride>,
283 active_event_indices: Vec<usize>,
284 approximate_animation_bucket: i64,
285}
286
287const APPROXIMATE_ANIMATION_FRAME_BUCKET_MS: i64 = 500;
291const APPROXIMATE_HEAVY_ANIMATION_FRAME_BUCKET_MS: i64 = 1000;
292
293const APPROXIMATE_SQUASH_PLANE_THRESHOLD: usize = 96;
299const APPROXIMATE_MULTILINE_FAST_PATH_THRESHOLD: usize = 4;
300const APPROXIMATE_MASSIVE_ACTIVE_FAST_PATH_THRESHOLD: usize = 64;
301const APPROXIMATE_ADJACENT_LINE_CHANGE_WINDOW_MS: i64 = 150;
302
303#[derive(Default)]
304struct OwnedImageList {
305 bitmaps: Vec<Vec<u8>>,
306 nodes: Vec<Box<ASS_Image>>,
307}
308
309impl OwnedImageList {
310 fn from_planes(planes: Vec<rassa_core::ImagePlane>) -> Self {
311 let mut bitmaps = Vec::with_capacity(planes.len());
312 let mut nodes = Vec::with_capacity(planes.len());
313
314 for plane in planes {
315 bitmaps.push(plane.bitmap);
316 let bitmap = bitmaps.last_mut().expect("bitmap just pushed");
317 nodes.push(Box::new(ASS_Image {
318 w: plane.size.width,
319 h: plane.size.height,
320 stride: plane.stride,
321 bitmap: if bitmap.is_empty() {
322 ptr::null_mut()
323 } else {
324 bitmap.as_mut_ptr()
325 },
326 color: plane.color.0,
327 dst_x: plane.destination.x,
328 dst_y: plane.destination.y,
329 next: ptr::null_mut(),
330 type_: plane.kind as c_int,
331 }));
332 }
333
334 for index in 0..nodes.len() {
335 let next = nodes
336 .get_mut(index + 1)
337 .map(|node| &mut **node as *mut ASS_Image)
338 .unwrap_or(ptr::null_mut());
339 nodes[index].next = next;
340 }
341
342 Self { bitmaps, nodes }
343 }
344
345 fn head_ptr(&mut self) -> *mut ASS_Image {
346 self.nodes
347 .first_mut()
348 .map(|node| &mut **node as *mut ASS_Image)
349 .unwrap_or(ptr::null_mut())
350 }
351}
352
353#[derive(Clone)]
354struct SquashedPlaneGroup {
355 color: RgbaColor,
356 kind: ass::ImageType,
357 min_x: i32,
358 min_y: i32,
359 max_x: i32,
360 max_y: i32,
361 planes: Vec<ImagePlane>,
362}
363
364fn squash_dense_planes_approximately(planes: Vec<ImagePlane>) -> Vec<ImagePlane> {
365 if planes.len() < APPROXIMATE_SQUASH_PLANE_THRESHOLD {
366 return planes;
367 }
368
369 let mut groups: Vec<SquashedPlaneGroup> = Vec::new();
370 for plane in planes {
371 if plane.size.width <= 0
372 || plane.size.height <= 0
373 || plane.stride <= 0
374 || plane.bitmap.is_empty()
375 {
376 continue;
377 }
378
379 let min_x = plane.destination.x;
380 let min_y = plane.destination.y;
381 let max_x = min_x.saturating_add(plane.size.width);
382 let max_y = min_y.saturating_add(plane.size.height);
383 if let Some(group) = groups
384 .iter_mut()
385 .find(|group| group.color == plane.color && group.kind == plane.kind)
386 {
387 group.min_x = group.min_x.min(min_x);
388 group.min_y = group.min_y.min(min_y);
389 group.max_x = group.max_x.max(max_x);
390 group.max_y = group.max_y.max(max_y);
391 group.planes.push(plane);
392 } else {
393 groups.push(SquashedPlaneGroup {
394 color: plane.color,
395 kind: plane.kind,
396 min_x,
397 min_y,
398 max_x,
399 max_y,
400 planes: vec![plane],
401 });
402 }
403 }
404
405 groups
406 .into_iter()
407 .filter_map(squash_plane_group_approximately)
408 .collect()
409}
410
411fn squash_plane_group_approximately(group: SquashedPlaneGroup) -> Option<ImagePlane> {
412 let width = group.max_x.checked_sub(group.min_x)?;
413 let height = group.max_y.checked_sub(group.min_y)?;
414 if width <= 0 || height <= 0 {
415 return None;
416 }
417
418 let width_usize = usize::try_from(width).ok()?;
419 let height_usize = usize::try_from(height).ok()?;
420 let len = width_usize.checked_mul(height_usize)?;
421 let mut bitmap = vec![0_u8; len];
422
423 for plane in group.planes {
424 let plane_width = usize::try_from(plane.size.width).ok()?;
425 let plane_height = usize::try_from(plane.size.height).ok()?;
426 let stride = usize::try_from(plane.stride).ok()?;
427 let dx = usize::try_from(plane.destination.x.checked_sub(group.min_x)?).ok()?;
428 let dy = usize::try_from(plane.destination.y.checked_sub(group.min_y)?).ok()?;
429
430 for row in 0..plane_height {
431 let src_row = row.checked_mul(stride)?;
432 let dst_row = dy.checked_add(row)?.checked_mul(width_usize)?;
433 for column in 0..plane_width {
434 let src = *plane.bitmap.get(src_row.checked_add(column)?)?;
435 let dst_index = dst_row.checked_add(dx)?.checked_add(column)?;
436 let dst = bitmap.get_mut(dst_index)?;
437 *dst = (*dst).max(src);
438 }
439 }
440 }
441
442 Some(ImagePlane {
443 size: Size { width, height },
444 stride: width,
445 color: group.color,
446 destination: Point {
447 x: group.min_x,
448 y: group.min_y,
449 },
450 kind: group.kind,
451 bitmap,
452 })
453}
454
455fn render_frame_planes(
456 parsed: &ParsedTrack,
457 renderer: &mut ASS_Renderer,
458 library: *mut ASS_Library,
459 now: i64,
460 renderer_config: &RendererConfig,
461) -> Vec<ImagePlane> {
462 let provider = cached_font_provider(renderer, library);
463 let provider: &dyn FontProvider = unsafe { &*provider };
464 let planes = RenderEngine::new().render_frame_with_provider_and_config(
465 parsed,
466 &provider,
467 now,
468 renderer_config,
469 );
470 squash_dense_planes_approximately(planes)
471}
472
473fn approximate_massive_active_event_planes(
474 track: &ParsedTrack,
475 active_event_indices: &[usize],
476 renderer_config: &RendererConfig,
477) -> Option<Vec<ImagePlane>> {
478 if active_event_indices.len() < APPROXIMATE_MASSIVE_ACTIVE_FAST_PATH_THRESHOLD {
479 return None;
480 }
481
482 let frame_width = renderer_config.frame.width.max(1);
483 let frame_height = renderer_config.frame.height.max(1);
484 let width_usize = usize::try_from(frame_width).ok()?;
485 let height_usize = usize::try_from(frame_height).ok()?;
486 let mut bitmap = vec![0_u8; width_usize.checked_mul(height_usize)?];
487 let mut color = None;
488 let scale_y =
489 renderer_config.frame.height as f64 / renderer_config.storage.height.max(1) as f64;
490
491 for (fallback_row, index) in active_event_indices.iter().enumerate() {
492 let event = track.events.get(*index)?;
493 let style = track
494 .styles
495 .get(event.style.max(0) as usize)
496 .or_else(|| track.styles.first())?;
497 color.get_or_insert(RgbaColor(ass_color_to_rgba(style.primary_colour)));
498
499 let visible = strip_ass_override_tags(&event.text)
500 .replace("\\N", "\n")
501 .replace("\\n", "\n");
502 let visible_lines: Vec<&str> = visible
503 .lines()
504 .filter(|line| !line.trim().is_empty())
505 .collect();
506 let row_count = visible_lines.len().max(1);
507 let font_height = (style.font_size * scale_y).clamp(6.0, 96.0);
508 let line_height = (font_height * 1.10).round().max(6.0) as i32;
509 let longest_chars = visible_lines
510 .iter()
511 .map(|line| line.chars().filter(|ch| !ch.is_control()).count())
512 .max()
513 .unwrap_or(6)
514 .max(6);
515 let width = ((longest_chars as f64 * font_height * 0.42)
516 .round()
517 .max(font_height * 1.4)
518 .min(frame_width as f64)) as i32;
519 let height = (row_count as i32 * line_height).max(line_height);
520 let (anchor_x, anchor_y) =
521 approximate_event_anchor(event, style, renderer_config, fallback_row);
522 let (x, y) = align_approximate_text_box(anchor_x, anchor_y, width, height, style.alignment);
523 paint_rect_onto_bitmap(
524 &mut bitmap,
525 Size {
526 width: frame_width,
527 height: frame_height,
528 },
529 Point { x, y },
530 Size { width, height },
531 185,
532 );
533 }
534
535 Some(vec![ImagePlane {
536 size: Size {
537 width: frame_width,
538 height: frame_height,
539 },
540 stride: frame_width,
541 color: color.unwrap_or(RgbaColor(ass_color_to_rgba(0x00ff_ffff))),
542 destination: Point { x: 0, y: 0 },
543 kind: ass::ImageType::Character,
544 bitmap,
545 }])
546}
547
548fn paint_rect_onto_bitmap(
549 bitmap: &mut [u8],
550 frame_size: Size,
551 destination: Point,
552 rect_size: Size,
553 alpha: u8,
554) {
555 let min_x = destination.x.clamp(0, frame_size.width);
556 let min_y = destination.y.clamp(0, frame_size.height);
557 let max_x = destination
558 .x
559 .saturating_add(rect_size.width)
560 .clamp(0, frame_size.width);
561 let max_y = destination
562 .y
563 .saturating_add(rect_size.height)
564 .clamp(0, frame_size.height);
565 if min_x >= max_x || min_y >= max_y {
566 return;
567 }
568 let Ok(stride) = usize::try_from(frame_size.width) else {
569 return;
570 };
571 let Ok(min_x) = usize::try_from(min_x) else {
572 return;
573 };
574 let Ok(max_x) = usize::try_from(max_x) else {
575 return;
576 };
577 for row in min_y..max_y {
578 let Ok(row) = usize::try_from(row) else {
579 continue;
580 };
581 let Some(start) = row
582 .checked_mul(stride)
583 .and_then(|base| base.checked_add(min_x))
584 else {
585 continue;
586 };
587 let Some(end) = row
588 .checked_mul(stride)
589 .and_then(|base| base.checked_add(max_x))
590 else {
591 continue;
592 };
593 if let Some(slice) = bitmap.get_mut(start..end) {
594 for value in slice {
595 *value = (*value).max(alpha);
596 }
597 }
598 }
599}
600
601fn should_use_approximate_multiline_fast_path(
602 track: &ParsedTrack,
603 active_event_indices: &[usize],
604 now: i64,
605) -> bool {
606 active_event_indices.len() >= APPROXIMATE_MULTILINE_FAST_PATH_THRESHOLD
607 || active_events_have_adjacent_line_change(track, active_event_indices, now)
608}
609
610fn active_events_have_adjacent_line_change(
611 track: &ParsedTrack,
612 active_event_indices: &[usize],
613 now: i64,
614) -> bool {
615 active_event_indices.iter().any(|index| {
616 let Some(event) = track.events.get(*index) else {
617 return false;
618 };
619 let near_own_boundary = (now - event.start).abs()
620 <= APPROXIMATE_ADJACENT_LINE_CHANGE_WINDOW_MS
621 || (now - (event.start + event.duration)).abs()
622 <= APPROXIMATE_ADJACENT_LINE_CHANGE_WINDOW_MS;
623 if !near_own_boundary {
624 return false;
625 }
626 let event_end = event.start + event.duration;
627 track.events.iter().enumerate().any(|(other_index, other)| {
628 other_index != *index
629 && ((other.start - event_end).abs() <= APPROXIMATE_ADJACENT_LINE_CHANGE_WINDOW_MS
630 || ((other.start + other.duration) - event.start).abs()
631 <= APPROXIMATE_ADJACENT_LINE_CHANGE_WINDOW_MS)
632 })
633 })
634}
635
636fn approximate_multiline_text_planes(
637 track: &ParsedTrack,
638 active_event_indices: &[usize],
639 renderer_config: &RendererConfig,
640) -> Option<Vec<ImagePlane>> {
641 let mut planes = Vec::with_capacity(active_event_indices.len());
642 for (fallback_row, index) in active_event_indices.iter().enumerate() {
643 let event = track.events.get(*index)?;
644 if !event.effect.trim().is_empty() || event_text_contains_vector_or_drawing(&event.text) {
645 return None;
646 }
647 let style = track
648 .styles
649 .get(event.style.max(0) as usize)
650 .or_else(|| track.styles.first())?;
651 let visible = strip_ass_override_tags(&event.text)
652 .replace("\\N", "\n")
653 .replace("\\n", "\n");
654 let visible_lines: Vec<&str> = visible
655 .lines()
656 .filter(|line| !line.trim().is_empty())
657 .collect();
658 let row_count = visible_lines.len().max(1);
659 let scale_x =
660 renderer_config.frame.width as f64 / renderer_config.storage.width.max(1) as f64;
661 let scale_y =
662 renderer_config.frame.height as f64 / renderer_config.storage.height.max(1) as f64;
663 let font_height = (style.font_size * scale_y).clamp(8.0, 160.0);
664 let line_height = (font_height * 1.18).round().max(8.0) as i32;
665 let longest_chars = visible_lines
666 .iter()
667 .map(|line| line.chars().filter(|ch| !ch.is_control()).count())
668 .max()
669 .unwrap_or_else(|| visible.chars().count())
670 .max(1);
671 let width = ((longest_chars as f64 * font_height * 0.56 * scale_x)
672 .round()
673 .max(font_height)
674 .min(renderer_config.frame.width as f64)) as i32;
675 let height = (row_count as i32 * line_height).max(line_height);
676 let (anchor_x, anchor_y) =
677 approximate_event_anchor(event, style, renderer_config, fallback_row);
678 let (x, y) = align_approximate_text_box(anchor_x, anchor_y, width, height, style.alignment);
679 planes.push(make_filled_plane(
680 x.clamp(-width, renderer_config.frame.width),
681 y.clamp(-height, renderer_config.frame.height),
682 width,
683 height,
684 RgbaColor(ass_color_to_rgba(style.primary_colour)),
685 ass::ImageType::Character,
686 210,
687 ));
688 }
689 (!planes.is_empty()).then_some(planes)
690}
691
692fn make_filled_plane(
693 x: i32,
694 y: i32,
695 width: i32,
696 height: i32,
697 color: RgbaColor,
698 kind: ass::ImageType,
699 alpha: u8,
700) -> ImagePlane {
701 let width = width.max(1);
702 let height = height.max(1);
703 let len = (width as usize).saturating_mul(height as usize);
704 ImagePlane {
705 size: Size { width, height },
706 stride: width,
707 color,
708 destination: Point { x, y },
709 kind,
710 bitmap: vec![alpha; len],
711 }
712}
713
714fn approximate_event_anchor(
715 event: &ParsedEvent,
716 style: &ParsedStyle,
717 renderer_config: &RendererConfig,
718 fallback_row: usize,
719) -> (i32, i32) {
720 if let Some((x, y)) = parse_pos_override(&event.text) {
721 let scale_x =
722 renderer_config.frame.width as f64 / renderer_config.storage.width.max(1) as f64;
723 let scale_y =
724 renderer_config.frame.height as f64 / renderer_config.storage.height.max(1) as f64;
725 return ((x * scale_x).round() as i32, (y * scale_y).round() as i32);
726 }
727
728 let x = renderer_config.frame.width / 2;
729 let margin_v = event.margin_v.max(style.margin_v).max(0);
730 let y = renderer_config
731 .frame
732 .height
733 .saturating_sub(margin_v)
734 .saturating_sub((fallback_row as i32) * (style.font_size.round() as i32 + 8));
735 (x, y)
736}
737
738fn align_approximate_text_box(
739 anchor_x: i32,
740 anchor_y: i32,
741 width: i32,
742 height: i32,
743 alignment: i32,
744) -> (i32, i32) {
745 let halign = alignment & 0x03;
746 let valign = alignment & 0x0c;
747 let x = match halign {
748 ass::HALIGN_LEFT => anchor_x,
749 ass::HALIGN_RIGHT => anchor_x - width,
750 _ => anchor_x - width / 2,
751 };
752 let y = match valign {
753 ass::VALIGN_TOP => anchor_y,
754 ass::VALIGN_CENTER => anchor_y - height / 2,
755 _ => anchor_y - height,
756 };
757 (x, y)
758}
759
760fn strip_ass_override_tags(text: &str) -> String {
761 let mut output = String::with_capacity(text.len());
762 let mut in_tag = false;
763 for ch in text.chars() {
764 match ch {
765 '{' => in_tag = true,
766 '}' => in_tag = false,
767 _ if !in_tag => output.push(ch),
768 _ => {}
769 }
770 }
771 output
772}
773
774fn parse_pos_override(text: &str) -> Option<(f64, f64)> {
775 let lower = text.to_ascii_lowercase();
776 let start = lower.find("\\pos(")? + 5;
777 let end = lower[start..].find(')')? + start;
778 parse_two_numbers(&text[start..end])
779}
780
781fn parse_two_numbers(value: &str) -> Option<(f64, f64)> {
782 let mut parts = value.split(',').map(str::trim);
783 let x = parts.next()?.parse().ok()?;
784 let y = parts.next()?.parse().ok()?;
785 Some((x, y))
786}
787
788fn event_text_contains_vector_or_drawing(text: &str) -> bool {
789 let text = text.to_ascii_lowercase();
790 text.contains("\\clip(")
791 || text.contains("\\iclip(")
792 || (0..=9).any(|value| text.contains(&format!("\\p{value}")))
793 || text.contains("\\p ")
794}
795
796fn ass_color_to_rgba(color: u32) -> u32 {
797 let alpha = (color >> 24) & 0xff;
798 let blue = (color >> 16) & 0xff;
799 let green = (color >> 8) & 0xff;
800 let red = color & 0xff;
801 (red << 24) | (green << 16) | (blue << 8) | alpha
802}
803
804impl OwnedStyleOverride {
805 unsafe fn from_ffi(style: *mut ASS_Style) -> Option<Self> {
806 let style = style.as_ref()?;
807 Some(Self {
808 style: ParsedStyle {
809 name: string_option_from_ptr(style.Name).unwrap_or_default(),
810 font_name: string_option_from_ptr(style.FontName).unwrap_or_default(),
811 font_size: style.FontSize,
812 primary_colour: style.PrimaryColour,
813 secondary_colour: style.SecondaryColour,
814 outline_colour: style.OutlineColour,
815 back_colour: style.BackColour,
816 bold: ffi_bold_is_active(style.Bold),
817 font_weight: ffi_bold_weight(style.Bold),
818 italic: style.Italic != 0,
819 underline: style.Underline != 0,
820 strike_out: style.StrikeOut != 0,
821 scale_x: style.ScaleX,
822 scale_y: style.ScaleY,
823 spacing: style.Spacing,
824 angle: style.Angle,
825 border_style: style.BorderStyle,
826 outline: style.Outline,
827 shadow: style.Shadow,
828 alignment: style.Alignment,
829 margin_l: style.MarginL,
830 margin_r: style.MarginR,
831 margin_v: style.MarginV,
832 encoding: style.Encoding,
833 treat_fontname_as_pattern: style.treat_fontname_as_pattern,
834 blur: style.Blur,
835 justify: style.Justify,
836 },
837 })
838 }
839}
840
841impl Default for ASS_Library {
842 fn default() -> Self {
843 Self {
844 fonts_dir: None,
845 extract_fonts: false,
846 style_overrides: Vec::new(),
847 message_cb: ptr::null_mut(),
848 message_data: ptr::null_mut(),
849 fonts: Vec::new(),
850 }
851 }
852}
853
854impl Default for ASS_Renderer {
855 fn default() -> Self {
856 Self {
857 frame_width: 0,
858 frame_height: 0,
859 storage_width: 0,
860 storage_height: 0,
861 margins: [0; 4],
862 use_margins: false,
863 pixel_aspect: 0.0,
864 shaping: ass::ShapingLevel::Complex as c_int,
865 font_scale: 1.0,
866 hinting: ass::Hinting::None as c_int,
867 line_spacing: 0.0,
868 line_position: 0.0,
869 default_font: None,
870 default_family: None,
871 default_provider: ass::DefaultFontProvider::Autodetect as c_int,
872 fontconfig_config: None,
873 fontconfig_update: true,
874 selective_override_bits: 0,
875 selective_override_style: None,
876 cache_limits: (0, 0),
877 font_provider_cache: None,
878 frame_cache_signature: None,
879 last_timestamp: None,
880 last_active_count: 0,
881 rendered_images: None,
882 }
883 }
884}
885
886#[unsafe(no_mangle)]
887pub unsafe extern "C" fn ass_library_version() -> c_int {
888 ass::LIBASS_VERSION
889}
890
891#[unsafe(no_mangle)]
892pub unsafe extern "C" fn ass_library_init() -> *mut ASS_Library {
893 Box::into_raw(Box::new(ASS_Library::default()))
894}
895
896#[unsafe(no_mangle)]
897pub unsafe extern "C" fn ass_library_done(priv_: *mut ASS_Library) {
898 if !priv_.is_null() {
899 drop(Box::from_raw(priv_));
900 }
901}
902
903#[unsafe(no_mangle)]
904pub unsafe extern "C" fn ass_set_fonts_dir(priv_: *mut ASS_Library, fonts_dir: *const c_char) {
905 if let Some(library) = priv_.as_mut() {
906 library.fonts_dir = string_option_from_ptr(fonts_dir);
907 }
908}
909
910#[unsafe(no_mangle)]
911pub unsafe extern "C" fn ass_set_extract_fonts(priv_: *mut ASS_Library, extract: c_int) {
912 if let Some(library) = priv_.as_mut() {
913 library.extract_fonts = extract != 0;
914 }
915}
916
917#[unsafe(no_mangle)]
918pub unsafe extern "C" fn ass_set_style_overrides(priv_: *mut ASS_Library, list: *mut *mut c_char) {
919 let Some(library) = priv_.as_mut() else {
920 return;
921 };
922
923 library.style_overrides.clear();
924 if list.is_null() {
925 return;
926 }
927
928 let mut index = 0;
929 loop {
930 let entry = *list.add(index);
931 if entry.is_null() {
932 break;
933 }
934 library.style_overrides.push(string_from_ptr(entry));
935 index += 1;
936 }
937}
938
939#[unsafe(no_mangle)]
940pub unsafe extern "C" fn ass_process_force_style(track: *mut ASS_Track) {
941 let Some(track_ref) = track.as_mut() else {
942 return;
943 };
944 let Some(library) = track_ref.library.as_ref() else {
945 return;
946 };
947
948 let overrides = library.style_overrides.clone();
949 for override_entry in overrides {
950 let Some((raw_key, raw_value)) = override_entry.rsplit_once('=') else {
951 continue;
952 };
953 let key = raw_key.trim();
954 let value = raw_value.trim();
955 if key.is_empty() {
956 continue;
957 }
958
959 if apply_track_override(track_ref, key, value) {
960 continue;
961 }
962
963 let (style_name, field_name) = match key.rsplit_once('.') {
964 Some((style_name, field_name)) if !style_name.trim().is_empty() => {
965 (Some(style_name.trim()), field_name.trim())
966 }
967 _ => (None, key),
968 };
969
970 if field_name.is_empty() || track_ref.styles.is_null() || track_ref.n_styles <= 0 {
971 continue;
972 }
973
974 for style in slice::from_raw_parts_mut(track_ref.styles, track_ref.n_styles as usize) {
975 let matches_style = style_name.is_none_or(|target| {
976 string_option_from_ptr(style.Name)
977 .is_some_and(|name| name.eq_ignore_ascii_case(target))
978 });
979 if matches_style {
980 apply_style_override(style, field_name, value);
981 }
982 }
983 }
984 invalidate_parsed_track_cache(track);
985}
986
987#[unsafe(no_mangle)]
988pub unsafe extern "C" fn ass_set_message_cb(
989 priv_: *mut ASS_Library,
990 msg_cb: *mut c_void,
991 data: *mut c_void,
992) {
993 if let Some(library) = priv_.as_mut() {
994 library.message_cb = msg_cb;
995 library.message_data = data;
996 }
997}
998
999#[unsafe(no_mangle)]
1000pub unsafe extern "C" fn ass_renderer_init(_library: *mut ASS_Library) -> *mut ASS_Renderer {
1001 Box::into_raw(Box::new(ASS_Renderer::default()))
1002}
1003
1004#[unsafe(no_mangle)]
1005pub unsafe extern "C" fn ass_renderer_done(priv_: *mut ASS_Renderer) {
1006 if !priv_.is_null() {
1007 drop(Box::from_raw(priv_));
1008 }
1009}
1010
1011#[unsafe(no_mangle)]
1012pub unsafe extern "C" fn ass_set_frame_size(priv_: *mut ASS_Renderer, w: c_int, h: c_int) {
1013 if let Some(renderer) = priv_.as_mut() {
1014 let (w, h) = sanitize_size_pair(w, h);
1015 renderer.frame_width = w;
1016 renderer.frame_height = h;
1017 }
1018}
1019
1020#[unsafe(no_mangle)]
1021pub unsafe extern "C" fn ass_set_storage_size(priv_: *mut ASS_Renderer, w: c_int, h: c_int) {
1022 if let Some(renderer) = priv_.as_mut() {
1023 let (w, h) = sanitize_size_pair(w, h);
1024 renderer.storage_width = w;
1025 renderer.storage_height = h;
1026 }
1027}
1028
1029#[unsafe(no_mangle)]
1030pub unsafe extern "C" fn ass_set_shaper(priv_: *mut ASS_Renderer, level: c_int) {
1031 if let Some(renderer) = priv_.as_mut() {
1032 renderer.shaping = if level == ass::ShapingLevel::Simple as c_int
1033 || level == ass::ShapingLevel::Complex as c_int
1034 {
1035 level
1036 } else {
1037 ass::ShapingLevel::Complex as c_int
1038 };
1039 }
1040}
1041
1042#[unsafe(no_mangle)]
1043pub unsafe extern "C" fn ass_set_margins(
1044 priv_: *mut ASS_Renderer,
1045 t: c_int,
1046 b: c_int,
1047 l: c_int,
1048 r: c_int,
1049) {
1050 if let Some(renderer) = priv_.as_mut() {
1051 renderer.margins = [t, b, l, r];
1052 }
1053}
1054
1055#[unsafe(no_mangle)]
1056pub unsafe extern "C" fn ass_set_use_margins(priv_: *mut ASS_Renderer, use_margins: c_int) {
1057 if let Some(renderer) = priv_.as_mut() {
1058 renderer.use_margins = use_margins != 0;
1059 }
1060}
1061
1062#[unsafe(no_mangle)]
1063pub unsafe extern "C" fn ass_set_pixel_aspect(priv_: *mut ASS_Renderer, par: c_double) {
1064 if let Some(renderer) = priv_.as_mut() {
1065 renderer.pixel_aspect = if par < 0.0 { 0.0 } else { par };
1066 }
1067}
1068
1069#[unsafe(no_mangle)]
1070pub unsafe extern "C" fn ass_set_aspect_ratio(
1071 priv_: *mut ASS_Renderer,
1072 dar: c_double,
1073 sar: c_double,
1074) {
1075 if sar == 0.0 {
1076 ass_set_pixel_aspect(priv_, 0.0);
1077 } else {
1078 ass_set_pixel_aspect(priv_, dar / sar);
1079 }
1080}
1081
1082#[unsafe(no_mangle)]
1083pub unsafe extern "C" fn ass_set_font_scale(priv_: *mut ASS_Renderer, font_scale: c_double) {
1084 if let Some(renderer) = priv_.as_mut() {
1085 renderer.font_scale = font_scale;
1086 }
1087}
1088
1089#[unsafe(no_mangle)]
1090pub unsafe extern "C" fn ass_set_hinting(priv_: *mut ASS_Renderer, hinting: c_int) {
1091 if let Some(renderer) = priv_.as_mut() {
1092 renderer.hinting = hinting;
1093 }
1094}
1095
1096#[unsafe(no_mangle)]
1097pub unsafe extern "C" fn ass_set_line_spacing(priv_: *mut ASS_Renderer, line_spacing: c_double) {
1098 if let Some(renderer) = priv_.as_mut() {
1099 renderer.line_spacing = line_spacing;
1100 }
1101}
1102
1103#[unsafe(no_mangle)]
1104pub unsafe extern "C" fn ass_set_line_position(priv_: *mut ASS_Renderer, line_position: c_double) {
1105 if let Some(renderer) = priv_.as_mut() {
1106 renderer.line_position = line_position;
1107 }
1108}
1109
1110#[unsafe(no_mangle)]
1111pub unsafe extern "C" fn ass_get_available_font_providers(
1112 _priv_: *mut ASS_Library,
1113 providers: *mut *mut c_int,
1114 size: *mut usize,
1115) {
1116 if providers.is_null() || size.is_null() {
1117 return;
1118 }
1119
1120 let values = [
1121 ass::DefaultFontProvider::None as c_int,
1122 ass::DefaultFontProvider::Autodetect as c_int,
1123 ass::DefaultFontProvider::Fontconfig as c_int,
1124 ];
1125 let allocation_size = mem::size_of_val(&values);
1126 let allocation = ass_malloc(allocation_size) as *mut c_int;
1127 if allocation.is_null() {
1128 *providers = ptr::null_mut();
1129 *size = usize::MAX;
1130 return;
1131 }
1132
1133 ptr::copy_nonoverlapping(values.as_ptr(), allocation, values.len());
1134 *providers = allocation;
1135 *size = values.len();
1136}
1137
1138#[unsafe(no_mangle)]
1139pub unsafe extern "C" fn ass_set_fonts(
1140 priv_: *mut ASS_Renderer,
1141 default_font: *const c_char,
1142 default_family: *const c_char,
1143 dfp: c_int,
1144 config: *const c_char,
1145 update: c_int,
1146) {
1147 if let Some(renderer) = priv_.as_mut() {
1148 renderer.default_font = string_option_from_ptr(default_font);
1149 renderer.default_family = string_option_from_ptr(default_family);
1150 renderer.default_provider = dfp;
1151 renderer.fontconfig_config = string_option_from_ptr(config);
1152 renderer.fontconfig_update = update != 0;
1153 }
1154}
1155
1156#[unsafe(no_mangle)]
1157pub unsafe extern "C" fn ass_set_selective_style_override_enabled(
1158 priv_: *mut ASS_Renderer,
1159 bits: c_int,
1160) {
1161 if let Some(renderer) = priv_.as_mut() {
1162 renderer.selective_override_bits = bits;
1163 }
1164}
1165
1166#[unsafe(no_mangle)]
1167pub unsafe extern "C" fn ass_set_selective_style_override(
1168 priv_: *mut ASS_Renderer,
1169 style: *mut ASS_Style,
1170) {
1171 if let Some(renderer) = priv_.as_mut() {
1172 renderer.selective_override_style = OwnedStyleOverride::from_ffi(style);
1173 }
1174}
1175
1176#[unsafe(no_mangle)]
1177pub unsafe extern "C" fn ass_fonts_update(_priv_: *mut ASS_Renderer) -> c_int {
1178 1
1179}
1180
1181#[unsafe(no_mangle)]
1182pub unsafe extern "C" fn ass_set_cache_limits(
1183 priv_: *mut ASS_Renderer,
1184 glyph_max: c_int,
1185 bitmap_max_size: c_int,
1186) {
1187 if let Some(renderer) = priv_.as_mut() {
1188 renderer.cache_limits = (glyph_max, bitmap_max_size);
1189 }
1190}
1191
1192#[unsafe(no_mangle)]
1193pub unsafe extern "C" fn ass_render_frame(
1194 priv_: *mut ASS_Renderer,
1195 track: *mut ASS_Track,
1196 now: i64,
1197 detect_change: *mut c_int,
1198) -> *mut ASS_Image {
1199 let Some(renderer) = priv_.as_mut() else {
1200 return ptr::null_mut();
1201 };
1202
1203 if let Some(state) = track_state_mut(track) {
1204 state.rendered = true;
1205 }
1206
1207 if let Some(delay) = track_state_mut(track).and_then(|state| state.prune_delay) {
1208 ass_prune_events(track, now - delay);
1209 }
1210
1211 let active_event_indices = active_event_indices(track, now);
1212 let active_count = active_event_indices.len();
1213 if let Some(detect_change) = detect_change.as_mut() {
1214 *detect_change =
1215 if renderer.last_timestamp == Some(now) && renderer.last_active_count == active_count {
1216 0
1217 } else if renderer.last_active_count == active_count {
1218 1
1219 } else {
1220 2
1221 };
1222 }
1223
1224 renderer.last_timestamp = Some(now);
1225 renderer.last_active_count = active_count;
1226
1227 let Some(track_ref) = track.as_ref() else {
1228 renderer.rendered_images = None;
1229 renderer.frame_cache_signature = None;
1230 return ptr::null_mut();
1231 };
1232
1233 let cached = cached_parsed_track_from_ffi(track, track_ref);
1234 let override_active = selective_style_overrides_active(renderer);
1235 let parsed_with_overrides;
1236 let parsed = if override_active {
1237 let mut parsed = cached.clone();
1238 apply_selective_style_overrides(&mut parsed, renderer);
1239 parsed_with_overrides = parsed;
1240 &parsed_with_overrides
1241 } else {
1242 cached
1243 };
1244 let renderer_config = renderer_config(renderer, parsed);
1245 let font_provider_signature = font_provider_cache_signature(renderer, track_ref.library);
1246 let track_generation = track_state_ref(track)
1247 .map(|state| state.cache_generation)
1248 .unwrap_or_default();
1249 let approximate_animation_bucket = frame_cache_time_bucket(parsed, &active_event_indices, now);
1250 let frame_cache_signature = approximate_animation_bucket.map(|approximate_animation_bucket| {
1251 RenderedFrameCacheSignature {
1252 track: track as usize,
1253 track_generation,
1254 parsed_track: parsed_track_cache_signature(track_ref),
1255 renderer_config: renderer_config.clone(),
1256 font_provider: font_provider_signature,
1257 selective_override_bits: renderer.selective_override_bits,
1258 selective_override_style: renderer.selective_override_style.clone(),
1259 active_event_indices: active_event_indices.clone(),
1260 approximate_animation_bucket,
1261 }
1262 });
1263 if frame_cache_signature.is_some()
1264 && renderer.frame_cache_signature == frame_cache_signature
1265 && renderer.rendered_images.is_some()
1266 {
1267 return renderer
1268 .rendered_images
1269 .as_mut()
1270 .map(OwnedImageList::head_ptr)
1271 .unwrap_or(ptr::null_mut());
1272 }
1273
1274 let planes =
1275 approximate_massive_active_event_planes(parsed, &active_event_indices, &renderer_config)
1276 .unwrap_or_else(|| {
1277 if should_use_approximate_multiline_fast_path(parsed, &active_event_indices, now) {
1278 approximate_multiline_text_planes(
1279 parsed,
1280 &active_event_indices,
1281 &renderer_config,
1282 )
1283 .unwrap_or_else(|| {
1284 render_frame_planes(
1285 parsed,
1286 renderer,
1287 track_ref.library,
1288 now,
1289 &renderer_config,
1290 )
1291 })
1292 } else {
1293 render_frame_planes(parsed, renderer, track_ref.library, now, &renderer_config)
1294 }
1295 });
1296 renderer.rendered_images = Some(OwnedImageList::from_planes(planes));
1297 renderer.frame_cache_signature = frame_cache_signature;
1298 renderer
1299 .rendered_images
1300 .as_mut()
1301 .map(OwnedImageList::head_ptr)
1302 .unwrap_or(ptr::null_mut())
1303}
1304
1305#[unsafe(no_mangle)]
1306pub unsafe extern "C" fn ass_new_track(library: *mut ASS_Library) -> *mut ASS_Track {
1307 let state = Box::new(TrackState {
1308 check_readorder: true,
1309 ..TrackState::default()
1310 });
1311 let parser_priv = Box::into_raw(state) as *mut ASS_ParserPriv;
1312 let track = ASS_Track {
1313 n_styles: 0,
1314 max_styles: 0,
1315 n_events: 0,
1316 max_events: 0,
1317 styles: ptr::null_mut(),
1318 events: ptr::null_mut(),
1319 style_format: ptr::null_mut(),
1320 event_format: ptr::null_mut(),
1321 track_type: ass::TrackType::Unknown as c_int,
1322 PlayResX: 384,
1323 PlayResY: 288,
1324 Timer: 100.0,
1325 WrapStyle: 0,
1326 ScaledBorderAndShadow: 1,
1327 Kerning: 1,
1328 Language: ptr::null_mut(),
1329 YCbCrMatrix: ass::YCbCrMatrix::Default as c_int,
1330 default_style: 0,
1331 name: ptr::null_mut(),
1332 library,
1333 parser_priv,
1334 LayoutResX: 0,
1335 LayoutResY: 0,
1336 };
1337
1338 Box::into_raw(Box::new(track))
1339}
1340
1341#[unsafe(no_mangle)]
1342pub unsafe extern "C" fn ass_track_set_feature(
1343 track: *mut ASS_Track,
1344 feature: c_int,
1345 enable: c_int,
1346) -> c_int {
1347 let Some(state) = track_state_mut(track) else {
1348 return -1;
1349 };
1350 if state.rendered {
1351 return -1;
1352 }
1353 let Some(slot) = state.features.get_mut(feature as usize) else {
1354 return -1;
1355 };
1356 *slot = enable != 0;
1357 0
1358}
1359
1360#[unsafe(no_mangle)]
1361pub unsafe extern "C" fn ass_free_track(track: *mut ASS_Track) {
1362 if track.is_null() {
1363 return;
1364 }
1365
1366 let mut boxed = Box::from_raw(track);
1367 free_track_contents(&mut boxed);
1368 if !boxed.parser_priv.is_null() {
1369 drop(Box::from_raw(boxed.parser_priv as *mut TrackState));
1370 boxed.parser_priv = ptr::null_mut();
1371 }
1372}
1373
1374#[unsafe(no_mangle)]
1375pub unsafe extern "C" fn ass_alloc_style(track: *mut ASS_Track) -> c_int {
1376 let Some(track_ref) = track.as_mut() else {
1377 return -1;
1378 };
1379 let mut styles = take_styles(track_ref);
1380 styles.push(ASS_Style::default());
1381 let id = (styles.len() - 1) as c_int;
1382 store_styles(track_ref, styles);
1383 id
1384}
1385
1386#[unsafe(no_mangle)]
1387pub unsafe extern "C" fn ass_alloc_event(track: *mut ASS_Track) -> c_int {
1388 let Some(track_ref) = track.as_mut() else {
1389 return -1;
1390 };
1391 let mut events = take_events(track_ref);
1392 let event = ASS_Event {
1393 ReadOrder: events.len() as c_int,
1394 ..ASS_Event::default()
1395 };
1396 events.push(event);
1397 let id = (events.len() - 1) as c_int;
1398 store_events(track_ref, events);
1399 id
1400}
1401
1402#[unsafe(no_mangle)]
1403pub unsafe extern "C" fn ass_free_style(track: *mut ASS_Track, sid: c_int) {
1404 let Some(track_ref) = track.as_mut() else {
1405 return;
1406 };
1407 let mut styles = take_styles(track_ref);
1408 if let Some(style) = styles.get_mut(sid as usize) {
1409 free_style(style);
1410 *style = ASS_Style::default();
1411 }
1412 store_styles(track_ref, styles);
1413}
1414
1415#[unsafe(no_mangle)]
1416pub unsafe extern "C" fn ass_free_event(track: *mut ASS_Track, eid: c_int) {
1417 let Some(track_ref) = track.as_mut() else {
1418 return;
1419 };
1420 let mut events = take_events(track_ref);
1421 if let Some(event) = events.get_mut(eid as usize) {
1422 free_event(event);
1423 *event = ASS_Event::default();
1424 }
1425 store_events(track_ref, events);
1426}
1427
1428#[unsafe(no_mangle)]
1429pub unsafe extern "C" fn ass_process_data(track: *mut ASS_Track, data: *const c_char, size: c_int) {
1430 if track.is_null() || data.is_null() || size < 0 {
1431 return;
1432 }
1433
1434 let bytes = slice::from_raw_parts(data as *const u8, size as usize);
1435 if let Ok(parsed) = parse_script_bytes(bytes) {
1436 maybe_extract_parsed_fonts(track, &parsed);
1437 replace_track_from_parsed(track, parsed);
1438 ass_process_force_style(track);
1439 }
1440}
1441
1442#[unsafe(no_mangle)]
1443pub unsafe extern "C" fn ass_process_codec_private(
1444 track: *mut ASS_Track,
1445 data: *const c_char,
1446 size: c_int,
1447) {
1448 ass_process_data(track, data, size);
1449}
1450
1451#[unsafe(no_mangle)]
1452pub unsafe extern "C" fn ass_process_chunk(
1453 track: *mut ASS_Track,
1454 data: *const c_char,
1455 size: c_int,
1456 timecode: i64,
1457 duration: i64,
1458) {
1459 let Some(track_ref) = track.as_mut() else {
1460 return;
1461 };
1462 if data.is_null() || size < 0 {
1463 return;
1464 }
1465
1466 let bytes = slice::from_raw_parts(data as *const u8, size as usize);
1467 let text = String::from_utf8_lossy(bytes).into_owned();
1468 let mut events = take_events(track_ref);
1469 events.push(make_event(&ParsedEvent {
1470 start: timecode,
1471 duration,
1472 read_order: if track_state_mut(track)
1473 .map(|state| state.check_readorder)
1474 .unwrap_or(true)
1475 {
1476 events.len() as c_int
1477 } else {
1478 0
1479 },
1480 layer: 0,
1481 style: 0,
1482 name: String::new(),
1483 margin_l: 0,
1484 margin_r: 0,
1485 margin_v: 0,
1486 effect: String::new(),
1487 text,
1488 }));
1489 store_events(track_ref, events);
1490}
1491
1492#[unsafe(no_mangle)]
1493pub unsafe extern "C" fn ass_set_check_readorder(track: *mut ASS_Track, check_readorder: c_int) {
1494 if let Some(state) = track_state_mut(track) {
1495 state.check_readorder = check_readorder == 1;
1496 }
1497}
1498
1499#[unsafe(no_mangle)]
1500pub unsafe extern "C" fn ass_prune_events(track: *mut ASS_Track, deadline: i64) {
1501 let Some(track_ref) = track.as_mut() else {
1502 return;
1503 };
1504
1505 let mut events = take_events(track_ref);
1506 events.retain_mut(|event| {
1507 let keep = event.Start + event.Duration >= deadline;
1508 if !keep {
1509 free_event(event);
1510 }
1511 keep
1512 });
1513 for (index, event) in events.iter_mut().enumerate() {
1514 event.ReadOrder = index as c_int;
1515 }
1516 store_events(track_ref, events);
1517}
1518
1519#[unsafe(no_mangle)]
1520pub unsafe extern "C" fn ass_configure_prune(track: *mut ASS_Track, delay: i64) {
1521 if let Some(state) = track_state_mut(track) {
1522 state.prune_delay = (delay >= 0).then_some(delay);
1523 }
1524}
1525
1526#[unsafe(no_mangle)]
1527pub unsafe extern "C" fn ass_flush_events(track: *mut ASS_Track) {
1528 let Some(track_ref) = track.as_mut() else {
1529 return;
1530 };
1531
1532 let mut events = take_events(track_ref);
1533 for event in &mut events {
1534 free_event(event);
1535 }
1536 store_events(track_ref, Vec::new());
1537}
1538
1539#[unsafe(no_mangle)]
1540pub unsafe extern "C" fn ass_read_file(
1541 library: *mut ASS_Library,
1542 fname: *const c_char,
1543 codepage: *const c_char,
1544) -> *mut ASS_Track {
1545 let Some(path) = string_option_from_ptr(fname) else {
1546 return ptr::null_mut();
1547 };
1548 let codepage = string_option_from_ptr(codepage);
1549 let Ok(bytes) = fs::read(path) else {
1550 return ptr::null_mut();
1551 };
1552 let Ok(parsed) = parse_script_bytes_with_codepage(&bytes, codepage.as_deref()) else {
1553 return ptr::null_mut();
1554 };
1555 maybe_extract_fonts_to_library(library, &parsed.attachments);
1556 let track = track_from_parsed(library, parsed);
1557 ass_process_force_style(track);
1558 track
1559}
1560
1561#[unsafe(no_mangle)]
1562pub unsafe extern "C" fn ass_read_memory(
1563 library: *mut ASS_Library,
1564 buf: *mut c_char,
1565 bufsize: usize,
1566 codepage: *const c_char,
1567) -> *mut ASS_Track {
1568 if buf.is_null() {
1569 return ptr::null_mut();
1570 }
1571
1572 let codepage = string_option_from_ptr(codepage);
1573 let bytes = slice::from_raw_parts(buf as *const u8, bufsize);
1574 let Ok(parsed) = parse_script_bytes_with_codepage(bytes, codepage.as_deref()) else {
1575 return ptr::null_mut();
1576 };
1577 maybe_extract_fonts_to_library(library, &parsed.attachments);
1578 let track = track_from_parsed(library, parsed);
1579 ass_process_force_style(track);
1580 track
1581}
1582
1583#[unsafe(no_mangle)]
1584pub unsafe extern "C" fn ass_read_styles(
1585 track: *mut ASS_Track,
1586 fname: *const c_char,
1587 codepage: *const c_char,
1588) -> c_int {
1589 let Some(path) = string_option_from_ptr(fname) else {
1590 return 1;
1591 };
1592 let codepage = string_option_from_ptr(codepage);
1593 let Ok(bytes) = fs::read(path) else {
1594 return 1;
1595 };
1596 let Ok(parsed) = parse_script_bytes_with_codepage(&bytes, codepage.as_deref()) else {
1597 return 1;
1598 };
1599 let Some(track_ref) = track.as_mut() else {
1600 return 1;
1601 };
1602
1603 if track_styles_match_parsed(track_ref, &parsed) {
1604 return 0;
1605 }
1606
1607 let mut styles = take_styles(track_ref);
1608 for mut style in styles.drain(..) {
1609 free_style(&mut style);
1610 }
1611 let new_styles = parsed.styles.iter().map(make_style).collect();
1612 store_styles(track_ref, new_styles);
1613 replace_string(&mut track_ref.style_format, &parsed.style_format);
1614 track_ref.track_type = parsed.track_type as c_int;
1615 0
1616}
1617
1618#[unsafe(no_mangle)]
1619pub unsafe extern "C" fn ass_add_font(
1620 library: *mut ASS_Library,
1621 name: *const c_char,
1622 data: *const c_char,
1623 data_size: c_int,
1624) {
1625 let Some(library) = library.as_mut() else {
1626 return;
1627 };
1628 if data.is_null() || data_size < 0 {
1629 return;
1630 }
1631
1632 library.fonts.push(FontAttachment {
1633 name: string_option_from_ptr(name).unwrap_or_default(),
1634 data: slice::from_raw_parts(data as *const u8, data_size as usize).to_vec(),
1635 });
1636}
1637
1638#[unsafe(no_mangle)]
1639pub unsafe extern "C" fn ass_clear_fonts(library: *mut ASS_Library) {
1640 if let Some(library) = library.as_mut() {
1641 library.fonts.clear();
1642 }
1643}
1644
1645fn font_provider_cache_signature(
1646 renderer: &ASS_Renderer,
1647 library: *mut ASS_Library,
1648) -> FontProviderCacheSignature {
1649 let library_ref = unsafe { library.as_ref() };
1650 let library_fonts_data = library_ref
1651 .map(|library| {
1652 library
1653 .fonts
1654 .iter()
1655 .map(|font| (font.data.as_ptr() as usize, font.data.len()))
1656 .collect()
1657 })
1658 .unwrap_or_default();
1659 FontProviderCacheSignature {
1660 library: library as usize,
1661 library_fonts_len: library_ref.map(|library| library.fonts.len()).unwrap_or(0),
1662 library_fonts_data,
1663 default_font: renderer.default_font.clone(),
1664 default_family: renderer.default_family.clone(),
1665 default_provider: renderer.default_provider,
1666 fontconfig_config: renderer.fontconfig_config.clone(),
1667 fontconfig_update: renderer.fontconfig_update,
1668 }
1669}
1670
1671fn cached_font_provider(
1672 renderer: &mut ASS_Renderer,
1673 library: *mut ASS_Library,
1674) -> *const dyn FontProvider {
1675 let signature = font_provider_cache_signature(renderer, library);
1676 if renderer
1677 .font_provider_cache
1678 .as_ref()
1679 .is_none_or(|cache| cache.signature != signature)
1680 {
1681 let provider = build_font_provider(renderer, library);
1682 renderer.font_provider_cache = Some(CachedFontProvider {
1683 signature: signature.clone(),
1684 provider,
1685 });
1686 }
1687 &*renderer
1688 .font_provider_cache
1689 .as_ref()
1690 .expect("font provider cached")
1691 .provider
1692}
1693
1694fn build_font_provider(
1695 renderer: &ASS_Renderer,
1696 library: *mut ASS_Library,
1697) -> Box<dyn FontProvider> {
1698 let has_system_provider = matches!(
1699 renderer.default_provider,
1700 value if value == ass::DefaultFontProvider::Autodetect as c_int
1701 || value == ass::DefaultFontProvider::Fontconfig as c_int
1702 );
1703 let system_provider: Box<dyn FontProvider> = match renderer.default_provider {
1704 _ if has_system_provider => {
1705 if let Some(fallback_family) = renderer.default_family.as_deref() {
1706 Box::new(FontconfigProvider::with_fallback_family(fallback_family))
1707 } else {
1708 Box::new(FontconfigProvider::new())
1709 }
1710 }
1711 _ => Box::new(NullFontProvider),
1712 };
1713
1714 let Some(library) = (unsafe { library.as_ref() }) else {
1715 return wrap_default_font_path(system_provider, renderer);
1716 };
1717 if library.fonts.is_empty() {
1718 return wrap_default_font_path(system_provider, renderer);
1719 }
1720
1721 let attachments = library
1722 .fonts
1723 .iter()
1724 .map(|font| ProviderFontAttachment {
1725 name: font.name.clone(),
1726 data: font.data.clone(),
1727 })
1728 .collect::<Vec<_>>();
1729 let attached = if let Some(fonts_dir) = library.fonts_dir.as_deref() {
1730 AttachedFontProvider::from_attachments_in_dir(&attachments, Some(fonts_dir))
1731 } else {
1732 AttachedFontProvider::from_attachments(&attachments)
1733 };
1734
1735 let provider: Box<dyn FontProvider> = if has_system_provider {
1736 Box::new(MergedFontProvider::new(attached, system_provider))
1737 } else {
1738 Box::new(attached)
1739 };
1740 wrap_default_font_path(provider, renderer)
1741}
1742
1743fn wrap_default_font_path(
1744 provider: Box<dyn FontProvider>,
1745 renderer: &ASS_Renderer,
1746) -> Box<dyn FontProvider> {
1747 let Some(default_font) = renderer.default_font.as_deref() else {
1748 return provider;
1749 };
1750
1751 let fallback = DefaultFontFileProvider::new(provider, default_font);
1752 if let Some(default_family) = renderer.default_family.as_deref() {
1753 Box::new(fallback.with_family(default_family))
1754 } else {
1755 Box::new(fallback)
1756 }
1757}
1758
1759fn renderer_config(renderer: &ASS_Renderer, track: &ParsedTrack) -> RendererConfig {
1760 RendererConfig {
1761 frame: Size {
1762 width: if renderer.frame_width > 0 {
1763 renderer.frame_width
1764 } else {
1765 track.play_res_x
1766 },
1767 height: if renderer.frame_height > 0 {
1768 renderer.frame_height
1769 } else {
1770 track.play_res_y
1771 },
1772 },
1773 storage: Size {
1774 width: renderer.storage_width,
1775 height: renderer.storage_height,
1776 },
1777 margins: Margins {
1778 top: renderer.margins[0],
1779 bottom: renderer.margins[1],
1780 left: renderer.margins[2],
1781 right: renderer.margins[3],
1782 },
1783 use_margins: renderer.use_margins,
1784 pixel_aspect: renderer.pixel_aspect,
1785 font_scale: renderer.font_scale,
1786 line_spacing: renderer.line_spacing,
1787 line_position: renderer.line_position,
1788 hinting: match renderer.hinting {
1789 value if value == ass::Hinting::Native as c_int => ass::Hinting::Native,
1790 value if value == ass::Hinting::Light as c_int => ass::Hinting::Light,
1791 value if value == ass::Hinting::Normal as c_int => ass::Hinting::Normal,
1792 _ => ass::Hinting::None,
1793 },
1794 shaping: match renderer.shaping {
1795 value if value == ass::ShapingLevel::Simple as c_int => ass::ShapingLevel::Simple,
1796 value if value == ass::ShapingLevel::Complex as c_int => ass::ShapingLevel::Complex,
1797 _ => ass::ShapingLevel::Complex,
1798 },
1799 }
1800}
1801
1802fn maybe_extract_parsed_fonts(track: *mut ASS_Track, parsed: &ParsedTrack) {
1803 let Some(track_ref) = (unsafe { track.as_ref() }) else {
1804 return;
1805 };
1806 maybe_extract_fonts_to_library(track_ref.library, &parsed.attachments);
1807}
1808
1809fn maybe_extract_fonts_to_library(library: *mut ASS_Library, attachments: &[ParsedAttachment]) {
1810 let Some(library) = (unsafe { library.as_mut() }) else {
1811 return;
1812 };
1813 if !library.extract_fonts || attachments.is_empty() {
1814 return;
1815 }
1816
1817 for attachment in attachments {
1818 library.fonts.push(FontAttachment {
1819 name: attachment.name.clone(),
1820 data: attachment.data.clone(),
1821 });
1822 }
1823}
1824
1825fn apply_track_override(track: &mut ASS_Track, key: &str, value: &str) -> bool {
1826 if key.eq_ignore_ascii_case("PlayResX") {
1827 track.PlayResX = parse_override_i32(value, track.PlayResX);
1828 } else if key.eq_ignore_ascii_case("PlayResY") {
1829 track.PlayResY = parse_override_i32(value, track.PlayResY);
1830 } else if key.eq_ignore_ascii_case("LayoutResX") {
1831 track.LayoutResX = parse_override_i32(value, track.LayoutResX);
1832 } else if key.eq_ignore_ascii_case("LayoutResY") {
1833 track.LayoutResY = parse_override_i32(value, track.LayoutResY);
1834 } else if key.eq_ignore_ascii_case("Timer") {
1835 track.Timer = parse_override_f64(value, track.Timer);
1836 } else if key.eq_ignore_ascii_case("WrapStyle") {
1837 track.WrapStyle = parse_override_i32(value, track.WrapStyle);
1838 } else if key.eq_ignore_ascii_case("ScaledBorderAndShadow") {
1839 track.ScaledBorderAndShadow =
1840 parse_override_bool(value, track.ScaledBorderAndShadow != 0) as c_int;
1841 } else if key.eq_ignore_ascii_case("Kerning") {
1842 track.Kerning = parse_override_bool(value, track.Kerning != 0) as c_int;
1843 } else {
1844 return false;
1845 }
1846
1847 true
1848}
1849
1850unsafe fn apply_style_override(style: &mut ASS_Style, field_name: &str, value: &str) {
1851 if field_name.eq_ignore_ascii_case("FontName") {
1852 replace_string(&mut style.FontName, value);
1853 } else if field_name.eq_ignore_ascii_case("PrimaryColour") {
1854 style.PrimaryColour = parse_override_color(value, style.PrimaryColour);
1855 } else if field_name.eq_ignore_ascii_case("SecondaryColour") {
1856 style.SecondaryColour = parse_override_color(value, style.SecondaryColour);
1857 } else if field_name.eq_ignore_ascii_case("OutlineColour") {
1858 style.OutlineColour = parse_override_color(value, style.OutlineColour);
1859 } else if field_name.eq_ignore_ascii_case("BackColour") {
1860 style.BackColour = parse_override_color(value, style.BackColour);
1861 } else if field_name.eq_ignore_ascii_case("FontSize") {
1862 style.FontSize = parse_override_f64(value, style.FontSize);
1863 } else if field_name.eq_ignore_ascii_case("Bold") {
1864 style.Bold = parse_override_bold(value, ffi_bold_is_active(style.Bold)) as c_int;
1865 } else if field_name.eq_ignore_ascii_case("Italic") {
1866 style.Italic = parse_override_bool(value, style.Italic != 0) as c_int;
1867 } else if field_name.eq_ignore_ascii_case("Underline") {
1868 style.Underline = parse_override_bool(value, style.Underline != 0) as c_int;
1869 } else if field_name.eq_ignore_ascii_case("StrikeOut") {
1870 style.StrikeOut = parse_override_bool(value, style.StrikeOut != 0) as c_int;
1871 } else if field_name.eq_ignore_ascii_case("Spacing") {
1872 style.Spacing = parse_override_f64(value, style.Spacing);
1873 } else if field_name.eq_ignore_ascii_case("Angle") {
1874 style.Angle = parse_override_f64(value, style.Angle);
1875 } else if field_name.eq_ignore_ascii_case("BorderStyle") {
1876 style.BorderStyle = parse_override_i32(value, style.BorderStyle);
1877 } else if field_name.eq_ignore_ascii_case("Alignment") {
1878 style.Alignment = parse_override_i32(value, style.Alignment);
1879 } else if field_name.eq_ignore_ascii_case("Justify") {
1880 style.Justify = parse_override_i32(value, style.Justify);
1881 } else if field_name.eq_ignore_ascii_case("MarginL") {
1882 style.MarginL = parse_override_i32(value, style.MarginL);
1883 } else if field_name.eq_ignore_ascii_case("MarginR") {
1884 style.MarginR = parse_override_i32(value, style.MarginR);
1885 } else if field_name.eq_ignore_ascii_case("MarginV") {
1886 style.MarginV = parse_override_i32(value, style.MarginV);
1887 } else if field_name.eq_ignore_ascii_case("Encoding") {
1888 style.Encoding = parse_override_i32(value, style.Encoding);
1889 } else if field_name.eq_ignore_ascii_case("ScaleX") {
1890 style.ScaleX = parse_override_f64(value, style.ScaleX);
1891 } else if field_name.eq_ignore_ascii_case("ScaleY") {
1892 style.ScaleY = parse_override_f64(value, style.ScaleY);
1893 } else if field_name.eq_ignore_ascii_case("Outline") {
1894 style.Outline = parse_override_f64(value, style.Outline);
1895 } else if field_name.eq_ignore_ascii_case("Shadow") {
1896 style.Shadow = parse_override_f64(value, style.Shadow);
1897 } else if field_name.eq_ignore_ascii_case("Blur") {
1898 style.Blur = parse_override_f64(value, style.Blur);
1899 }
1900}
1901
1902fn sanitize_size_pair(w: c_int, h: c_int) -> (c_int, c_int) {
1903 if w <= 0 || h <= 0 || i64::from(w) > i64::from(c_int::MAX) / i64::from(h) {
1904 (0, 0)
1905 } else {
1906 (w, h)
1907 }
1908}
1909
1910fn parse_override_i32(value: &str, default: i32) -> i32 {
1911 value.trim().parse::<i32>().unwrap_or(default)
1912}
1913
1914fn parse_override_f64(value: &str, default: f64) -> f64 {
1915 value.trim().parse::<f64>().unwrap_or(default)
1916}
1917
1918fn parse_override_bool(value: &str, default: bool) -> bool {
1919 if value.eq_ignore_ascii_case("yes") || value.eq_ignore_ascii_case("true") {
1920 true
1921 } else if value.eq_ignore_ascii_case("no") || value.eq_ignore_ascii_case("false") {
1922 false
1923 } else {
1924 value
1925 .trim()
1926 .parse::<i32>()
1927 .map(|parsed| parsed != 0)
1928 .unwrap_or(default)
1929 }
1930}
1931
1932fn ffi_bold_is_active(value: c_int) -> bool {
1933 value == 1 || !(0..700).contains(&value)
1934}
1935
1936fn ffi_bold_weight(value: c_int) -> i32 {
1937 match value {
1938 0 => 400,
1939 1 => 700,
1940 other => other,
1941 }
1942}
1943
1944fn parse_override_bold(value: &str, default: bool) -> bool {
1945 if value.eq_ignore_ascii_case("yes") || value.eq_ignore_ascii_case("true") {
1946 true
1947 } else if value.eq_ignore_ascii_case("no") || value.eq_ignore_ascii_case("false") {
1948 false
1949 } else {
1950 value
1951 .trim()
1952 .parse::<c_int>()
1953 .map(ffi_bold_is_active)
1954 .unwrap_or(default)
1955 }
1956}
1957
1958fn parse_override_color(value: &str, default: u32) -> u32 {
1959 let trimmed = value.trim();
1960 let normalized = trimmed
1961 .strip_prefix("&H")
1962 .or_else(|| trimmed.strip_prefix("&h"))
1963 .unwrap_or(trimmed)
1964 .trim_end_matches('&');
1965
1966 u32::from_str_radix(normalized, 16)
1967 .or_else(|_| trimmed.parse::<u32>())
1968 .unwrap_or(default)
1969}
1970
1971#[unsafe(no_mangle)]
1972pub unsafe extern "C" fn ass_step_sub(track: *mut ASS_Track, now: i64, movement: c_int) -> i64 {
1973 let Some(track_ref) = track.as_ref() else {
1974 return 0;
1975 };
1976 if track_ref.events.is_null() || track_ref.n_events <= 0 {
1977 return 0;
1978 }
1979
1980 let events = slice::from_raw_parts(track_ref.events, track_ref.n_events as usize);
1981 let direction = movement.signum();
1982 let mut remaining = movement;
1983 let mut target = now;
1984 let mut best_start = None;
1985
1986 loop {
1987 let mut closest = None;
1988 let mut closest_time = now;
1989 for event in events {
1990 if direction < 0 {
1991 let end = event.Start.saturating_add(event.Duration);
1992 if end < target && closest.is_none_or(|_| end > closest_time) {
1993 closest = Some(event.Start);
1994 closest_time = end;
1995 }
1996 } else if direction > 0 {
1997 let start = event.Start;
1998 if start > target && closest.is_none_or(|_| start < closest_time) {
1999 closest = Some(start);
2000 closest_time = start;
2001 }
2002 } else {
2003 let start = event.Start;
2004 if start < target && closest.is_none_or(|_| start >= closest_time) {
2005 closest = Some(start);
2006 closest_time = start;
2007 }
2008 }
2009 }
2010
2011 target = closest_time + i64::from(direction);
2012 remaining -= direction;
2013 if let Some(start) = closest {
2014 best_start = Some(start);
2015 }
2016 if remaining == 0 {
2017 break;
2018 }
2019 }
2020
2021 best_start.map_or(0, |start| start - now)
2022}
2023
2024#[unsafe(no_mangle)]
2025pub unsafe extern "C" fn ass_malloc(size: usize) -> *mut c_void {
2026 #[cfg(not(target_arch = "wasm32"))]
2027 {
2028 malloc(size)
2029 }
2030
2031 #[cfg(target_arch = "wasm32")]
2032 {
2033 let mut bytes = Vec::<u8>::with_capacity(size);
2034 let ptr = bytes.as_mut_ptr();
2035 std::mem::forget(bytes);
2036 ptr.cast()
2037 }
2038}
2039
2040#[unsafe(no_mangle)]
2041pub unsafe extern "C" fn ass_free(ptr: *mut c_void) {
2042 if ptr.is_null() {
2043 return;
2044 }
2045
2046 #[cfg(not(target_arch = "wasm32"))]
2047 {
2048 free(ptr);
2049 }
2050
2051 #[cfg(target_arch = "wasm32")]
2052 {
2053 let _ = ptr;
2054 }
2055}
2056
2057unsafe fn track_from_parsed(library: *mut ASS_Library, parsed: ParsedTrack) -> *mut ASS_Track {
2058 let track = ass_new_track(library);
2059 replace_track_from_parsed(track, parsed);
2060 track
2061}
2062
2063unsafe fn replace_track_from_parsed(track: *mut ASS_Track, parsed: ParsedTrack) {
2064 let Some(track_ref) = track.as_mut() else {
2065 return;
2066 };
2067
2068 ass_process_force_style(track);
2069 let library = track_ref.library;
2070 let parser_priv = track_ref.parser_priv;
2071 free_track_contents(track_ref);
2072 *track_ref = build_track(parsed, library, parser_priv);
2073}
2074
2075unsafe fn build_track(
2076 parsed: ParsedTrack,
2077 library: *mut ASS_Library,
2078 parser_priv: *mut ASS_ParserPriv,
2079) -> ASS_Track {
2080 let mut styles = parsed.styles.iter().map(make_style).collect::<Vec<_>>();
2081 let mut events = parsed.events.iter().map(make_event).collect::<Vec<_>>();
2082
2083 let track = ASS_Track {
2084 n_styles: styles.len() as c_int,
2085 max_styles: styles.capacity() as c_int,
2086 n_events: events.len() as c_int,
2087 max_events: events.capacity() as c_int,
2088 styles: styles.as_mut_ptr(),
2089 events: events.as_mut_ptr(),
2090 style_format: string_to_c_ptr(&parsed.style_format),
2091 event_format: string_to_c_ptr(&parsed.event_format),
2092 track_type: parsed.track_type as c_int,
2093 PlayResX: parsed.play_res_x,
2094 PlayResY: parsed.play_res_y,
2095 Timer: parsed.timer,
2096 WrapStyle: parsed.wrap_style,
2097 ScaledBorderAndShadow: parsed.scaled_border_and_shadow as c_int,
2098 Kerning: parsed.kerning as c_int,
2099 Language: string_to_c_ptr(&parsed.language),
2100 YCbCrMatrix: parsed.ycbcr_matrix as c_int,
2101 default_style: parsed.default_style,
2102 name: ptr::null_mut(),
2103 library,
2104 parser_priv,
2105 LayoutResX: parsed.layout_res_x,
2106 LayoutResY: parsed.layout_res_y,
2107 };
2108
2109 mem::forget(styles);
2110 mem::forget(events);
2111 track
2112}
2113
2114unsafe fn free_track_contents(track: &mut ASS_Track) {
2115 for mut style in take_styles(track) {
2116 free_style(&mut style);
2117 }
2118 for mut event in take_events(track) {
2119 free_event(&mut event);
2120 }
2121 free_c_string(&mut track.style_format);
2122 free_c_string(&mut track.event_format);
2123 free_c_string(&mut track.Language);
2124 free_c_string(&mut track.name);
2125 track.track_type = ass::TrackType::Unknown as c_int;
2126 track.PlayResX = 384;
2127 track.PlayResY = 288;
2128 track.Timer = 100.0;
2129 track.WrapStyle = 0;
2130 track.ScaledBorderAndShadow = 1;
2131 track.Kerning = 1;
2132 track.YCbCrMatrix = ass::YCbCrMatrix::Default as c_int;
2133 track.default_style = 0;
2134 track.LayoutResX = 0;
2135 track.LayoutResY = 0;
2136}
2137
2138unsafe fn track_styles_match_parsed(track: &ASS_Track, parsed: &ParsedTrack) -> bool {
2139 let current_styles = if track.styles.is_null() || track.n_styles <= 0 {
2140 Vec::new()
2141 } else {
2142 slice::from_raw_parts(track.styles, track.n_styles as usize)
2143 .iter()
2144 .map(|style| parsed_style_from_ffi(style))
2145 .collect::<Vec<_>>()
2146 };
2147
2148 current_styles == parsed.styles
2149 && string_option_from_ptr(track.style_format).unwrap_or_default() == parsed.style_format
2150 && track.track_type == parsed.track_type as c_int
2151}
2152
2153unsafe fn take_styles(track: &mut ASS_Track) -> Vec<ASS_Style> {
2154 if track.styles.is_null() || track.max_styles <= 0 {
2155 track.styles = ptr::null_mut();
2156 track.n_styles = 0;
2157 track.max_styles = 0;
2158 Vec::new()
2159 } else {
2160 let vec = Vec::from_raw_parts(
2161 track.styles,
2162 track.n_styles as usize,
2163 track.max_styles as usize,
2164 );
2165 track.styles = ptr::null_mut();
2166 track.n_styles = 0;
2167 track.max_styles = 0;
2168 vec
2169 }
2170}
2171
2172unsafe fn store_styles(track: &mut ASS_Track, mut styles: Vec<ASS_Style>) {
2173 invalidate_parsed_track_cache_for_track(track);
2174 track.n_styles = styles.len() as c_int;
2175 track.max_styles = styles.capacity() as c_int;
2176 track.styles = if styles.capacity() == 0 {
2177 ptr::null_mut()
2178 } else {
2179 styles.as_mut_ptr()
2180 };
2181 mem::forget(styles);
2182}
2183
2184unsafe fn take_events(track: &mut ASS_Track) -> Vec<ASS_Event> {
2185 if track.events.is_null() || track.max_events <= 0 {
2186 track.events = ptr::null_mut();
2187 track.n_events = 0;
2188 track.max_events = 0;
2189 Vec::new()
2190 } else {
2191 let vec = Vec::from_raw_parts(
2192 track.events,
2193 track.n_events as usize,
2194 track.max_events as usize,
2195 );
2196 track.events = ptr::null_mut();
2197 track.n_events = 0;
2198 track.max_events = 0;
2199 vec
2200 }
2201}
2202
2203unsafe fn store_events(track: &mut ASS_Track, mut events: Vec<ASS_Event>) {
2204 invalidate_parsed_track_cache_for_track(track);
2205 track.n_events = events.len() as c_int;
2206 track.max_events = events.capacity() as c_int;
2207 track.events = if events.capacity() == 0 {
2208 ptr::null_mut()
2209 } else {
2210 events.as_mut_ptr()
2211 };
2212 mem::forget(events);
2213}
2214
2215unsafe fn free_style(style: &mut ASS_Style) {
2216 free_c_string(&mut style.Name);
2217 free_c_string(&mut style.FontName);
2218}
2219
2220unsafe fn free_event(event: &mut ASS_Event) {
2221 free_c_string(&mut event.Name);
2222 free_c_string(&mut event.Effect);
2223 free_c_string(&mut event.Text);
2224}
2225
2226unsafe fn free_c_string(value: &mut *mut c_char) {
2227 if !value.is_null() {
2228 drop(CString::from_raw(*value));
2229 *value = ptr::null_mut();
2230 }
2231}
2232
2233unsafe fn replace_string(target: &mut *mut c_char, value: &str) {
2234 free_c_string(target);
2235 *target = string_to_c_ptr(value);
2236}
2237
2238fn make_style(style: &ParsedStyle) -> ASS_Style {
2239 ASS_Style {
2240 Name: string_to_c_ptr(&style.name),
2241 FontName: string_to_c_ptr(&style.font_name),
2242 FontSize: style.font_size,
2243 PrimaryColour: style.primary_colour,
2244 SecondaryColour: style.secondary_colour,
2245 OutlineColour: style.outline_colour,
2246 BackColour: style.back_colour,
2247 Bold: style.bold as c_int,
2248 Italic: style.italic as c_int,
2249 Underline: style.underline as c_int,
2250 StrikeOut: style.strike_out as c_int,
2251 ScaleX: style.scale_x,
2252 ScaleY: style.scale_y,
2253 Spacing: style.spacing,
2254 Angle: style.angle,
2255 BorderStyle: style.border_style,
2256 Outline: style.outline,
2257 Shadow: style.shadow,
2258 Alignment: style.alignment,
2259 MarginL: style.margin_l,
2260 MarginR: style.margin_r,
2261 MarginV: style.margin_v,
2262 Encoding: style.encoding,
2263 treat_fontname_as_pattern: style.treat_fontname_as_pattern,
2264 Blur: style.blur,
2265 Justify: style.justify,
2266 }
2267}
2268
2269fn make_event(event: &ParsedEvent) -> ASS_Event {
2270 ASS_Event {
2271 Start: event.start,
2272 Duration: event.duration,
2273 ReadOrder: event.read_order,
2274 Layer: event.layer,
2275 Style: event.style,
2276 Name: string_to_c_ptr(&event.name),
2277 MarginL: event.margin_l,
2278 MarginR: event.margin_r,
2279 MarginV: event.margin_v,
2280 Effect: string_to_c_ptr(&event.effect),
2281 Text: string_to_c_ptr(&event.text),
2282 render_priv: ptr::null_mut(),
2283 }
2284}
2285
2286fn string_to_c_ptr(value: &str) -> *mut c_char {
2287 let sanitized = value.replace('\0', " ");
2288 CString::new(sanitized)
2289 .map(CString::into_raw)
2290 .unwrap_or(ptr::null_mut())
2291}
2292
2293unsafe fn string_option_from_ptr(value: *const c_char) -> Option<String> {
2294 if value.is_null() {
2295 None
2296 } else {
2297 Some(string_from_ptr(value))
2298 }
2299}
2300
2301unsafe fn string_from_ptr(value: *const c_char) -> String {
2302 CStr::from_ptr(value).to_string_lossy().into_owned()
2303}
2304
2305unsafe fn track_state_ref(track: *mut ASS_Track) -> Option<&'static TrackState> {
2306 track.as_ref().and_then(|track| {
2307 (!track.parser_priv.is_null()).then(|| &*(track.parser_priv as *const TrackState))
2308 })
2309}
2310
2311unsafe fn track_state_mut(track: *mut ASS_Track) -> Option<&'static mut TrackState> {
2312 let track = track.as_mut()?;
2313 (!track.parser_priv.is_null()).then_some(&mut *(track.parser_priv as *mut TrackState))
2314}
2315
2316unsafe fn invalidate_parsed_track_cache(track: *mut ASS_Track) {
2317 if let Some(state) = track_state_mut(track) {
2318 state.parsed_cache_signature = None;
2319 state.parsed_cache = None;
2320 state.cache_generation = state.cache_generation.wrapping_add(1);
2321 }
2322}
2323
2324unsafe fn invalidate_parsed_track_cache_for_track(track: &mut ASS_Track) {
2325 if !track.parser_priv.is_null() {
2326 let state = &mut *(track.parser_priv as *mut TrackState);
2327 state.parsed_cache_signature = None;
2328 state.parsed_cache = None;
2329 state.cache_generation = state.cache_generation.wrapping_add(1);
2330 }
2331}
2332
2333fn parsed_track_cache_signature(track: &ASS_Track) -> ParsedTrackCacheSignature {
2334 ParsedTrackCacheSignature {
2335 n_styles: track.n_styles,
2336 styles: track.styles as usize,
2337 n_events: track.n_events,
2338 events: track.events as usize,
2339 style_format: track.style_format as usize,
2340 event_format: track.event_format as usize,
2341 track_type: track.track_type,
2342 play_res_x: track.PlayResX,
2343 play_res_y: track.PlayResY,
2344 timer_bits: track.Timer.to_bits(),
2345 wrap_style: track.WrapStyle,
2346 scaled_border_and_shadow: track.ScaledBorderAndShadow,
2347 kerning: track.Kerning,
2348 language: track.Language as usize,
2349 ycbcr_matrix: track.YCbCrMatrix,
2350 default_style: track.default_style,
2351 layout_res_x: track.LayoutResX,
2352 layout_res_y: track.LayoutResY,
2353 }
2354}
2355
2356unsafe fn cached_parsed_track_from_ffi<'a>(
2357 track: *mut ASS_Track,
2358 track_ref: &ASS_Track,
2359) -> &'a ParsedTrack {
2360 let signature = parsed_track_cache_signature(track_ref);
2361 let Some(state) = track_state_mut(track) else {
2362 panic!("ASS_Track missing parser state");
2363 };
2364 if state.parsed_cache_signature != Some(signature) || state.parsed_cache.is_none() {
2365 state.parsed_cache = Some(parsed_track_from_ffi(track_ref));
2366 state.parsed_cache_signature = Some(signature);
2367 }
2368 state.parsed_cache.as_ref().expect("parsed track cached")
2369}
2370
2371unsafe fn active_event_indices(track: *mut ASS_Track, now: i64) -> Vec<usize> {
2372 let Some(track) = track.as_ref() else {
2373 return Vec::new();
2374 };
2375 if track.events.is_null() || track.n_events <= 0 {
2376 return Vec::new();
2377 }
2378
2379 slice::from_raw_parts(track.events, track.n_events as usize)
2380 .iter()
2381 .enumerate()
2382 .filter_map(|(index, event)| {
2383 (now >= event.Start && now < event.Start + event.Duration).then_some(index)
2384 })
2385 .collect()
2386}
2387
2388fn frame_cache_time_bucket(
2389 track: &ParsedTrack,
2390 active_event_indices: &[usize],
2391 now: i64,
2392) -> Option<i64> {
2393 if active_event_indices
2394 .iter()
2395 .any(|index| track.events.get(*index).is_none())
2396 {
2397 return None;
2398 }
2399
2400 if active_events_are_static(track, active_event_indices) {
2401 return Some(0);
2402 }
2403
2404 let bucket_ms = if active_events_have_heavy_animation(track, active_event_indices) {
2405 APPROXIMATE_HEAVY_ANIMATION_FRAME_BUCKET_MS
2406 } else {
2407 APPROXIMATE_ANIMATION_FRAME_BUCKET_MS
2408 };
2409 Some(now.div_euclid(bucket_ms))
2410}
2411
2412fn active_events_have_heavy_animation(track: &ParsedTrack, active_event_indices: &[usize]) -> bool {
2413 active_event_indices.iter().any(|index| {
2414 track
2415 .events
2416 .get(*index)
2417 .is_some_and(|event| event_text_has_heavy_animation(&event.text))
2418 })
2419}
2420
2421fn active_events_are_static(track: &ParsedTrack, active_event_indices: &[usize]) -> bool {
2422 active_event_indices.iter().all(|index| {
2423 track.events.get(*index).is_some_and(|event| {
2424 event_text_is_static(&event.text) && event.effect.trim().is_empty()
2425 })
2426 })
2427}
2428
2429fn event_text_is_static(text: &str) -> bool {
2430 let text = text.to_ascii_lowercase();
2431 !(text.contains("\\move")
2432 || text.contains("\\fad")
2433 || text.contains("\\fade")
2434 || text.contains("\\t(")
2435 || text.contains("\\k")
2436 || text.contains("\\ko"))
2437}
2438
2439fn event_text_has_heavy_animation(text: &str) -> bool {
2440 let text = text.to_ascii_lowercase();
2441 text.contains("\\t(")
2442 || text.contains("\\k")
2443 || text.contains("\\ko")
2444 || text.contains("\\clip")
2445 || text.contains("\\iclip")
2446}
2447
2448unsafe fn parsed_track_from_ffi(track: &ASS_Track) -> ParsedTrack {
2449 let styles = if track.styles.is_null() || track.n_styles <= 0 {
2450 Vec::new()
2451 } else {
2452 slice::from_raw_parts(track.styles, track.n_styles as usize)
2453 .iter()
2454 .map(|style| unsafe { parsed_style_from_ffi(style) })
2455 .collect()
2456 };
2457
2458 let events = if track.events.is_null() || track.n_events <= 0 {
2459 Vec::new()
2460 } else {
2461 slice::from_raw_parts(track.events, track.n_events as usize)
2462 .iter()
2463 .map(|event| unsafe { parsed_event_from_ffi(event) })
2464 .collect()
2465 };
2466
2467 ParsedTrack {
2468 styles,
2469 events,
2470 attachments: Vec::new(),
2471 style_format: string_option_from_ptr(track.style_format).unwrap_or_default(),
2472 event_format: string_option_from_ptr(track.event_format).unwrap_or_default(),
2473 track_type: match track.track_type {
2474 value if value == ass::TrackType::Ass as c_int => ass::TrackType::Ass,
2475 value if value == ass::TrackType::Ssa as c_int => ass::TrackType::Ssa,
2476 _ => ass::TrackType::Unknown,
2477 },
2478 play_res_x: track.PlayResX,
2479 play_res_y: track.PlayResY,
2480 timer: track.Timer,
2481 wrap_style: track.WrapStyle,
2482 scaled_border_and_shadow: track.ScaledBorderAndShadow != 0,
2483 kerning: track.Kerning != 0,
2484 language: string_option_from_ptr(track.Language).unwrap_or_default(),
2485 ycbcr_matrix: match track.YCbCrMatrix {
2486 value if value == ass::YCbCrMatrix::None as c_int => ass::YCbCrMatrix::None,
2487 value if value == ass::YCbCrMatrix::Bt601Tv as c_int => ass::YCbCrMatrix::Bt601Tv,
2488 value if value == ass::YCbCrMatrix::Bt601Pc as c_int => ass::YCbCrMatrix::Bt601Pc,
2489 value if value == ass::YCbCrMatrix::Bt709Tv as c_int => ass::YCbCrMatrix::Bt709Tv,
2490 value if value == ass::YCbCrMatrix::Bt709Pc as c_int => ass::YCbCrMatrix::Bt709Pc,
2491 value if value == ass::YCbCrMatrix::Smpte240mTv as c_int => {
2492 ass::YCbCrMatrix::Smpte240mTv
2493 }
2494 value if value == ass::YCbCrMatrix::Smpte240mPc as c_int => {
2495 ass::YCbCrMatrix::Smpte240mPc
2496 }
2497 value if value == ass::YCbCrMatrix::FccTv as c_int => ass::YCbCrMatrix::FccTv,
2498 value if value == ass::YCbCrMatrix::FccPc as c_int => ass::YCbCrMatrix::FccPc,
2499 value if value == ass::YCbCrMatrix::Unknown as c_int => ass::YCbCrMatrix::Unknown,
2500 _ => ass::YCbCrMatrix::Default,
2501 },
2502 default_style: track.default_style,
2503 layout_res_x: track.LayoutResX,
2504 layout_res_y: track.LayoutResY,
2505 }
2506}
2507
2508unsafe fn parsed_style_from_ffi(style: &ASS_Style) -> ParsedStyle {
2509 ParsedStyle {
2510 name: string_option_from_ptr(style.Name).unwrap_or_default(),
2511 font_name: string_option_from_ptr(style.FontName).unwrap_or_default(),
2512 font_size: style.FontSize,
2513 primary_colour: style.PrimaryColour,
2514 secondary_colour: style.SecondaryColour,
2515 outline_colour: style.OutlineColour,
2516 back_colour: style.BackColour,
2517 bold: ffi_bold_is_active(style.Bold),
2518 font_weight: ffi_bold_weight(style.Bold),
2519 italic: style.Italic != 0,
2520 underline: style.Underline != 0,
2521 strike_out: style.StrikeOut != 0,
2522 scale_x: style.ScaleX,
2523 scale_y: style.ScaleY,
2524 spacing: style.Spacing,
2525 angle: style.Angle,
2526 border_style: style.BorderStyle,
2527 outline: style.Outline,
2528 shadow: style.Shadow,
2529 alignment: style.Alignment,
2530 margin_l: style.MarginL,
2531 margin_r: style.MarginR,
2532 margin_v: style.MarginV,
2533 encoding: style.Encoding,
2534 treat_fontname_as_pattern: style.treat_fontname_as_pattern,
2535 blur: style.Blur,
2536 justify: style.Justify,
2537 }
2538}
2539
2540fn selective_style_overrides_active(renderer: &ASS_Renderer) -> bool {
2541 renderer.selective_override_style.is_some()
2542 && renderer.selective_override_bits != ass::override_bits::DEFAULT
2543}
2544
2545fn apply_selective_style_overrides(track: &mut ParsedTrack, renderer: &ASS_Renderer) {
2546 let Some(user_style) = renderer
2547 .selective_override_style
2548 .as_ref()
2549 .map(|style| &style.style)
2550 else {
2551 return;
2552 };
2553
2554 let mut requested = renderer.selective_override_bits;
2555 if requested == ass::override_bits::DEFAULT {
2556 return;
2557 }
2558
2559 if requested & ass::override_bits::STYLE != 0 {
2560 requested |= ass::override_bits::FONT_NAME
2561 | ass::override_bits::FONT_SIZE_FIELDS
2562 | ass::override_bits::COLORS
2563 | ass::override_bits::BORDER
2564 | ass::override_bits::ATTRIBUTES;
2565 }
2566
2567 for style in &mut track.styles {
2568 if requested & ass::override_bits::FULL_STYLE != 0 {
2569 *style = user_style.clone();
2570 continue;
2571 }
2572
2573 if requested & ass::override_bits::FONT_NAME != 0 {
2574 style.font_name = user_style.font_name.clone();
2575 style.treat_fontname_as_pattern = user_style.treat_fontname_as_pattern;
2576 }
2577 if requested & ass::override_bits::FONT_SIZE_FIELDS != 0 {
2578 style.font_size = user_style.font_size;
2579 style.spacing = user_style.spacing;
2580 style.scale_x = user_style.scale_x;
2581 style.scale_y = user_style.scale_y;
2582 }
2583 if requested & ass::override_bits::COLORS != 0 {
2584 style.primary_colour = user_style.primary_colour;
2585 style.secondary_colour = user_style.secondary_colour;
2586 style.outline_colour = user_style.outline_colour;
2587 style.back_colour = user_style.back_colour;
2588 }
2589 if requested & ass::override_bits::ATTRIBUTES != 0 {
2590 style.bold = user_style.bold;
2591 style.italic = user_style.italic;
2592 style.underline = user_style.underline;
2593 style.strike_out = user_style.strike_out;
2594 }
2595 if requested & ass::override_bits::BORDER != 0 {
2596 style.border_style = user_style.border_style;
2597 style.outline = user_style.outline;
2598 style.shadow = user_style.shadow;
2599 }
2600 if requested & ass::override_bits::ALIGNMENT != 0 {
2601 style.alignment = user_style.alignment;
2602 }
2603 if requested & ass::override_bits::MARGINS != 0 {
2604 style.margin_l = user_style.margin_l;
2605 style.margin_r = user_style.margin_r;
2606 style.margin_v = user_style.margin_v;
2607 }
2608 if requested & ass::override_bits::JUSTIFY != 0 {
2609 style.justify = user_style.justify;
2610 }
2611 if requested & ass::override_bits::BLUR != 0 {
2612 style.blur = user_style.blur;
2613 }
2614 }
2615}
2616
2617unsafe fn parsed_event_from_ffi(event: &ASS_Event) -> ParsedEvent {
2618 ParsedEvent {
2619 start: event.Start,
2620 duration: event.Duration,
2621 read_order: event.ReadOrder,
2622 layer: event.Layer,
2623 style: event.Style,
2624 name: string_option_from_ptr(event.Name).unwrap_or_default(),
2625 margin_l: event.MarginL,
2626 margin_r: event.MarginR,
2627 margin_v: event.MarginV,
2628 effect: string_option_from_ptr(event.Effect).unwrap_or_default(),
2629 text: string_option_from_ptr(event.Text).unwrap_or_default(),
2630 }
2631}