Skip to main content

rassa_capi/
lib.rs

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
287// Approximate animated ASS tags by reusing the previous rendered image within a
288// time bucket. This intentionally trades sub-frame pixel accuracy/smoothness for
289// much higher FPS on transform/karaoke/clip-heavy scripts.
290const APPROXIMATE_ANIMATION_FRAME_BUCKET_MS: i64 = 500;
291const APPROXIMATE_HEAVY_ANIMATION_FRAME_BUCKET_MS: i64 = 1000;
292
293// If a frame contains hundreds/thousands of ASS_Image nodes, downstream
294// compositors can spend more time walking tiny planes than rassa spent caching
295// the frame. Collapse same-color/same-kind planes into coarse union bitmaps once
296// the list is large. This is an intentional approximation: it preserves rough
297// shape/color coverage while giving up exact per-glyph layering/overlap order.
298const 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}