Skip to main content

rassa_raster/
lib.rs

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(&regular.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}