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