Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286//! 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<u8>,}
/// 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<LineSegment> { 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<LineSegment>,) { 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<GlyphBitmap> { 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"); }}