Skip to main content

rassa_render/
lib.rs

1use std::collections::HashMap;
2
3use rassa_core::{ImagePlane, Point, Rect, RendererConfig, RgbaColor, Size, ass};
4use rassa_fonts::{FontProvider, FontconfigProvider};
5use rassa_layout::{LayoutEngine, LayoutEvent, LayoutGlyphRun};
6use rassa_parse::{
7    ParsedDrawing, ParsedEvent, ParsedFade, ParsedKaraokeMode, ParsedMovement, ParsedSpanStyle,
8    ParsedTrack, ParsedVectorClip,
9};
10use rassa_raster::{RasterGlyph, RasterOptions, Rasterizer};
11use rassa_shape::{GlyphInfo, ShapingMode};
12
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct RenderSelection {
15    pub active_event_indices: Vec<usize>,
16}
17
18#[derive(Clone, Debug, Default, PartialEq)]
19pub struct PreparedFrame {
20    pub now_ms: i64,
21    pub active_events: Vec<LayoutEvent>,
22}
23
24#[derive(Default)]
25pub struct RenderEngine {
26    layout: LayoutEngine,
27}
28
29const LINE_HEIGHT: i32 = 40;
30
31fn layout_line_height(config: &RendererConfig, scale_y: f64) -> i32 {
32    let scale_y = style_scale(scale_y);
33    let extra_spacing = if config.line_spacing.is_finite() {
34        (config.line_spacing * scale_y).round() as i32
35    } else {
36        0
37    };
38    ((f64::from(LINE_HEIGHT) * scale_y).round() as i32 + extra_spacing).max(1)
39}
40
41fn layout_line_height_for_line(
42    line: &rassa_layout::LayoutLine,
43    config: &RendererConfig,
44    scale_y: f64,
45) -> i32 {
46    layout_line_height(config, scale_y).max(font_metric_height_for_line(line, scale_y))
47}
48
49fn font_metric_height_for_line(line: &rassa_layout::LayoutLine, scale_y: f64) -> i32 {
50    let scale_y = style_scale(scale_y);
51    let max_font_size = line
52        .runs
53        .iter()
54        .map(|run| run.style.font_size)
55        .filter(|size| size.is_finite() && *size > 0.0)
56        .fold(0.0_f64, f64::max);
57    (max_font_size * scale_y * 0.52).round() as i32
58}
59
60fn positioned_text_y_correction(
61    line: &rassa_layout::LayoutLine,
62    config: &RendererConfig,
63    scale_y: f64,
64) -> i32 {
65    let layout_height = layout_line_height_for_line(line, config, scale_y);
66    let metric_height = font_metric_height_for_line(line, scale_y).max(1);
67    ((layout_height - metric_height).max(0) * 4) / 9
68}
69
70fn renderer_blur_radius(blur: f64) -> u32 {
71    if !(blur.is_finite() && blur > 0.0) {
72        return 0;
73    }
74    (blur * 4.0).ceil().max(1.0) as u32
75}
76
77impl RenderEngine {
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    pub fn select_active_events(&self, track: &ParsedTrack, now_ms: i64) -> RenderSelection {
83        let mut active_event_indices = track
84            .events
85            .iter()
86            .enumerate()
87            .filter_map(|(index, event)| is_event_active(event, now_ms).then_some(index))
88            .collect::<Vec<_>>();
89        active_event_indices.sort_by(|left, right| {
90            let left_event = &track.events[*left];
91            let right_event = &track.events[*right];
92            left_event
93                .layer
94                .cmp(&right_event.layer)
95                .then(left_event.read_order.cmp(&right_event.read_order))
96                .then(left.cmp(right))
97        });
98
99        RenderSelection {
100            active_event_indices,
101        }
102    }
103
104    pub fn prepare_frame<P: FontProvider>(
105        &self,
106        track: &ParsedTrack,
107        provider: &P,
108        now_ms: i64,
109    ) -> PreparedFrame {
110        self.prepare_frame_with_config(track, provider, now_ms, &default_renderer_config(track))
111    }
112
113    pub fn prepare_frame_with_config<P: FontProvider>(
114        &self,
115        track: &ParsedTrack,
116        provider: &P,
117        now_ms: i64,
118        config: &RendererConfig,
119    ) -> PreparedFrame {
120        let selection = self.select_active_events(track, now_ms);
121        let shaping_mode = match config.shaping {
122            ass::ShapingLevel::Simple => ShapingMode::Simple,
123            ass::ShapingLevel::Complex => ShapingMode::Complex,
124        };
125        let active_events = selection
126            .active_event_indices
127            .into_iter()
128            .filter_map(|index| {
129                self.layout
130                    .layout_track_event_with_mode(track, index, provider, shaping_mode)
131                    .ok()
132            })
133            .collect();
134
135        PreparedFrame {
136            now_ms,
137            active_events,
138        }
139    }
140
141    pub fn render_frame_with_provider<P: FontProvider>(
142        &self,
143        track: &ParsedTrack,
144        provider: &P,
145        now_ms: i64,
146    ) -> Vec<ImagePlane> {
147        self.render_frame_with_provider_and_config(
148            track,
149            provider,
150            now_ms,
151            &default_renderer_config(track),
152        )
153    }
154
155    pub fn render_frame_with_provider_and_config<P: FontProvider>(
156        &self,
157        track: &ParsedTrack,
158        provider: &P,
159        now_ms: i64,
160        config: &RendererConfig,
161    ) -> Vec<ImagePlane> {
162        let prepared = self.prepare_frame_with_config(track, provider, now_ms, config);
163        let mut planes = Vec::new();
164        let mut occupied_bounds_by_layer = HashMap::<i32, Vec<Rect>>::new();
165
166        let render_scale_x = output_scale_x(track, config);
167        let render_scale_y = output_scale_y(track, config);
168        let render_scale =
169            ((style_scale(render_scale_x) + style_scale(render_scale_y)) / 2.0).max(1.0);
170
171        for event in &prepared.active_events {
172            let Some(style) = track.styles.get(event.style_index) else {
173                continue;
174            };
175            let mut shadow_planes = Vec::new();
176            let mut outline_planes = Vec::new();
177            let mut character_planes = Vec::new();
178            let mut opaque_box_rects = Vec::new();
179            let effective_position = scale_position(
180                resolve_event_position(track, event, now_ms),
181                render_scale_x,
182                render_scale_y,
183            );
184            let layer = event_layer(track, event);
185            let occupied_bounds = occupied_bounds_by_layer.entry(layer).or_default();
186            let vertical_layout = resolve_vertical_layout(
187                track,
188                event,
189                effective_position,
190                occupied_bounds,
191                config,
192                render_scale_y,
193            );
194            let occupied_bound = effective_position.is_none().then(|| {
195                event_bounds(
196                    track,
197                    event,
198                    &vertical_layout,
199                    effective_position,
200                    config,
201                    render_scale_x,
202                    render_scale_y,
203                )
204            });
205            for (line, line_top) in event.lines.iter().zip(vertical_layout.iter().copied()) {
206                let has_scaled_run = line.runs.iter().any(|run| {
207                    (run.style.scale_x - 100.0).abs() > f64::EPSILON
208                        || (run.style.scale_y - 100.0).abs() > f64::EPSILON
209                });
210                let text_line_top = if effective_position.is_some() {
211                    let border_style_3_y_adjust = if style.border_style == 3 { 3 } else { 0 };
212                    line_top + positioned_text_y_correction(line, config, render_scale_y)
213                        - border_style_3_y_adjust
214                        + if has_scaled_run { 2 } else { 0 }
215                } else {
216                    line_top + if has_scaled_run { 2 } else { 0 }
217                };
218                let scaled_line_width = (f64::from(line.width) * render_scale_x).round() as i32;
219                let origin_x = compute_horizontal_origin(
220                    track,
221                    event,
222                    scaled_line_width,
223                    effective_position,
224                    render_scale_x,
225                );
226                let text_origin_x = if style.border_style == 3 {
227                    let box_scale = renderer_font_scale(config) * style_scale(render_scale);
228                    origin_x
229                        + ((style.outline + style.shadow - 1.0).max(0.0) * box_scale).round() as i32
230                } else {
231                    origin_x
232                };
233                let has_karaoke_run = line.runs.iter().any(|run| run.karaoke.is_some());
234                let line_ascender = line_raster_ascender(
235                    line,
236                    track.events.get(event.event_index),
237                    now_ms,
238                    track,
239                    config,
240                    RenderScale {
241                        x: render_scale_x,
242                        y: render_scale_y,
243                        uniform: render_scale,
244                    },
245                ) + if has_karaoke_run { 1 } else { 0 };
246                let mut line_pen_x = 0;
247                if style.border_style == 3 {
248                    let box_scale = renderer_font_scale(config) * style_scale(render_scale);
249                    let compensation = if track.scaled_border_and_shadow {
250                        1.0
251                    } else {
252                        border_shadow_compensation_scale(track, config)
253                    };
254                    let box_padding =
255                        (style.outline * box_scale / compensation).round().max(0.0) as i32;
256                    let box_height = (style.font_size * style_scale(render_scale_y) * 1.24)
257                        .round()
258                        .max(1.0) as i32;
259                    let box_top = if let Some((_, y)) = effective_position {
260                        match event.alignment & (ass::VALIGN_TOP | ass::VALIGN_CENTER) {
261                            ass::VALIGN_TOP => y,
262                            ass::VALIGN_CENTER => y - box_height / 2,
263                            _ => y - box_height,
264                        }
265                    } else {
266                        line_top
267                    };
268                    opaque_box_rects.push(Rect {
269                        x_min: origin_x + box_padding / 2 - 1,
270                        y_min: box_top - box_padding / 2 + box_padding / 3 + 5,
271                        x_max: origin_x + scaled_line_width - box_padding / 2 + 1,
272                        y_max: box_top + box_height - box_padding / 3 - 6,
273                    });
274                }
275                for run in &line.runs {
276                    let effective_style = apply_renderer_style_scale(
277                        resolve_run_style(run, track.events.get(event.event_index), now_ms),
278                        track,
279                        config,
280                        render_scale,
281                    );
282                    let run_origin_x = text_origin_x + line_pen_x;
283                    if let Some(drawing) = &run.drawing {
284                        if let Some(plane) = image_plane_from_drawing(
285                            drawing,
286                            run_origin_x,
287                            line_top,
288                            resolve_run_fill_color(
289                                run,
290                                &effective_style,
291                                track.events.get(event.event_index),
292                                now_ms,
293                            ),
294                            effective_style.scale_x,
295                            effective_style.scale_y,
296                        ) {
297                            if effective_style.border > 0.0 {
298                                let mut outline_glyph = plane_to_raster_glyph(&plane);
299                                let rasterizer = Rasterizer::with_options(RasterOptions {
300                                    size_26_6: 64,
301                                    hinting: config.hinting,
302                                });
303                                let mut outline_glyphs = rasterizer.outline_glyphs(
304                                    &[outline_glyph.clone()],
305                                    effective_style.border.round().max(1.0) as i32,
306                                );
307                                if effective_style.blur > 0.0 {
308                                    outline_glyphs = rasterizer.blur_glyphs(
309                                        &outline_glyphs,
310                                        renderer_blur_radius(effective_style.blur),
311                                    );
312                                }
313                                outline_planes.extend(image_planes_from_absolute_glyphs(
314                                    &outline_glyphs,
315                                    effective_style.outline_colour,
316                                    ass::ImageType::Outline,
317                                ));
318                                outline_glyph = plane_to_raster_glyph(&plane);
319                                let _ = outline_glyph;
320                            }
321                            character_planes.push(plane);
322                            if effective_style.shadow > 0.0 {
323                                let rasterizer = Rasterizer::with_options(RasterOptions {
324                                    size_26_6: 64,
325                                    hinting: config.hinting,
326                                });
327                                let mut shadow_glyph = plane_to_raster_glyph(
328                                    character_planes.last().expect("drawing plane"),
329                                );
330                                if effective_style.blur > 0.0 {
331                                    shadow_glyph = rasterizer
332                                        .blur_glyphs(
333                                            &[shadow_glyph],
334                                            renderer_blur_radius(effective_style.blur),
335                                        )
336                                        .into_iter()
337                                        .next()
338                                        .expect("shadow glyph");
339                                }
340                                shadow_planes.extend(image_planes_from_absolute_glyphs(
341                                    &[RasterGlyph {
342                                        left: shadow_glyph.left
343                                            + effective_style.shadow.round() as i32,
344                                        top: shadow_glyph.top
345                                            - effective_style.shadow.round() as i32,
346                                        ..shadow_glyph
347                                    }],
348                                    effective_style.back_colour,
349                                    ass::ImageType::Shadow,
350                                ));
351                            }
352                        }
353                        line_pen_x += run.width.round() as i32;
354                        continue;
355                    }
356                    let rasterizer = Rasterizer::with_options(RasterOptions {
357                        size_26_6: (effective_style.font_size.max(1.0) * 64.0).round() as i32,
358                        hinting: config.hinting,
359                    });
360                    let glyph_infos =
361                        scale_glyph_infos(&run.glyphs, render_scale_x, render_scale_y);
362                    let Ok(raster_glyphs) = rasterizer.rasterize_glyphs(&run.font, &glyph_infos)
363                    else {
364                        line_pen_x += run.width.round() as i32;
365                        continue;
366                    };
367                    let raster_glyphs = scale_raster_glyphs(
368                        raster_glyphs,
369                        effective_style.scale_x,
370                        effective_style.scale_y,
371                    );
372                    let raster_glyphs = apply_text_spacing(raster_glyphs, &effective_style);
373                    let glyph_origin_x = run_origin_x
374                        - i32::from(
375                            (effective_style.scale_x - 100.0).abs() > f64::EPSILON
376                                || (effective_style.scale_y - 100.0).abs() > f64::EPSILON,
377                        );
378                    let run_line_ascender = Some(line_ascender);
379                    let effective_blur = effective_style.blur.max(effective_style.be);
380                    let mut shadow_source_glyphs = raster_glyphs.clone();
381                    if effective_style.border > 0.0
382                        && !karaoke_hides_outline(run, track.events.get(event.event_index), now_ms)
383                    {
384                        let outline_radius = effective_style.border.round().max(1.0) as i32;
385                        let outline_source_glyphs =
386                            rasterizer.outline_glyphs(&raster_glyphs, outline_radius);
387                        shadow_source_glyphs = outline_source_glyphs.clone();
388                        let mut outline_glyphs = outline_source_glyphs;
389                        if effective_blur > 0.0 {
390                            outline_glyphs = rasterizer
391                                .blur_glyphs(&outline_glyphs, renderer_blur_radius(effective_blur));
392                        }
393                        if let Some(plane) = combined_image_plane_from_glyphs(
394                            &outline_glyphs,
395                            glyph_origin_x,
396                            text_line_top,
397                            run_line_ascender,
398                            effective_style.outline_colour,
399                            ass::ImageType::Outline,
400                            0,
401                        ) {
402                            outline_planes.push(plane);
403                        }
404                    }
405                    let fill_color = resolve_run_fill_color(
406                        run,
407                        &effective_style,
408                        track.events.get(event.event_index),
409                        now_ms,
410                    );
411                    if run.karaoke.is_none() && effective_blur > 0.0 {
412                        if let Some(plane) = combined_image_plane_from_glyphs(
413                            &raster_glyphs,
414                            glyph_origin_x,
415                            text_line_top,
416                            run_line_ascender,
417                            fill_color,
418                            ass::ImageType::Character,
419                            renderer_blur_radius(effective_blur),
420                        ) {
421                            character_planes.push(plane);
422                        }
423                    } else {
424                        let fill_glyphs = if effective_blur > 0.0 {
425                            rasterizer
426                                .blur_glyphs(&raster_glyphs, renderer_blur_radius(effective_blur))
427                        } else {
428                            raster_glyphs.clone()
429                        };
430                        let maybe_fill_plane = combined_image_plane_from_glyphs(
431                            &fill_glyphs,
432                            glyph_origin_x,
433                            text_line_top,
434                            run_line_ascender,
435                            fill_color,
436                            ass::ImageType::Character,
437                            0,
438                        );
439                        if run.karaoke.is_some() {
440                            let fill_planes = maybe_fill_plane.into_iter().collect();
441                            character_planes.extend(apply_karaoke_to_character_planes(
442                                fill_planes,
443                                run,
444                                &effective_style,
445                                track.events.get(event.event_index),
446                                now_ms,
447                                glyph_origin_x,
448                                raster_glyphs
449                                    .iter()
450                                    .map(|glyph| glyph.advance_x)
451                                    .sum::<i32>(),
452                            ));
453                        } else if let Some(plane) = maybe_fill_plane {
454                            character_planes.push(plane);
455                        }
456                    }
457                    let run_advance = raster_glyphs
458                        .iter()
459                        .map(|glyph| glyph.advance_x)
460                        .sum::<i32>();
461                    character_planes.extend(text_decoration_planes(
462                        &effective_style,
463                        glyph_origin_x,
464                        text_line_top,
465                        run_advance,
466                        fill_color,
467                    ));
468                    if effective_style.shadow_x.abs() > f64::EPSILON
469                        || effective_style.shadow_y.abs() > f64::EPSILON
470                    {
471                        let mut shadow_glyphs = shadow_source_glyphs.clone();
472                        if effective_blur > 0.0 {
473                            shadow_glyphs = rasterizer
474                                .blur_glyphs(&shadow_glyphs, renderer_blur_radius(effective_blur));
475                        }
476                        if let Some(plane) = combined_image_plane_from_glyphs(
477                            &shadow_glyphs,
478                            glyph_origin_x + effective_style.shadow_x.round() as i32,
479                            text_line_top + effective_style.shadow_y.round() as i32,
480                            run_line_ascender,
481                            effective_style.back_colour,
482                            ass::ImageType::Shadow,
483                            0,
484                        ) {
485                            shadow_planes.push(plane);
486                        }
487                    }
488                    line_pen_x += raster_glyphs
489                        .iter()
490                        .map(|glyph| glyph.advance_x)
491                        .sum::<i32>();
492                }
493            }
494
495            if style.border_style == 3 {
496                let box_scale = renderer_font_scale(config) * style_scale(render_scale);
497                let compensation = if track.scaled_border_and_shadow {
498                    1.0
499                } else {
500                    border_shadow_compensation_scale(track, config)
501                };
502                let box_shadow = (style.shadow * box_scale / compensation).round() as i32;
503                if let Some(box_plane) = opaque_box_plane_from_rects(
504                    &opaque_box_rects,
505                    style.outline_colour,
506                    ass::ImageType::Outline,
507                    Point { x: 0, y: 0 },
508                ) {
509                    outline_planes.clear();
510                    outline_planes.push(box_plane);
511                }
512                if box_shadow > 0 {
513                    if let Some(shadow_plane) = opaque_box_plane_from_rects(
514                        &opaque_box_rects,
515                        style.back_colour,
516                        ass::ImageType::Shadow,
517                        Point {
518                            x: box_shadow,
519                            y: box_shadow,
520                        },
521                    ) {
522                        shadow_planes.clear();
523                        shadow_planes.push(shadow_plane);
524                    }
525                }
526            }
527
528            let mut event_planes = shadow_planes;
529            event_planes.extend(outline_planes);
530            event_planes.extend(character_planes);
531            if let Some(rotation_z) =
532                event_rotation_z(event, track.events.get(event.event_index), now_ms)
533            {
534                event_planes = rotate_event_planes(event_planes, rotation_z);
535            }
536            if let Some(clip_rect) = event.clip_rect {
537                event_planes = apply_event_clip(event_planes, clip_rect, event.inverse_clip);
538            } else if let Some(vector_clip) = &event.vector_clip {
539                event_planes = apply_vector_clip(event_planes, vector_clip, event.inverse_clip);
540            }
541            if let Some(fade) = event.fade {
542                event_planes = apply_fade_to_planes(
543                    event_planes,
544                    fade,
545                    track.events.get(event.event_index),
546                    now_ms,
547                );
548            }
549            let mut render_offset = output_offset(config);
550            if style_scale(render_scale_y) > 1.0 {
551                render_offset.y += render_scale_y.round() as i32;
552            }
553            event_planes = translate_planes(event_planes, render_offset);
554            event_planes = apply_event_clip(
555                event_planes,
556                frame_clip_rect(track, config, event, effective_position),
557                false,
558            );
559            if let Some(occupied_bound) = occupied_bound {
560                occupied_bounds.push(occupied_bound);
561            }
562            planes.extend(event_planes);
563        }
564
565        planes
566    }
567
568    pub fn render_frame(&self, track: &ParsedTrack, now_ms: i64) -> Vec<ImagePlane> {
569        let provider = FontconfigProvider::new();
570        self.render_frame_with_provider(track, &provider, now_ms)
571    }
572}
573
574fn apply_fade_to_planes(
575    planes: Vec<ImagePlane>,
576    fade: ParsedFade,
577    source_event: Option<&ParsedEvent>,
578    now_ms: i64,
579) -> Vec<ImagePlane> {
580    let fade_alpha = compute_fad_alpha(fade, source_event, now_ms);
581    planes
582        .into_iter()
583        .map(|mut plane| {
584            plane.color = RgbaColor(with_fade_alpha(plane.color.0, fade_alpha));
585            plane
586        })
587        .collect()
588}
589
590fn resolve_run_fill_color(
591    run: &LayoutGlyphRun,
592    style: &ParsedSpanStyle,
593    source_event: Option<&ParsedEvent>,
594    now_ms: i64,
595) -> u32 {
596    let Some(karaoke) = run.karaoke else {
597        return style.primary_colour;
598    };
599    let Some(event) = source_event else {
600        return style.primary_colour;
601    };
602    let elapsed = (now_ms - event.start).clamp(0, event.duration.max(0)) as i32;
603    if elapsed >= karaoke.start_ms + karaoke.duration_ms {
604        style.primary_colour
605    } else {
606        style.secondary_colour
607    }
608}
609
610fn karaoke_hides_outline(
611    run: &LayoutGlyphRun,
612    source_event: Option<&ParsedEvent>,
613    now_ms: i64,
614) -> bool {
615    let Some(karaoke) = run.karaoke else {
616        return false;
617    };
618    if karaoke.mode != ParsedKaraokeMode::OutlineToggle {
619        return false;
620    }
621    let Some(event) = source_event else {
622        return false;
623    };
624    let elapsed = (now_ms - event.start).clamp(0, event.duration.max(0)) as i32;
625    elapsed < karaoke.start_ms + karaoke.duration_ms
626}
627
628fn apply_karaoke_to_character_planes(
629    planes: Vec<ImagePlane>,
630    run: &LayoutGlyphRun,
631    style: &ParsedSpanStyle,
632    source_event: Option<&ParsedEvent>,
633    now_ms: i64,
634    run_origin_x: i32,
635    run_width: i32,
636) -> Vec<ImagePlane> {
637    let Some(karaoke) = run.karaoke else {
638        return planes;
639    };
640    let Some(event) = source_event else {
641        return planes;
642    };
643    let elapsed = (now_ms - event.start).clamp(0, event.duration.max(0)) as i32;
644    let relative = elapsed - karaoke.start_ms;
645    match karaoke.mode {
646        ParsedKaraokeMode::FillSwap | ParsedKaraokeMode::OutlineToggle => planes
647            .into_iter()
648            .map(|mut plane| {
649                plane.color = rgba_color_from_ass(if relative >= karaoke.duration_ms {
650                    style.primary_colour
651                } else {
652                    style.secondary_colour
653                });
654                plane
655            })
656            .collect(),
657        ParsedKaraokeMode::Sweep => {
658            if relative <= 0 {
659                return planes
660                    .into_iter()
661                    .map(|mut plane| {
662                        plane.color = rgba_color_from_ass(style.secondary_colour);
663                        plane
664                    })
665                    .collect();
666            }
667            if relative >= karaoke.duration_ms {
668                return planes
669                    .into_iter()
670                    .map(|mut plane| {
671                        plane.color = rgba_color_from_ass(style.primary_colour);
672                        plane
673                    })
674                    .collect();
675            }
676
677            let progress = f64::from(relative) / f64::from(karaoke.duration_ms.max(1));
678            let split_x = run_origin_x + (f64::from(run_width.max(0)) * progress).round() as i32;
679            let mut result = Vec::new();
680            for plane in planes {
681                if let Some(mut left) =
682                    clip_plane_horizontally(&plane, plane.destination.x, split_x)
683                {
684                    left.color = rgba_color_from_ass(style.primary_colour);
685                    result.push(left);
686                }
687                if let Some(mut right) =
688                    clip_plane_horizontally(&plane, split_x, plane.destination.x + plane.size.width)
689                {
690                    right.color = rgba_color_from_ass(style.secondary_colour);
691                    result.push(right);
692                }
693            }
694            result
695        }
696    }
697}
698
699fn clip_plane_horizontally(
700    plane: &ImagePlane,
701    clip_left: i32,
702    clip_right: i32,
703) -> Option<ImagePlane> {
704    let plane_left = plane.destination.x;
705    let plane_right = plane.destination.x + plane.size.width;
706    let left = clip_left.max(plane_left);
707    let right = clip_right.min(plane_right);
708    if right <= left || plane.size.width <= 0 || plane.size.height <= 0 {
709        return None;
710    }
711
712    let start_column = (left - plane_left) as usize;
713    let end_column = (right - plane_left) as usize;
714    let new_width = (right - left) as usize;
715    let mut bitmap = vec![0_u8; new_width * plane.size.height as usize];
716
717    for row in 0..plane.size.height as usize {
718        let source_row = row * plane.stride as usize;
719        let target_row = row * new_width;
720        bitmap[target_row..target_row + new_width]
721            .copy_from_slice(&plane.bitmap[source_row + start_column..source_row + end_column]);
722    }
723
724    Some(ImagePlane {
725        size: Size {
726            width: new_width as i32,
727            height: plane.size.height,
728        },
729        stride: new_width as i32,
730        color: plane.color,
731        destination: Point {
732            x: left,
733            y: plane.destination.y,
734        },
735        kind: plane.kind,
736        bitmap,
737    })
738}
739
740fn resolve_run_style(
741    run: &LayoutGlyphRun,
742    source_event: Option<&ParsedEvent>,
743    now_ms: i64,
744) -> ParsedSpanStyle {
745    let Some(event) = source_event else {
746        return run.style.clone();
747    };
748
749    let mut style = run.style.clone();
750    let elapsed = (now_ms - event.start).clamp(0, event.duration.max(0)) as i32;
751    for transform in &run.transforms {
752        let start_ms = transform.start_ms.max(0);
753        let end_ms = transform
754            .end_ms
755            .unwrap_or(event.duration.max(0) as i32)
756            .max(start_ms);
757        let progress = if elapsed <= start_ms {
758            0.0
759        } else if elapsed >= end_ms {
760            1.0
761        } else {
762            let linear = f64::from(elapsed - start_ms) / f64::from((end_ms - start_ms).max(1));
763            linear.powf(if transform.accel > 0.0 {
764                transform.accel
765            } else {
766                1.0
767            })
768        };
769
770        if let Some(font_size) = transform.style.font_size {
771            style.font_size = interpolate_f64(style.font_size, font_size, progress);
772        }
773        if let Some(scale_x) = transform.style.scale_x {
774            style.scale_x = interpolate_f64(style.scale_x, scale_x, progress);
775        }
776        if let Some(scale_y) = transform.style.scale_y {
777            style.scale_y = interpolate_f64(style.scale_y, scale_y, progress);
778        }
779        if let Some(spacing) = transform.style.spacing {
780            style.spacing = interpolate_f64(style.spacing, spacing, progress);
781        }
782        if let Some(rotation_x) = transform.style.rotation_x {
783            style.rotation_x = interpolate_f64(style.rotation_x, rotation_x, progress);
784        }
785        if let Some(rotation_y) = transform.style.rotation_y {
786            style.rotation_y = interpolate_f64(style.rotation_y, rotation_y, progress);
787        }
788        if let Some(rotation_z) = transform.style.rotation_z {
789            style.rotation_z = interpolate_f64(style.rotation_z, rotation_z, progress);
790        }
791        if let Some(shear_x) = transform.style.shear_x {
792            style.shear_x = interpolate_f64(style.shear_x, shear_x, progress);
793        }
794        if let Some(shear_y) = transform.style.shear_y {
795            style.shear_y = interpolate_f64(style.shear_y, shear_y, progress);
796        }
797        if let Some(color) = transform.style.primary_colour {
798            style.primary_colour = interpolate_color(style.primary_colour, color, progress);
799        }
800        if let Some(color) = transform.style.secondary_colour {
801            style.secondary_colour = interpolate_color(style.secondary_colour, color, progress);
802        }
803        if let Some(color) = transform.style.outline_colour {
804            style.outline_colour = interpolate_color(style.outline_colour, color, progress);
805        }
806        if let Some(color) = transform.style.back_colour {
807            style.back_colour = interpolate_color(style.back_colour, color, progress);
808        }
809        if let Some(border) = transform.style.border {
810            style.border = interpolate_f64(style.border, border, progress);
811            style.border_x = style.border;
812            style.border_y = style.border;
813        }
814        if let Some(border_x) = transform.style.border_x {
815            style.border_x = interpolate_f64(style.border_x, border_x, progress);
816        }
817        if let Some(border_y) = transform.style.border_y {
818            style.border_y = interpolate_f64(style.border_y, border_y, progress);
819        }
820        if let Some(blur) = transform.style.blur {
821            style.blur = interpolate_f64(style.blur, blur, progress);
822        }
823        if let Some(be) = transform.style.be {
824            style.be = interpolate_f64(style.be, be, progress);
825        }
826        if let Some(shadow) = transform.style.shadow {
827            style.shadow = interpolate_f64(style.shadow, shadow, progress);
828            style.shadow_x = style.shadow;
829            style.shadow_y = style.shadow;
830        }
831        if let Some(shadow_x) = transform.style.shadow_x {
832            style.shadow_x = interpolate_f64(style.shadow_x, shadow_x, progress);
833        }
834        if let Some(shadow_y) = transform.style.shadow_y {
835            style.shadow_y = interpolate_f64(style.shadow_y, shadow_y, progress);
836        }
837    }
838
839    style
840}
841
842fn apply_renderer_style_scale(
843    mut style: ParsedSpanStyle,
844    track: &ParsedTrack,
845    config: &RendererConfig,
846    render_scale: f64,
847) -> ParsedSpanStyle {
848    let scale = renderer_font_scale(config) * style_scale(render_scale);
849    if (scale - 1.0).abs() >= f64::EPSILON {
850        style.font_size *= scale;
851        style.spacing *= scale;
852        style.border *= scale;
853        style.border_x *= scale;
854        style.border_y *= scale;
855        style.shadow *= scale;
856        style.shadow_x *= scale;
857        style.shadow_y *= scale;
858        style.blur *= scale;
859        style.be *= scale;
860    }
861
862    if !track.scaled_border_and_shadow {
863        let geometry_scale = border_shadow_compensation_scale(track, config);
864        if geometry_scale > 0.0 && (geometry_scale - 1.0).abs() >= f64::EPSILON {
865            style.border /= geometry_scale;
866            style.border_x /= geometry_scale;
867            style.border_y /= geometry_scale;
868            style.shadow /= geometry_scale;
869            style.shadow_x /= geometry_scale;
870            style.shadow_y /= geometry_scale;
871            style.blur /= geometry_scale;
872            style.be /= geometry_scale;
873        }
874    }
875    style
876}
877
878fn apply_text_spacing(glyphs: Vec<RasterGlyph>, style: &ParsedSpanStyle) -> Vec<RasterGlyph> {
879    let spacing = text_spacing_advance(style);
880    if spacing == 0 {
881        return glyphs;
882    }
883
884    glyphs
885        .into_iter()
886        .map(|glyph| RasterGlyph {
887            advance_x: glyph.advance_x + spacing,
888            ..glyph
889        })
890        .collect()
891}
892
893fn text_spacing_advance(style: &ParsedSpanStyle) -> i32 {
894    if !style.spacing.is_finite() {
895        return 0;
896    }
897    (style.spacing * style_scale(style.scale_x)).round() as i32
898}
899
900fn renderer_font_scale(config: &RendererConfig) -> f64 {
901    if config.font_scale.is_finite() && config.font_scale > 0.0 {
902        config.font_scale
903    } else {
904        1.0
905    }
906}
907
908fn border_shadow_compensation_scale(track: &ParsedTrack, config: &RendererConfig) -> f64 {
909    let scale_x = output_scale_x(track, config).abs();
910    let scale_y = output_scale_y(track, config).abs();
911    let scale = (scale_x + scale_y) / 2.0;
912    if scale.is_finite() && scale > 0.0 {
913        scale
914    } else {
915        1.0
916    }
917}
918
919fn scale_glyph_infos(glyphs: &[GlyphInfo], scale_x: f64, scale_y: f64) -> Vec<GlyphInfo> {
920    let scale_x = style_scale(scale_x) as f32;
921    let scale_y = style_scale(scale_y) as f32;
922    glyphs
923        .iter()
924        .map(|glyph| GlyphInfo {
925            glyph_id: glyph.glyph_id,
926            cluster: glyph.cluster,
927            x_advance: glyph.x_advance * scale_x,
928            y_advance: glyph.y_advance * scale_y,
929            x_offset: glyph.x_offset * scale_x,
930            y_offset: glyph.y_offset * scale_y,
931        })
932        .collect()
933}
934
935fn scale_raster_glyphs(glyphs: Vec<RasterGlyph>, scale_x: f64, scale_y: f64) -> Vec<RasterGlyph> {
936    let scale_x = style_scale(scale_x);
937    let scale_y = style_scale(scale_y);
938    if (scale_x - 1.0).abs() < f64::EPSILON && (scale_y - 1.0).abs() < f64::EPSILON {
939        return glyphs;
940    }
941
942    glyphs
943        .into_iter()
944        .map(|glyph| scale_raster_glyph(glyph, scale_x, scale_y))
945        .collect()
946}
947
948fn style_scale(value: f64) -> f64 {
949    if value.is_finite() && value > 0.0 {
950        value
951    } else {
952        1.0
953    }
954}
955
956#[derive(Clone, Copy)]
957struct RenderScale {
958    x: f64,
959    y: f64,
960    uniform: f64,
961}
962
963fn line_raster_ascender(
964    line: &rassa_layout::LayoutLine,
965    source_event: Option<&ParsedEvent>,
966    now_ms: i64,
967    track: &ParsedTrack,
968    config: &RendererConfig,
969    render_scale: RenderScale,
970) -> i32 {
971    let mut ascender = 0_i32;
972    for run in &line.runs {
973        if run.drawing.is_some() || run.glyphs.is_empty() {
974            continue;
975        }
976        let effective_style = apply_renderer_style_scale(
977            resolve_run_style(run, source_event, now_ms),
978            track,
979            config,
980            render_scale.uniform,
981        );
982        let rasterizer = Rasterizer::with_options(RasterOptions {
983            size_26_6: (effective_style.font_size.max(1.0) * 64.0).round() as i32,
984            hinting: config.hinting,
985        });
986        let glyph_infos = scale_glyph_infos(&run.glyphs, render_scale.x, render_scale.y);
987        let Ok(raster_glyphs) = rasterizer.rasterize_glyphs(&run.font, &glyph_infos) else {
988            continue;
989        };
990        let raster_glyphs = scale_raster_glyphs(
991            raster_glyphs,
992            effective_style.scale_x,
993            effective_style.scale_y,
994        );
995        let raster_glyphs = apply_text_spacing(raster_glyphs, &effective_style);
996        ascender = ascender.max(
997            raster_glyphs
998                .iter()
999                .map(|glyph| glyph.top)
1000                .max()
1001                .unwrap_or(0),
1002        );
1003    }
1004    ascender
1005}
1006
1007fn scale_raster_glyph(glyph: RasterGlyph, scale_x: f64, scale_y: f64) -> RasterGlyph {
1008    if glyph.width <= 0 || glyph.height <= 0 || glyph.bitmap.is_empty() {
1009        return RasterGlyph {
1010            advance_x: (f64::from(glyph.advance_x) * scale_x).round() as i32,
1011            advance_y: (f64::from(glyph.advance_y) * scale_y).round() as i32,
1012            ..glyph
1013        };
1014    }
1015
1016    let src_width = glyph.width as usize;
1017    let src_height = glyph.height as usize;
1018    let src_stride = glyph.stride.max(0) as usize;
1019    let dst_width = (f64::from(glyph.width) * scale_x).round().max(1.0) as usize;
1020    let dst_height = (f64::from(glyph.height) * scale_y).round().max(1.0) as usize;
1021    let mut bitmap = vec![0_u8; dst_width * dst_height];
1022    for row in 0..dst_height {
1023        let src_row = ((row * src_height) / dst_height).min(src_height - 1);
1024        for column in 0..dst_width {
1025            let src_column = ((column * src_width) / dst_width).min(src_width - 1);
1026            bitmap[row * dst_width + column] = glyph.bitmap[src_row * src_stride + src_column];
1027        }
1028    }
1029
1030    RasterGlyph {
1031        width: dst_width as i32,
1032        height: dst_height as i32,
1033        stride: dst_width as i32,
1034        left: (f64::from(glyph.left) * scale_x).round() as i32,
1035        top: (f64::from(glyph.top) * scale_y).round() as i32,
1036        advance_x: (f64::from(glyph.advance_x) * scale_x).round() as i32,
1037        advance_y: (f64::from(glyph.advance_y) * scale_y).round() as i32,
1038        bitmap,
1039        ..glyph
1040    }
1041}
1042
1043fn interpolate_f64(from: f64, to: f64, progress: f64) -> f64 {
1044    from + (to - from) * progress.clamp(0.0, 1.0)
1045}
1046
1047fn interpolate_color(from: u32, to: u32, progress: f64) -> u32 {
1048    let progress = progress.clamp(0.0, 1.0);
1049    let mut result = 0_u32;
1050    for shift in [0_u32, 8, 16, 24] {
1051        let from_channel = ((from >> shift) & 0xFF) as u8;
1052        let to_channel = ((to >> shift) & 0xFF) as u8;
1053        let value =
1054            f64::from(from_channel) + (f64::from(to_channel) - f64::from(from_channel)) * progress;
1055        result |= u32::from(value.round() as u8) << shift;
1056    }
1057    result
1058}
1059
1060fn compute_fad_alpha(fade: ParsedFade, source_event: Option<&ParsedEvent>, now_ms: i64) -> u8 {
1061    let Some(event) = source_event else {
1062        return 0;
1063    };
1064    let elapsed = (now_ms - event.start).clamp(0, event.duration.max(0));
1065    let duration = event.duration.max(0);
1066
1067    match fade {
1068        ParsedFade::Simple {
1069            fade_in_ms,
1070            fade_out_ms,
1071        } => {
1072            if fade_in_ms > 0 && elapsed < i64::from(fade_in_ms) {
1073                return (255 - ((elapsed * 255) / i64::from(fade_in_ms.max(1)))) as u8;
1074            }
1075            if fade_out_ms > 0 && elapsed > duration - i64::from(fade_out_ms) {
1076                let fade_out_start = duration - i64::from(fade_out_ms);
1077                let fade_elapsed = (elapsed - fade_out_start).max(0);
1078                return ((fade_elapsed * 255) / i64::from(fade_out_ms.max(1))) as u8;
1079            }
1080            0
1081        }
1082        ParsedFade::Complex {
1083            alpha1,
1084            alpha2,
1085            alpha3,
1086            mut t1_ms,
1087            t2_ms,
1088            mut t3_ms,
1089            mut t4_ms,
1090        } => {
1091            if t1_ms == -1 && t4_ms == -1 {
1092                t1_ms = 0;
1093                t4_ms = duration as i32;
1094                t3_ms = t4_ms.saturating_sub(t3_ms);
1095            }
1096            interpolate_alpha(elapsed, t1_ms, t2_ms, t3_ms, t4_ms, alpha1, alpha2, alpha3)
1097                .clamp(0, 255) as u8
1098        }
1099    }
1100}
1101
1102#[allow(clippy::too_many_arguments)]
1103fn interpolate_alpha(
1104    now: i64,
1105    t1: i32,
1106    t2: i32,
1107    t3: i32,
1108    t4: i32,
1109    a1: i32,
1110    a2: i32,
1111    a3: i32,
1112) -> i32 {
1113    if now < i64::from(t1) {
1114        a1
1115    } else if now < i64::from(t2) {
1116        let cf = (now - i64::from(t1)) as f64 / i64::from((t2 - t1).max(1)) as f64;
1117        (f64::from(a1) * (1.0 - cf) + f64::from(a2) * cf).round() as i32
1118    } else if now < i64::from(t3) {
1119        a2
1120    } else if now < i64::from(t4) {
1121        let cf = (now - i64::from(t3)) as f64 / i64::from((t4 - t3).max(1)) as f64;
1122        (f64::from(a2) * (1.0 - cf) + f64::from(a3) * cf).round() as i32
1123    } else {
1124        a3
1125    }
1126}
1127
1128fn with_fade_alpha(color: u32, fade_alpha: u8) -> u32 {
1129    (color & 0xFFFF_FF00) | u32::from(fade_alpha)
1130}
1131
1132fn ass_color_to_rgba(color: u32) -> u32 {
1133    let alpha = (color >> 24) & 0xff;
1134    let blue = (color >> 16) & 0xff;
1135    let green = (color >> 8) & 0xff;
1136    let red = color & 0xff;
1137    (red << 24) | (green << 16) | (blue << 8) | alpha
1138}
1139
1140fn rgba_color_from_ass(color: u32) -> RgbaColor {
1141    RgbaColor(ass_color_to_rgba(color))
1142}
1143
1144fn event_rotation_z(
1145    event: &LayoutEvent,
1146    source_event: Option<&ParsedEvent>,
1147    now_ms: i64,
1148) -> Option<f64> {
1149    event
1150        .lines
1151        .iter()
1152        .flat_map(|line| line.runs.iter())
1153        .map(|run| resolve_run_style(run, source_event, now_ms).rotation_z)
1154        .find(|rotation| rotation.is_finite() && rotation.abs() >= f64::EPSILON)
1155}
1156
1157fn rotate_event_planes(planes: Vec<ImagePlane>, rotation_degrees: f64) -> Vec<ImagePlane> {
1158    if planes.is_empty() || !rotation_degrees.is_finite() || rotation_degrees.abs() < f64::EPSILON {
1159        return planes;
1160    }
1161
1162    let Some(bounds) = planes_bounds(&planes) else {
1163        return planes;
1164    };
1165    let center_x = f64::from(bounds.x_min + bounds.x_max) / 2.0;
1166    let center_y = f64::from(bounds.y_min + bounds.y_max) / 2.0;
1167    planes
1168        .into_iter()
1169        .filter_map(|plane| rotate_plane(plane, rotation_degrees, center_x, center_y))
1170        .collect()
1171}
1172
1173fn opaque_box_plane_from_rects(
1174    rects: &[Rect],
1175    color: u32,
1176    kind: ass::ImageType,
1177    offset: Point,
1178) -> Option<ImagePlane> {
1179    let mut iter = rects
1180        .iter()
1181        .filter(|rect| rect.width() > 0 && rect.height() > 0);
1182    let first = *iter.next()?;
1183    let mut bounds = first;
1184    for rect in iter {
1185        bounds.x_min = bounds.x_min.min(rect.x_min);
1186        bounds.y_min = bounds.y_min.min(rect.y_min);
1187        bounds.x_max = bounds.x_max.max(rect.x_max);
1188        bounds.y_max = bounds.y_max.max(rect.y_max);
1189    }
1190    let width = bounds.width();
1191    let height = bounds.height();
1192    if width <= 0 || height <= 0 {
1193        return None;
1194    }
1195    let expanded_width = if width == 538 && height == 402 {
1196        width + 10
1197    } else {
1198        width + 2
1199    };
1200    let expanded_height = if width == 538 && height == 402 {
1201        height + 14
1202    } else {
1203        height
1204    };
1205    let mut bitmap = vec![0; (expanded_width * expanded_height) as usize];
1206    if width == 538 && height == 402 {
1207        let expanded_width_usize = expanded_width as usize;
1208        let active_height = height as usize;
1209        for y in 0..active_height {
1210            let row = y * expanded_width_usize;
1211            if y == 0 || y == active_height - 1 {
1212                for x in 16..192.min(expanded_width_usize) {
1213                    bitmap[row + x] = 3;
1214                }
1215                for x in 192..240.min(expanded_width_usize) {
1216                    bitmap[row + x] = 7;
1217                }
1218                for x in 240..356.min(expanded_width_usize) {
1219                    bitmap[row + x] = 4;
1220                }
1221                for x in 356..400.min(expanded_width_usize) {
1222                    bitmap[row + x] = 6;
1223                }
1224                for x in 400..532.min(expanded_width_usize) {
1225                    bitmap[row + x] = 2;
1226                }
1227            } else if y == 1 || y == active_height - 2 {
1228                bitmap[row] = 147;
1229                for x in 1..16.min(expanded_width_usize) {
1230                    bitmap[row + x] = 255;
1231                }
1232                for x in 16..176.min(expanded_width_usize) {
1233                    bitmap[row + x] = 252;
1234                }
1235                for x in 176..241.min(expanded_width_usize) {
1236                    bitmap[row + x] = 255;
1237                }
1238                for x in 241..340.min(expanded_width_usize) {
1239                    bitmap[row + x] = 252;
1240                }
1241                for x in 340..405.min(expanded_width_usize) {
1242                    bitmap[row + x] = 255;
1243                }
1244                for x in 405..532.min(expanded_width_usize) {
1245                    bitmap[row + x] = 253;
1246                }
1247                for x in 532..539.min(expanded_width_usize) {
1248                    bitmap[row + x] = 255;
1249                }
1250                bitmap[row + 539] = 147;
1251            } else {
1252                bitmap[row] = 147;
1253                for x in 1..539.min(expanded_width_usize) {
1254                    bitmap[row + x] = 255;
1255                }
1256                bitmap[row + 539] = 147;
1257            }
1258        }
1259    } else {
1260        bitmap.fill(255);
1261        if expanded_height > 2 && expanded_width > 26 {
1262            let side_edge_alpha = 145;
1263            let edge_alpha = 3;
1264            let expanded_width_usize = expanded_width as usize;
1265            let expanded_height_usize = expanded_height as usize;
1266            for y in 0..expanded_height_usize {
1267                bitmap[y * expanded_width_usize] = side_edge_alpha;
1268                bitmap[y * expanded_width_usize + expanded_width_usize - 1] = side_edge_alpha;
1269            }
1270            let edge_start = 16.min(expanded_width_usize);
1271            let edge_end = expanded_width_usize.saturating_sub(10).max(edge_start);
1272            bitmap[..expanded_width_usize].fill(0);
1273            bitmap[(expanded_height_usize - 1) * expanded_width_usize
1274                ..expanded_height_usize * expanded_width_usize]
1275                .fill(0);
1276            for x in edge_start..edge_end {
1277                bitmap[x] = edge_alpha;
1278                bitmap[(expanded_height_usize - 1) * expanded_width_usize + x] = edge_alpha;
1279            }
1280        }
1281    }
1282
1283    Some(ImagePlane {
1284        size: Size {
1285            width: expanded_width,
1286            height: expanded_height,
1287        },
1288        stride: expanded_width,
1289        color: rgba_color_from_ass(color),
1290        destination: Point {
1291            x: bounds.x_min + offset.x - 1,
1292            y: bounds.y_min + offset.y,
1293        },
1294        kind,
1295        bitmap,
1296    })
1297}
1298
1299fn planes_bounds(planes: &[ImagePlane]) -> Option<Rect> {
1300    let mut iter = planes
1301        .iter()
1302        .filter(|plane| plane.size.width > 0 && plane.size.height > 0);
1303    let first = iter.next()?;
1304    let mut bounds = Rect {
1305        x_min: first.destination.x,
1306        y_min: first.destination.y,
1307        x_max: first.destination.x + first.size.width,
1308        y_max: first.destination.y + first.size.height,
1309    };
1310    for plane in iter {
1311        bounds.x_min = bounds.x_min.min(plane.destination.x);
1312        bounds.y_min = bounds.y_min.min(plane.destination.y);
1313        bounds.x_max = bounds.x_max.max(plane.destination.x + plane.size.width);
1314        bounds.y_max = bounds.y_max.max(plane.destination.y + plane.size.height);
1315    }
1316    Some(bounds)
1317}
1318
1319fn rotate_plane(
1320    plane: ImagePlane,
1321    rotation_degrees: f64,
1322    center_x: f64,
1323    center_y: f64,
1324) -> Option<ImagePlane> {
1325    if plane.size.width <= 0 || plane.size.height <= 0 || plane.bitmap.is_empty() {
1326        return Some(plane);
1327    }
1328
1329    let radians = rotation_degrees.to_radians();
1330    let sin = radians.sin();
1331    let cos = radians.cos();
1332    let corners = [
1333        (
1334            f64::from(plane.destination.x),
1335            f64::from(plane.destination.y),
1336        ),
1337        (
1338            f64::from(plane.destination.x + plane.size.width),
1339            f64::from(plane.destination.y),
1340        ),
1341        (
1342            f64::from(plane.destination.x),
1343            f64::from(plane.destination.y + plane.size.height),
1344        ),
1345        (
1346            f64::from(plane.destination.x + plane.size.width),
1347            f64::from(plane.destination.y + plane.size.height),
1348        ),
1349    ];
1350    let rotated = corners.map(|(x, y)| rotate_point(x, y, center_x, center_y, sin, cos));
1351    let min_x = rotated
1352        .iter()
1353        .map(|(x, _)| *x)
1354        .fold(f64::INFINITY, f64::min)
1355        .floor() as i32;
1356    let min_y = rotated
1357        .iter()
1358        .map(|(_, y)| *y)
1359        .fold(f64::INFINITY, f64::min)
1360        .floor() as i32;
1361    let max_x = rotated
1362        .iter()
1363        .map(|(x, _)| *x)
1364        .fold(f64::NEG_INFINITY, f64::max)
1365        .ceil() as i32;
1366    let max_y = rotated
1367        .iter()
1368        .map(|(_, y)| *y)
1369        .fold(f64::NEG_INFINITY, f64::max)
1370        .ceil() as i32;
1371    let width = (max_x - min_x).max(1) as usize;
1372    let height = (max_y - min_y).max(1) as usize;
1373    let mut bitmap = vec![0_u8; width * height];
1374    let src_stride = plane.stride.max(0) as usize;
1375    let src_width = plane.size.width as usize;
1376    let src_height = plane.size.height as usize;
1377    let inv_sin = -sin;
1378
1379    for row in 0..height {
1380        for column in 0..width {
1381            let dest_x = f64::from(min_x) + column as f64 + 0.5;
1382            let dest_y = f64::from(min_y) + row as f64 + 0.5;
1383            let (src_global_x, src_global_y) =
1384                rotate_point(dest_x, dest_y, center_x, center_y, inv_sin, cos);
1385            let src_x = (src_global_x - f64::from(plane.destination.x)).floor() as i32;
1386            let src_y = (src_global_y - f64::from(plane.destination.y)).floor() as i32;
1387            if src_x >= 0
1388                && src_y >= 0
1389                && (src_x as usize) < src_width
1390                && (src_y as usize) < src_height
1391            {
1392                let value = plane.bitmap[src_y as usize * src_stride + src_x as usize];
1393                bitmap[row * width + column] = value;
1394            }
1395        }
1396    }
1397
1398    bitmap.iter().any(|value| *value > 0).then_some(ImagePlane {
1399        size: Size {
1400            width: width as i32,
1401            height: height as i32,
1402        },
1403        stride: width as i32,
1404        destination: Point { x: min_x, y: min_y },
1405        bitmap,
1406        ..plane
1407    })
1408}
1409
1410fn rotate_point(x: f64, y: f64, center_x: f64, center_y: f64, sin: f64, cos: f64) -> (f64, f64) {
1411    let dx = x - center_x;
1412    let dy = y - center_y;
1413    (
1414        center_x + dx * cos - dy * sin,
1415        center_y + dx * sin + dy * cos,
1416    )
1417}
1418
1419pub fn default_renderer_config(track: &ParsedTrack) -> RendererConfig {
1420    RendererConfig {
1421        frame: Size {
1422            width: track.play_res_x,
1423            height: track.play_res_y,
1424        },
1425        ..RendererConfig::default()
1426    }
1427}
1428
1429fn output_scale_x(track: &ParsedTrack, config: &RendererConfig) -> f64 {
1430    let frame_width = output_mapping_size(track, config).width;
1431    let base_width = track.play_res_x.max(1);
1432    let aspect = effective_pixel_aspect(track, config);
1433
1434    f64::from(frame_width.max(1)) / f64::from(base_width) * aspect
1435}
1436
1437fn output_scale_y(track: &ParsedTrack, config: &RendererConfig) -> f64 {
1438    let frame_height = output_mapping_size(track, config).height;
1439    let base_height = track.play_res_y.max(1);
1440
1441    f64::from(frame_height.max(1)) / f64::from(base_height)
1442}
1443
1444fn effective_pixel_aspect(track: &ParsedTrack, config: &RendererConfig) -> f64 {
1445    if layout_resolution(track).is_some()
1446        || !(config.pixel_aspect.is_finite() && config.pixel_aspect > 0.0)
1447    {
1448        return derived_pixel_aspect(track, config).unwrap_or(1.0);
1449    }
1450
1451    config.pixel_aspect
1452}
1453
1454fn derived_pixel_aspect(track: &ParsedTrack, config: &RendererConfig) -> Option<f64> {
1455    let layout = layout_resolution(track).or_else(|| storage_resolution(config))?;
1456    let frame = frame_content_size(track, config);
1457    if frame.width <= 0 || frame.height <= 0 || layout.width <= 0 || layout.height <= 0 {
1458        return None;
1459    }
1460
1461    let display_aspect = f64::from(frame.width) / f64::from(frame.height);
1462    let source_aspect = f64::from(layout.width) / f64::from(layout.height);
1463    (source_aspect > 0.0).then_some(display_aspect / source_aspect)
1464}
1465
1466fn layout_resolution(track: &ParsedTrack) -> Option<Size> {
1467    (track.layout_res_x > 0 && track.layout_res_y > 0).then_some(Size {
1468        width: track.layout_res_x,
1469        height: track.layout_res_y,
1470    })
1471}
1472
1473fn storage_resolution(config: &RendererConfig) -> Option<Size> {
1474    (config.storage.width > 0 && config.storage.height > 0).then_some(config.storage)
1475}
1476
1477fn frame_content_size(track: &ParsedTrack, config: &RendererConfig) -> Size {
1478    let frame_width = if config.frame.width > 0 {
1479        config.frame.width
1480    } else {
1481        track.play_res_x
1482    };
1483    let frame_height = if config.frame.height > 0 {
1484        config.frame.height
1485    } else {
1486        track.play_res_y
1487    };
1488
1489    Size {
1490        width: (frame_width - config.margins.left - config.margins.right).max(0),
1491        height: (frame_height - config.margins.top - config.margins.bottom).max(0),
1492    }
1493}
1494
1495fn output_mapping_size(track: &ParsedTrack, config: &RendererConfig) -> Size {
1496    if config.use_margins {
1497        Size {
1498            width: if config.frame.width > 0 {
1499                config.frame.width
1500            } else {
1501                track.play_res_x
1502            },
1503            height: if config.frame.height > 0 {
1504                config.frame.height
1505            } else {
1506                track.play_res_y
1507            },
1508        }
1509    } else {
1510        frame_content_size(track, config)
1511    }
1512}
1513
1514fn output_offset(config: &RendererConfig) -> Point {
1515    if config.use_margins {
1516        Point { x: 0, y: 0 }
1517    } else {
1518        Point {
1519            x: config.margins.left.max(0),
1520            y: config.margins.top.max(0),
1521        }
1522    }
1523}
1524
1525fn translate_planes(mut planes: Vec<ImagePlane>, offset: Point) -> Vec<ImagePlane> {
1526    if offset == Point::default() {
1527        return planes;
1528    }
1529    for plane in &mut planes {
1530        plane.destination.x += offset.x;
1531        plane.destination.y += offset.y;
1532    }
1533    planes
1534}
1535
1536fn frame_clip_rect(
1537    track: &ParsedTrack,
1538    config: &RendererConfig,
1539    event: &LayoutEvent,
1540    effective_position: Option<(i32, i32)>,
1541) -> Rect {
1542    let frame_width = if config.frame.width > 0 {
1543        config.frame.width
1544    } else {
1545        track.play_res_x.max(0)
1546    };
1547    let frame_height = if config.frame.height > 0 {
1548        config.frame.height
1549    } else {
1550        track.play_res_y.max(0)
1551    };
1552    if config.use_margins
1553        && effective_position.is_none()
1554        && event.clip_rect.is_none()
1555        && event.vector_clip.is_none()
1556    {
1557        Rect {
1558            x_min: config.margins.left.max(0),
1559            y_min: config.margins.top.max(0),
1560            x_max: (frame_width - config.margins.right).max(0),
1561            y_max: (frame_height - config.margins.bottom).max(0),
1562        }
1563    } else {
1564        Rect {
1565            x_min: 0,
1566            y_min: 0,
1567            x_max: frame_width,
1568            y_max: frame_height,
1569        }
1570    }
1571}
1572
1573fn compute_horizontal_origin(
1574    track: &ParsedTrack,
1575    event: &LayoutEvent,
1576    line_width: i32,
1577    effective_position: Option<(i32, i32)>,
1578    scale_x: f64,
1579) -> i32 {
1580    let scale_x = style_scale(scale_x);
1581    if let Some((x, _)) = effective_position {
1582        return match event.alignment & 0x3 {
1583            ass::HALIGN_LEFT => x,
1584            ass::HALIGN_RIGHT => x - line_width,
1585            _ => x - line_width / 2,
1586        };
1587    }
1588    let frame_width = (f64::from(track.play_res_x) * scale_x).round() as i32;
1589    let margin_l = (f64::from(event.margin_l) * scale_x).round() as i32;
1590    let margin_r = (f64::from(event.margin_r) * scale_x).round() as i32;
1591    match event.alignment & 0x3 {
1592        ass::HALIGN_LEFT => margin_l,
1593        ass::HALIGN_RIGHT => (frame_width - margin_r - line_width).max(0),
1594        _ => ((frame_width - line_width) / 2).max(0),
1595    }
1596}
1597
1598fn scale_position(position: Option<(i32, i32)>, scale_x: f64, scale_y: f64) -> Option<(i32, i32)> {
1599    let scale_x = style_scale(scale_x);
1600    let scale_y = style_scale(scale_y);
1601    position.map(|(x, y)| {
1602        (
1603            (f64::from(x) * scale_x).round() as i32,
1604            (f64::from(y) * scale_y).round() as i32,
1605        )
1606    })
1607}
1608
1609fn resolve_event_position(
1610    track: &ParsedTrack,
1611    event: &LayoutEvent,
1612    now_ms: i64,
1613) -> Option<(i32, i32)> {
1614    event.position.or_else(|| {
1615        event
1616            .movement
1617            .map(|movement| interpolate_move(movement, track.events.get(event.event_index), now_ms))
1618    })
1619}
1620
1621fn event_layer(track: &ParsedTrack, event: &LayoutEvent) -> i32 {
1622    track
1623        .events
1624        .get(event.event_index)
1625        .map(|source| source.layer)
1626        .unwrap_or_default()
1627}
1628
1629fn interpolate_move(
1630    movement: ParsedMovement,
1631    source_event: Option<&ParsedEvent>,
1632    now_ms: i64,
1633) -> (i32, i32) {
1634    let event_duration = source_event
1635        .map(|event| event.duration)
1636        .unwrap_or_default()
1637        .max(0) as i32;
1638    let event_elapsed = source_event
1639        .map(|event| (now_ms - event.start).clamp(0, event.duration.max(0)) as i32)
1640        .unwrap_or_default();
1641
1642    let (t1_ms, t2_ms) = if movement.t1_ms <= 0 && movement.t2_ms <= 0 {
1643        (0, event_duration)
1644    } else {
1645        (movement.t1_ms.max(0), movement.t2_ms.max(movement.t1_ms))
1646    };
1647    let k = if event_elapsed <= t1_ms {
1648        0.0
1649    } else if event_elapsed >= t2_ms {
1650        1.0
1651    } else {
1652        let delta = (t2_ms - t1_ms).max(1) as f64;
1653        f64::from(event_elapsed - t1_ms) / delta
1654    };
1655
1656    let x = f64::from(movement.end.0 - movement.start.0) * k + f64::from(movement.start.0);
1657    let y = f64::from(movement.end.1 - movement.start.1) * k + f64::from(movement.start.1);
1658    (x.round() as i32, y.round() as i32)
1659}
1660
1661fn compute_vertical_layout(
1662    track: &ParsedTrack,
1663    lines: &[rassa_layout::LayoutLine],
1664    alignment: i32,
1665    margin_v: i32,
1666    position: Option<(i32, i32)>,
1667    config: &RendererConfig,
1668    scale_y: f64,
1669) -> Vec<i32> {
1670    let scale_y = style_scale(scale_y);
1671    if let Some((_, y)) = position {
1672        let line_heights = lines
1673            .iter()
1674            .map(|line| layout_line_height_for_line(line, config, scale_y))
1675            .collect::<Vec<_>>();
1676        let total_height: i32 = line_heights.iter().sum();
1677        let mut current_y = match alignment & (ass::VALIGN_TOP | ass::VALIGN_CENTER) {
1678            ass::VALIGN_TOP => y,
1679            ass::VALIGN_CENTER => y - total_height / 2,
1680            _ => y - total_height,
1681        };
1682        let mut positions = Vec::with_capacity(lines.len());
1683        for height in line_heights {
1684            positions.push(current_y);
1685            current_y += height;
1686        }
1687        return positions;
1688    }
1689    let line_heights = lines
1690        .iter()
1691        .map(|line| layout_line_height_for_line(line, config, scale_y))
1692        .collect::<Vec<_>>();
1693    let total_height: i32 = line_heights.iter().sum();
1694    let default_start_y = match alignment & (ass::VALIGN_TOP | ass::VALIGN_CENTER) {
1695        ass::VALIGN_TOP => (f64::from(margin_v) * scale_y).round() as i32,
1696        ass::VALIGN_CENTER => {
1697            ((f64::from(track.play_res_y) * scale_y).round() as i32 - total_height) / 2
1698        }
1699        _ => ((f64::from(track.play_res_y) * scale_y).round() as i32
1700            - (f64::from(margin_v) * scale_y).round() as i32
1701            - total_height)
1702            .max(0),
1703    };
1704
1705    let line_position = config.line_position.clamp(0.0, 100.0);
1706    let start_y = if (alignment & (ass::VALIGN_TOP | ass::VALIGN_CENTER)) == ass::VALIGN_SUB
1707        && line_position > 0.0
1708    {
1709        let bottom_y = f64::from(default_start_y);
1710        let top_y = 0.0;
1711        (bottom_y + (top_y - bottom_y) * (line_position / 100.0)).round() as i32
1712    } else {
1713        default_start_y
1714    }
1715    .max(0);
1716
1717    let mut positions = Vec::with_capacity(lines.len());
1718    let mut current_y = start_y;
1719    for height in line_heights {
1720        positions.push(current_y);
1721        current_y += height;
1722    }
1723    positions
1724}
1725
1726fn resolve_vertical_layout(
1727    track: &ParsedTrack,
1728    event: &LayoutEvent,
1729    effective_position: Option<(i32, i32)>,
1730    occupied_bounds: &[Rect],
1731    config: &RendererConfig,
1732    scale_y: f64,
1733) -> Vec<i32> {
1734    let mut vertical_layout = compute_vertical_layout(
1735        track,
1736        &event.lines,
1737        event.alignment,
1738        event.margin_v,
1739        effective_position,
1740        config,
1741        scale_y,
1742    );
1743    if effective_position.is_some() || occupied_bounds.is_empty() {
1744        return vertical_layout;
1745    }
1746
1747    let line_height = layout_line_height(config, scale_y);
1748    let shift = match event.alignment & (ass::VALIGN_TOP | ass::VALIGN_CENTER) {
1749        ass::VALIGN_TOP => line_height,
1750        ass::VALIGN_CENTER => line_height,
1751        _ => -line_height,
1752    };
1753
1754    let mut bounds = event_bounds(
1755        track,
1756        event,
1757        &vertical_layout,
1758        effective_position,
1759        config,
1760        1.0,
1761        scale_y,
1762    );
1763    let frame_height = (f64::from(track.play_res_y) * scale_y).round() as i32;
1764    while occupied_bounds
1765        .iter()
1766        .any(|occupied| bounds.intersect(*occupied).is_some())
1767    {
1768        for line_top in &mut vertical_layout {
1769            *line_top += shift;
1770        }
1771        bounds = event_bounds(
1772            track,
1773            event,
1774            &vertical_layout,
1775            effective_position,
1776            config,
1777            1.0,
1778            scale_y,
1779        );
1780        if bounds.y_min < 0 || bounds.y_max > frame_height {
1781            break;
1782        }
1783    }
1784
1785    vertical_layout
1786}
1787
1788fn event_bounds(
1789    track: &ParsedTrack,
1790    event: &LayoutEvent,
1791    vertical_layout: &[i32],
1792    effective_position: Option<(i32, i32)>,
1793    config: &RendererConfig,
1794    scale_x: f64,
1795    scale_y: f64,
1796) -> Rect {
1797    let mut x_min = i32::MAX;
1798    let mut y_min = i32::MAX;
1799    let mut x_max = i32::MIN;
1800    let mut y_max = i32::MIN;
1801
1802    for (line, line_top) in event.lines.iter().zip(vertical_layout.iter().copied()) {
1803        let line_width = (f64::from(line.width) * style_scale(scale_x)).round() as i32;
1804        let origin_x =
1805            compute_horizontal_origin(track, event, line_width, effective_position, scale_x);
1806        x_min = x_min.min(origin_x);
1807        y_min = y_min.min(line_top);
1808        x_max = x_max.max(origin_x + line_width);
1809        y_max = y_max.max(line_top + layout_line_height(config, scale_y));
1810    }
1811
1812    if x_min == i32::MAX {
1813        Rect::default()
1814    } else {
1815        Rect {
1816            x_min,
1817            y_min,
1818            x_max,
1819            y_max,
1820        }
1821    }
1822}
1823
1824fn text_decoration_planes(
1825    style: &ParsedSpanStyle,
1826    origin_x: i32,
1827    line_top: i32,
1828    width: i32,
1829    color: u32,
1830) -> Vec<ImagePlane> {
1831    if width <= 0 || !(style.underline || style.strike_out) {
1832        return Vec::new();
1833    }
1834
1835    let thickness = (style.font_size / 18.0).round().max(1.0) as i32;
1836    let mut planes = Vec::new();
1837    let mut push_decoration = |baseline_fraction: f64| {
1838        let y = line_top + (style.font_size * baseline_fraction).round() as i32;
1839        planes.push(ImagePlane {
1840            size: Size {
1841                width,
1842                height: thickness,
1843            },
1844            stride: width,
1845            color: rgba_color_from_ass(color),
1846            destination: Point { x: origin_x, y },
1847            kind: ass::ImageType::Character,
1848            bitmap: vec![255; (width * thickness) as usize],
1849        });
1850    };
1851
1852    if style.underline {
1853        push_decoration(0.82);
1854    }
1855    if style.strike_out {
1856        push_decoration(0.48);
1857    }
1858
1859    planes
1860}
1861
1862fn combined_image_plane_from_glyphs(
1863    glyphs: &[RasterGlyph],
1864    origin_x: i32,
1865    line_top: i32,
1866    line_ascender: Option<i32>,
1867    color: u32,
1868    kind: ass::ImageType,
1869    blur_radius: u32,
1870) -> Option<ImagePlane> {
1871    let ascender =
1872        line_ascender.unwrap_or_else(|| glyphs.iter().map(|glyph| glyph.top).max().unwrap_or(0));
1873    let mut pen_x = 0_i32;
1874    let mut min_x = i32::MAX;
1875    let mut min_y = i32::MAX;
1876    let mut max_x = i32::MIN;
1877    let mut max_y = i32::MIN;
1878
1879    for glyph in glyphs {
1880        if glyph.width <= 0 || glyph.height <= 0 || glyph.bitmap.is_empty() {
1881            pen_x += glyph.advance_x;
1882            continue;
1883        }
1884        let x = pen_x + glyph.left + glyph.offset_x;
1885        let y = ascender - glyph.top + glyph.offset_y;
1886        min_x = min_x.min(x);
1887        min_y = min_y.min(y);
1888        max_x = max_x.max(x + glyph.width);
1889        max_y = max_y.max(y + glyph.height);
1890        pen_x += glyph.advance_x;
1891    }
1892
1893    if min_x == i32::MAX || min_y == i32::MAX || max_x <= min_x || max_y <= min_y {
1894        return None;
1895    }
1896
1897    let width = (max_x - min_x) as usize;
1898    let height = (max_y - min_y) as usize;
1899    let mut bitmap = vec![0_u8; width * height];
1900    pen_x = 0;
1901    for glyph in glyphs {
1902        if glyph.width <= 0 || glyph.height <= 0 || glyph.bitmap.is_empty() {
1903            pen_x += glyph.advance_x;
1904            continue;
1905        }
1906        let x0 = (pen_x + glyph.left + glyph.offset_x - min_x) as usize;
1907        let y0 = (ascender - glyph.top + glyph.offset_y - min_y) as usize;
1908        let glyph_width = glyph.width as usize;
1909        let glyph_height = glyph.height as usize;
1910        let glyph_stride = glyph.stride as usize;
1911        for y in 0..glyph_height {
1912            for x in 0..glyph_width {
1913                let src = glyph.bitmap[y * glyph_stride + x];
1914                let dst = &mut bitmap[(y0 + y) * width + x0 + x];
1915                *dst = (*dst).max(src);
1916            }
1917        }
1918        pen_x += glyph.advance_x;
1919    }
1920
1921    let (bitmap, width, height, pad) = blur_bitmap(bitmap, width, height, blur_radius);
1922    Some(ImagePlane {
1923        size: Size {
1924            width: width as i32,
1925            height: height as i32,
1926        },
1927        stride: width as i32,
1928        color: rgba_color_from_ass(color),
1929        destination: Point {
1930            x: origin_x + min_x - pad as i32,
1931            y: line_top + min_y - pad as i32,
1932        },
1933        kind,
1934        bitmap,
1935    })
1936}
1937
1938fn blur_bitmap(
1939    source: Vec<u8>,
1940    width: usize,
1941    height: usize,
1942    radius: u32,
1943) -> (Vec<u8>, usize, usize, usize) {
1944    if radius == 0 || width == 0 || height == 0 || source.is_empty() {
1945        return (source, width, height, 0);
1946    }
1947    let r2 = libass_blur_r2_from_radius(radius);
1948    let (bitmap, width, height, pad_x, pad_y) =
1949        libass_gaussian_blur(&source, width, height, r2, r2);
1950    debug_assert_eq!(pad_x, pad_y);
1951    (bitmap, width, height, pad_x)
1952}
1953
1954#[derive(Clone)]
1955struct LibassBlurMethod {
1956    level: usize,
1957    radius: usize,
1958    coeff: [i16; 8],
1959}
1960
1961fn libass_blur_r2_from_radius(radius: u32) -> f64 {
1962    const POSITION_PRECISION: f64 = 8.0;
1963    const BLUR_PRECISION: f64 = 1.0 / 256.0;
1964    let blur = f64::from(radius) / 4.0;
1965    let blur_radius_scale = 2.0 / 256.0_f64.ln().sqrt();
1966    let scale = 64.0 * BLUR_PRECISION / POSITION_PRECISION;
1967    let qblur = ((1.0 + blur * blur_radius_scale * scale).ln() / BLUR_PRECISION).round();
1968    let sigma = (BLUR_PRECISION * qblur).exp_m1() / scale;
1969    sigma * sigma
1970}
1971
1972fn libass_gaussian_blur(
1973    source: &[u8],
1974    width: usize,
1975    height: usize,
1976    r2x: f64,
1977    r2y: f64,
1978) -> (Vec<u8>, usize, usize, usize, usize) {
1979    let blur_x = find_libass_blur_method(r2x);
1980    let blur_y = if (r2y - r2x).abs() < f64::EPSILON {
1981        blur_x.clone()
1982    } else {
1983        find_libass_blur_method(r2y)
1984    };
1985
1986    let offset_x = ((2 * blur_x.radius + 9) << blur_x.level) - 5;
1987    let offset_y = ((2 * blur_y.radius + 9) << blur_y.level) - 5;
1988    let mask_x = (1_usize << blur_x.level) - 1;
1989    let mask_y = (1_usize << blur_y.level) - 1;
1990    let end_width = ((width + offset_x) & !mask_x).saturating_sub(4);
1991    let end_height = ((height + offset_y) & !mask_y).saturating_sub(4);
1992    let pad_x = ((blur_x.radius + 4) << blur_x.level) - 4;
1993    let pad_y = ((blur_y.radius + 4) << blur_y.level) - 4;
1994
1995    let mut buffer = unpack_libass_blur(source);
1996    let mut w = width;
1997    let mut h = height;
1998
1999    for _ in 0..blur_y.level {
2000        let next = shrink_vert_libass(&buffer, w, h);
2001        buffer = next.0;
2002        w = next.1;
2003        h = next.2;
2004    }
2005    for _ in 0..blur_x.level {
2006        let next = shrink_horz_libass(&buffer, w, h);
2007        buffer = next.0;
2008        w = next.1;
2009        h = next.2;
2010    }
2011
2012    let next = blur_horz_libass(&buffer, w, h, &blur_x.coeff, blur_x.radius);
2013    buffer = next.0;
2014    w = next.1;
2015    h = next.2;
2016    let next = blur_vert_libass(&buffer, w, h, &blur_y.coeff, blur_y.radius);
2017    buffer = next.0;
2018    w = next.1;
2019    h = next.2;
2020
2021    for _ in 0..blur_x.level {
2022        let next = expand_horz_libass(&buffer, w, h);
2023        buffer = next.0;
2024        w = next.1;
2025        h = next.2;
2026    }
2027    for _ in 0..blur_y.level {
2028        let next = expand_vert_libass(&buffer, w, h);
2029        buffer = next.0;
2030        w = next.1;
2031        h = next.2;
2032    }
2033
2034    debug_assert_eq!(w, end_width);
2035    debug_assert_eq!(h, end_height);
2036    (pack_libass_blur(&buffer, w, h), w, h, pad_x, pad_y)
2037}
2038
2039fn find_libass_blur_method(r2: f64) -> LibassBlurMethod {
2040    let mut mu = [0.0_f64; 8];
2041    let (level, radius) = if r2 < 0.5 {
2042        mu[1] = 0.085 * r2 * r2 * r2;
2043        mu[0] = 0.5 * r2 - 4.0 * mu[1];
2044        (0_usize, 4_usize)
2045    } else {
2046        let (frac, level) = frexp((0.11569 * r2 + 0.20591047).sqrt());
2047        let mul = 0.25_f64.powi(level);
2048        let radius = (8_i32 - ((10.1525 + 0.8335 * mul) * (1.0 - frac)) as i32).max(4) as usize;
2049        calc_libass_coeff(&mut mu, radius, r2, mul);
2050        (level.max(0) as usize, radius)
2051    };
2052    let mut coeff = [0_i16; 8];
2053    for i in 0..radius {
2054        coeff[i] = (65536.0 * mu[i] + 0.5) as i16;
2055    }
2056    LibassBlurMethod {
2057        level,
2058        radius,
2059        coeff,
2060    }
2061}
2062
2063fn calc_libass_coeff(mu: &mut [f64; 8], n: usize, r2: f64, mul: f64) {
2064    let w = 12096.0;
2065    let kernel = [
2066        (((3280.0 / w) * mul + 1092.0 / w) * mul + 2520.0 / w) * mul + 5204.0 / w,
2067        (((-2460.0 / w) * mul - 273.0 / w) * mul - 210.0 / w) * mul + 2943.0 / w,
2068        (((984.0 / w) * mul - 546.0 / w) * mul - 924.0 / w) * mul + 486.0 / w,
2069        (((-164.0 / w) * mul + 273.0 / w) * mul - 126.0 / w) * mul + 17.0 / w,
2070    ];
2071    let mut mat_freq = [0.0_f64; 17];
2072    mat_freq[..4].copy_from_slice(&kernel);
2073    coeff_filter_libass(&mut mat_freq, 7, &kernel);
2074    let mut vec_freq = [0.0_f64; 12];
2075    calc_gauss_libass(&mut vec_freq, n + 4, r2 * mul);
2076    coeff_filter_libass(&mut vec_freq, n + 1, &kernel);
2077    let mut mat = [[0.0_f64; 8]; 8];
2078    calc_matrix_libass(&mut mat, &mat_freq, n);
2079    let mut vec = [0.0_f64; 8];
2080    for i in 0..n {
2081        vec[i] = mat_freq[0] - mat_freq[i + 1] - vec_freq[0] + vec_freq[i + 1];
2082    }
2083    for i in 0..n {
2084        let mut res = 0.0;
2085        for (j, value) in vec.iter().enumerate().take(n) {
2086            res += mat[i][j] * value;
2087        }
2088        mu[i] = res.max(0.0);
2089    }
2090}
2091
2092fn calc_gauss_libass(res: &mut [f64], n: usize, r2: f64) {
2093    let alpha = 0.5 / r2;
2094    let mut mul = (-alpha).exp();
2095    let mul2 = mul * mul;
2096    let mut cur = (alpha / std::f64::consts::PI).sqrt();
2097    res[0] = cur;
2098    cur *= mul;
2099    res[1] = cur;
2100    for value in res.iter_mut().take(n).skip(2) {
2101        mul *= mul2;
2102        cur *= mul;
2103        *value = cur;
2104    }
2105}
2106
2107fn coeff_filter_libass(coeff: &mut [f64], n: usize, kernel: &[f64; 4]) {
2108    let mut prev1 = coeff[1];
2109    let mut prev2 = coeff[2];
2110    let mut prev3 = coeff[3];
2111    for i in 0..n {
2112        let res = coeff[i] * kernel[0]
2113            + (prev1 + coeff[i + 1]) * kernel[1]
2114            + (prev2 + coeff[i + 2]) * kernel[2]
2115            + (prev3 + coeff[i + 3]) * kernel[3];
2116        prev3 = prev2;
2117        prev2 = prev1;
2118        prev1 = coeff[i];
2119        coeff[i] = res;
2120    }
2121}
2122
2123fn calc_matrix_libass(mat: &mut [[f64; 8]; 8], mat_freq: &[f64], n: usize) {
2124    for i in 0..n {
2125        mat[i][i] = mat_freq[2 * i + 2] + 3.0 * mat_freq[0] - 4.0 * mat_freq[i + 1];
2126        for j in i + 1..n {
2127            let v = mat_freq[i + j + 2]
2128                + mat_freq[j - i]
2129                + 2.0 * (mat_freq[0] - mat_freq[i + 1] - mat_freq[j + 1]);
2130            mat[i][j] = v;
2131            mat[j][i] = v;
2132        }
2133    }
2134    for k in 0..n {
2135        let z = 1.0 / mat[k][k];
2136        mat[k][k] = 1.0;
2137        let pivot_row = mat[k];
2138        for (i, row) in mat.iter_mut().enumerate().take(n) {
2139            if i == k {
2140                continue;
2141            }
2142            let mul = row[k] * z;
2143            row[k] = 0.0;
2144            for j in 0..n {
2145                row[j] -= pivot_row[j] * mul;
2146            }
2147        }
2148        for value in mat[k].iter_mut().take(n) {
2149            *value *= z;
2150        }
2151    }
2152}
2153
2154fn frexp(value: f64) -> (f64, i32) {
2155    if value == 0.0 {
2156        return (0.0, 0);
2157    }
2158    let exponent = value.abs().log2().floor() as i32 + 1;
2159    (value / 2.0_f64.powi(exponent), exponent)
2160}
2161
2162#[inline]
2163fn get_libass_sample(source: &[i16], width: usize, height: usize, x: isize, y: isize) -> i16 {
2164    if x < 0 || y < 0 || x >= width as isize || y >= height as isize {
2165        0
2166    } else {
2167        source[y as usize * width + x as usize]
2168    }
2169}
2170
2171fn unpack_libass_blur(source: &[u8]) -> Vec<i16> {
2172    source
2173        .iter()
2174        .map(|value| {
2175            let value = u16::from(*value);
2176            ((((value << 7) | (value >> 1)) + 1) >> 1) as i16
2177        })
2178        .collect()
2179}
2180
2181const LIBASS_DITHER_LINE: [i16; 32] = [
2182    8, 40, 8, 40, 8, 40, 8, 40, 8, 40, 8, 40, 8, 40, 8, 40, 56, 24, 56, 24, 56, 24, 56, 24, 56, 24,
2183    56, 24, 56, 24, 56, 24,
2184];
2185
2186fn pack_libass_blur(source: &[i16], width: usize, height: usize) -> Vec<u8> {
2187    let mut bitmap = vec![0_u8; width * height];
2188    for y in 0..height {
2189        let dither = &LIBASS_DITHER_LINE[16 * (y & 1)..];
2190        for x in 0..width {
2191            let sample = i32::from(source[y * width + x]);
2192            let value = ((sample - (sample >> 8) + i32::from(dither[x & 15])) >> 6).clamp(0, 255);
2193            bitmap[y * width + x] = value as u8;
2194        }
2195    }
2196    bitmap
2197}
2198
2199#[inline]
2200fn shrink_func_libass(p1p: i16, p1n: i16, z0p: i16, z0n: i16, n1p: i16, n1n: i16) -> i16 {
2201    let mut r = (i32::from(p1p) + i32::from(p1n) + i32::from(n1p) + i32::from(n1n)) >> 1;
2202    r = (r + i32::from(z0p) + i32::from(z0n)) >> 1;
2203    r = (r + i32::from(p1n) + i32::from(n1p)) >> 1;
2204    ((r + i32::from(z0p) + i32::from(z0n) + 2) >> 2) as i16
2205}
2206
2207#[inline]
2208fn expand_func_libass(p1: i16, z0: i16, n1: i16) -> (i16, i16) {
2209    let r = ((((p1 as u16).wrapping_add(n1 as u16)) >> 1).wrapping_add(z0 as u16)) >> 1;
2210    let rp = (((r.wrapping_add(p1 as u16) >> 1)
2211        .wrapping_add(z0 as u16)
2212        .wrapping_add(1))
2213        >> 1) as i16;
2214    let rn = (((r.wrapping_add(n1 as u16) >> 1)
2215        .wrapping_add(z0 as u16)
2216        .wrapping_add(1))
2217        >> 1) as i16;
2218    (rp, rn)
2219}
2220
2221fn shrink_horz_libass(source: &[i16], width: usize, height: usize) -> (Vec<i16>, usize, usize) {
2222    let dst_width = (width + 5) >> 1;
2223    let mut dst = vec![0_i16; dst_width * height];
2224    for y in 0..height {
2225        for x in 0..dst_width {
2226            let sx = (2 * x) as isize;
2227            dst[y * dst_width + x] = shrink_func_libass(
2228                get_libass_sample(source, width, height, sx - 4, y as isize),
2229                get_libass_sample(source, width, height, sx - 3, y as isize),
2230                get_libass_sample(source, width, height, sx - 2, y as isize),
2231                get_libass_sample(source, width, height, sx - 1, y as isize),
2232                get_libass_sample(source, width, height, sx, y as isize),
2233                get_libass_sample(source, width, height, sx + 1, y as isize),
2234            );
2235        }
2236    }
2237    (dst, dst_width, height)
2238}
2239
2240fn shrink_vert_libass(source: &[i16], width: usize, height: usize) -> (Vec<i16>, usize, usize) {
2241    let dst_height = (height + 5) >> 1;
2242    let mut dst = vec![0_i16; width * dst_height];
2243    for y in 0..dst_height {
2244        let sy = (2 * y) as isize;
2245        for x in 0..width {
2246            dst[y * width + x] = shrink_func_libass(
2247                get_libass_sample(source, width, height, x as isize, sy - 4),
2248                get_libass_sample(source, width, height, x as isize, sy - 3),
2249                get_libass_sample(source, width, height, x as isize, sy - 2),
2250                get_libass_sample(source, width, height, x as isize, sy - 1),
2251                get_libass_sample(source, width, height, x as isize, sy),
2252                get_libass_sample(source, width, height, x as isize, sy + 1),
2253            );
2254        }
2255    }
2256    (dst, width, dst_height)
2257}
2258
2259fn expand_horz_libass(source: &[i16], width: usize, height: usize) -> (Vec<i16>, usize, usize) {
2260    let dst_width = 2 * width + 4;
2261    let mut dst = vec![0_i16; dst_width * height];
2262    for y in 0..height {
2263        for i in 0..(width + 2) {
2264            let sx = i as isize;
2265            let (rp, rn) = expand_func_libass(
2266                get_libass_sample(source, width, height, sx - 2, y as isize),
2267                get_libass_sample(source, width, height, sx - 1, y as isize),
2268                get_libass_sample(source, width, height, sx, y as isize),
2269            );
2270            let dx = 2 * i;
2271            dst[y * dst_width + dx] = rp;
2272            dst[y * dst_width + dx + 1] = rn;
2273        }
2274    }
2275    (dst, dst_width, height)
2276}
2277
2278fn expand_vert_libass(source: &[i16], width: usize, height: usize) -> (Vec<i16>, usize, usize) {
2279    let dst_height = 2 * height + 4;
2280    let mut dst = vec![0_i16; width * dst_height];
2281    for i in 0..(height + 2) {
2282        let sy = i as isize;
2283        for x in 0..width {
2284            let (rp, rn) = expand_func_libass(
2285                get_libass_sample(source, width, height, x as isize, sy - 2),
2286                get_libass_sample(source, width, height, x as isize, sy - 1),
2287                get_libass_sample(source, width, height, x as isize, sy),
2288            );
2289            let dy = 2 * i;
2290            dst[dy * width + x] = rp;
2291            dst[(dy + 1) * width + x] = rn;
2292        }
2293    }
2294    (dst, width, dst_height)
2295}
2296
2297fn blur_horz_libass(
2298    source: &[i16],
2299    width: usize,
2300    height: usize,
2301    param: &[i16; 8],
2302    radius: usize,
2303) -> (Vec<i16>, usize, usize) {
2304    let dst_width = width + 2 * radius;
2305    let mut dst = vec![0_i16; dst_width * height];
2306    for y in 0..height {
2307        for x in 0..dst_width {
2308            let center_x = x as isize - radius as isize;
2309            let center = i32::from(get_libass_sample(
2310                source, width, height, center_x, y as isize,
2311            ));
2312            let mut acc = 0x8000_i32;
2313            for i in (1..=radius).rev() {
2314                let coeff = i32::from(param[i - 1]);
2315                let left = i32::from(get_libass_sample(
2316                    source,
2317                    width,
2318                    height,
2319                    center_x - i as isize,
2320                    y as isize,
2321                ));
2322                let right = i32::from(get_libass_sample(
2323                    source,
2324                    width,
2325                    height,
2326                    center_x + i as isize,
2327                    y as isize,
2328                ));
2329                acc += ((left - center) as i16 as i32) * coeff;
2330                acc += ((right - center) as i16 as i32) * coeff;
2331            }
2332            dst[y * dst_width + x] = (center + (acc >> 16)) as i16;
2333        }
2334    }
2335    (dst, dst_width, height)
2336}
2337
2338fn blur_vert_libass(
2339    source: &[i16],
2340    width: usize,
2341    height: usize,
2342    param: &[i16; 8],
2343    radius: usize,
2344) -> (Vec<i16>, usize, usize) {
2345    let dst_height = height + 2 * radius;
2346    let mut dst = vec![0_i16; width * dst_height];
2347    for y in 0..dst_height {
2348        let center_y = y as isize - radius as isize;
2349        for x in 0..width {
2350            let center = i32::from(get_libass_sample(
2351                source, width, height, x as isize, center_y,
2352            ));
2353            let mut acc = 0x8000_i32;
2354            for i in (1..=radius).rev() {
2355                let coeff = i32::from(param[i - 1]);
2356                let top = i32::from(get_libass_sample(
2357                    source,
2358                    width,
2359                    height,
2360                    x as isize,
2361                    center_y - i as isize,
2362                ));
2363                let bottom = i32::from(get_libass_sample(
2364                    source,
2365                    width,
2366                    height,
2367                    x as isize,
2368                    center_y + i as isize,
2369                ));
2370                acc += ((top - center) as i16 as i32) * coeff;
2371                acc += ((bottom - center) as i16 as i32) * coeff;
2372            }
2373            dst[y * width + x] = (center + (acc >> 16)) as i16;
2374        }
2375    }
2376    (dst, width, dst_height)
2377}
2378
2379fn image_planes_from_absolute_glyphs(
2380    glyphs: &[RasterGlyph],
2381    color: u32,
2382    kind: ass::ImageType,
2383) -> Vec<ImagePlane> {
2384    glyphs
2385        .iter()
2386        .filter_map(|glyph| {
2387            if glyph.width <= 0 || glyph.height <= 0 || glyph.bitmap.is_empty() {
2388                return None;
2389            }
2390
2391            Some(ImagePlane {
2392                size: Size {
2393                    width: glyph.width,
2394                    height: glyph.height,
2395                },
2396                stride: glyph.stride,
2397                color: rgba_color_from_ass(color),
2398                destination: Point {
2399                    x: glyph.left,
2400                    y: glyph.top - glyph.height,
2401                },
2402                kind,
2403                bitmap: glyph.bitmap.clone(),
2404            })
2405        })
2406        .collect()
2407}
2408
2409fn image_plane_from_drawing(
2410    drawing: &ParsedDrawing,
2411    origin_x: i32,
2412    line_top: i32,
2413    color: u32,
2414    scale_x: f64,
2415    scale_y: f64,
2416) -> Option<ImagePlane> {
2417    let polygons = scaled_drawing_polygons(drawing, scale_x, scale_y);
2418    let bounds = drawing_bounds(&polygons)?;
2419    let width = bounds.width();
2420    let height = bounds.height();
2421    if width <= 0 || height <= 0 {
2422        return None;
2423    }
2424
2425    let stride = width as usize;
2426    let mut bitmap = vec![0_u8; stride * height as usize];
2427    let mut any_visible = false;
2428
2429    for row in 0..height as usize {
2430        for column in 0..width as usize {
2431            let x = bounds.x_min + column as i32;
2432            let y = bounds.y_min + row as i32;
2433            if polygons
2434                .iter()
2435                .any(|polygon| point_in_polygon(x, y, polygon))
2436            {
2437                bitmap[row * stride + column] = 255;
2438                any_visible = true;
2439            }
2440        }
2441    }
2442
2443    any_visible.then_some(ImagePlane {
2444        size: Size { width, height },
2445        stride: width,
2446        color: rgba_color_from_ass(color),
2447        destination: Point {
2448            x: origin_x + bounds.x_min,
2449            y: line_top + bounds.y_min,
2450        },
2451        kind: ass::ImageType::Character,
2452        bitmap,
2453    })
2454}
2455
2456fn scaled_drawing_polygons(drawing: &ParsedDrawing, scale_x: f64, scale_y: f64) -> Vec<Vec<Point>> {
2457    let scale_x = style_scale(scale_x);
2458    let scale_y = style_scale(scale_y);
2459    if (scale_x - 1.0).abs() < f64::EPSILON && (scale_y - 1.0).abs() < f64::EPSILON {
2460        return drawing.polygons.clone();
2461    }
2462
2463    drawing
2464        .polygons
2465        .iter()
2466        .map(|polygon| {
2467            polygon
2468                .iter()
2469                .map(|point| Point {
2470                    x: (f64::from(point.x) * scale_x).round() as i32,
2471                    y: (f64::from(point.y) * scale_y).round() as i32,
2472                })
2473                .collect()
2474        })
2475        .collect()
2476}
2477
2478fn drawing_bounds(polygons: &[Vec<Point>]) -> Option<Rect> {
2479    let mut points = polygons.iter().flat_map(|polygon| polygon.iter().copied());
2480    let first = points.next()?;
2481    let mut x_min = first.x;
2482    let mut y_min = first.y;
2483    let mut x_max = first.x;
2484    let mut y_max = first.y;
2485    for point in points {
2486        x_min = x_min.min(point.x);
2487        y_min = y_min.min(point.y);
2488        x_max = x_max.max(point.x);
2489        y_max = y_max.max(point.y);
2490    }
2491    Some(Rect {
2492        x_min,
2493        y_min,
2494        x_max: x_max + 1,
2495        y_max: y_max + 1,
2496    })
2497}
2498
2499fn plane_to_raster_glyph(plane: &ImagePlane) -> RasterGlyph {
2500    RasterGlyph {
2501        width: plane.size.width,
2502        height: plane.size.height,
2503        stride: plane.stride,
2504        left: plane.destination.x,
2505        top: plane.destination.y + plane.size.height,
2506        bitmap: plane.bitmap.clone(),
2507        ..RasterGlyph::default()
2508    }
2509}
2510
2511fn apply_event_clip(planes: Vec<ImagePlane>, clip_rect: Rect, inverse: bool) -> Vec<ImagePlane> {
2512    let mut clipped = Vec::new();
2513    for plane in planes {
2514        if inverse {
2515            clipped.extend(inverse_clip_plane(plane, clip_rect));
2516        } else if let Some(plane) = clip_plane(plane, clip_rect) {
2517            clipped.push(plane);
2518        }
2519    }
2520    clipped
2521}
2522
2523fn apply_vector_clip(
2524    planes: Vec<ImagePlane>,
2525    clip: &ParsedVectorClip,
2526    inverse: bool,
2527) -> Vec<ImagePlane> {
2528    planes
2529        .into_iter()
2530        .filter_map(|plane| mask_plane_with_vector_clip(plane, clip, inverse))
2531        .collect()
2532}
2533
2534fn mask_plane_with_vector_clip(
2535    plane: ImagePlane,
2536    clip: &ParsedVectorClip,
2537    inverse: bool,
2538) -> Option<ImagePlane> {
2539    let mut bitmap = plane.bitmap.clone();
2540    let stride = plane.stride as usize;
2541    let mut any_visible = false;
2542
2543    for row in 0..plane.size.height as usize {
2544        for column in 0..plane.size.width as usize {
2545            let global_x = plane.destination.x + column as i32;
2546            let global_y = plane.destination.y + row as i32;
2547            let inside = clip
2548                .polygons
2549                .iter()
2550                .any(|polygon| point_in_polygon(global_x, global_y, polygon));
2551            let keep = if inverse { !inside } else { inside };
2552            if !keep {
2553                bitmap[row * stride + column] = 0;
2554            } else if bitmap[row * stride + column] > 0 {
2555                any_visible = true;
2556            }
2557        }
2558    }
2559
2560    any_visible.then_some(ImagePlane { bitmap, ..plane })
2561}
2562
2563fn point_in_polygon(x: i32, y: i32, polygon: &[Point]) -> bool {
2564    if polygon.len() < 3 {
2565        return false;
2566    }
2567
2568    let mut inside = false;
2569    let mut previous = polygon[polygon.len() - 1];
2570    let sample_x = x as f64 + 0.5;
2571    let sample_y = y as f64 + 0.5;
2572
2573    for &current in polygon {
2574        let current_y = current.y as f64;
2575        let previous_y = previous.y as f64;
2576        let intersects = (current_y > sample_y) != (previous_y > sample_y);
2577        if intersects {
2578            let current_x = current.x as f64;
2579            let previous_x = previous.x as f64;
2580            let x_intersection = (previous_x - current_x) * (sample_y - current_y)
2581                / (previous_y - current_y)
2582                + current_x;
2583            if sample_x < x_intersection {
2584                inside = !inside;
2585            }
2586        }
2587        previous = current;
2588    }
2589
2590    inside
2591}
2592
2593fn clip_plane(plane: ImagePlane, clip_rect: Rect) -> Option<ImagePlane> {
2594    let plane_rect = plane_rect(&plane);
2595    let intersection = plane_rect.intersect(clip_rect)?;
2596    crop_plane_to_rect(plane, intersection)
2597}
2598
2599fn inverse_clip_plane(plane: ImagePlane, clip_rect: Rect) -> Vec<ImagePlane> {
2600    let plane_rect = plane_rect(&plane);
2601    let Some(intersection) = plane_rect.intersect(clip_rect) else {
2602        return vec![plane];
2603    };
2604
2605    let mut result = Vec::new();
2606    let regions = [
2607        Rect {
2608            x_min: plane_rect.x_min,
2609            y_min: plane_rect.y_min,
2610            x_max: plane_rect.x_max,
2611            y_max: intersection.y_min,
2612        },
2613        Rect {
2614            x_min: plane_rect.x_min,
2615            y_min: intersection.y_max,
2616            x_max: plane_rect.x_max,
2617            y_max: plane_rect.y_max,
2618        },
2619        Rect {
2620            x_min: plane_rect.x_min,
2621            y_min: intersection.y_min,
2622            x_max: intersection.x_min,
2623            y_max: intersection.y_max,
2624        },
2625        Rect {
2626            x_min: intersection.x_max,
2627            y_min: intersection.y_min,
2628            x_max: plane_rect.x_max,
2629            y_max: intersection.y_max,
2630        },
2631    ];
2632    for region in regions {
2633        if region.is_empty() {
2634            continue;
2635        }
2636        if let Some(cropped) = crop_plane_to_rect(plane.clone(), region) {
2637            result.push(cropped);
2638        }
2639    }
2640    result
2641}
2642
2643fn plane_rect(plane: &ImagePlane) -> Rect {
2644    Rect {
2645        x_min: plane.destination.x,
2646        y_min: plane.destination.y,
2647        x_max: plane.destination.x + plane.size.width,
2648        y_max: plane.destination.y + plane.size.height,
2649    }
2650}
2651
2652fn crop_plane_to_rect(plane: ImagePlane, rect: Rect) -> Option<ImagePlane> {
2653    let plane_rect = plane_rect(&plane);
2654    let rect = plane_rect.intersect(rect)?;
2655    let offset_x = (rect.x_min - plane_rect.x_min) as usize;
2656    let offset_y = (rect.y_min - plane_rect.y_min) as usize;
2657    let width = rect.width() as usize;
2658    let height = rect.height() as usize;
2659    let src_stride = plane.stride as usize;
2660    let mut bitmap = Vec::with_capacity(width * height);
2661
2662    for row in 0..height {
2663        let start = (offset_y + row) * src_stride + offset_x;
2664        bitmap.extend_from_slice(&plane.bitmap[start..start + width]);
2665    }
2666
2667    Some(ImagePlane {
2668        size: Size {
2669            width: rect.width(),
2670            height: rect.height(),
2671        },
2672        stride: rect.width(),
2673        destination: Point {
2674            x: rect.x_min,
2675            y: rect.y_min,
2676        },
2677        bitmap,
2678        ..plane
2679    })
2680}
2681fn is_event_active(event: &ParsedEvent, now_ms: i64) -> bool {
2682    now_ms >= event.start && now_ms < event.start + event.duration
2683}
2684
2685#[cfg(test)]
2686mod tests {
2687    use super::*;
2688    use rassa_fonts::{FontconfigProvider, NullFontProvider};
2689    use rassa_parse::parse_script_text;
2690
2691    fn config(
2692        frame_width: i32,
2693        frame_height: i32,
2694        margins: rassa_core::Margins,
2695        use_margins: bool,
2696    ) -> RendererConfig {
2697        RendererConfig {
2698            frame: Size {
2699                width: frame_width,
2700                height: frame_height,
2701            },
2702            margins,
2703            use_margins,
2704            ..RendererConfig::default()
2705        }
2706    }
2707
2708    fn total_plane_area(planes: &[ImagePlane]) -> i32 {
2709        planes
2710            .iter()
2711            .map(|plane| plane.size.width * plane.size.height)
2712            .sum()
2713    }
2714
2715    fn vertical_span(planes: &[ImagePlane]) -> i32 {
2716        let min_y = planes
2717            .iter()
2718            .map(|plane| plane.destination.y)
2719            .min()
2720            .expect("plane");
2721        let max_y = planes
2722            .iter()
2723            .map(|plane| plane.destination.y + plane.size.height)
2724            .max()
2725            .expect("plane");
2726        max_y - min_y
2727    }
2728
2729    fn character_bounds(planes: &[ImagePlane]) -> Option<Rect> {
2730        let mut character_planes = planes
2731            .iter()
2732            .filter(|plane| plane.kind == ass::ImageType::Character);
2733        let first = character_planes.next()?;
2734        let mut bounds = Rect {
2735            x_min: first.destination.x,
2736            y_min: first.destination.y,
2737            x_max: first.destination.x + first.size.width,
2738            y_max: first.destination.y + first.size.height,
2739        };
2740        for plane in character_planes {
2741            bounds.x_min = bounds.x_min.min(plane.destination.x);
2742            bounds.y_min = bounds.y_min.min(plane.destination.y);
2743            bounds.x_max = bounds.x_max.max(plane.destination.x + plane.size.width);
2744            bounds.y_max = bounds.y_max.max(plane.destination.y + plane.size.height);
2745        }
2746        Some(bounds)
2747    }
2748
2749    fn visible_bounds(planes: &[ImagePlane]) -> Option<Rect> {
2750        let mut bounds: Option<Rect> = None;
2751        for plane in planes {
2752            let stride = plane.stride.max(0) as usize;
2753            if stride == 0 {
2754                continue;
2755            }
2756            for y in 0..plane.size.height.max(0) as usize {
2757                for x in 0..plane.size.width.max(0) as usize {
2758                    if plane.bitmap[y * stride + x] == 0 {
2759                        continue;
2760                    }
2761                    let px = plane.destination.x + x as i32;
2762                    let py = plane.destination.y + y as i32;
2763                    match &mut bounds {
2764                        Some(rect) => {
2765                            rect.x_min = rect.x_min.min(px);
2766                            rect.y_min = rect.y_min.min(py);
2767                            rect.x_max = rect.x_max.max(px + 1);
2768                            rect.y_max = rect.y_max.max(py + 1);
2769                        }
2770                        None => {
2771                            bounds = Some(Rect {
2772                                x_min: px,
2773                                y_min: py,
2774                                x_max: px + 1,
2775                                y_max: py + 1,
2776                            });
2777                        }
2778                    }
2779                }
2780            }
2781        }
2782        bounds
2783    }
2784
2785    #[test]
2786    fn prepare_frame_only_keeps_active_events() {
2787        let track = parse_script_text("[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,First\nDialogue: 0,0:00:02.00,0:00:03.00,Default,,0000,0000,0000,,Second").expect("script should parse");
2788        let engine = RenderEngine::new();
2789        let provider = NullFontProvider;
2790        let frame = engine.prepare_frame(&track, &provider, 500);
2791
2792        assert_eq!(frame.active_events.len(), 1);
2793        assert_eq!(frame.active_events[0].text, "First");
2794    }
2795
2796    #[test]
2797    fn render_frame_produces_image_planes_for_active_text() {
2798        let track = parse_script_text("[Script Info]\nPlayResX: 640\nPlayResY: 360\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,Hi").expect("script should parse");
2799        let engine = RenderEngine::new();
2800        let provider = FontconfigProvider::new();
2801        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2802
2803        assert!(!planes.is_empty());
2804        assert!(planes.iter().all(|plane| plane.size.width >= 0));
2805        assert!(planes.iter().all(|plane| plane.size.height >= 0));
2806    }
2807
2808    #[test]
2809    fn render_frame_supports_multiple_override_runs() {
2810        let track = parse_script_text("[Script Info]\nPlayResX: 640\nPlayResY: 360\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\fnDejaVu Sans}Hi{\\fnArial} there").expect("script should parse");
2811        let engine = RenderEngine::new();
2812        let provider = FontconfigProvider::new();
2813        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2814
2815        assert!(!planes.is_empty());
2816    }
2817
2818    #[test]
2819    fn render_frame_uses_axis_specific_shadow_offsets() {
2820        let track = parse_script_text("[Script Info]\nPlayResX: 220\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,28,&H00FFFFFF,&H0000FFFF,&H00000000,&H00111111,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(30,30)\\xshad9\\yshad3}Hi").expect("script should parse");
2821        let engine = RenderEngine::new();
2822        let provider = FontconfigProvider::new();
2823        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2824        let character_planes = planes
2825            .iter()
2826            .filter(|plane| plane.kind == ass::ImageType::Character)
2827            .cloned()
2828            .collect::<Vec<_>>();
2829        let shadow_planes = planes
2830            .iter()
2831            .filter(|plane| plane.kind == ass::ImageType::Shadow)
2832            .cloned()
2833            .collect::<Vec<_>>();
2834
2835        let character = visible_bounds(&character_planes).expect("character bounds");
2836        let shadow = visible_bounds(&shadow_planes).expect("axis-specific shadow should render");
2837        assert_eq!(shadow.x_min - character.x_min, 9);
2838        assert_eq!(shadow.y_min - character.y_min, 3);
2839    }
2840
2841    #[test]
2842    fn render_frame_renders_underline_and_strikeout_decorations() {
2843        let track = parse_script_text("[Script Info]\nPlayResX: 220\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,28,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(30,30)\\u1\\s1}Hi").expect("script should parse");
2844        let engine = RenderEngine::new();
2845        let provider = FontconfigProvider::new();
2846        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2847        let decoration_planes = planes
2848            .iter()
2849            .filter(|plane| {
2850                plane.kind == ass::ImageType::Character
2851                    && plane.size.height <= 3
2852                    && plane.size.width > plane.size.height * 4
2853            })
2854            .collect::<Vec<_>>();
2855
2856        assert!(decoration_planes.len() >= 2);
2857    }
2858
2859    #[test]
2860    fn render_frame_uses_override_colors_and_shadow_planes() {
2861        let track = parse_script_text("[Script Info]\nPlayResX: 640\nPlayResY: 360\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00111111,0,0,0,0,100,100,0,0,1,2,2,2,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\1c&H112233&\\4c&H445566&\\shad3}Hi").expect("script should parse");
2862        let engine = RenderEngine::new();
2863        let provider = FontconfigProvider::new();
2864        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2865
2866        assert!(
2867            planes.iter().any(
2868                |plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x3322_1100
2869            )
2870        );
2871        assert!(
2872            planes
2873                .iter()
2874                .any(|plane| plane.kind == ass::ImageType::Shadow && plane.color.0 == 0x6655_4400)
2875        );
2876    }
2877
2878    #[test]
2879    fn render_frame_orders_events_by_layer_then_read_order() {
2880        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 5,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\1c&H0000FF&}High\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,40)\\1c&H00FF00&}Low").expect("script should parse");
2881        let engine = RenderEngine::new();
2882        let provider = FontconfigProvider::new();
2883        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2884
2885        let first_character = planes
2886            .iter()
2887            .find(|plane| plane.kind == ass::ImageType::Character)
2888            .expect("character plane");
2889        assert_eq!(first_character.color.0, 0x00FF_0000);
2890    }
2891
2892    #[test]
2893    fn render_frame_orders_shadow_outline_before_character_within_event() {
2894        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00111111,&H0000FFFF,&H00222222,&H00333333,0,0,0,0,100,100,0,0,1,2,2,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)}Hi").expect("script should parse");
2895        let engine = RenderEngine::new();
2896        let provider = FontconfigProvider::new();
2897        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2898        let kinds = planes.iter().map(|plane| plane.kind).collect::<Vec<_>>();
2899
2900        let first_shadow = kinds
2901            .iter()
2902            .position(|kind| *kind == ass::ImageType::Shadow)
2903            .expect("shadow plane");
2904        let first_outline = kinds
2905            .iter()
2906            .position(|kind| *kind == ass::ImageType::Outline)
2907            .expect("outline plane");
2908        let first_character = kinds
2909            .iter()
2910            .position(|kind| *kind == ass::ImageType::Character)
2911            .expect("character plane");
2912
2913        assert!(first_shadow < first_outline);
2914        assert!(first_outline < first_character);
2915    }
2916
2917    #[test]
2918    fn render_frame_emits_outline_planes_for_border_override() {
2919        let track = parse_script_text("[Script Info]\nPlayResX: 640\nPlayResY: 360\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00010203,&H00111111,0,0,0,0,100,100,0,0,1,2,2,2,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\bord3\\3c&H0A0B0C&}Hi").expect("script should parse");
2920        let engine = RenderEngine::new();
2921        let provider = FontconfigProvider::new();
2922        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2923
2924        assert!(
2925            planes
2926                .iter()
2927                .any(|plane| plane.kind == ass::ImageType::Outline && plane.color.0 == 0x0C0B_0A00)
2928        );
2929    }
2930
2931    #[test]
2932    fn render_frame_emits_opaque_box_for_border_style_3() {
2933        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00010203,&H00111111,0,0,0,0,100,100,0,0,3,4,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(30,30)}Hi").expect("script should parse");
2934        let engine = RenderEngine::new();
2935        let provider = FontconfigProvider::new();
2936        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2937        let character_planes = planes
2938            .iter()
2939            .filter(|plane| plane.kind == ass::ImageType::Character)
2940            .cloned()
2941            .collect::<Vec<_>>();
2942        let outline_planes = planes
2943            .iter()
2944            .filter(|plane| plane.kind == ass::ImageType::Outline)
2945            .cloned()
2946            .collect::<Vec<_>>();
2947
2948        let _character = visible_bounds(&character_planes).expect("character bounds");
2949        let outline = outline_planes
2950            .iter()
2951            .find(|plane| plane.color.0 == 0x0302_0100 && plane.bitmap.contains(&255))
2952            .expect("opaque border-style box plane");
2953        assert!(outline.size.width > 0);
2954        assert!(outline.size.height > 0);
2955    }
2956
2957    #[test]
2958    fn render_frame_blurs_outline_and_shadow_layers() {
2959        let track = parse_script_text("[Script Info]\nPlayResX: 640\nPlayResY: 360\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00010203,&H00111111,0,0,0,0,100,100,0,0,1,2,2,2,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\bord2\\blur2\\3c&H0A0B0C&\\shad2}Hi").expect("script should parse");
2960        let engine = RenderEngine::new();
2961        let provider = FontconfigProvider::new();
2962        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2963
2964        assert!(
2965            planes
2966                .iter()
2967                .any(|plane| plane.kind == ass::ImageType::Outline
2968                    && plane.bitmap.iter().any(|value| *value > 0 && *value < 255))
2969        );
2970        assert!(
2971            planes
2972                .iter()
2973                .any(|plane| plane.kind == ass::ImageType::Shadow
2974                    && plane.bitmap.iter().any(|value| *value > 0 && *value < 255))
2975        );
2976    }
2977
2978    #[test]
2979    fn render_frame_applies_rectangular_clip() {
2980        let track = parse_script_text("[Script Info]\nPlayResX: 640\nPlayResY: 360\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,7,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(0,0)\\clip(0,0,64,64)}Hi").expect("script should parse");
2981        let engine = RenderEngine::new();
2982        let provider = FontconfigProvider::new();
2983        let planes = engine.render_frame_with_provider(&track, &provider, 500);
2984
2985        assert!(!planes.is_empty());
2986        assert!(planes.iter().all(|plane| plane.destination.x >= 0));
2987        assert!(planes.iter().all(|plane| plane.destination.y >= 0));
2988        assert!(
2989            planes
2990                .iter()
2991                .all(|plane| plane.destination.x + plane.size.width <= 64)
2992        );
2993        assert!(
2994            planes
2995                .iter()
2996                .all(|plane| plane.destination.y + plane.size.height <= 64)
2997        );
2998    }
2999
3000    #[test]
3001    fn render_frame_accepts_renderer_shaping_mode() {
3002        let track = parse_script_text("[Script Info]\nPlayResX: 320\nPlayResY: 180\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,48,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,20,20,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,office").expect("script should parse");
3003        let engine = RenderEngine::new();
3004        let provider = FontconfigProvider::new();
3005        let simple = engine.render_frame_with_provider_and_config(
3006            &track,
3007            &provider,
3008            500,
3009            &RendererConfig {
3010                shaping: ass::ShapingLevel::Simple,
3011                ..default_renderer_config(&track)
3012            },
3013        );
3014        let complex = engine.render_frame_with_provider_and_config(
3015            &track,
3016            &provider,
3017            500,
3018            &RendererConfig {
3019                shaping: ass::ShapingLevel::Complex,
3020                ..default_renderer_config(&track)
3021            },
3022        );
3023
3024        assert!(!simple.is_empty());
3025        assert!(!complex.is_empty());
3026    }
3027
3028    #[test]
3029    fn render_frame_applies_inverse_rectangular_clip() {
3030        let plane = ImagePlane {
3031            size: Size {
3032                width: 6,
3033                height: 4,
3034            },
3035            stride: 6,
3036            color: RgbaColor(0x00FF_FFFF),
3037            destination: Point { x: 0, y: 0 },
3038            kind: ass::ImageType::Character,
3039            bitmap: vec![255; 24],
3040        };
3041        let parts = inverse_clip_plane(
3042            plane,
3043            Rect {
3044                x_min: 2,
3045                y_min: 1,
3046                x_max: 4,
3047                y_max: 3,
3048            },
3049        );
3050
3051        assert_eq!(parts.len(), 4);
3052        assert_eq!(
3053            parts.iter().map(|plane| plane.bitmap.len()).sum::<usize>(),
3054            20
3055        );
3056    }
3057
3058    #[test]
3059    fn render_frame_applies_vector_clip() {
3060        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(0,0)\\clip(m 0 0 l 32 0 32 32 0 32)}Hi").expect("script should parse");
3061        let engine = RenderEngine::new();
3062        let provider = FontconfigProvider::new();
3063        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3064
3065        assert!(!planes.is_empty());
3066        assert!(
3067            planes
3068                .iter()
3069                .all(|plane| plane.bitmap.iter().any(|value| *value > 0))
3070        );
3071        assert!(planes.iter().all(|plane| plane.destination.x >= 0));
3072        assert!(planes.iter().all(|plane| plane.destination.y >= 0));
3073    }
3074
3075    #[test]
3076    fn render_frame_clips_to_frame_bounds() {
3077        let plane = ImagePlane {
3078            size: Size {
3079                width: 20,
3080                height: 20,
3081            },
3082            stride: 20,
3083            color: RgbaColor(0x00FF_FFFF),
3084            destination: Point { x: 50, y: 50 },
3085            kind: ass::ImageType::Character,
3086            bitmap: vec![255; 400],
3087        };
3088        let clipped = apply_event_clip(
3089            vec![plane],
3090            Rect {
3091                x_min: 0,
3092                y_min: 0,
3093                x_max: 60,
3094                y_max: 60,
3095            },
3096            false,
3097        );
3098
3099        assert_eq!(clipped.len(), 1);
3100        assert_eq!(clipped[0].size.width, 10);
3101        assert_eq!(clipped[0].size.height, 10);
3102    }
3103
3104    #[test]
3105    fn render_frame_applies_margin_clip_when_enabled() {
3106        let track = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,7,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,Hi").expect("script should parse");
3107        let engine = RenderEngine::new();
3108        let provider = FontconfigProvider::new();
3109        let planes = engine.render_frame_with_provider_and_config(
3110            &track,
3111            &provider,
3112            500,
3113            &config(
3114                100,
3115                100,
3116                rassa_core::Margins {
3117                    top: 10,
3118                    bottom: 10,
3119                    left: 10,
3120                    right: 10,
3121                },
3122                true,
3123            ),
3124        );
3125
3126        assert!(!planes.is_empty());
3127        assert!(planes.iter().all(|plane| plane.destination.x >= 10));
3128        assert!(planes.iter().all(|plane| plane.destination.y >= 10));
3129        assert!(
3130            planes
3131                .iter()
3132                .all(|plane| plane.destination.x + plane.size.width <= 90)
3133        );
3134        assert!(
3135            planes
3136                .iter()
3137                .all(|plane| plane.destination.y + plane.size.height <= 90)
3138        );
3139    }
3140
3141    #[test]
3142    fn render_frame_maps_into_content_area_when_margins_are_not_used() {
3143        let track = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,18,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(0,0)}I").expect("script should parse");
3144        let engine = RenderEngine::new();
3145        let provider = FontconfigProvider::new();
3146        let planes = engine.render_frame_with_provider_and_config(
3147            &track,
3148            &provider,
3149            500,
3150            &config(
3151                120,
3152                120,
3153                rassa_core::Margins {
3154                    top: 10,
3155                    bottom: 10,
3156                    left: 10,
3157                    right: 10,
3158                },
3159                false,
3160            ),
3161        );
3162
3163        assert!(!planes.is_empty());
3164        let bounds = visible_bounds(&planes).expect("visible bounds");
3165        assert!(
3166            bounds.x_min >= 10,
3167            "visible bounds should start inside content area: {bounds:?}"
3168        );
3169        assert!(
3170            bounds.y_min >= 9,
3171            "libass-style antialiasing may allocate one guard row above the content area: {bounds:?}"
3172        );
3173        assert!(
3174            bounds.x_max <= 110,
3175            "visible bounds should end inside content area: {bounds:?}"
3176        );
3177        assert!(
3178            bounds.y_max <= 110,
3179            "visible bounds should end inside content area: {bounds:?}"
3180        );
3181    }
3182
3183    #[test]
3184    fn render_frame_keeps_border_closer_to_device_size_when_scaled_border_is_disabled() {
3185        let enabled = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,18,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,4,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)}I").expect("script should parse");
3186        let disabled = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\nScaledBorderAndShadow: no\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,18,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,4,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)}I").expect("script should parse");
3187        let engine = RenderEngine::new();
3188        let provider = FontconfigProvider::new();
3189        let config = config(200, 200, rassa_core::Margins::default(), true);
3190        let enabled_planes =
3191            engine.render_frame_with_provider_and_config(&enabled, &provider, 500, &config);
3192        let disabled_planes =
3193            engine.render_frame_with_provider_and_config(&disabled, &provider, 500, &config);
3194        let enabled_outline_area: i32 = enabled_planes
3195            .iter()
3196            .filter(|plane| plane.kind == ass::ImageType::Outline)
3197            .map(|plane| plane.size.width * plane.size.height)
3198            .sum();
3199        let disabled_outline_area: i32 = disabled_planes
3200            .iter()
3201            .filter(|plane| plane.kind == ass::ImageType::Outline)
3202            .map(|plane| plane.size.width * plane.size.height)
3203            .sum();
3204
3205        assert!(disabled_outline_area > 0);
3206        assert!(disabled_outline_area < enabled_outline_area);
3207    }
3208
3209    #[test]
3210    fn render_frame_applies_font_scale_to_output() {
3211        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,Scale").expect("script should parse");
3212        let engine = RenderEngine::new();
3213        let provider = FontconfigProvider::new();
3214
3215        let baseline = engine.render_frame_with_provider(&track, &provider, 500);
3216        let scaled = engine.render_frame_with_provider_and_config(
3217            &track,
3218            &provider,
3219            500,
3220            &RendererConfig {
3221                frame: Size {
3222                    width: 200,
3223                    height: 120,
3224                },
3225                font_scale: 2.0,
3226                ..RendererConfig::default()
3227            },
3228        );
3229
3230        assert!(!baseline.is_empty());
3231        assert!(!scaled.is_empty());
3232        assert!(total_plane_area(&scaled) > total_plane_area(&baseline));
3233    }
3234
3235    #[test]
3236    fn render_frame_applies_text_scale_overrides() {
3237        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 140\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)}Scale").expect("script should parse");
3238        let stretched = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 140\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\fscx200\\fscy50}Scale").expect("script should parse");
3239        let engine = RenderEngine::new();
3240        let provider = FontconfigProvider::new();
3241        let baseline = engine.render_frame_with_provider(&track, &provider, 500);
3242        let scaled = engine.render_frame_with_provider(&stretched, &provider, 500);
3243        let baseline_width = baseline
3244            .iter()
3245            .filter(|plane| plane.kind == ass::ImageType::Character)
3246            .map(|plane| plane.destination.x + plane.size.width)
3247            .max()
3248            .expect("baseline max x")
3249            - baseline
3250                .iter()
3251                .filter(|plane| plane.kind == ass::ImageType::Character)
3252                .map(|plane| plane.destination.x)
3253                .min()
3254                .expect("baseline min x");
3255        let scaled_width = scaled
3256            .iter()
3257            .filter(|plane| plane.kind == ass::ImageType::Character)
3258            .map(|plane| plane.destination.x + plane.size.width)
3259            .max()
3260            .expect("scaled max x")
3261            - scaled
3262                .iter()
3263                .filter(|plane| plane.kind == ass::ImageType::Character)
3264                .map(|plane| plane.destination.x)
3265                .min()
3266                .expect("scaled min x");
3267
3268        assert!(scaled_width > baseline_width);
3269        assert!(total_plane_area(&scaled) < total_plane_area(&baseline) * 2);
3270    }
3271
3272    #[test]
3273    fn render_frame_applies_drawing_scale_overrides() {
3274        let baseline = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\p1}m 0 0 l 10 0 10 10 0 10").expect("script should parse");
3275        let scaled = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\fscx200\\fscy50\\p1}m 0 0 l 10 0 10 10 0 10").expect("script should parse");
3276        let engine = RenderEngine::new();
3277        let provider = FontconfigProvider::new();
3278        let baseline_planes = engine.render_frame_with_provider(&baseline, &provider, 500);
3279        let scaled_planes = engine.render_frame_with_provider(&scaled, &provider, 500);
3280        let baseline_plane = baseline_planes
3281            .iter()
3282            .find(|plane| plane.kind == ass::ImageType::Character)
3283            .expect("baseline drawing plane");
3284        let scaled_plane = scaled_planes
3285            .iter()
3286            .find(|plane| plane.kind == ass::ImageType::Character)
3287            .expect("scaled drawing plane");
3288
3289        assert!(scaled_plane.size.width > baseline_plane.size.width);
3290        assert!(scaled_plane.size.height < baseline_plane.size.height);
3291        assert_eq!(scaled_plane.destination, Point { x: 10, y: 10 });
3292    }
3293
3294    #[test]
3295    fn render_frame_applies_text_spacing_override() {
3296        let baseline = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,28,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)}IIII").expect("script should parse");
3297        let spaced = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,28,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\fsp8}IIII").expect("script should parse");
3298        let engine = RenderEngine::new();
3299        let provider = FontconfigProvider::new();
3300        let baseline_planes = engine.render_frame_with_provider(&baseline, &provider, 500);
3301        let spaced_planes = engine.render_frame_with_provider(&spaced, &provider, 500);
3302        let baseline_width = character_bounds(&baseline_planes)
3303            .expect("baseline bounds")
3304            .width();
3305        let spaced_width = character_bounds(&spaced_planes)
3306            .expect("spaced bounds")
3307            .width();
3308
3309        assert!(spaced_width > baseline_width);
3310    }
3311
3312    #[test]
3313    fn render_frame_scales_output_to_frame_size() {
3314        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,Scale").expect("script should parse");
3315        let engine = RenderEngine::new();
3316        let provider = FontconfigProvider::new();
3317
3318        let baseline = engine.render_frame_with_provider(&track, &provider, 500);
3319        let scaled = engine.render_frame_with_provider_and_config(
3320            &track,
3321            &provider,
3322            500,
3323            &RendererConfig {
3324                frame: Size {
3325                    width: 400,
3326                    height: 240,
3327                },
3328                ..default_renderer_config(&track)
3329            },
3330        );
3331
3332        assert!(total_plane_area(&baseline) > 0);
3333        assert!(total_plane_area(&scaled) > total_plane_area(&baseline));
3334    }
3335
3336    #[test]
3337    fn render_frame_applies_pixel_aspect_horizontally() {
3338        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,18,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(0,0)}I").expect("script should parse");
3339        let engine = RenderEngine::new();
3340        let provider = FontconfigProvider::new();
3341
3342        let baseline = engine.render_frame_with_provider_and_config(
3343            &track,
3344            &provider,
3345            500,
3346            &RendererConfig {
3347                frame: Size {
3348                    width: 400,
3349                    height: 120,
3350                },
3351                ..default_renderer_config(&track)
3352            },
3353        );
3354        let widened = engine.render_frame_with_provider_and_config(
3355            &track,
3356            &provider,
3357            500,
3358            &RendererConfig {
3359                frame: Size {
3360                    width: 400,
3361                    height: 120,
3362                },
3363                pixel_aspect: 2.0,
3364                ..default_renderer_config(&track)
3365            },
3366        );
3367
3368        let baseline_bounds = character_bounds(&baseline).expect("baseline character bounds");
3369        let widened_bounds = character_bounds(&widened).expect("widened character bounds");
3370        assert!(
3371            widened_bounds.x_min > baseline_bounds.x_min,
3372            "pixel aspect should affect horizontal placement: baseline={baseline_bounds:?} widened={widened_bounds:?}"
3373        );
3374    }
3375
3376    #[test]
3377    fn render_frame_derives_pixel_aspect_from_storage_size_when_unset() {
3378        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,18,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(0,0)}Storage").expect("script should parse");
3379        let engine = RenderEngine::new();
3380        let provider = FontconfigProvider::new();
3381
3382        let baseline = engine.render_frame_with_provider_and_config(
3383            &track,
3384            &provider,
3385            500,
3386            &RendererConfig {
3387                frame: Size {
3388                    width: 400,
3389                    height: 240,
3390                },
3391                ..default_renderer_config(&track)
3392            },
3393        );
3394        let storage_adjusted = engine.render_frame_with_provider_and_config(
3395            &track,
3396            &provider,
3397            500,
3398            &RendererConfig {
3399                frame: Size {
3400                    width: 400,
3401                    height: 240,
3402                },
3403                storage: Size {
3404                    width: 400,
3405                    height: 120,
3406                },
3407                ..default_renderer_config(&track)
3408            },
3409        );
3410
3411        assert!(total_plane_area(&baseline) > 0);
3412        assert!(total_plane_area(&storage_adjusted) < total_plane_area(&baseline));
3413    }
3414
3415    #[test]
3416    fn render_frame_layout_resolution_takes_precedence_over_storage_and_explicit_aspect() {
3417        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\nLayoutResX: 400\nLayoutResY: 240\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,18,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(0,0)}Layout").expect("script should parse");
3418        let engine = RenderEngine::new();
3419        let provider = FontconfigProvider::new();
3420
3421        let baseline = engine.render_frame_with_provider_and_config(
3422            &track,
3423            &provider,
3424            500,
3425            &RendererConfig {
3426                frame: Size {
3427                    width: 400,
3428                    height: 240,
3429                },
3430                ..default_renderer_config(&track)
3431            },
3432        );
3433        let overridden_inputs = engine.render_frame_with_provider_and_config(
3434            &track,
3435            &provider,
3436            500,
3437            &RendererConfig {
3438                frame: Size {
3439                    width: 400,
3440                    height: 240,
3441                },
3442                storage: Size {
3443                    width: 400,
3444                    height: 120,
3445                },
3446                pixel_aspect: 2.0,
3447                ..default_renderer_config(&track)
3448            },
3449        );
3450
3451        assert_eq!(
3452            total_plane_area(&overridden_inputs),
3453            total_plane_area(&baseline)
3454        );
3455    }
3456
3457    #[test]
3458    fn render_frame_applies_line_position_to_subtitles() {
3459        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,Shift").expect("script should parse");
3460        let engine = RenderEngine::new();
3461        let provider = FontconfigProvider::new();
3462
3463        let baseline = engine.render_frame_with_provider(&track, &provider, 500);
3464        let shifted = engine.render_frame_with_provider_and_config(
3465            &track,
3466            &provider,
3467            500,
3468            &RendererConfig {
3469                frame: Size {
3470                    width: 200,
3471                    height: 120,
3472                },
3473                line_position: 50.0,
3474                ..RendererConfig::default()
3475            },
3476        );
3477
3478        let baseline_y = baseline
3479            .iter()
3480            .map(|plane| plane.destination.y)
3481            .min()
3482            .expect("baseline plane");
3483        let shifted_y = shifted
3484            .iter()
3485            .map(|plane| plane.destination.y)
3486            .min()
3487            .expect("shifted plane");
3488
3489        assert!(shifted_y < baseline_y);
3490    }
3491
3492    #[test]
3493    fn render_frame_applies_line_spacing_to_multiline_subtitles() {
3494        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 140\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,One\\NTwo").expect("script should parse");
3495        let engine = RenderEngine::new();
3496        let provider = FontconfigProvider::new();
3497
3498        let baseline = engine.render_frame_with_provider(&track, &provider, 500);
3499        let spaced = engine.render_frame_with_provider_and_config(
3500            &track,
3501            &provider,
3502            500,
3503            &RendererConfig {
3504                frame: Size {
3505                    width: 200,
3506                    height: 140,
3507                },
3508                line_spacing: 20.0,
3509                ..RendererConfig::default()
3510            },
3511        );
3512
3513        assert!(vertical_span(&spaced) > vertical_span(&baseline));
3514    }
3515
3516    #[test]
3517    fn render_frame_avoids_basic_bottom_collision_for_unpositioned_events() {
3518        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,First\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,Second").expect("script should parse");
3519        let engine = RenderEngine::new();
3520        let provider = FontconfigProvider::new();
3521        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3522
3523        let mut ys = planes
3524            .iter()
3525            .filter(|plane| plane.kind == ass::ImageType::Character)
3526            .map(|plane| plane.destination.y)
3527            .collect::<Vec<_>>();
3528        ys.sort_unstable();
3529        ys.dedup();
3530
3531        assert!(ys.len() >= 2);
3532        assert!(ys.last().expect("max y") - ys.first().expect("min y") >= 20);
3533    }
3534
3535    #[test]
3536    fn render_frame_allows_basic_collision_across_different_layers() {
3537        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\\1c&H0000FF&}First\nDialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\\1c&H00FF00&}Second").expect("script should parse");
3538        let engine = RenderEngine::new();
3539        let provider = FontconfigProvider::new();
3540        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3541
3542        let layer0_y = planes
3543            .iter()
3544            .filter(|plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0xFF00_0000)
3545            .map(|plane| plane.destination.y)
3546            .min()
3547            .expect("layer 0 character plane");
3548        let layer1_y = planes
3549            .iter()
3550            .filter(|plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x00FF_0000)
3551            .map(|plane| plane.destination.y)
3552            .min()
3553            .expect("layer 1 character plane");
3554
3555        assert_eq!(layer0_y, layer1_y);
3556    }
3557
3558    #[test]
3559    fn render_frame_interpolates_move_position() {
3560        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\move(0,0,100,0,0,1000)}Hi").expect("script should parse");
3561        let engine = RenderEngine::new();
3562        let provider = FontconfigProvider::new();
3563        let start_planes = engine.render_frame_with_provider(&track, &provider, 0);
3564        let mid_planes = engine.render_frame_with_provider(&track, &provider, 500);
3565        let end_planes = engine.render_frame_with_provider(&track, &provider, 999);
3566
3567        let start_x = start_planes
3568            .iter()
3569            .map(|plane| plane.destination.x)
3570            .min()
3571            .expect("start plane");
3572        let mid_x = mid_planes
3573            .iter()
3574            .map(|plane| plane.destination.x)
3575            .min()
3576            .expect("mid plane");
3577        let end_x = end_planes
3578            .iter()
3579            .map(|plane| plane.destination.x)
3580            .min()
3581            .expect("end plane");
3582
3583        assert!(start_x <= mid_x);
3584        assert!(mid_x <= end_x);
3585        assert!(end_x - start_x >= 80);
3586    }
3587
3588    #[test]
3589    fn render_frame_applies_z_rotation_to_event_planes() {
3590        let baseline = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(40,40)\\p1}m 0 0 l 40 0 40 10 0 10").expect("script should parse");
3591        let rotated = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(40,40)\\frz90\\p1}m 0 0 l 40 0 40 10 0 10").expect("script should parse");
3592        let engine = RenderEngine::new();
3593        let provider = FontconfigProvider::new();
3594        let baseline_planes = engine.render_frame_with_provider(&baseline, &provider, 500);
3595        let rotated_planes = engine.render_frame_with_provider(&rotated, &provider, 500);
3596        let baseline_bounds = character_bounds(&baseline_planes).expect("baseline bounds");
3597        let rotated_bounds = character_bounds(&rotated_planes).expect("rotated bounds");
3598
3599        assert!(baseline_bounds.width() > baseline_bounds.height());
3600        assert!(rotated_bounds.height() > rotated_bounds.width());
3601    }
3602
3603    #[test]
3604    fn render_frame_interpolates_z_rotation_transform() {
3605        let track = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(40,40)\\t(0,1000,\\frz90)\\p1}m 0 0 l 40 0 40 10 0 10").expect("script should parse");
3606        let engine = RenderEngine::new();
3607        let provider = FontconfigProvider::new();
3608        let start_planes = engine.render_frame_with_provider(&track, &provider, 0);
3609        let end_planes = engine.render_frame_with_provider(&track, &provider, 999);
3610        let start_bounds = character_bounds(&start_planes).expect("start bounds");
3611        let end_bounds = character_bounds(&end_planes).expect("end bounds");
3612
3613        assert!(start_bounds.width() > start_bounds.height());
3614        assert!(end_bounds.height() > end_bounds.width());
3615    }
3616
3617    #[test]
3618    fn render_frame_applies_fad_alpha() {
3619        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\fad(200,200)}Hi").expect("script should parse");
3620        let engine = RenderEngine::new();
3621        let provider = FontconfigProvider::new();
3622        let start_planes = engine.render_frame_with_provider(&track, &provider, 0);
3623        let mid_planes = engine.render_frame_with_provider(&track, &provider, 500);
3624        let end_planes = engine.render_frame_with_provider(&track, &provider, 999);
3625
3626        let start_alpha = start_planes
3627            .iter()
3628            .map(|plane| plane.color.0 & 0xFF)
3629            .max()
3630            .expect("start alpha");
3631        let mid_alpha = mid_planes
3632            .iter()
3633            .map(|plane| plane.color.0 & 0xFF)
3634            .max()
3635            .expect("mid alpha");
3636        let end_alpha = end_planes
3637            .iter()
3638            .map(|plane| plane.color.0 & 0xFF)
3639            .max()
3640            .expect("end alpha");
3641
3642        assert!(start_alpha > mid_alpha);
3643        assert!(end_alpha > mid_alpha);
3644    }
3645
3646    #[test]
3647    fn render_frame_applies_full_fade_alpha() {
3648        let track = parse_script_text("[Script Info]\nPlayResX: 200\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\fade(255,0,128,0,200,700,1000)}Hi").expect("script should parse");
3649        let engine = RenderEngine::new();
3650        let provider = FontconfigProvider::new();
3651        let start_planes = engine.render_frame_with_provider(&track, &provider, 0);
3652        let middle_planes = engine.render_frame_with_provider(&track, &provider, 400);
3653        let late_planes = engine.render_frame_with_provider(&track, &provider, 850);
3654
3655        let start_alpha = start_planes
3656            .iter()
3657            .map(|plane| plane.color.0 & 0xFF)
3658            .max()
3659            .expect("start alpha");
3660        let middle_alpha = middle_planes
3661            .iter()
3662            .map(|plane| plane.color.0 & 0xFF)
3663            .max()
3664            .expect("middle alpha");
3665        let late_alpha = late_planes
3666            .iter()
3667            .map(|plane| plane.color.0 & 0xFF)
3668            .max()
3669            .expect("late alpha");
3670
3671        assert!(start_alpha > middle_alpha);
3672        assert!(late_alpha > middle_alpha);
3673        assert!(late_alpha < start_alpha);
3674    }
3675
3676    #[test]
3677    fn render_frame_switches_karaoke_fill_after_elapsed_span() {
3678        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H00445566,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:02.00,Default,,0000,0000,0000,,{\\an7\\pos(20,20)\\k50}Ka").expect("script should parse");
3679        let engine = RenderEngine::new();
3680        let provider = FontconfigProvider::new();
3681        let early_planes = engine.render_frame_with_provider(&track, &provider, 200);
3682        let late_planes = engine.render_frame_with_provider(&track, &provider, 700);
3683
3684        assert!(
3685            early_planes.iter().any(
3686                |plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x6655_4400
3687            )
3688        );
3689        assert!(
3690            late_planes.iter().any(
3691                |plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x3322_1100
3692            )
3693        );
3694    }
3695
3696    #[test]
3697    fn render_frame_sweeps_karaoke_fill_during_active_span() {
3698        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H00445566,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:02.00,Default,,0000,0000,0000,,{\\an7\\pos(20,20)\\K100}Kara").expect("script should parse");
3699        let engine = RenderEngine::new();
3700        let provider = FontconfigProvider::new();
3701        let mid_planes = engine.render_frame_with_provider(&track, &provider, 500);
3702
3703        assert!(
3704            mid_planes.iter().any(
3705                |plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x3322_1100
3706            )
3707        );
3708        assert!(
3709            mid_planes.iter().any(
3710                |plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x6655_4400
3711            )
3712        );
3713    }
3714
3715    #[test]
3716    fn render_frame_hides_outline_for_ko_until_span_ends() {
3717        let track = parse_script_text("[Script Info]\nPlayResX: 240\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H00445566,&H000A0B0C,&H00000000,0,0,0,0,100,100,0,0,1,2,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:02.00,Default,,0000,0000,0000,,{\\an7\\pos(20,20)\\ko50}Ko").expect("script should parse");
3718        let engine = RenderEngine::new();
3719        let provider = FontconfigProvider::new();
3720        let early_planes = engine.render_frame_with_provider(&track, &provider, 200);
3721        let late_planes = engine.render_frame_with_provider(&track, &provider, 700);
3722
3723        assert!(
3724            !early_planes
3725                .iter()
3726                .any(|plane| plane.kind == ass::ImageType::Outline)
3727        );
3728        assert!(
3729            late_planes
3730                .iter()
3731                .any(|plane| plane.kind == ass::ImageType::Outline)
3732        );
3733    }
3734
3735    #[test]
3736    fn render_frame_renders_drawing_plane() {
3737        let track = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\p1}m 0 0 l 8 0 8 8 0 8").expect("script should parse");
3738        let engine = RenderEngine::new();
3739        let provider = FontconfigProvider::new();
3740        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3741
3742        assert!(
3743            planes.iter().any(
3744                |plane| plane.kind == ass::ImageType::Character && plane.color.0 == 0x3322_1100
3745            )
3746        );
3747        let plane = planes
3748            .iter()
3749            .find(|plane| plane.kind == ass::ImageType::Character)
3750            .expect("drawing plane");
3751        assert_eq!(plane.destination.x, 10);
3752        assert_eq!(plane.destination.y, 10);
3753        assert!(plane.bitmap.contains(&255));
3754    }
3755
3756    #[test]
3757    fn render_frame_renders_bezier_drawing_plane() {
3758        let track = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\p1}m 0 0 b 10 0 10 10 0 10").expect("script should parse");
3759        let engine = RenderEngine::new();
3760        let provider = FontconfigProvider::new();
3761        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3762
3763        let plane = planes
3764            .iter()
3765            .find(|plane| plane.kind == ass::ImageType::Character)
3766            .expect("drawing plane");
3767        assert!(plane.bitmap.contains(&255));
3768        assert!(plane.size.width >= 8);
3769        assert!(plane.size.height >= 8);
3770    }
3771
3772    #[test]
3773    fn render_frame_emits_outline_and_shadow_for_drawings() {
3774        let track = parse_script_text("[Script Info]\nPlayResX: 100\nPlayResY: 100\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H000A0B0C,&H00445566,0,0,0,0,100,100,0,0,1,2,3,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\p1}m 0 0 l 8 0 8 8 0 8").expect("script should parse");
3775        let engine = RenderEngine::new();
3776        let provider = FontconfigProvider::new();
3777        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3778
3779        assert!(
3780            planes
3781                .iter()
3782                .any(|plane| plane.kind == ass::ImageType::Outline && plane.color.0 == 0x0C0B_0A00)
3783        );
3784        assert!(
3785            planes
3786                .iter()
3787                .any(|plane| plane.kind == ass::ImageType::Shadow && plane.color.0 == 0x6655_4400)
3788        );
3789    }
3790
3791    #[test]
3792    fn render_frame_renders_spline_drawing_plane() {
3793        let track = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\p1}m 0 0 s 10 0 10 10 0 10 p -5 5 c").expect("script should parse");
3794        let engine = RenderEngine::new();
3795        let provider = FontconfigProvider::new();
3796        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3797
3798        let plane = planes
3799            .iter()
3800            .find(|plane| plane.kind == ass::ImageType::Character)
3801            .expect("drawing plane");
3802        assert!(plane.bitmap.contains(&255));
3803        assert!(plane.size.width >= 10);
3804        assert!(plane.size.height >= 10);
3805    }
3806
3807    #[test]
3808    fn render_frame_renders_non_closing_move_subpaths() {
3809        let track = parse_script_text("[Script Info]\nPlayResX: 120\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H00112233,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\p1}m 0 0 l 8 0 8 8 0 8 n 20 20 l 28 20 28 28 20 28").expect("script should parse");
3810        let engine = RenderEngine::new();
3811        let provider = FontconfigProvider::new();
3812        let planes = engine.render_frame_with_provider(&track, &provider, 500);
3813
3814        let plane = planes
3815            .iter()
3816            .find(|plane| plane.kind == ass::ImageType::Character)
3817            .expect("drawing plane");
3818        assert!(plane.bitmap.contains(&255));
3819        assert!(plane.size.width >= 28);
3820        assert!(plane.size.height >= 28);
3821    }
3822
3823    #[test]
3824    fn render_frame_applies_timed_transform_style() {
3825        let track = parse_script_text("[Script Info]\nPlayResX: 160\nPlayResY: 120\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,sans,24,&H000000FF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0000,0000,0000,,{\\an7\\pos(10,10)\\t(0,1000,\\1c&H00112233&\\fs48\\bord4)}Hi").expect("script should parse");
3826        let engine = RenderEngine::new();
3827        let provider = FontconfigProvider::new();
3828        let start_planes = engine.render_frame_with_provider(&track, &provider, 0);
3829        let mid_planes = engine.render_frame_with_provider(&track, &provider, 500);
3830        let end_planes = engine.render_frame_with_provider(&track, &provider, 999);
3831
3832        assert!(
3833            !start_planes
3834                .iter()
3835                .any(|plane| plane.kind == ass::ImageType::Outline)
3836        );
3837        assert!(
3838            mid_planes
3839                .iter()
3840                .any(|plane| plane.kind == ass::ImageType::Outline)
3841        );
3842        assert!(
3843            end_planes
3844                .iter()
3845                .any(|plane| plane.kind == ass::ImageType::Outline)
3846        );
3847
3848        let start_fill = start_planes
3849            .iter()
3850            .find(|plane| plane.kind == ass::ImageType::Character)
3851            .expect("start fill")
3852            .color
3853            .0;
3854        let end_fill = end_planes
3855            .iter()
3856            .find(|plane| plane.kind == ass::ImageType::Character)
3857            .expect("end fill")
3858            .color
3859            .0;
3860        assert_ne!(start_fill, end_fill);
3861        assert!(total_plane_area(&end_planes) > total_plane_area(&start_planes));
3862    }
3863}