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