···66use std::fmt;
7788mod parse;
99+pub mod rasterizer;
910mod tables;
10111212+pub use rasterizer::GlyphBitmap;
1113pub use tables::cmap::CmapTable;
1214pub use tables::glyf::{Contour, GlyphOutline, Point};
1315pub use tables::head::HeadTable;
···221223 .table_data(b"glyf")
222224 .ok_or(FontError::MissingTable("glyf"))?;
223225 tables::glyf::parse_glyph(glyph_id, glyf_data, &loca)
226226+ }
227227+228228+ /// Rasterize a glyph outline into an anti-aliased bitmap at the given pixel size.
229229+ pub fn rasterize_glyph(&self, glyph_id: u16, size_px: f32) -> Option<GlyphBitmap> {
230230+ let head = self.head().ok()?;
231231+ let scale = size_px / head.units_per_em as f32;
232232+ let outline = self.glyph_outline(glyph_id).ok()??;
233233+ rasterizer::rasterize(&outline, scale)
224234 }
225235226236 /// Returns true if this is a TrueType font (vs CFF/PostScript outlines).
+285
crates/text/src/font/rasterizer.rs
···11+//! Glyph rasterization: converts vector outlines to anti-aliased grayscale bitmaps.
22+33+use crate::font::tables::glyf::GlyphOutline;
44+55+/// A rasterized glyph bitmap.
66+#[derive(Debug, Clone, PartialEq, Eq)]
77+pub struct GlyphBitmap {
88+ /// Width of the bitmap in pixels.
99+ pub width: u32,
1010+ /// Height of the bitmap in pixels.
1111+ pub height: u32,
1212+ /// Horizontal offset from origin to left edge of bitmap.
1313+ pub bearing_x: i32,
1414+ /// Vertical offset from origin to top edge of bitmap.
1515+ pub bearing_y: i32,
1616+ /// 8-bit grayscale coverage data (0 = transparent, 255 = opaque).
1717+ /// Size is `width * height`.
1818+ pub data: Vec<u8>,
1919+}
2020+2121+/// A line segment for the scanline rasterizer.
2222+#[derive(Debug, Clone, Copy)]
2323+struct LineSegment {
2424+ x0: f32,
2525+ y0: f32,
2626+ x1: f32,
2727+ y1: f32,
2828+}
2929+3030+impl LineSegment {
3131+ /// Intersects the horizontal line at `y`. Returns `None` if parallel or out of bounds.
3232+ /// The mathematical intersection `x` is returned along with direction `dir` (+1 for up, -1 for down).
3333+ fn intersect_horizontal(&self, y: f32) -> Option<(f32, i32)> {
3434+ // Only intersect if y is strictly between y0 and y1.
3535+ let (min_y, max_y, dir) = if self.y0 < self.y1 {
3636+ (self.y0, self.y1, 1)
3737+ } else {
3838+ (self.y1, self.y0, -1)
3939+ };
4040+4141+ if y < min_y || y >= max_y {
4242+ return None;
4343+ }
4444+4545+ let t = (y - self.y0) / (self.y1 - self.y0);
4646+ let x = self.x0 + t * (self.x1 - self.x0);
4747+ Some((x, dir))
4848+ }
4949+}
5050+5151+/// Scale and flatten a glyph outline into reproducible line segments.
5252+fn flatten_outline(outline: &GlyphOutline, scale: f32) -> Vec<LineSegment> {
5353+ let mut segments = Vec::new();
5454+5555+ for contour in &outline.contours {
5656+ if contour.points.is_empty() {
5757+ continue;
5858+ }
5959+6060+ let pts = &contour.points;
6161+6262+ // Iterate through implied points.
6363+ // TrueType defines curves with an implicit on-curve point
6464+ // halfway between any two consecutive off-curve points.
6565+ let mut explicit_points = Vec::with_capacity(pts.len() * 2);
6666+6767+ for (i, p) in pts.iter().enumerate() {
6868+ let next_p = &pts[(i + 1) % pts.len()];
6969+7070+ // Add the current point
7171+ explicit_points.push((p.x as f32 * scale, p.y as f32 * scale, p.on_curve));
7272+7373+ // If current and next are both off-curve, insert a midpoint
7474+ if !p.on_curve && !next_p.on_curve {
7575+ let mx = (p.x as f32 + next_p.x as f32) * 0.5 * scale;
7676+ let my = (p.y as f32 + next_p.y as f32) * 0.5 * scale;
7777+ explicit_points.push((mx, my, true));
7878+ }
7979+ }
8080+8181+ // Now traverse the explicit points. The path starts with the first point.
8282+ // But what if the first point is off-curve?
8383+ // Let's find an on-curve point to start.
8484+ let start_idx = explicit_points.iter().position(|p| p.2).unwrap_or(0);
8585+8686+ let len = explicit_points.len();
8787+ let mut i = 0;
8888+ let mut cur = explicit_points[start_idx];
8989+9090+ // Ensure we loop back to start.
9191+ while i < len {
9292+ let next_idx = (start_idx + i + 1) % len;
9393+ let p1 = explicit_points[next_idx];
9494+9595+ if p1.2 {
9696+ // Line to next on-curve point
9797+ segments.push(LineSegment {
9898+ x0: cur.0,
9999+ y0: cur.1,
100100+ x1: p1.0,
101101+ y1: p1.1,
102102+ });
103103+ cur = p1;
104104+ i += 1;
105105+ } else {
106106+ // Quadratic bezier: p1 is control, need next on-curve point
107107+ let p2_idx = (next_idx + 1) % len;
108108+ let p2 = explicit_points[p2_idx];
109109+110110+ // Flatten the quadratic bezier curve.
111111+ flatten_quadratic(cur.0, cur.1, p1.0, p1.1, p2.0, p2.1, &mut segments);
112112+113113+ cur = p2;
114114+ i += 2;
115115+ }
116116+ }
117117+ }
118118+119119+ segments
120120+}
121121+122122+/// Flattens a quadratic bézier into line segments using a fixed subdivision.
123123+fn flatten_quadratic(
124124+ x0: f32,
125125+ y0: f32,
126126+ x1: f32,
127127+ y1: f32,
128128+ x2: f32,
129129+ y2: f32,
130130+ segments: &mut Vec<LineSegment>,
131131+) {
132132+ const STEPS: usize = 8;
133133+ let mut prev_x = x0;
134134+ let mut prev_y = y0;
135135+136136+ for i in 1..=STEPS {
137137+ let t = i as f32 / STEPS as f32;
138138+ let t_inv = 1.0 - t;
139139+140140+ // Quadratic formula: P = (1-t)^2*P0 + 2t(1-t)*P1 + t^2*P2
141141+ let cur_x = t_inv * t_inv * x0 + 2.0 * t * t_inv * x1 + t * t * x2;
142142+ let cur_y = t_inv * t_inv * y0 + 2.0 * t * t_inv * y1 + t * t * y2;
143143+144144+ segments.push(LineSegment {
145145+ x0: prev_x,
146146+ y0: prev_y,
147147+ x1: cur_x,
148148+ y1: cur_y,
149149+ });
150150+151151+ prev_x = cur_x;
152152+ prev_y = cur_y;
153153+ }
154154+}
155155+156156+/// Rasterizes a scaled outline into a 0-255 coverage bitmap using 16x16 supersampling.
157157+pub fn rasterize(outline: &GlyphOutline, scale: f32) -> Option<GlyphBitmap> {
158158+ if outline.contours.is_empty() {
159159+ return None;
160160+ }
161161+162162+ let segments = flatten_outline(outline, scale);
163163+164164+ let x_min = outline.x_min as f32 * scale;
165165+ let y_min = outline.y_min as f32 * scale;
166166+ let x_max = outline.x_max as f32 * scale;
167167+ let y_max = outline.y_max as f32 * scale;
168168+169169+ let bitmap_width = (x_max.ceil() - x_min.floor()) as i32;
170170+ let bitmap_height = (y_max.ceil() - y_min.floor()) as i32;
171171+172172+ if bitmap_width <= 0 || bitmap_height <= 0 {
173173+ return None;
174174+ }
175175+176176+ let bearing_x = x_min.floor() as i32;
177177+ // Y-axis in TrueType goes UP, but in screen space it goes DOWN.
178178+ // So the top-left of the bounding box is at y_max.
179179+ let bearing_y = y_max.ceil() as i32;
180180+181181+ let width = bitmap_width as u32;
182182+ let height = bitmap_height as u32;
183183+184184+ // 16x16 supersampling => 256 subpixels per pixel.
185185+ const SUB_PIXELS: u32 = 16;
186186+ let mut coverage = vec![0u16; (width * height) as usize];
187187+188188+ for sy in 0..(height * SUB_PIXELS) {
189189+ // TrueType Y goes up. The top pixel is row 0.
190190+ // Therefore, pixel row `py` corresponds to `bearing_y - py - 1`.
191191+ // The subpixel offset is from the top of the pixel box going downwards.
192192+ let sub_y_rel = (sy % SUB_PIXELS) as f32 + 0.5;
193193+ let py = sy / SUB_PIXELS;
194194+ let real_y = bearing_y as f32 - (py as f32 + sub_y_rel / SUB_PIXELS as f32);
195195+196196+ // Find intersections.
197197+ let mut intersections = Vec::with_capacity(32);
198198+ for seg in &segments {
199199+ if let Some((x, dir)) = seg.intersect_horizontal(real_y) {
200200+ intersections.push((x, dir));
201201+ }
202202+ }
203203+204204+ // Sort by X coordinate.
205205+ intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
206206+207207+ // Traverse intersections to calculate winding and fill pixels.
208208+ let mut winding = 0;
209209+ let mut active_start_x = None;
210210+211211+ for (x, dir) in intersections {
212212+ let prev_winding = winding;
213213+ winding += dir;
214214+215215+ if prev_winding == 0 && winding != 0 {
216216+ // Entered shape.
217217+ active_start_x = Some(x);
218218+ } else if prev_winding != 0 && winding == 0 {
219219+ // Exited shape. Fill subpixels between active_start_x and x.
220220+ if let Some(start_x) = active_start_x {
221221+ // Map to subpixel coordinates on X axis.
222222+ let start_sx =
223223+ ((start_x - bearing_x as f32) * SUB_PIXELS as f32).round() as i32;
224224+ let end_sx = ((x - bearing_x as f32) * SUB_PIXELS as f32).round() as i32;
225225+226226+ let s_start = start_sx.max(0) as u32;
227227+ let s_end = (end_sx.max(0) as u32).min(width * SUB_PIXELS);
228228+229229+ for sx in s_start..s_end {
230230+ let px = sx / SUB_PIXELS;
231231+ let idx = (py * width + px) as usize;
232232+ coverage[idx] += 1;
233233+ }
234234+ }
235235+ active_start_x = None;
236236+ }
237237+ }
238238+ }
239239+240240+ // Convert coverage (0-256) to 0-255 u8.
241241+ let data = coverage.into_iter().map(|c| c.min(255) as u8).collect();
242242+243243+ Some(GlyphBitmap {
244244+ width,
245245+ height,
246246+ bearing_x,
247247+ bearing_y,
248248+ data,
249249+ })
250250+}
251251+252252+#[cfg(test)]
253253+mod tests {
254254+ use super::*;
255255+ use crate::font::Font;
256256+257257+ fn load_test_font() -> Font {
258258+ crate::font::load_system_font().expect("failed to load system font")
259259+ }
260260+261261+ #[test]
262262+ fn rasterize_basic_glyph() {
263263+ let font = load_test_font();
264264+ let head = font.head().unwrap();
265265+266266+ let gid = font.glyph_index(0x0041).unwrap().unwrap(); // 'A'
267267+ let outline = font.glyph_outline(gid).unwrap().unwrap();
268268+269269+ // 16px size.
270270+ let scale = 16.0 / head.units_per_em as f32;
271271+ let bitmap = rasterize(&outline, scale).unwrap();
272272+273273+ assert!(bitmap.width > 0);
274274+ assert!(bitmap.height > 0);
275275+ assert_eq!(bitmap.data.len(), (bitmap.width * bitmap.height) as usize);
276276+277277+ // Assert it is anti-aliased (has values other than 0 and 255).
278278+ let has_intermediate = bitmap.data.iter().any(|&v| v > 0 && v < 255);
279279+ assert!(has_intermediate, "Bitmap must be anti-aliased");
280280+281281+ // Assert it actually covers some area (has values > 128).
282282+ let has_coverage = bitmap.data.iter().any(|&v| v > 128);
283283+ assert!(has_coverage, "Bitmap must have solid pixels");
284284+ }
285285+}