//! Glyph rasterization: converts vector outlines to anti-aliased grayscale bitmaps. use crate::font::tables::glyf::GlyphOutline; /// A rasterized glyph bitmap. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GlyphBitmap { /// Width of the bitmap in pixels. pub width: u32, /// Height of the bitmap in pixels. pub height: u32, /// Horizontal offset from origin to left edge of bitmap. pub bearing_x: i32, /// Vertical offset from origin to top edge of bitmap. pub bearing_y: i32, /// 8-bit grayscale coverage data (0 = transparent, 255 = opaque). /// Size is `width * height`. pub data: Vec, } /// A line segment for the scanline rasterizer. #[derive(Debug, Clone, Copy)] struct LineSegment { x0: f32, y0: f32, x1: f32, y1: f32, } impl LineSegment { /// Intersects the horizontal line at `y`. Returns `None` if parallel or out of bounds. /// The mathematical intersection `x` is returned along with direction `dir` (+1 for up, -1 for down). fn intersect_horizontal(&self, y: f32) -> Option<(f32, i32)> { // Only intersect if y is strictly between y0 and y1. let (min_y, max_y, dir) = if self.y0 < self.y1 { (self.y0, self.y1, 1) } else { (self.y1, self.y0, -1) }; if y < min_y || y >= max_y { return None; } let t = (y - self.y0) / (self.y1 - self.y0); let x = self.x0 + t * (self.x1 - self.x0); Some((x, dir)) } } /// Scale and flatten a glyph outline into reproducible line segments. fn flatten_outline(outline: &GlyphOutline, scale: f32) -> Vec { let mut segments = Vec::new(); for contour in &outline.contours { if contour.points.is_empty() { continue; } let pts = &contour.points; // Iterate through implied points. // TrueType defines curves with an implicit on-curve point // halfway between any two consecutive off-curve points. let mut explicit_points = Vec::with_capacity(pts.len() * 2); for (i, p) in pts.iter().enumerate() { let next_p = &pts[(i + 1) % pts.len()]; // Add the current point explicit_points.push((p.x as f32 * scale, p.y as f32 * scale, p.on_curve)); // If current and next are both off-curve, insert a midpoint if !p.on_curve && !next_p.on_curve { let mx = (p.x as f32 + next_p.x as f32) * 0.5 * scale; let my = (p.y as f32 + next_p.y as f32) * 0.5 * scale; explicit_points.push((mx, my, true)); } } // Now traverse the explicit points. The path starts with the first point. // But what if the first point is off-curve? // Let's find an on-curve point to start. let start_idx = explicit_points.iter().position(|p| p.2).unwrap_or(0); let len = explicit_points.len(); let mut i = 0; let mut cur = explicit_points[start_idx]; // Ensure we loop back to start. while i < len { let next_idx = (start_idx + i + 1) % len; let p1 = explicit_points[next_idx]; if p1.2 { // Line to next on-curve point segments.push(LineSegment { x0: cur.0, y0: cur.1, x1: p1.0, y1: p1.1, }); cur = p1; i += 1; } else { // Quadratic bezier: p1 is control, need next on-curve point let p2_idx = (next_idx + 1) % len; let p2 = explicit_points[p2_idx]; // Flatten the quadratic bezier curve. flatten_quadratic(cur.0, cur.1, p1.0, p1.1, p2.0, p2.1, &mut segments); cur = p2; i += 2; } } } segments } /// Flattens a quadratic bézier into line segments using a fixed subdivision. fn flatten_quadratic( x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, segments: &mut Vec, ) { const STEPS: usize = 8; let mut prev_x = x0; let mut prev_y = y0; for i in 1..=STEPS { let t = i as f32 / STEPS as f32; let t_inv = 1.0 - t; // Quadratic formula: P = (1-t)^2*P0 + 2t(1-t)*P1 + t^2*P2 let cur_x = t_inv * t_inv * x0 + 2.0 * t * t_inv * x1 + t * t * x2; let cur_y = t_inv * t_inv * y0 + 2.0 * t * t_inv * y1 + t * t * y2; segments.push(LineSegment { x0: prev_x, y0: prev_y, x1: cur_x, y1: cur_y, }); prev_x = cur_x; prev_y = cur_y; } } /// Rasterizes a scaled outline into a 0-255 coverage bitmap using 16x16 supersampling. pub fn rasterize(outline: &GlyphOutline, scale: f32) -> Option { if outline.contours.is_empty() { return None; } let segments = flatten_outline(outline, scale); let x_min = outline.x_min as f32 * scale; let y_min = outline.y_min as f32 * scale; let x_max = outline.x_max as f32 * scale; let y_max = outline.y_max as f32 * scale; let bitmap_width = (x_max.ceil() - x_min.floor()) as i32; let bitmap_height = (y_max.ceil() - y_min.floor()) as i32; if bitmap_width <= 0 || bitmap_height <= 0 { return None; } let bearing_x = x_min.floor() as i32; // Y-axis in TrueType goes UP, but in screen space it goes DOWN. // So the top-left of the bounding box is at y_max. let bearing_y = y_max.ceil() as i32; let width = bitmap_width as u32; let height = bitmap_height as u32; // 16x16 supersampling => 256 subpixels per pixel. const SUB_PIXELS: u32 = 16; let mut coverage = vec![0u16; (width * height) as usize]; for sy in 0..(height * SUB_PIXELS) { // TrueType Y goes up. The top pixel is row 0. // Therefore, pixel row `py` corresponds to `bearing_y - py - 1`. // The subpixel offset is from the top of the pixel box going downwards. let sub_y_rel = (sy % SUB_PIXELS) as f32 + 0.5; let py = sy / SUB_PIXELS; let real_y = bearing_y as f32 - (py as f32 + sub_y_rel / SUB_PIXELS as f32); // Find intersections. let mut intersections = Vec::with_capacity(32); for seg in &segments { if let Some((x, dir)) = seg.intersect_horizontal(real_y) { intersections.push((x, dir)); } } // Sort by X coordinate. intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); // Traverse intersections to calculate winding and fill pixels. let mut winding = 0; let mut active_start_x = None; for (x, dir) in intersections { let prev_winding = winding; winding += dir; if prev_winding == 0 && winding != 0 { // Entered shape. active_start_x = Some(x); } else if prev_winding != 0 && winding == 0 { // Exited shape. Fill subpixels between active_start_x and x. if let Some(start_x) = active_start_x { // Map to subpixel coordinates on X axis. let start_sx = ((start_x - bearing_x as f32) * SUB_PIXELS as f32).round() as i32; let end_sx = ((x - bearing_x as f32) * SUB_PIXELS as f32).round() as i32; let s_start = start_sx.max(0) as u32; let s_end = (end_sx.max(0) as u32).min(width * SUB_PIXELS); for sx in s_start..s_end { let px = sx / SUB_PIXELS; let idx = (py * width + px) as usize; coverage[idx] += 1; } } active_start_x = None; } } } // Convert coverage (0-256) to 0-255 u8. let data = coverage.into_iter().map(|c| c.min(255) as u8).collect(); Some(GlyphBitmap { width, height, bearing_x, bearing_y, data, }) } #[cfg(test)] mod tests { use super::*; use crate::font::Font; fn load_test_font() -> Font { crate::font::load_system_font().expect("failed to load system font") } #[test] fn rasterize_basic_glyph() { let font = load_test_font(); let head = font.head().unwrap(); let gid = font.glyph_index(0x0041).unwrap().unwrap(); // 'A' let outline = font.glyph_outline(gid).unwrap().unwrap(); // 16px size. let scale = 16.0 / head.units_per_em as f32; let bitmap = rasterize(&outline, scale).unwrap(); assert!(bitmap.width > 0); assert!(bitmap.height > 0); assert_eq!(bitmap.data.len(), (bitmap.width * bitmap.height) as usize); // Assert it is anti-aliased (has values other than 0 and 255). let has_intermediate = bitmap.data.iter().any(|&v| v > 0 && v < 255); assert!(has_intermediate, "Bitmap must be anti-aliased"); // Assert it actually covers some area (has values > 128). let has_coverage = bitmap.data.iter().any(|&v| v > 128); assert!(has_coverage, "Bitmap must have solid pixels"); } }