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 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 ¤t 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}