Skip to main content

densa_text/
raster.rs

1use skrifa::instance::{LocationRef, Size};
2use skrifa::outline::{DrawSettings, OutlinePen};
3use skrifa::MetadataProvider;
4use zeno::{Command, Format, Mask, Point};
5
6use crate::{Font, GlyphId};
7
8/// An 8-bit coverage bitmap of one glyph, positioned relative to the pen.
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
10pub struct GlyphBitmap {
11    /// Width in pixels; 0 for glyphs without ink (spaces).
12    pub width: u32,
13    /// Height in pixels; 0 for glyphs without ink.
14    pub height: u32,
15    /// Offset from the pen position to the bitmap's left edge, in pixels.
16    pub left: i32,
17    /// Offset from the baseline to the bitmap's top edge, in pixels (negative = above).
18    pub top: i32,
19    /// Row-major coverage, `width` bytes per row, 0 (none) to 255 (full).
20    pub coverage: Vec<u8>,
21}
22
23/// Rasterizes `glyph` at `px` pixels per em, shifted right by `x_offset` pixels (use a
24/// fraction in `0..1` for subpixel positioning).
25///
26/// Glyphs without an outline, or outlines that fail to draw, yield an empty bitmap: a
27/// broken glyph renders as nothing rather than taking the frame down.
28pub fn rasterize(font: &Font, glyph: GlyphId, px: f32, x_offset: f32) -> GlyphBitmap {
29    let Some(font_ref) = font.font_ref() else {
30        return GlyphBitmap::default();
31    };
32    let outlines = font_ref.outline_glyphs();
33    let Some(outline) = outlines.get(skrifa::GlyphId::new(glyph.0)) else {
34        return GlyphBitmap::default();
35    };
36
37    let mut pen = PathPen {
38        commands: Vec::new(),
39        dx: x_offset,
40    };
41    let settings = DrawSettings::unhinted(Size::new(px), LocationRef::default());
42    if outline.draw(settings, &mut pen).is_err() || pen.commands.is_empty() {
43        return GlyphBitmap::default();
44    }
45
46    let (coverage, placement) = Mask::new(&pen.commands[..]).format(Format::Alpha).render();
47    if placement.width == 0 || placement.height == 0 {
48        return GlyphBitmap::default();
49    }
50    GlyphBitmap {
51        width: placement.width,
52        height: placement.height,
53        left: placement.left,
54        top: placement.top,
55        coverage,
56    }
57}
58
59/// Collects outline segments as zeno path commands, flipping from font space (y up) to
60/// raster space (y down) and applying the subpixel shift.
61struct PathPen {
62    commands: Vec<Command>,
63    dx: f32,
64}
65
66impl PathPen {
67    fn point(&self, x: f32, y: f32) -> Point {
68        Point::new(x + self.dx, -y)
69    }
70}
71
72impl OutlinePen for PathPen {
73    fn move_to(&mut self, x: f32, y: f32) {
74        let p = self.point(x, y);
75        self.commands.push(Command::MoveTo(p));
76    }
77
78    fn line_to(&mut self, x: f32, y: f32) {
79        let p = self.point(x, y);
80        self.commands.push(Command::LineTo(p));
81    }
82
83    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
84        let (c, p) = (self.point(cx0, cy0), self.point(x, y));
85        self.commands.push(Command::QuadTo(c, p));
86    }
87
88    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
89        let (c0, c1, p) = (self.point(cx0, cy0), self.point(cx1, cy1), self.point(x, y));
90        self.commands.push(Command::CurveTo(c0, c1, p));
91    }
92
93    fn close(&mut self) {
94        self.commands.push(Command::Close);
95    }
96}