1use rassa_core::{RassaError, RassaResult, Rect, ass};
2use rassa_fonts::{FontMatch, FontProvider, FontQuery};
3use rassa_parse::{
4 ParsedDrawing, ParsedEvent, ParsedFade, ParsedKaraokeSpan, ParsedMovement, ParsedSpanStyle,
5 ParsedSpanTransform, ParsedTrack, ParsedVectorClip, parse_dialogue_text,
6};
7use rassa_shape::{GlyphInfo, ShapeEngine, ShapeRequest, ShapingMode};
8use rassa_unicode::BidiDirection;
9
10#[derive(Clone, Debug, Default, PartialEq)]
11pub struct LayoutGlyphRun {
12 pub text: String,
13 pub direction: BidiDirection,
14 pub font_family: String,
15 pub font: FontMatch,
16 pub glyphs: Vec<GlyphInfo>,
17 pub width: f32,
18 pub style: ParsedSpanStyle,
19 pub transforms: Vec<ParsedSpanTransform>,
20 pub karaoke: Option<ParsedKaraokeSpan>,
21 pub drawing: Option<ParsedDrawing>,
22}
23
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct LayoutLine {
26 pub event_index: usize,
27 pub style_index: usize,
28 pub text: String,
29 pub direction: BidiDirection,
30 pub glyph_count: usize,
31 pub width: f32,
32 pub runs: Vec<LayoutGlyphRun>,
33}
34
35#[derive(Clone, Debug, Default, PartialEq)]
36pub struct LayoutEvent {
37 pub event_index: usize,
38 pub style_index: usize,
39 pub text: String,
40 pub font_family: String,
41 pub font: FontMatch,
42 pub alignment: i32,
43 pub justify: i32,
44 pub margin_l: i32,
45 pub margin_r: i32,
46 pub margin_v: i32,
47 pub position: Option<(i32, i32)>,
48 pub movement: Option<ParsedMovement>,
49 pub fade: Option<ParsedFade>,
50 pub clip_rect: Option<Rect>,
51 pub vector_clip: Option<ParsedVectorClip>,
52 pub inverse_clip: bool,
53 pub wrap_style: Option<i32>,
54 pub origin: Option<(i32, i32)>,
55 pub lines: Vec<LayoutLine>,
56}
57
58#[derive(Default)]
59pub struct LayoutEngine {
60 shaper: ShapeEngine,
61}
62
63impl LayoutEngine {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn layout_track_event_with_mode<P: FontProvider>(
69 &self,
70 track: &ParsedTrack,
71 event_index: usize,
72 provider: &P,
73 shaping_mode: ShapingMode,
74 ) -> RassaResult<LayoutEvent> {
75 let event = track
76 .events
77 .get(event_index)
78 .ok_or_else(|| RassaError::new(format!("event index {event_index} out of range")))?;
79 let style_index = normalize_style_index(track, event);
80 let style = track
81 .styles
82 .get(style_index)
83 .unwrap_or(&track.styles[track.default_style as usize]);
84 let parsed_text = parse_dialogue_text(&event.text, style, &track.styles);
85 let font = provider.resolve(&FontQuery {
86 family: style.font_name.clone(),
87 style: None,
88 });
89 let lines = parsed_text
90 .lines
91 .iter()
92 .map(|line| {
93 layout_line_from_text(
94 event_index,
95 style_index,
96 line,
97 provider,
98 &self.shaper,
99 &track.language,
100 shaping_mode,
101 )
102 })
103 .collect::<RassaResult<Vec<_>>>()?;
104
105 Ok(LayoutEvent {
106 event_index,
107 style_index,
108 text: parsed_text
109 .lines
110 .iter()
111 .map(|line| line.text.as_str())
112 .collect::<Vec<_>>()
113 .join("\n"),
114 font_family: font.family.clone(),
115 font: font.clone(),
116 alignment: parsed_text.alignment.unwrap_or(style.alignment),
117 justify: normalize_justify(style.justify, style.alignment),
118 margin_l: resolve_margin(event.margin_l, style.margin_l),
119 margin_r: resolve_margin(event.margin_r, style.margin_r),
120 margin_v: resolve_margin(event.margin_v, style.margin_v),
121 position: parsed_text.position,
122 movement: parsed_text.movement,
123 fade: parsed_text.fade,
124 clip_rect: parsed_text.clip_rect,
125 vector_clip: parsed_text.vector_clip,
126 inverse_clip: parsed_text.inverse_clip,
127 wrap_style: parsed_text.wrap_style,
128 origin: parsed_text.origin,
129 lines,
130 })
131 }
132
133 pub fn layout_track_event<P: FontProvider>(
134 &self,
135 track: &ParsedTrack,
136 event_index: usize,
137 provider: &P,
138 ) -> RassaResult<LayoutEvent> {
139 self.layout_track_event_with_mode(track, event_index, provider, ShapingMode::Complex)
140 }
141}
142
143fn layout_line_from_text<P: FontProvider>(
144 event_index: usize,
145 style_index: usize,
146 line: &rassa_parse::ParsedTextLine,
147 provider: &P,
148 shaper: &ShapeEngine,
149 language: &str,
150 shaping_mode: ShapingMode,
151) -> RassaResult<LayoutLine> {
152 let mut runs = Vec::new();
153 let mut line_direction = BidiDirection::LeftToRight;
154 for span in &line.spans {
155 if span.text.is_empty() {
156 continue;
157 }
158 let font = provider.resolve(&FontQuery {
159 family: span.style.font_name.clone(),
160 style: font_style_name(&span.style),
161 });
162 if let Some(drawing) = &span.drawing {
163 let width = drawing
164 .bounds()
165 .map(|bounds| bounds.width() as f32 * span.style.scale_x.max(0.0) as f32)
166 .unwrap_or_default();
167 runs.push(LayoutGlyphRun {
168 text: span.text.clone(),
169 direction: line_direction,
170 font_family: font.family.clone(),
171 font: font.clone(),
172 glyphs: Vec::new(),
173 width,
174 style: span.style.clone(),
175 transforms: span.transforms.clone(),
176 karaoke: span.karaoke,
177 drawing: Some(drawing.clone()),
178 });
179 continue;
180 }
181 let shaped = shaper.shape_text(
182 provider,
183 &ShapeRequest::new(&span.text, &span.style.font_name)
184 .with_style(font_style_name(&span.style).unwrap_or_default())
185 .with_language(language)
186 .with_font_size(span.style.font_size as f32)
187 .with_mode(shaping_mode),
188 )?;
189 for shaped_run in shaped.runs {
190 line_direction = shaped_run.direction;
191 runs.push(LayoutGlyphRun {
192 text: shaped_run.text,
193 direction: shaped_run.direction,
194 font_family: font.family.clone(),
195 font: font.clone(),
196 width: text_run_width(&shaped_run.glyphs, &span.style),
197 glyphs: shaped_run.glyphs,
198 style: span.style.clone(),
199 transforms: span.transforms.clone(),
200 karaoke: span.karaoke,
201 drawing: None,
202 });
203 }
204 }
205
206 let glyph_count = runs.iter().map(|run| run.glyphs.len()).sum();
207 let width = runs.iter().map(|run| run.width).sum();
208 Ok(LayoutLine {
209 event_index,
210 style_index,
211 text: line.text.clone(),
212 direction: line_direction,
213 glyph_count,
214 width,
215 runs,
216 })
217}
218
219fn text_run_width(glyphs: &[GlyphInfo], style: &ParsedSpanStyle) -> f32 {
220 let scale_x = style.scale_x.max(0.0) as f32;
221 let spacing = if style.spacing.is_finite() {
222 style.spacing as f32 * scale_x
223 } else {
224 0.0
225 };
226 glyphs
227 .iter()
228 .map(|glyph| glyph.x_advance * scale_x + spacing)
229 .sum()
230}
231
232fn font_style_name(style: &ParsedSpanStyle) -> Option<String> {
233 match (style.bold, style.italic) {
234 (true, true) => Some("Bold Italic".to_string()),
235 (true, false) => Some("Bold".to_string()),
236 (false, true) => Some("Italic".to_string()),
237 (false, false) => None,
238 }
239}
240
241fn normalize_style_index(track: &ParsedTrack, event: &ParsedEvent) -> usize {
242 if track.styles.is_empty() {
243 return 0;
244 }
245
246 let candidate = usize::try_from(event.style).unwrap_or(0);
247 if candidate < track.styles.len() {
248 candidate
249 } else {
250 usize::try_from(track.default_style)
251 .ok()
252 .filter(|index| *index < track.styles.len())
253 .unwrap_or(0)
254 }
255}
256
257fn resolve_margin(event_margin: i32, style_margin: i32) -> i32 {
258 if event_margin == 0 {
259 style_margin
260 } else {
261 event_margin
262 }
263}
264
265fn normalize_justify(justify: i32, alignment: i32) -> i32 {
266 if justify != ass::ASS_JUSTIFY_AUTO {
267 return justify;
268 }
269
270 match alignment & 0x3 {
271 ass::HALIGN_LEFT => ass::ASS_JUSTIFY_LEFT,
272 ass::HALIGN_RIGHT => ass::ASS_JUSTIFY_RIGHT,
273 _ => ass::ASS_JUSTIFY_CENTER,
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use rassa_fonts::{FontconfigProvider, NullFontProvider};
281 use rassa_parse::{ParsedKaraokeMode, ParsedTrack, parse_script_text};
282
283 fn parse_track(input: &str) -> ParsedTrack {
284 parse_script_text(input).expect("script should parse")
285 }
286
287 #[test]
288 fn layout_uses_style_font_and_event_margins() {
289 let track = parse_track(
290 "[Script Info]\nLanguage: en\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, Justify\nStyle: Default,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,11,12,13,1,0\nStyle: Sign,DejaVu Sans,28,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,9,21,22,23,1,0\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Sign,,0030,0000,0040,,Visible text",
291 );
292 let engine = LayoutEngine::new();
293 let provider = NullFontProvider;
294 let layout = engine
295 .layout_track_event(&track, 0, &provider)
296 .expect("layout should succeed");
297
298 assert_eq!(layout.style_index, 1);
299 assert_eq!(layout.font_family, "DejaVu Sans");
300 assert_eq!(layout.margin_l, 30);
301 assert_eq!(layout.margin_r, 22);
302 assert_eq!(layout.margin_v, 40);
303 assert_eq!(layout.lines.len(), 1);
304 assert_eq!(layout.lines[0].glyph_count, "Visible text".chars().count());
305 assert_eq!(layout.lines[0].runs.len(), 1);
306 }
307
308 #[test]
309 fn layout_splits_lines_on_mandatory_breaks() {
310 let mut track = parse_track(
311 "[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,,seed",
312 );
313 track.events[0].text = "a\nb".to_string();
314 let engine = LayoutEngine::new();
315 let provider = NullFontProvider;
316 let layout = engine
317 .layout_track_event(&track, 0, &provider)
318 .expect("layout should succeed");
319
320 assert_eq!(layout.lines.len(), 2);
321 assert_eq!(layout.lines[0].text, "a");
322 assert_eq!(layout.lines[1].text, "b");
323 }
324
325 #[test]
326 fn layout_applies_font_override_runs() {
327 let track = parse_track(
328 "[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,,{\\fnDejaVu Sans}Hello{\\fnArial} world",
329 );
330 let engine = LayoutEngine::new();
331 let provider = NullFontProvider;
332 let layout = engine
333 .layout_track_event(&track, 0, &provider)
334 .expect("layout should succeed");
335
336 assert_eq!(layout.lines.len(), 1);
337 assert_eq!(layout.lines[0].runs.len(), 2);
338 assert_eq!(layout.lines[0].runs[0].style.font_name, "DejaVu Sans");
339 assert_eq!(layout.lines[0].runs[1].style.font_name, "Arial");
340 }
341
342 #[test]
343 fn layout_carries_clip_metadata() {
344 let track = parse_track(
345 "[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,,{\\iclip(10,20,30,40)}Clip",
346 );
347 let engine = LayoutEngine::new();
348 let provider = NullFontProvider;
349 let layout = engine
350 .layout_track_event(&track, 0, &provider)
351 .expect("layout should succeed");
352
353 assert_eq!(
354 layout.clip_rect,
355 Some(Rect {
356 x_min: 10,
357 y_min: 20,
358 x_max: 30,
359 y_max: 40
360 })
361 );
362 assert!(layout.vector_clip.is_none());
363 assert!(layout.inverse_clip);
364 }
365
366 #[test]
367 fn layout_carries_vector_clip_metadata() {
368 let track = parse_track(
369 "[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,,{\\clip(m 0 0 l 8 0 8 8 0 8)}Clip",
370 );
371 let engine = LayoutEngine::new();
372 let provider = NullFontProvider;
373 let layout = engine
374 .layout_track_event(&track, 0, &provider)
375 .expect("layout should succeed");
376
377 assert!(layout.clip_rect.is_none());
378 assert!(layout.vector_clip.is_some());
379 assert!(!layout.inverse_clip);
380 }
381
382 #[test]
383 fn layout_carries_move_metadata() {
384 let track = parse_track(
385 "[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,,{\\move(1,2,3,4,50,150)}Move",
386 );
387 let engine = LayoutEngine::new();
388 let provider = NullFontProvider;
389 let layout = engine
390 .layout_track_event(&track, 0, &provider)
391 .expect("layout should succeed");
392
393 assert_eq!(
394 layout.movement,
395 Some(ParsedMovement {
396 start: (1, 2),
397 end: (3, 4),
398 t1_ms: 50,
399 t2_ms: 150,
400 })
401 );
402 }
403
404 #[test]
405 fn layout_carries_fade_metadata() {
406 let track = parse_track(
407 "[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,,{\\fad(100,200)}Fade",
408 );
409 let engine = LayoutEngine::new();
410 let provider = NullFontProvider;
411 let layout = engine
412 .layout_track_event(&track, 0, &provider)
413 .expect("layout should succeed");
414
415 assert_eq!(
416 layout.fade,
417 Some(ParsedFade::Simple {
418 fade_in_ms: 100,
419 fade_out_ms: 200,
420 })
421 );
422 }
423
424 #[test]
425 fn layout_carries_full_fade_metadata() {
426 let track = parse_track(
427 "[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,,{\\fade(10,20,30,40,50,60,70)}Fade",
428 );
429 let engine = LayoutEngine::new();
430 let provider = NullFontProvider;
431 let layout = engine
432 .layout_track_event(&track, 0, &provider)
433 .expect("layout should succeed");
434
435 assert_eq!(
436 layout.fade,
437 Some(ParsedFade::Complex {
438 alpha1: 10,
439 alpha2: 20,
440 alpha3: 30,
441 t1_ms: 40,
442 t2_ms: 50,
443 t3_ms: 60,
444 t4_ms: 70,
445 })
446 );
447 }
448
449 #[test]
450 fn layout_carries_karaoke_metadata() {
451 let track = parse_track(
452 "[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,,{\\k10}Ka{\\k20}ra",
453 );
454 let engine = LayoutEngine::new();
455 let provider = NullFontProvider;
456 let layout = engine
457 .layout_track_event(&track, 0, &provider)
458 .expect("layout should succeed");
459
460 assert_eq!(layout.lines[0].runs.len(), 2);
461 assert_eq!(
462 layout.lines[0].runs[0].karaoke,
463 Some(ParsedKaraokeSpan {
464 start_ms: 0,
465 duration_ms: 100,
466 mode: ParsedKaraokeMode::FillSwap,
467 })
468 );
469 assert_eq!(
470 layout.lines[0].runs[1].karaoke,
471 Some(ParsedKaraokeSpan {
472 start_ms: 100,
473 duration_ms: 200,
474 mode: ParsedKaraokeMode::FillSwap,
475 })
476 );
477 }
478
479 #[test]
480 fn layout_carries_transform_metadata() {
481 let track = parse_track(
482 "[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,&H000000FF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,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,,{\\t(0,1000,\\bord4\\1c&H00112233&)}Hi",
483 );
484 let engine = LayoutEngine::new();
485 let provider = NullFontProvider;
486 let layout = engine
487 .layout_track_event(&track, 0, &provider)
488 .expect("layout should succeed");
489
490 assert_eq!(layout.lines[0].runs[0].transforms.len(), 1);
491 assert_eq!(
492 layout.lines[0].runs[0].transforms[0].style.border,
493 Some(4.0)
494 );
495 assert_eq!(
496 layout.lines[0].runs[0].transforms[0].style.primary_colour,
497 Some(0x0011_2233)
498 );
499 }
500
501 #[test]
502 fn layout_carries_drawing_runs() {
503 let track = parse_track(
504 "[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,,{\\p1}m 0 0 l 8 0 8 8 0 8",
505 );
506 let engine = LayoutEngine::new();
507 let provider = NullFontProvider;
508 let layout = engine
509 .layout_track_event(&track, 0, &provider)
510 .expect("layout should succeed");
511
512 assert_eq!(layout.lines[0].runs.len(), 1);
513 assert!(layout.lines[0].runs[0].drawing.is_some());
514 assert_eq!(layout.lines[0].runs[0].width, 9.0);
515 }
516
517 #[test]
518 fn layout_carries_missing_override_metadata() {
519 let track = parse_track(
520 "[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,,{\\u1\\s1\\a10\\q2\\org(320,240)\\frx12\\fry-8\\fax0.25\\fay-0.5\\xbord3\\ybord4\\xshad5\\yshad-6\\be2\\pbo7}Meta",
521 );
522 let engine = LayoutEngine::new();
523 let provider = NullFontProvider;
524 let layout = engine
525 .layout_track_event(&track, 0, &provider)
526 .expect("layout should succeed");
527
528 assert_eq!(layout.alignment, ass::VALIGN_CENTER | ass::HALIGN_CENTER);
529 assert_eq!(layout.wrap_style, Some(2));
530 assert_eq!(layout.origin, Some((320, 240)));
531 let style = &layout.lines[0].runs[0].style;
532 assert!(style.underline);
533 assert!(style.strike_out);
534 assert_eq!(style.rotation_x, 12.0);
535 assert_eq!(style.rotation_y, -8.0);
536 assert_eq!(style.shear_x, 0.25);
537 assert_eq!(style.shear_y, -0.5);
538 assert_eq!(style.border_x, 3.0);
539 assert_eq!(style.border_y, 4.0);
540 assert_eq!(style.shadow_x, 5.0);
541 assert_eq!(style.shadow_y, -6.0);
542 assert_eq!(style.be, 2.0);
543 assert_eq!(style.pbo, 7.0);
544 }
545
546 #[test]
547 fn layout_accepts_explicit_shaping_mode() {
548 let track = parse_track(
549 "[Script Info]\nLanguage: en\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,36,&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,,office",
550 );
551 let engine = LayoutEngine::new();
552 let provider = FontconfigProvider::new();
553 let simple = engine
554 .layout_track_event_with_mode(&track, 0, &provider, ShapingMode::Simple)
555 .expect("simple layout should succeed");
556 let complex = engine
557 .layout_track_event_with_mode(&track, 0, &provider, ShapingMode::Complex)
558 .expect("complex layout should succeed");
559
560 assert_eq!(simple.lines.len(), 1);
561 assert_eq!(complex.lines.len(), 1);
562 assert_eq!(simple.lines[0].text, "office");
563 assert_eq!(complex.lines[0].text, "office");
564 }
565}