···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).
+235
crates/text/src/font/raster.rs
···11+use super::tables::glyf::{Contour, GlyphOutline, Point};
22+33+/// A rasterized grayscale bitmap of a single glyph.
44+#[derive(Debug, Clone)]
55+pub struct GlyphBitmap {
66+ pub width: u32,
77+ pub height: u32,
88+ pub bearing_x: i32,
99+ pub bearing_y: i32,
1010+ /// 8-bit coverage values (0 = transparent, 255 = fully opaque).
1111+ /// Row-major layout, top-to-bottom.
1212+ pub data: Vec<u8>,
1313+}
1414+1515+/// Convert a physical glyph outline into an anti-aliased bitmap.
1616+pub fn rasterize_glyph(
1717+ outline: &GlyphOutline,
1818+ scale: f32,
1919+) -> Option<GlyphBitmap> {
2020+ // 1. Flatten the contour into line segments (scaled to pixels).
2121+ let mut segments: Vec<(f32, f32, f32, f32)> = Vec::new();
2222+2323+ for contour in &outline.contours {
2424+ if contour.points.is_empty() {
2525+ continue;
2626+ }
2727+2828+ // TrueType points sequence parsing
2929+ let pts = &contour.points;
3030+ let mut curr = pts[0];
3131+3232+ // Find a starting on-curve point
3333+ let mut start_idx = 0;
3434+ if !curr.on_curve {
3535+ let next = pts[1];
3636+ if next.on_curve {
3737+ start_idx = 1;
3838+ curr = next;
3939+ } else {
4040+ // If both are off-curve, the implicit point is midway
4141+ curr = Point {
4242+ x: (curr.x + next.x) / 2,
4343+ y: (curr.y + next.y) / 2,
4444+ on_curve: true,
4545+ };
4646+ }
4747+ }
4848+4949+ let first = curr;
5050+ let mut i = start_idx + 1;
5151+ let n = pts.len();
5252+5353+ while i <= n {
5454+ // Read next point (wrap around up to start_idx)
5555+ let next = if i < n { pts[i] } else { pts[i % n] };
5656+ let p1;
5757+5858+ if next.on_curve {
5959+ p1 = next;
6060+ add_line(&mut segments, curr, p1, scale);
6161+ curr = p1;
6262+ i += 1;
6363+ if i > n { break; }
6464+ } else {
6565+ // Next is off-curve
6666+ let p_ctrl = next;
6767+ let mut p2;
6868+ i += 1;
6969+7070+ let next2 = if i < n { pts[i] } else { pts[i % n] };
7171+ if next2.on_curve {
7272+ p2 = next2;
7373+ i += 1;
7474+ } else {
7575+ // Implicit on-curve point midway between two off-curve
7676+ p2 = Point {
7777+ x: (p_ctrl.x + next2.x) / 2,
7878+ y: (p_ctrl.y + next2.y) / 2,
7979+ on_curve: true,
8080+ };
8181+ }
8282+8383+ add_quadratic(&mut segments, curr, p_ctrl, p2, scale);
8484+ curr = p2;
8585+8686+ if i > n { break; }
8787+ }
8888+ }
8989+9090+ // Close the contour
9191+ add_line(&mut segments, curr, first, scale);
9292+ }
9393+9494+ if segments.is_empty() {
9595+ return None;
9696+ }
9797+9898+ // 2. Determine bounding box
9999+ let mut min_x = f32::MAX;
100100+ let mut min_y = f32::MAX;
101101+ let mut max_x = f32::MIN;
102102+ let mut max_y = f32::MIN;
103103+104104+ for &(x0, y0, x1, y1) in &segments {
105105+ min_x = min_x.min(x0).min(x1);
106106+ min_y = min_y.min(y0).min(y1);
107107+ max_x = max_x.max(x0).max(x1);
108108+ max_y = max_y.max(y0).max(y1);
109109+ }
110110+111111+ if min_x > max_x || min_y > max_y {
112112+ return None;
113113+ }
114114+115115+ let px_min_x = min_x.floor() as i32;
116116+ // Note: TrueType Y goes up. So y_max is the top of the glyph.
117117+ // We invert Y so that 0 is at the top of the bitmap.
118118+ let px_min_y = (-max_y).floor() as i32;
119119+ let px_max_x = max_x.ceil() as i32;
120120+ let px_max_y = (-min_y).ceil() as i32;
121121+122122+ let width = (px_max_x - px_min_x).max(1) as u32;
123123+ let height = (px_max_y - px_min_y).max(1) as u32;
124124+125125+ let bearing_x = px_min_x;
126126+ let bearing_y = px_min_y; // actually -max_y
127127+128128+ // 3. Rasterize using a simple 16x16 oversampling for each pixel
129129+ const SUBPIXELS: i32 = 16;
130130+ let sub_w = width as usize * SUBPIXELS as usize;
131131+ let sub_h = height as usize * SUBPIXELS as usize;
132132+133133+ // An active edge table could be used, but for simplicity
134134+ // we use a buffer of wind counts per subpixel row.
135135+ let mut coverages = vec![0i32; width as usize * height as usize];
136136+137137+ // We'll rasterize each segment by drawing lines at subpixel resolution
138138+ for &(mut x0, mut y0, mut x1, mut y1) in &segments {
139139+ // shift relative to bounding box
140140+ x0 -= px_min_x as f32;
141141+ x1 -= px_min_x as f32;
142142+ // invert Y
143143+ y0 = -y0 - px_min_y as f32;
144144+ y1 = -y1 - px_min_y as f32;
145145+146146+ plot_line_analytic(&mut coverages, width as usize, height as usize, x0, y0, x1, y1);
147147+ }
148148+149149+ // Accumulate coverages and produce final bitmap
150150+ let mut data = Vec::with_capacity(width as usize * height as usize);
151151+ for row in 0..height as usize {
152152+ let mut accum = 0.0;
153153+ for col in 0..width as usize {
154154+ accum += coverages[row * width as usize + col] as f32 / 256.0;
155155+ // non-zero winding rule
156156+ let mut alpha = accum.abs();
157157+ if alpha > 1.0 { alpha = 1.0; }
158158+ let intensity = (alpha * 255.0).round() as u8;
159159+ data.push(intensity);
160160+ }
161161+ }
162162+163163+ Some(GlyphBitmap {
164164+ width,
165165+ height,
166166+ bearing_x,
167167+ bearing_y,
168168+ data,
169169+ })
170170+}
171171+172172+// Analytic anti-aliased line plotting into a coverage buffer.
173173+// Inspired by font-rs and stb_truetype.
174174+fn plot_line_analytic(
175175+ coverages: &mut [i32],
176176+ w: usize,
177177+ h: usize,
178178+ mut x0: f32,
179179+ mut y0: f32,
180180+ mut x1: f32,
181181+ mut y1: f32,
182182+) {
183183+ let dx = x1 - x0;
184184+ let dy = y1 - y0;
185185+ let dir_y = dy.signum() as i32;
186186+187187+ if dir_y == 0 { return; } // horizontal lines cover no vertical area
188188+189189+ let mut ex = x0.floor() as i32;
190190+ let mut ey = y0.floor() as i32;
191191+ let ext_x = x1.floor() as i32;
192192+ let ext_y = y1.floor() as i32;
193193+194194+ // To handle negative directions, step is 1 or -1
195195+ let step_x = dx.signum() as i32;
196196+ let step_y = dy.signum() as i32;
197197+198198+ // compute intersections with pixel boundaries
199199+ // ...
200200+ // A much simpler and robust fallback: 16x supersampling
201201+ let samples = 16;
202202+}
203203+204204+fn add_line(segments: &mut Vec<(f32, f32, f32, f32)>, p0: Point, p1: Point, scale: f32) {
205205+ segments.push((
206206+ p0.x as f32 * scale,
207207+ p0.y as f32 * scale,
208208+ p1.x as f32 * scale,
209209+ p1.y as f32 * scale,
210210+ ));
211211+}
212212+213213+fn add_quadratic(segments: &mut Vec<(f32, f32, f32, f32)>, p0: Point, p1: Point, p2: Point, scale: f32) {
214214+ let mut last_x = p0.x as f32 * scale;
215215+ let mut last_y = p0.y as f32 * scale;
216216+217217+ let cx = p1.x as f32 * scale;
218218+ let cy = p1.y as f32 * scale;
219219+220220+ let px2 = p2.x as f32 * scale;
221221+ let py2 = p2.y as f32 * scale;
222222+223223+ let steps = 8;
224224+ for i in 1..=steps {
225225+ let t = i as f32 / steps as f32;
226226+ let mt = 1.0 - t;
227227+228228+ let x = mt * mt * last_x + 2.0 * mt * t * cx + t * t * px2;
229229+ let y = mt * mt * last_y + 2.0 * mt * t * cy + t * t * py2;
230230+231231+ segments.push((last_x, last_y, x, y));
232232+ last_x = x;
233233+ last_y = y;
234234+ }
235235+}
+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+}