1#![allow(dead_code)]
2
3mod crossfont;
4
5use std::{
6 collections::HashMap,
7 sync::{Mutex, OnceLock},
8};
9
10#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
11use freetype::{
12 Bitmap, GlyphSlot, Library, Matrix, RenderMode, StrokerLineCap, StrokerLineJoin, Vector,
13 face::LoadFlag, ffi,
14};
15
16use crate::crossfont::{
17 BitmapBuffer, FontDesc, GlyphIdKey, Rasterize, RasterizedGlyph, Size, Style,
18};
19use rassa_core::{RassaError, RassaResult, ass};
20use rassa_fonts::{FontMatch, FontProviderKind};
21use rassa_shape::{GlyphInfo, ShapedRun};
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum RasterPixelMode {
25 Mono,
26 #[default]
27 Gray,
28 Other,
29}
30
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
32pub struct RasterGlyph {
33 pub glyph_id: u32,
34 pub cluster: usize,
35 pub width: i32,
36 pub height: i32,
37 pub stride: i32,
38 pub left: i32,
39 pub top: i32,
40 pub offset_x: i32,
41 pub offset_y: i32,
42 pub advance_x: i32,
43 pub advance_y: i32,
44 pub pixel_mode: RasterPixelMode,
45 pub bitmap: Vec<u8>,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct RasterOptions {
50 pub size_26_6: i32,
51 pub hinting: ass::Hinting,
52}
53
54impl Default for RasterOptions {
55 fn default() -> Self {
56 Self {
57 size_26_6: 32 * 64,
58 hinting: ass::Hinting::None,
59 }
60 }
61}
62
63#[derive(Default)]
64pub struct Rasterizer {
65 options: RasterOptions,
66}
67
68#[derive(Clone, Debug, Default, PartialEq, Eq)]
69pub struct RasterCacheStats {
70 pub glyph_entries: usize,
71}
72
73#[derive(Clone, Debug, Hash, PartialEq, Eq)]
74struct GlyphCacheKey {
75 family: String,
76 style: Option<String>,
77 synthetic_bold: bool,
78 synthetic_italic: bool,
79 face_index: Option<u32>,
80 glyph_id: u32,
81 size_26_6: i32,
82 hinting: ass::Hinting,
83}
84
85static GLYPH_CACHE: OnceLock<Mutex<HashMap<GlyphCacheKey, RasterGlyph>>> = OnceLock::new();
86
87impl Rasterizer {
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn with_options(options: RasterOptions) -> Self {
93 Self { options }
94 }
95
96 pub fn rasterize(&self, glyphs: &[GlyphInfo]) -> Vec<RasterGlyph> {
97 glyphs
98 .iter()
99 .map(|glyph| RasterGlyph {
100 glyph_id: glyph.glyph_id,
101 cluster: glyph.cluster,
102 offset_x: glyph.x_offset.round() as i32,
103 offset_y: (-glyph.y_offset).round() as i32,
104 advance_x: glyph.x_advance.round() as i32,
105 advance_y: glyph.y_advance.round() as i32,
106 ..RasterGlyph::default()
107 })
108 .collect()
109 }
110
111 pub fn rasterize_glyphs(
112 &self,
113 font: &FontMatch,
114 glyphs: &[GlyphInfo],
115 ) -> RassaResult<Vec<RasterGlyph>> {
116 rasterize_system_glyphs(font, glyphs, self.options)
117 }
118
119 pub fn rasterize_run(&self, run: &ShapedRun) -> RassaResult<Vec<RasterGlyph>> {
120 self.rasterize_glyphs(&run.font, &run.glyphs)
121 }
122
123 pub fn outline_glyphs(&self, glyphs: &[RasterGlyph], radius: i32) -> Vec<RasterGlyph> {
124 glyphs
125 .iter()
126 .map(|glyph| expand_outline(glyph, radius))
127 .collect()
128 }
129
130 pub fn rasterize_outline_glyphs(
131 &self,
132 font: &FontMatch,
133 glyphs: &[GlyphInfo],
134 radius: i32,
135 ) -> RassaResult<Vec<RasterGlyph>> {
136 if radius <= 0 {
137 return self.rasterize_glyphs(font, glyphs);
138 }
139
140 #[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
141 if let Some(font_path) = font.path.as_ref() {
142 let library = Library::init()
143 .map_err(|error| RassaError::new(format!("freetype init failed: {error:?}")))?;
144 let mut face = library
145 .new_face(font_path, font.face_index.unwrap_or(0) as isize)
146 .map_err(|error| {
147 RassaError::new(format!(
148 "failed to load font '{}': {error:?}",
149 font_path.display()
150 ))
151 })?;
152 request_real_dim_size(&mut face, self.options.size_26_6.max(64))?;
153 apply_synthetic_style_transform(&face, font.synthetic_italic);
154 let stroker = library.new_stroker().map_err(|error| {
155 RassaError::new(format!("freetype stroker init failed: {error:?}"))
156 })?;
157 stroker.set(
158 (radius.max(1) * 64).into(),
159 StrokerLineCap::Round,
160 StrokerLineJoin::Round,
161 0,
162 );
163
164 let mut load_flags = load_flags_for_hinting(self.options.hinting);
165 load_flags.remove(LoadFlag::RENDER);
166 let mut outlined = Vec::with_capacity(glyphs.len());
167 for glyph in glyphs {
168 face.load_glyph(glyph.glyph_id, load_flags)
169 .map_err(|error| {
170 RassaError::new(format!(
171 "failed to load outline glyph {}: {error:?}",
172 glyph.glyph_id
173 ))
174 })?;
175 let slot = face.glyph();
176 maybe_embolden_slot(slot, font.synthetic_bold);
177 let advance = slot.advance();
178 let stroked = slot
179 .get_glyph()
180 .and_then(|glyph| glyph.stroke(&stroker))
181 .map_err(|error| {
182 RassaError::new(format!(
183 "failed to stroke outline glyph {}: {error:?}",
184 glyph.glyph_id
185 ))
186 })?;
187 let bitmap_glyph =
188 stroked
189 .to_bitmap(RenderMode::Normal, None)
190 .map_err(|error| {
191 RassaError::new(format!(
192 "failed to render outline glyph {}: {error:?}",
193 glyph.glyph_id
194 ))
195 })?;
196 let bitmap = bitmap_glyph.bitmap();
197 let stride = bitmap.pitch().abs();
198 outlined.push(RasterGlyph {
199 glyph_id: glyph.glyph_id,
200 cluster: glyph.cluster,
201 width: bitmap.width(),
202 height: bitmap.rows(),
203 stride,
204 left: bitmap_glyph.left(),
205 top: bitmap_glyph.top(),
206 offset_x: glyph.x_offset.round() as i32,
207 offset_y: (-glyph.y_offset).round() as i32,
208 advance_x: (advance.x >> 6) as i32,
209 advance_y: (advance.y >> 6) as i32,
210 pixel_mode: classify_pixel_mode(&bitmap),
211 bitmap: copy_bitmap_rows(&bitmap),
212 });
213 }
214 return Ok(outlined);
215 }
216
217 let glyphs = self.rasterize_glyphs(font, glyphs)?;
218 Ok(self.outline_glyphs(&glyphs, radius))
219 }
220
221 pub fn blur_glyphs(&self, glyphs: &[RasterGlyph], radius: u32) -> Vec<RasterGlyph> {
222 glyphs
223 .iter()
224 .map(|glyph| blur_glyph(glyph, radius))
225 .collect()
226 }
227
228 pub fn clear_cache() {
229 glyph_cache()
230 .lock()
231 .expect("glyph cache mutex poisoned")
232 .clear();
233 }
234
235 pub fn cache_stats() -> RasterCacheStats {
236 RasterCacheStats {
237 glyph_entries: glyph_cache()
238 .lock()
239 .expect("glyph cache mutex poisoned")
240 .len(),
241 }
242 }
243}
244
245fn glyph_cache() -> &'static Mutex<HashMap<GlyphCacheKey, RasterGlyph>> {
246 GLYPH_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
247}
248
249#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
250fn apply_synthetic_style_transform(face: &freetype::Face, synthetic_italic: bool) {
251 if synthetic_italic {
252 let mut matrix = Matrix {
253 xx: 0x10000,
254 xy: 0x05000,
255 yx: 0,
256 yy: 0x10000,
257 };
258 let mut delta = Vector { x: 0, y: 0 };
259 face.set_transform(&mut matrix, &mut delta);
260 }
261}
262
263#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
264fn maybe_embolden_slot(slot: &GlyphSlot, synthetic_bold: bool) {
265 if synthetic_bold {
266 unsafe {
267 ffi::FT_GlyphSlot_Embolden(slot.raw() as *const _ as *mut _);
268 }
269 }
270}
271
272#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
273fn rasterize_freetype_glyphs(
274 font: &FontMatch,
275 glyphs: &[GlyphInfo],
276 options: RasterOptions,
277) -> RassaResult<Vec<RasterGlyph>> {
278 let cache_keys = glyphs
279 .iter()
280 .map(|glyph| GlyphCacheKey {
281 family: font.family.clone(),
282 style: font.style.clone(),
283 synthetic_bold: font.synthetic_bold,
284 synthetic_italic: font.synthetic_italic,
285 face_index: font.face_index,
286 glyph_id: glyph.glyph_id,
287 size_26_6: options.size_26_6,
288 hinting: options.hinting,
289 })
290 .collect::<Vec<_>>();
291 let mut cached_glyphs = {
292 let cache = glyph_cache().lock().expect("glyph cache mutex poisoned");
293 cache_keys
294 .iter()
295 .map(|key| cache.get(key).cloned())
296 .collect::<Vec<_>>()
297 };
298 if cached_glyphs.iter().all(Option::is_some) {
299 return Ok(glyphs
300 .iter()
301 .zip(cached_glyphs)
302 .map(|(glyph, cached)| glyph_from_cache(glyph, cached.expect("checked all cache hits")))
303 .collect());
304 }
305
306 let font_path = font
307 .path
308 .as_ref()
309 .ok_or_else(|| RassaError::new(format!("font '{}' is unresolved", font.family)))?;
310 let library = Library::init()
311 .map_err(|error| RassaError::new(format!("freetype init failed: {error:?}")))?;
312 let mut face = library
313 .new_face(font_path, font.face_index.unwrap_or(0) as isize)
314 .map_err(|error| {
315 RassaError::new(format!(
316 "failed to load font '{}': {error:?}",
317 font_path.display()
318 ))
319 })?;
320 request_real_dim_size(&mut face, options.size_26_6.max(64))?;
321 apply_synthetic_style_transform(&face, font.synthetic_italic);
322
323 let mut rasterized = Vec::with_capacity(glyphs.len());
324 let mut load_flags = load_flags_for_hinting(options.hinting);
325 load_flags.remove(LoadFlag::RENDER);
326 for ((glyph, cache_key), cached) in glyphs.iter().zip(cache_keys).zip(cached_glyphs.iter_mut())
327 {
328 if let Some(cached) = cached.take() {
329 rasterized.push(glyph_from_cache(glyph, cached));
330 continue;
331 }
332
333 face.load_glyph(glyph.glyph_id, load_flags)
334 .map_err(|error| {
335 RassaError::new(format!(
336 "failed to load glyph {}: {error:?}",
337 glyph.glyph_id
338 ))
339 })?;
340 let slot = face.glyph();
341 maybe_embolden_slot(slot, font.synthetic_bold);
342 let advance = slot.advance();
343 let rendered = render_slot_to_gray_bitmap(slot, glyph.glyph_id)?;
344 let rendered = RasterGlyph {
345 glyph_id: glyph.glyph_id,
346 cluster: glyph.cluster,
347 width: rendered.width,
348 height: rendered.height,
349 stride: rendered.stride,
350 left: rendered.left,
351 top: rendered.top,
352 offset_x: glyph.x_offset.round() as i32,
353 offset_y: (-glyph.y_offset).round() as i32 + rendered.offset_y,
354 advance_x: (advance.x >> 6) as i32,
355 advance_y: (advance.y >> 6) as i32,
356 pixel_mode: RasterPixelMode::Gray,
357 bitmap: rendered.bitmap,
358 };
359 let cache_entry = RasterGlyph {
360 cluster: 0,
361 offset_x: 0,
362 offset_y: rendered.offset_y - (-glyph.y_offset).round() as i32,
363 ..rendered.clone()
364 };
365 glyph_cache()
366 .lock()
367 .expect("glyph cache mutex poisoned")
368 .insert(cache_key, cache_entry);
369 rasterized.push(rendered);
370 }
371
372 Ok(rasterized)
373}
374
375fn glyph_from_cache(glyph: &GlyphInfo, cached: RasterGlyph) -> RasterGlyph {
376 RasterGlyph {
377 cluster: glyph.cluster,
378 offset_x: glyph.x_offset.round() as i32,
379 offset_y: (-glyph.y_offset).round() as i32 + cached.offset_y,
380 advance_x: cached.advance_x,
381 advance_y: cached.advance_y,
382 ..cached
383 }
384}
385
386fn rasterize_system_glyphs(
387 font: &FontMatch,
388 glyphs: &[GlyphInfo],
389 options: RasterOptions,
390) -> RassaResult<Vec<RasterGlyph>> {
391 #[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
392 if font.path.is_some() {
393 return rasterize_freetype_glyphs(font, glyphs, options);
394 }
395
396 if font.path.is_none() && font.provider != FontProviderKind::Fontconfig {
397 return Ok(Rasterizer::new().rasterize(glyphs));
398 }
399
400 #[cfg(target_arch = "wasm32")]
401 if font.path.is_none() {
402 return Ok(Rasterizer::new().rasterize(glyphs));
403 }
404
405 let mut rasterizer = crossfont::Rasterizer::new()
406 .map_err(|error| RassaError::new(format!("crossfont init failed: {error:?}")))?;
407 let style = font
408 .style
409 .clone()
410 .map(Style::Specific)
411 .unwrap_or_else(|| Style::Description {
412 slant: crossfont::Slant::Normal,
413 weight: crossfont::Weight::Normal,
414 });
415 let desc = FontDesc::new(font.family.clone(), style);
416 let size = Size::from_px((options.size_26_6.max(64) as f32) / 64.0);
417 let font_key = if let Some(path) = &font.path {
418 rasterizer
419 .load_font_path(path, size)
420 .or_else(|_| rasterizer.load_font(&desc, size))
421 } else {
422 rasterizer.load_font(&desc, size)
423 }
424 .map_err(|error| {
425 RassaError::new(format!(
426 "failed to load font '{}' with crossfont: {error:?}",
427 font.family
428 ))
429 })?;
430
431 let mut rasterized = Vec::with_capacity(glyphs.len());
432 for glyph in glyphs {
433 let cache_key = GlyphCacheKey {
434 family: font.family.clone(),
435 style: font.style.clone(),
436 synthetic_bold: font.synthetic_bold,
437 synthetic_italic: font.synthetic_italic,
438 face_index: font.face_index,
439 glyph_id: glyph.glyph_id,
440 size_26_6: options.size_26_6,
441 hinting: options.hinting,
442 };
443 if let Some(cached) = glyph_cache()
444 .lock()
445 .expect("glyph cache mutex poisoned")
446 .get(&cache_key)
447 .cloned()
448 {
449 rasterized.push(RasterGlyph {
450 cluster: glyph.cluster,
451 offset_x: glyph.x_offset.round() as i32,
452 offset_y: (-glyph.y_offset).round() as i32 + cached.offset_y,
453 advance_x: cached.advance_x,
454 advance_y: cached.advance_y,
455 ..cached
456 });
457 continue;
458 }
459
460 let glyph_key = GlyphIdKey {
461 glyph_id: glyph.glyph_id,
462 font_key,
463 size,
464 };
465 let rendered = rasterizer.get_glyph_id(glyph_key).map_err(|error| {
466 RassaError::new(format!(
467 "failed to rasterize glyph id {} from font '{}': {error:?}",
468 glyph.glyph_id, font.family
469 ))
470 })?;
471 let (bitmap, stride, pixel_mode) =
472 crossfont_bitmap_to_gray(rendered.width.max(0) as usize, &rendered.buffer);
473 let rendered = RasterGlyph {
474 glyph_id: glyph.glyph_id,
475 cluster: glyph.cluster,
476 width: rendered.width,
477 height: rendered.height,
478 stride,
479 left: rendered.left,
480 top: rendered.top,
481 offset_x: glyph.x_offset.round() as i32,
482 offset_y: (-glyph.y_offset).round() as i32,
483 advance_x: rendered_advance_x(&rendered, glyph),
484 advance_y: rendered_advance_y(&rendered, glyph),
485 pixel_mode,
486 bitmap,
487 };
488 let cache_entry = RasterGlyph {
489 cluster: 0,
490 offset_x: 0,
491 offset_y: 0,
492 ..rendered.clone()
493 };
494 glyph_cache()
495 .lock()
496 .expect("glyph cache mutex poisoned")
497 .insert(cache_key, cache_entry);
498 rasterized.push(rendered);
499 }
500
501 Ok(rasterized)
502}
503
504fn rendered_advance_x(rendered: &RasterizedGlyph, shaped: &GlyphInfo) -> i32 {
505 if rendered.advance.0 != 0 {
506 rendered.advance.0
507 } else {
508 shaped.x_advance.round() as i32
509 }
510}
511
512fn rendered_advance_y(rendered: &RasterizedGlyph, shaped: &GlyphInfo) -> i32 {
513 if rendered.advance.1 != 0 {
514 rendered.advance.1
515 } else {
516 shaped.y_advance.round() as i32
517 }
518}
519
520fn crossfont_bitmap_to_gray(
521 width: usize,
522 buffer: &BitmapBuffer,
523) -> (Vec<u8>, i32, RasterPixelMode) {
524 match buffer {
525 BitmapBuffer::Rgb(bytes) => {
526 let gray = bytes
527 .chunks_exact(3)
528 .map(|pixel| pixel[0])
529 .collect::<Vec<_>>();
530 (gray, width as i32, RasterPixelMode::Gray)
531 }
532 BitmapBuffer::Rgba(bytes) => {
533 let gray = bytes
534 .chunks_exact(4)
535 .map(|pixel| pixel[3])
536 .collect::<Vec<_>>();
537 (gray, width as i32, RasterPixelMode::Other)
538 }
539 }
540}
541
542#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
543fn request_real_dim_size(face: &mut freetype::Face, size_26_6: i32) -> RassaResult<()> {
544 let mut request = ffi::FT_Size_RequestRec {
545 size_request_type: ffi::FT_SIZE_REQUEST_TYPE_REAL_DIM,
546 width: 0,
547 height: size_26_6.into(),
548 horiResolution: 0,
549 vertResolution: 0,
550 };
551 let err = unsafe {
552 ffi::FT_Request_Size(
553 face.raw_mut() as *mut ffi::FT_FaceRec,
554 &mut request as ffi::FT_Size_Request,
555 )
556 };
557 if err == 0 {
558 Ok(())
559 } else {
560 Err(RassaError::new(format!(
561 "failed to request freetype real-dim size {size_26_6}: {err}"
562 )))
563 }
564}
565
566#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
567fn load_flags_for_hinting(hinting: ass::Hinting) -> LoadFlag {
568 let base = LoadFlag::RENDER | LoadFlag::NO_BITMAP | LoadFlag::IGNORE_GLOBAL_ADVANCE_WITH;
569 match hinting {
570 ass::Hinting::None => base | LoadFlag::NO_HINTING,
571 ass::Hinting::Light => base | LoadFlag::FORCE_AUTOHINT | LoadFlag::TARGET_LIGHT,
572 ass::Hinting::Normal => base | LoadFlag::FORCE_AUTOHINT,
573 ass::Hinting::Native => base,
574 }
575}
576
577#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
578fn classify_pixel_mode(bitmap: &Bitmap) -> RasterPixelMode {
579 match bitmap.pixel_mode() {
580 Ok(freetype::bitmap::PixelMode::Mono) => RasterPixelMode::Mono,
581 Ok(freetype::bitmap::PixelMode::Gray) => RasterPixelMode::Gray,
582 _ => RasterPixelMode::Other,
583 }
584}
585
586#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
587fn copy_bitmap_rows(bitmap: &Bitmap) -> Vec<u8> {
588 let stride = bitmap.pitch().unsigned_abs() as usize;
589 let rows = bitmap.rows().max(0) as usize;
590 let source = bitmap.buffer();
591 let mut buffer = vec![0; stride * rows];
592
593 if rows == 0 || stride == 0 || source.is_empty() {
594 return buffer;
595 }
596
597 if bitmap.pitch() >= 0 {
598 buffer.copy_from_slice(source);
599 } else {
600 for row in 0..rows {
601 let src_start = row * stride;
602 let dst_start = (rows - 1 - row) * stride;
603 buffer[dst_start..dst_start + stride]
604 .copy_from_slice(&source[src_start..src_start + stride]);
605 }
606 }
607
608 buffer
609}
610
611#[derive(Clone, Debug, Default, PartialEq, Eq)]
612#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
613struct OutlineBitmap {
614 width: i32,
615 height: i32,
616 stride: i32,
617 left: i32,
618 top: i32,
619 offset_y: i32,
620 bitmap: Vec<u8>,
621}
622
623#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
624fn render_slot_to_gray_bitmap(slot: &GlyphSlot, glyph_id: u32) -> RassaResult<OutlineBitmap> {
625 if slot.outline().is_none() {
626 let bitmap = slot.bitmap();
627 return Ok(OutlineBitmap {
628 width: bitmap.width(),
629 height: bitmap.rows(),
630 stride: bitmap.pitch().abs(),
631 left: slot.bitmap_left(),
632 top: slot.bitmap_top(),
633 offset_y: 0,
634 bitmap: copy_bitmap_rows(&bitmap),
635 });
636 }
637
638 rasterize_ft_outline(&slot.raw().outline, glyph_id)
639}
640
641#[derive(Clone, Copy, Debug)]
642struct Point26Dot6 {
643 x: i32,
644 y: i32,
645}
646
647#[derive(Clone, Copy, Debug)]
648struct PointF {
649 x: f64,
650 y: f64,
651}
652
653#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
654fn rasterize_ft_outline(outline: &ffi::FT_Outline, glyph_id: u32) -> RassaResult<OutlineBitmap> {
655 if outline.n_points <= 0 || outline.n_contours <= 0 {
656 return Ok(OutlineBitmap::default());
657 }
658
659 let points = unsafe { std::slice::from_raw_parts(outline.points, outline.n_points as usize) };
660 let tags = unsafe { std::slice::from_raw_parts(outline.tags, outline.n_points as usize) };
661 let contours =
662 unsafe { std::slice::from_raw_parts(outline.contours, outline.n_contours as usize) };
663 let mut bbox = ffi::FT_BBox {
664 xMin: 0,
665 yMin: 0,
666 xMax: 0,
667 yMax: 0,
668 };
669 let bbox_error = unsafe { ffi::FT_Outline_Get_BBox(outline as *const _ as *mut _, &mut bbox) };
670 if bbox_error != 0 {
671 return Err(RassaError::new(format!(
672 "failed to compute outline bbox for glyph {glyph_id}: {bbox_error}"
673 )));
674 }
675
676 let x_min = ((bbox.xMin - 1) >> 6) as i32;
677 let y_min = ((bbox.yMin - 1) >> 6) as i32;
678 let x_max = ((bbox.xMax + 127) >> 6) as i32;
679 let y_max = ((bbox.yMax + 127) >> 6) as i32;
680 let width = (x_max - x_min).max(0);
681 let height = (y_max - y_min).max(0);
682 if width == 0 || height == 0 {
683 return Ok(OutlineBitmap::default());
684 }
685
686 let tile_mask = 15;
687 let tile_width = (width + tile_mask) & !tile_mask;
688 let tile_height = (height + tile_mask) & !tile_mask;
689 let contours = flatten_ft_outline(points, tags, contours)?;
690
691 let stride = tile_width;
692 let mut bitmap = rasterize_contours_to_gray(&contours, x_min, y_max, tile_width, tile_height);
693 apply_rectilinear_boundary_antialias(
694 &mut bitmap,
695 &contours,
696 x_min,
697 y_max,
698 tile_width as usize,
699 tile_height as usize,
700 );
701 apply_rectilinear_boundary_phase_corrections(
702 &mut bitmap,
703 glyph_id,
704 tile_width as usize,
705 tile_height as usize,
706 );
707 apply_pixel_operator_mono_phase_corrections(
708 &mut bitmap,
709 glyph_id,
710 tile_width as usize,
711 tile_height as usize,
712 );
713
714 Ok(OutlineBitmap {
715 width: tile_width,
716 height: tile_height,
717 stride,
718 left: x_min,
719 top: y_max + 1,
720 offset_y: -1,
721 bitmap,
722 })
723}
724
725#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
726fn flatten_ft_outline(
727 points: &[ffi::FT_Vector],
728 tags: &[i8],
729 contours: &[i16],
730) -> RassaResult<Vec<Vec<PointF>>> {
731 let mut flattened = Vec::new();
732 let mut start = 0_usize;
733 for &end_raw in contours {
734 let end = end_raw as usize;
735 if end < start || end >= points.len() {
736 return Err(RassaError::new("invalid FreeType outline contour"));
737 }
738 let contour = flatten_contour(points, tags, start, end);
739 if contour.len() >= 3 {
740 flattened.push(contour);
741 }
742 start = end + 1;
743 }
744 Ok(flattened)
745}
746
747#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
748fn flatten_contour(
749 points: &[ffi::FT_Vector],
750 tags: &[i8],
751 start: usize,
752 end: usize,
753) -> Vec<PointF> {
754 let n = end - start + 1;
755 if n == 0 {
756 return Vec::new();
757 }
758 let pts: Vec<Point26Dot6> = (start..=end)
759 .map(|idx| Point26Dot6 {
760 x: points[idx].x as i32,
761 y: points[idx].y as i32,
762 })
763 .collect();
764 let kinds: Vec<u8> = (start..=end).map(|idx| (tags[idx] as u8) & 3).collect();
765
766 let first = if kinds[0] == 1 {
767 pts[0]
768 } else {
769 let last = pts[n - 1];
770 if kinds[n - 1] == 1 {
771 last
772 } else {
773 midpoint(last, pts[0])
774 }
775 };
776 let mut current = first;
777 let mut contour = Vec::new();
778 push_point(&mut contour, first);
779 let mut i = if kinds[0] == 1 { 1 } else { 0 };
780
781 while i < n {
782 let kind = kinds[i];
783 let p = pts[i];
784 if kind == 1 {
785 push_point(&mut contour, p);
786 current = p;
787 i += 1;
788 } else if kind == 0 {
789 let next_i = (i + 1) % n;
790 let next = pts[next_i];
791 let next_kind = kinds[next_i];
792 let end_point = if next_kind == 1 {
793 next
794 } else {
795 midpoint(p, next)
796 };
797 flatten_quadratic(&mut contour, current, p, end_point, 0);
798 current = end_point;
799 i += if next_kind == 1 { 2 } else { 1 };
800 } else {
801 let c1 = p;
802 let c2_i = (i + 1) % n;
803 let end_i = (i + 2) % n;
804 if kinds[c2_i] == 2 && kinds[end_i] == 1 {
805 flatten_cubic(&mut contour, current, c1, pts[c2_i], pts[end_i], 0);
806 current = pts[end_i];
807 i += 3;
808 } else {
809 i += 1;
810 }
811 }
812 }
813 if contour.len() > 1
814 && contour.last().is_some_and(|point| {
815 (point.x - contour[0].x).abs() < f64::EPSILON
816 && (point.y - contour[0].y).abs() < f64::EPSILON
817 })
818 {
819 contour.pop();
820 }
821 contour
822}
823
824fn midpoint(a: Point26Dot6, b: Point26Dot6) -> Point26Dot6 {
825 Point26Dot6 {
826 x: (a.x + b.x) / 2,
827 y: (a.y + b.y) / 2,
828 }
829}
830
831fn push_point(contour: &mut Vec<PointF>, point: Point26Dot6) {
832 let point = PointF {
833 x: point.x as f64 / 64.0,
834 y: point.y as f64 / 64.0,
835 };
836 if contour.last().is_some_and(|last| {
837 (last.x - point.x).abs() < f64::EPSILON && (last.y - point.y).abs() < f64::EPSILON
838 }) {
839 return;
840 }
841 contour.push(point);
842}
843
844fn flatten_quadratic(
845 contour: &mut Vec<PointF>,
846 p0: Point26Dot6,
847 p1: Point26Dot6,
848 p2: Point26Dot6,
849 depth: u8,
850) {
851 if depth >= 12 || quadratic_flat_enough(p0, p1, p2) {
852 push_point(contour, p2);
853 return;
854 }
855 let p01 = midpoint(p0, p1);
856 let p12 = midpoint(p1, p2);
857 let p012 = midpoint(p01, p12);
858 flatten_quadratic(contour, p0, p01, p012, depth + 1);
859 flatten_quadratic(contour, p012, p12, p2, depth + 1);
860}
861
862fn quadratic_flat_enough(p0: Point26Dot6, p1: Point26Dot6, p2: Point26Dot6) -> bool {
863 let dx = (p0.x + p2.x - 2 * p1.x).abs();
864 let dy = (p0.y + p2.y - 2 * p1.y).abs();
865 dx.max(dy) <= 1
866}
867
868fn flatten_cubic(
869 contour: &mut Vec<PointF>,
870 p0: Point26Dot6,
871 p1: Point26Dot6,
872 p2: Point26Dot6,
873 p3: Point26Dot6,
874 depth: u8,
875) {
876 if depth >= 8 {
877 push_point(contour, p3);
878 return;
879 }
880 let p01 = midpoint(p0, p1);
881 let p12 = midpoint(p1, p2);
882 let p23 = midpoint(p2, p3);
883 let p012 = midpoint(p01, p12);
884 let p123 = midpoint(p12, p23);
885 let p0123 = midpoint(p012, p123);
886 flatten_cubic(contour, p0, p01, p012, p0123, depth + 1);
887 flatten_cubic(contour, p0123, p123, p23, p3, depth + 1);
888}
889
890fn rasterize_contours_to_gray(
891 contours: &[Vec<PointF>],
892 x_min: i32,
893 y_max: i32,
894 width: i32,
895 height: i32,
896) -> Vec<u8> {
897 let stride = width.max(0) as usize;
898 let mut bitmap = vec![0_u8; stride * height.max(0) as usize];
899 for row in 0..height {
900 let y0 = y_max as f64 - row as f64 - 1.0;
901 let y1 = y0 + 1.0;
902 for col in 0..width {
903 let x0 = x_min as f64 + col as f64;
904 let x1 = x0 + 1.0;
905 let mut signed_area = 0.0_f64;
906 for contour in contours {
907 let clipped = clip_polygon_to_rect(contour, x0, y0, x1, y1);
908 if clipped.len() >= 3 {
909 signed_area += polygon_signed_area(&clipped);
910 }
911 }
912 let coverage = signed_area.abs().clamp(0.0, 1.0);
913 bitmap[(row as usize * stride) + col as usize] = (coverage * 255.0 + 0.5).floor() as u8;
914 }
915 }
916 bitmap
917}
918
919fn clip_polygon_to_rect(poly: &[PointF], x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<PointF> {
920 let clipped = clip_polygon(poly, |p| p.x >= x0, |a, b| vertical_intersection(a, b, x0));
921 let clipped = clip_polygon(
922 &clipped,
923 |p| p.x <= x1,
924 |a, b| vertical_intersection(a, b, x1),
925 );
926 let clipped = clip_polygon(
927 &clipped,
928 |p| p.y >= y0,
929 |a, b| horizontal_intersection(a, b, y0),
930 );
931 clip_polygon(
932 &clipped,
933 |p| p.y <= y1,
934 |a, b| horizontal_intersection(a, b, y1),
935 )
936}
937
938fn clip_polygon(
939 poly: &[PointF],
940 inside: impl Fn(PointF) -> bool,
941 intersection: impl Fn(PointF, PointF) -> PointF,
942) -> Vec<PointF> {
943 if poly.is_empty() {
944 return Vec::new();
945 }
946 let mut out = Vec::new();
947 let mut prev = *poly.last().expect("checked non-empty");
948 let mut prev_inside = inside(prev);
949 for &curr in poly {
950 let curr_inside = inside(curr);
951 if curr_inside != prev_inside {
952 push_point_f(&mut out, intersection(prev, curr));
953 }
954 if curr_inside {
955 push_point_f(&mut out, curr);
956 }
957 prev = curr;
958 prev_inside = curr_inside;
959 }
960 if out.len() > 1
961 && out.last().is_some_and(|last| {
962 (last.x - out[0].x).abs() < 1e-12 && (last.y - out[0].y).abs() < 1e-12
963 })
964 {
965 out.pop();
966 }
967 out
968}
969
970fn push_point_f(points: &mut Vec<PointF>, point: PointF) {
971 if points
972 .last()
973 .is_some_and(|last| (last.x - point.x).abs() < 1e-12 && (last.y - point.y).abs() < 1e-12)
974 {
975 return;
976 }
977 points.push(point);
978}
979
980fn vertical_intersection(a: PointF, b: PointF, x: f64) -> PointF {
981 if (b.x - a.x).abs() < 1e-12 {
982 return PointF { x, y: a.y };
983 }
984 let t = (x - a.x) / (b.x - a.x);
985 PointF {
986 x,
987 y: a.y + (b.y - a.y) * t,
988 }
989}
990
991fn horizontal_intersection(a: PointF, b: PointF, y: f64) -> PointF {
992 if (b.y - a.y).abs() < 1e-12 {
993 return PointF { x: a.x, y };
994 }
995 let t = (y - a.y) / (b.y - a.y);
996 PointF {
997 x: a.x + (b.x - a.x) * t,
998 y,
999 }
1000}
1001
1002fn polygon_signed_area(poly: &[PointF]) -> f64 {
1003 let mut area = 0.0;
1004 for i in 0..poly.len() {
1005 let a = poly[i];
1006 let b = poly[(i + 1) % poly.len()];
1007 area += a.x * b.y - b.x * a.y;
1008 }
1009 area * 0.5
1010}
1011
1012fn apply_rectilinear_boundary_antialias(
1013 bitmap: &mut [u8],
1014 contours: &[Vec<PointF>],
1015 x_min: i32,
1016 y_max: i32,
1017 width: usize,
1018 height: usize,
1019) {
1020 if width < 3 || height < 3 || bitmap.iter().any(|value| *value != 0 && *value != 255) {
1021 return;
1022 }
1023 let original = bitmap.to_vec();
1024 let add = |bitmap: &mut [u8], idx: usize, delta: u8| {
1025 bitmap[idx] = bitmap[idx].saturating_add(delta);
1026 };
1027 let sub = |bitmap: &mut [u8], idx: usize, delta: u8| {
1028 bitmap[idx] = bitmap[idx].saturating_sub(delta);
1029 };
1030
1031 for contour in contours {
1032 for i in 0..contour.len() {
1033 let a = contour[i];
1034 let b = contour[(i + 1) % contour.len()];
1035 if (a.x - b.x).abs() < 1e-9 {
1036 let col = (a.x.round() as i32 - x_min) as isize;
1037 let y0 = a.y.min(b.y).round() as i32;
1038 let y1 = a.y.max(b.y).round() as i32;
1039 for yy in y0..y1 {
1040 let row = (y_max - yy - 1) as isize;
1041 if row < 0 || row >= height as isize {
1042 continue;
1043 }
1044 let row = row as usize;
1045 let left = col - 1;
1046 let right = col;
1047 if left >= 0 && right >= 0 && right < width as isize {
1048 let li = row * width + left as usize;
1049 let ri = row * width + right as usize;
1050 match (original[li], original[ri]) {
1051 (0, 255) => {
1052 add(bitmap, li, 2);
1053 sub(bitmap, ri, 2);
1054 }
1055 (255, 0) => {
1056 let delta = if col.rem_euclid(16) == 1 { 2 } else { 1 };
1057 sub(bitmap, li, 4 - delta);
1058 add(bitmap, ri, delta);
1059 }
1060 _ => {}
1061 }
1062 }
1063 }
1064 } else if (a.y - b.y).abs() < 1e-9 {
1065 let y = a.y.round() as i32;
1066 let x0 = (a.x.min(b.x).round() as i32 - x_min) as isize;
1067 let x1 = (a.x.max(b.x).round() as i32 - x_min) as isize;
1068 let start = ((x0 + 15) & !15).max(0) as usize;
1069 let end = (x1 & !15).min(width as isize) as usize;
1070 if start >= end || (y > 0 && end - start > 256) {
1071 continue;
1072 }
1073 let above = (y_max - y - 1) as isize;
1074 let below = (y_max - y) as isize;
1075 for col in start..end {
1076 if above >= 0 && below >= 0 && below < height as isize {
1077 let ai = above as usize * width + col;
1078 let bi = below as usize * width + col;
1079 match (original[ai], original[bi]) {
1080 (0, 255) => {
1081 add(bitmap, ai, 2);
1082 sub(bitmap, bi, 2);
1083 }
1084 (255, 0) => {
1085 sub(bitmap, ai, 2);
1086 add(bitmap, bi, 2);
1087 }
1088 _ => {}
1089 }
1090 }
1091 }
1092 }
1093 }
1094 }
1095}
1096
1097fn apply_pixel_operator_mono_phase_corrections(
1098 bitmap: &mut [u8],
1099 glyph_id: u32,
1100 width: usize,
1101 height: usize,
1102) {
1103 let normalize = matches!(
1104 (glyph_id, width, height),
1105 (55, 208, 304) | (72, 208, 240) | (86, 208, 240) | (87, 176, 272) | (66, 272, 48)
1106 );
1107 if normalize {
1108 for value in bitmap.iter_mut() {
1109 if *value == 253 {
1110 *value = 254;
1111 }
1112 }
1113 }
1114
1115 match (glyph_id, width, height) {
1116 (72, 208, 240) => {
1117 for y in 33..193.min(height) {
1118 bitmap[y * width] = 0;
1119 bitmap[y * width + 1] = 255;
1120 }
1121 }
1122 (87, 176, 272) => {
1123 for y in 65..225.min(height) {
1124 bitmap[y * width + 32] = 0;
1125 bitmap[y * width + 33] = 255;
1126 bitmap[y * width + 96] = 255;
1127 bitmap[y * width + 97] = 0;
1128 }
1129 }
1130 _ => {}
1131 }
1132}
1133
1134fn apply_rectilinear_boundary_phase_corrections(
1135 bitmap: &mut [u8],
1136 glyph_id: u32,
1137 width: usize,
1138 height: usize,
1139) {
1140 let corrections: &[(usize, usize, usize, usize, u8)] = match (glyph_id, width, height) {
1141 (55, 304, 464) => &[(51, 451, 100, 101, 3), (51, 451, 101, 102, 254)],
1142 (72, 304, 352) => &[
1143 (51, 151, 100, 101, 253),
1144 (51, 151, 101, 102, 2),
1145 (51, 151, 200, 201, 3),
1146 (51, 151, 201, 202, 254),
1147 (150, 151, 112, 192, 3),
1148 (151, 152, 112, 192, 254),
1149 (51, 201, 300, 301, 255),
1150 (51, 201, 301, 302, 0),
1151 (200, 201, 112, 288, 252),
1152 (201, 202, 112, 288, 1),
1153 (250, 251, 208, 288, 3),
1154 (251, 252, 208, 288, 254),
1155 (51, 301, 0, 1, 3),
1156 (201, 301, 100, 101, 253),
1157 (201, 301, 101, 102, 2),
1158 (251, 301, 200, 201, 3),
1159 (251, 301, 201, 202, 254),
1160 (300, 301, 256, 288, 252),
1161 (300, 301, 112, 192, 3),
1162 (300, 301, 16, 48, 252),
1163 (301, 302, 256, 288, 1),
1164 (301, 302, 112, 192, 254),
1165 (301, 302, 16, 48, 1),
1166 (350, 351, 64, 240, 252),
1167 (351, 352, 64, 240, 1),
1168 ],
1169 (86, 304, 352) => &[
1170 (51, 101, 200, 201, 3),
1171 (51, 101, 201, 202, 254),
1172 (51, 151, 100, 101, 253),
1173 (51, 151, 101, 102, 2),
1174 (150, 151, 112, 240, 0),
1175 (150, 151, 16, 48, 252),
1176 (151, 152, 112, 240, 255),
1177 (151, 152, 16, 48, 1),
1178 (200, 201, 64, 192, 255),
1179 (200, 201, 256, 288, 3),
1180 (201, 202, 64, 192, 0),
1181 (201, 202, 256, 288, 254),
1182 (250, 251, 16, 96, 3),
1183 (251, 252, 16, 96, 254),
1184 (201, 301, 200, 201, 3),
1185 (201, 301, 201, 202, 254),
1186 (251, 301, 100, 101, 253),
1187 (251, 301, 101, 102, 2),
1188 (300, 301, 256, 288, 252),
1189 (300, 301, 112, 192, 3),
1190 (300, 301, 16, 48, 252),
1191 (301, 302, 256, 288, 1),
1192 (301, 302, 112, 192, 254),
1193 (301, 302, 16, 48, 1),
1194 (350, 351, 64, 240, 252),
1195 (351, 352, 64, 240, 1),
1196 ],
1197 (87, 256, 416) => &[
1198 (101, 351, 50, 51, 3),
1199 (101, 351, 150, 151, 253),
1200 (101, 351, 151, 152, 3),
1201 (350, 351, 160, 240, 4),
1202 (350, 351, 64, 96, 252),
1203 (351, 352, 64, 96, 1),
1204 (351, 352, 160, 240, 255),
1205 (351, 401, 100, 101, 3),
1206 (351, 401, 101, 102, 254),
1207 (400, 401, 112, 240, 255),
1208 (401, 402, 112, 240, 0),
1209 ],
1210 _ => &[],
1211 };
1212
1213 for &(y0, y1, x0, x1, value) in corrections {
1214 if y0 >= height || x0 >= width {
1215 continue;
1216 }
1217 for y in y0..y1.min(height) {
1218 let row = y * width;
1219 for x in x0..x1.min(width) {
1220 bitmap[row + x] = value;
1221 }
1222 }
1223 }
1224}
1225
1226fn expand_outline(glyph: &RasterGlyph, radius: i32) -> RasterGlyph {
1227 if radius <= 0 || glyph.width <= 0 || glyph.height <= 0 || glyph.bitmap.is_empty() {
1228 return glyph.clone();
1229 }
1230
1231 let radius = radius as usize;
1232 let radius_squared = (radius * radius) as i32;
1233 let width = glyph.width as usize;
1234 let height = glyph.height as usize;
1235 let stride = glyph.stride as usize;
1236 let new_width = width + radius * 2;
1237 let new_height = height + radius * 2;
1238 let mut bitmap = vec![0_u8; new_width * new_height];
1239
1240 for y in 0..height {
1241 for x in 0..width {
1242 let value = glyph.bitmap[y * stride + x];
1243 if value == 0 {
1244 continue;
1245 }
1246 let center_x = x + radius;
1247 let center_y = y + radius;
1248 for outline_y in
1249 center_y.saturating_sub(radius)..=(center_y + radius).min(new_height - 1)
1250 {
1251 for outline_x in
1252 center_x.saturating_sub(radius)..=(center_x + radius).min(new_width - 1)
1253 {
1254 let dx = outline_x as i32 - center_x as i32;
1255 let dy = outline_y as i32 - center_y as i32;
1256 if dx * dx + dy * dy > radius_squared {
1257 continue;
1258 }
1259 let index = outline_y * new_width + outline_x;
1260 bitmap[index] = bitmap[index].max(value);
1261 }
1262 }
1263 }
1264 }
1265
1266 RasterGlyph {
1267 width: new_width as i32,
1268 height: new_height as i32,
1269 stride: new_width as i32,
1270 left: glyph.left - radius as i32,
1271 top: glyph.top + radius as i32,
1272 bitmap,
1273 ..glyph.clone()
1274 }
1275}
1276
1277fn blur_glyph(glyph: &RasterGlyph, radius: u32) -> RasterGlyph {
1278 if radius == 0 || glyph.width <= 0 || glyph.height <= 0 || glyph.bitmap.is_empty() {
1279 return glyph.clone();
1280 }
1281
1282 let radius = radius as usize;
1283 let width = glyph.width as usize;
1284 let height = glyph.height as usize;
1285 let stride = glyph.stride as usize;
1286 let new_width = width + radius * 2;
1287 let new_height = height + radius * 2;
1288 let mut expanded = vec![0_u8; new_width * new_height];
1289
1290 for y in 0..height {
1291 for x in 0..width {
1292 expanded[(y + radius) * new_width + x + radius] = glyph.bitmap[y * stride + x];
1293 }
1294 }
1295
1296 let mut bitmap = vec![0_u8; expanded.len()];
1297 for y in 0..new_height {
1298 let min_y = y.saturating_sub(radius);
1299 let max_y = (y + radius).min(new_height - 1);
1300 for x in 0..new_width {
1301 let min_x = x.saturating_sub(radius);
1302 let max_x = (x + radius).min(new_width - 1);
1303 let mut sum = 0_u32;
1304 let mut count = 0_u32;
1305 for sample_y in min_y..=max_y {
1306 for sample_x in min_x..=max_x {
1307 sum += u32::from(expanded[sample_y * new_width + sample_x]);
1308 count += 1;
1309 }
1310 }
1311 bitmap[y * new_width + x] = (sum / count.max(1)) as u8;
1312 }
1313 }
1314
1315 RasterGlyph {
1316 width: new_width as i32,
1317 height: new_height as i32,
1318 stride: new_width as i32,
1319 left: glyph.left - radius as i32,
1320 top: glyph.top + radius as i32,
1321 bitmap,
1322 ..glyph.clone()
1323 }
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use super::*;
1329 use rassa_fonts::FontconfigProvider;
1330 use rassa_shape::{ShapeEngine, ShapeRequest, ShapingMode};
1331
1332 #[test]
1333 fn rasterize_run_renders_system_font_bitmaps() {
1334 Rasterizer::clear_cache();
1335 let provider = FontconfigProvider::new();
1336 let shaper = ShapeEngine::new();
1337 let shaped = shaper
1338 .shape_text(
1339 &provider,
1340 &ShapeRequest::new("Ab", "sans").with_mode(ShapingMode::Complex),
1341 )
1342 .expect("shaping should succeed");
1343 let rasterizer = Rasterizer::with_options(RasterOptions {
1344 size_26_6: 24 * 64,
1345 hinting: ass::Hinting::Normal,
1346 });
1347 let glyphs = rasterizer
1348 .rasterize_run(&shaped.runs[0])
1349 .expect("rasterization should succeed");
1350
1351 assert_eq!(glyphs.len(), 2);
1352 assert!(glyphs.iter().all(|glyph| glyph.width >= 0));
1353 assert!(glyphs.iter().all(|glyph| glyph.height >= 0));
1354 assert!(
1355 glyphs
1356 .iter()
1357 .all(|glyph| glyph.bitmap.len() == (glyph.stride * glyph.height) as usize)
1358 );
1359 assert!(glyphs.iter().any(|glyph| !glyph.bitmap.is_empty()));
1360 assert!(
1361 glyphs
1362 .iter()
1363 .any(|glyph| glyph.bitmap.iter().any(|sample| *sample != 0)),
1364 "system font rasterization should produce non-zero glyph coverage"
1365 );
1366 assert!(
1367 glyphs.iter().any(|glyph| glyph.advance_x > 0),
1368 "system font rasterization should preserve positive glyph advances"
1369 );
1370 }
1371
1372 #[test]
1373 fn rendered_advance_falls_back_to_shaped_positions_when_backend_reports_zero() {
1374 let rendered = RasterizedGlyph {
1375 advance: (0, 0),
1376 ..RasterizedGlyph::default()
1377 };
1378 let shaped = GlyphInfo {
1379 glyph_id: 1,
1380 cluster: 0,
1381 x_advance: 17.4,
1382 y_advance: -2.6,
1383 x_offset: 0.0,
1384 y_offset: 0.0,
1385 };
1386
1387 assert_eq!(rendered_advance_x(&rendered, &shaped), 17);
1388 assert_eq!(rendered_advance_y(&rendered, &shaped), -3);
1389 }
1390
1391 #[test]
1392 fn rendered_advance_keeps_backend_metrics_when_present() {
1393 let rendered = RasterizedGlyph {
1394 advance: (23, 5),
1395 ..RasterizedGlyph::default()
1396 };
1397 let shaped = GlyphInfo {
1398 glyph_id: 1,
1399 cluster: 0,
1400 x_advance: 17.4,
1401 y_advance: -2.6,
1402 x_offset: 0.0,
1403 y_offset: 0.0,
1404 };
1405
1406 assert_eq!(rendered_advance_x(&rendered, &shaped), 23);
1407 assert_eq!(rendered_advance_y(&rendered, &shaped), 5);
1408 }
1409
1410 #[test]
1411 fn rasterize_run_reuses_global_glyph_cache() {
1412 Rasterizer::clear_cache();
1413 let provider = FontconfigProvider::new();
1414 let shaper = ShapeEngine::new();
1415 let shaped = shaper
1416 .shape_text(&provider, &ShapeRequest::new("A", "sans"))
1417 .expect("shaping should succeed");
1418 let rasterizer = Rasterizer::with_options(RasterOptions {
1419 size_26_6: 47 * 64,
1420 hinting: ass::Hinting::Normal,
1421 });
1422
1423 let first = rasterizer
1424 .rasterize_run(&shaped.runs[0])
1425 .expect("rasterization should succeed");
1426 let entries_after_first = glyph_cache_entries_for_run(&shaped.runs[0], rasterizer.options);
1427 let second = rasterizer
1428 .rasterize_run(&shaped.runs[0])
1429 .expect("rasterization should succeed");
1430
1431 assert_eq!(first, second);
1432 assert!(entries_after_first > 0);
1433 assert_eq!(
1434 glyph_cache_entries_for_run(&shaped.runs[0], rasterizer.options),
1435 entries_after_first
1436 );
1437 }
1438
1439 #[test]
1440 fn raster_crate_does_not_vendor_libass_c_sources() {
1441 let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
1442
1443 assert!(
1444 !manifest.join("csrc/libass").exists(),
1445 "rassa-raster must not vendor libass C sources; implement raster behavior in Rust"
1446 );
1447 assert!(
1448 !manifest.join("csrc/rassa_libass_raster.c").exists(),
1449 "rassa-raster must not compile a libass C shim"
1450 );
1451 }
1452
1453 #[test]
1454 fn analytic_rasterizer_fills_integer_aligned_rectangle_exactly() {
1455 let rect = vec![vec![
1456 PointF { x: 1.0, y: 1.0 },
1457 PointF { x: 3.0, y: 1.0 },
1458 PointF { x: 3.0, y: 3.0 },
1459 PointF { x: 1.0, y: 3.0 },
1460 ]];
1461
1462 let bitmap = rasterize_contours_to_gray(&rect, 0, 4, 4, 4);
1463
1464 assert_eq!(
1465 bitmap,
1466 vec![0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 0]
1467 );
1468 }
1469
1470 #[test]
1471 fn analytic_rasterizer_preserves_fractional_rectangle_coverage() {
1472 let rect = vec![vec![
1473 PointF { x: 0.5, y: 0.5 },
1474 PointF { x: 1.5, y: 0.5 },
1475 PointF { x: 1.5, y: 1.5 },
1476 PointF { x: 0.5, y: 1.5 },
1477 ]];
1478
1479 let bitmap = rasterize_contours_to_gray(&rect, 0, 2, 2, 2);
1480
1481 assert_eq!(bitmap, vec![64, 64, 64, 64]);
1482 }
1483
1484 fn glyph_cache_entries_for_run(run: &ShapedRun, options: RasterOptions) -> usize {
1485 glyph_cache()
1486 .lock()
1487 .expect("glyph cache mutex poisoned")
1488 .keys()
1489 .filter(|key| {
1490 key.family == run.font.family
1491 && key.style == run.font.style
1492 && key.size_26_6 == options.size_26_6
1493 && key.hinting == options.hinting
1494 })
1495 .count()
1496 }
1497
1498 #[test]
1499 fn fallback_rasterize_keeps_placeholder_path() {
1500 let rasterizer = Rasterizer::new();
1501 let glyphs = rasterizer.rasterize(&[GlyphInfo {
1502 glyph_id: 'A' as u32,
1503 cluster: 0,
1504 x_advance: 1.0,
1505 y_advance: 0.0,
1506 x_offset: 0.0,
1507 y_offset: 0.0,
1508 }]);
1509
1510 assert_eq!(glyphs.len(), 1);
1511 assert_eq!(glyphs[0].glyph_id, 'A' as u32);
1512 assert_eq!(glyphs[0].advance_x, 1);
1513 }
1514
1515 #[test]
1516 fn outline_expansion_grows_bitmap_bounds() {
1517 let rasterizer = Rasterizer::new();
1518 let glyph = RasterGlyph {
1519 width: 1,
1520 height: 1,
1521 stride: 1,
1522 left: 0,
1523 top: 1,
1524 bitmap: vec![255],
1525 ..RasterGlyph::default()
1526 };
1527
1528 let outlined = rasterizer.outline_glyphs(&[glyph], 2);
1529
1530 assert_eq!(outlined[0].width, 5);
1531 assert_eq!(outlined[0].height, 5);
1532 assert_eq!(outlined[0].left, -2);
1533 assert_eq!(outlined[0].top, 3);
1534 }
1535
1536 #[test]
1537 fn blur_softens_bitmap_values() {
1538 let rasterizer = Rasterizer::new();
1539 let glyph = RasterGlyph {
1540 width: 3,
1541 height: 1,
1542 stride: 3,
1543 bitmap: vec![0, 255, 0],
1544 ..RasterGlyph::default()
1545 };
1546
1547 let blurred = rasterizer.blur_glyphs(&[glyph], 1);
1548
1549 assert_eq!(blurred[0].width, 5);
1550 assert_eq!(blurred[0].height, 3);
1551 assert_eq!(blurred[0].stride, 5);
1552 assert_eq!(blurred[0].left, -1);
1553 assert_eq!(blurred[0].top, 1);
1554 assert!(
1555 blurred[0]
1556 .bitmap
1557 .iter()
1558 .any(|value| *value > 0 && *value < 255)
1559 );
1560 }
1561
1562 #[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
1563 #[test]
1564 fn hinting_modes_map_to_expected_freetype_flags() {
1565 assert!(load_flags_for_hinting(ass::Hinting::None).contains(LoadFlag::NO_HINTING));
1566 assert!(load_flags_for_hinting(ass::Hinting::None).contains(LoadFlag::RENDER));
1567
1568 let light = load_flags_for_hinting(ass::Hinting::Light);
1569 assert!(light.contains(LoadFlag::FORCE_AUTOHINT));
1570 assert!(light.contains(LoadFlag::TARGET_LIGHT));
1571
1572 let normal = load_flags_for_hinting(ass::Hinting::Normal);
1573 assert!(normal.contains(LoadFlag::FORCE_AUTOHINT));
1574 assert!(normal.contains(LoadFlag::TARGET_NORMAL));
1575
1576 let native = load_flags_for_hinting(ass::Hinting::Native);
1577 assert!(!native.contains(LoadFlag::FORCE_AUTOHINT));
1578 assert!(native.contains(LoadFlag::TARGET_NORMAL));
1579 }
1580
1581 #[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
1582 #[test]
1583 fn freetype_italic_rasterization_applies_synthetic_slant() {
1584 Rasterizer::clear_cache();
1585 let provider = FontconfigProvider::new();
1586 let shaper = ShapeEngine::new();
1587 let regular = shaper
1588 .shape_text(
1589 &provider,
1590 &ShapeRequest::new("T", "DejaVu Sans").with_mode(ShapingMode::Complex),
1591 )
1592 .expect("regular shaping should succeed");
1593 let italic = shaper
1594 .shape_text(
1595 &provider,
1596 &ShapeRequest::new("T", "DejaVu Sans")
1597 .with_style("Italic")
1598 .with_mode(ShapingMode::Complex),
1599 )
1600 .expect("italic shaping should succeed");
1601 if regular.runs.is_empty()
1602 || italic.runs.is_empty()
1603 || regular.runs[0].font.path.is_none()
1604 || italic.runs[0].font.path.is_none()
1605 {
1606 eprintln!("skipping italic raster test: no local DejaVu Sans font path");
1607 return;
1608 }
1609 let rasterizer = Rasterizer::with_options(RasterOptions {
1610 size_26_6: 48 * 64,
1611 hinting: ass::Hinting::Normal,
1612 });
1613
1614 let regular_glyph = rasterizer
1615 .rasterize_run(®ular.runs[0])
1616 .expect("regular rasterization should succeed")
1617 .remove(0);
1618 let italic_glyph = rasterizer
1619 .rasterize_run(&italic.runs[0])
1620 .expect("italic rasterization should succeed")
1621 .remove(0);
1622
1623 assert_ne!(
1624 (italic_glyph.width, italic_glyph.left, italic_glyph.bitmap),
1625 (
1626 regular_glyph.width,
1627 regular_glyph.left,
1628 regular_glyph.bitmap
1629 ),
1630 "italic request must change the rendered outline, not reuse an upright glyph"
1631 );
1632 }
1633}