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