//! `glyf` — Glyph Data table. //! //! Parses TrueType glyph outlines: simple glyphs (quadratic Bézier contours) //! and compound/composite glyphs (assembled from other glyphs). //! Reference: use crate::font::parse::Reader; use crate::font::tables::loca::LocaTable; use crate::font::FontError; /// A point on a glyph outline. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Point { /// X coordinate in font design units. pub x: i16, /// Y coordinate in font design units. pub y: i16, /// Whether this point is on the curve (vs. an off-curve control point). pub on_curve: bool, } /// A single contour of a glyph outline (a closed path of points). #[derive(Debug, Clone)] pub struct Contour { pub points: Vec, } /// A complete glyph outline, consisting of one or more contours. #[derive(Debug, Clone)] pub struct GlyphOutline { /// Bounding box: minimum X. pub x_min: i16, /// Bounding box: minimum Y. pub y_min: i16, /// Bounding box: maximum X. pub x_max: i16, /// Bounding box: maximum Y. pub y_max: i16, /// The contours that make up this glyph. pub contours: Vec, } // Simple glyph flag bits. const ON_CURVE_POINT: u8 = 0x01; const X_SHORT_VECTOR: u8 = 0x02; const Y_SHORT_VECTOR: u8 = 0x04; const REPEAT_FLAG: u8 = 0x08; const X_IS_SAME_OR_POSITIVE: u8 = 0x10; const Y_IS_SAME_OR_POSITIVE: u8 = 0x20; // Compound glyph flag bits. const ARG_1_AND_2_ARE_WORDS: u16 = 0x0001; const ARGS_ARE_XY_VALUES: u16 = 0x0002; const WE_HAVE_A_SCALE: u16 = 0x0008; const MORE_COMPONENTS: u16 = 0x0020; const WE_HAVE_AN_X_AND_Y_SCALE: u16 = 0x0040; const WE_HAVE_A_TWO_BY_TWO: u16 = 0x0080; /// Glyph header fields parsed from the first 10 bytes. struct GlyphHeader { x_min: i16, y_min: i16, x_max: i16, y_max: i16, } /// Parse a simple glyph from raw bytes (starting after the glyph header). /// /// `number_of_contours` must be > 0. fn parse_simple_glyph( data: &[u8], number_of_contours: i16, header: &GlyphHeader, ) -> Result { let r = Reader::new(data); let n_contours = number_of_contours as usize; // Read endPtsOfContours array. let mut end_pts = Vec::with_capacity(n_contours); for i in 0..n_contours { end_pts.push(r.u16(i * 2)? as usize); } let total_points = match end_pts.last() { Some(&last) => last + 1, None => return Err(FontError::MalformedTable("glyf: no contour endpoints")), }; // Skip instructions. let instructions_offset = n_contours * 2; let instruction_length = r.u16(instructions_offset)? as usize; let flags_offset = instructions_offset + 2 + instruction_length; // Read flags (with REPEAT_FLAG expansion). let mut flags = Vec::with_capacity(total_points); let mut offset = flags_offset; while flags.len() < total_points { if offset >= data.len() { return Err(FontError::MalformedTable("glyf: flags truncated")); } let flag = data[offset]; offset += 1; flags.push(flag); if flag & REPEAT_FLAG != 0 { if offset >= data.len() { return Err(FontError::MalformedTable("glyf: repeat count truncated")); } let repeat_count = data[offset] as usize; offset += 1; for _ in 0..repeat_count { flags.push(flag); } } } // Read X coordinates (delta-encoded). let mut x_coords = Vec::with_capacity(total_points); let mut x: i16 = 0; for &flag in &flags[..total_points] { if flag & X_SHORT_VECTOR != 0 { if offset >= data.len() { return Err(FontError::MalformedTable("glyf: x coordinates truncated")); } let dx = data[offset] as i16; offset += 1; if flag & X_IS_SAME_OR_POSITIVE != 0 { x += dx; } else { x -= dx; } } else if flag & X_IS_SAME_OR_POSITIVE != 0 { // x is the same as previous (delta = 0). } else { if offset + 1 >= data.len() { return Err(FontError::MalformedTable("glyf: x coordinates truncated")); } let dx = i16::from_be_bytes([data[offset], data[offset + 1]]); offset += 2; x += dx; } x_coords.push(x); } // Read Y coordinates (delta-encoded). let mut y_coords = Vec::with_capacity(total_points); let mut y: i16 = 0; for &flag in &flags[..total_points] { if flag & Y_SHORT_VECTOR != 0 { if offset >= data.len() { return Err(FontError::MalformedTable("glyf: y coordinates truncated")); } let dy = data[offset] as i16; offset += 1; if flag & Y_IS_SAME_OR_POSITIVE != 0 { y += dy; } else { y -= dy; } } else if flag & Y_IS_SAME_OR_POSITIVE != 0 { // y is the same as previous (delta = 0). } else { if offset + 1 >= data.len() { return Err(FontError::MalformedTable("glyf: y coordinates truncated")); } let dy = i16::from_be_bytes([data[offset], data[offset + 1]]); offset += 2; y += dy; } y_coords.push(y); } // Build contours from endpoints. let mut contours = Vec::with_capacity(n_contours); let mut start = 0; for &end in &end_pts { let mut points = Vec::with_capacity(end - start + 1); for i in start..=end { points.push(Point { x: x_coords[i], y: y_coords[i], on_curve: flags[i] & ON_CURVE_POINT != 0, }); } contours.push(Contour { points }); start = end + 1; } Ok(GlyphOutline { x_min: header.x_min, y_min: header.y_min, x_max: header.x_max, y_max: header.y_max, contours, }) } /// Parse a compound/composite glyph, flattening component glyphs into a /// single outline. /// /// `glyf_data` is the entire glyf table; `loca` provides offsets for /// sub-glyph lookups. fn parse_compound_glyph( data: &[u8], header: &GlyphHeader, glyf_data: &[u8], loca: &LocaTable, depth: u8, ) -> Result { // Guard against infinite recursion from malformed fonts. if depth > 16 { return Err(FontError::MalformedTable( "glyf: compound glyph recursion too deep", )); } let mut contours = Vec::new(); let mut offset = 0; loop { if offset + 4 > data.len() { return Err(FontError::MalformedTable( "glyf: compound component truncated", )); } let flags = u16::from_be_bytes([data[offset], data[offset + 1]]); let glyph_index = u16::from_be_bytes([data[offset + 2], data[offset + 3]]); offset += 4; // Read translation arguments. let (arg1, arg2): (i16, i16); if flags & ARG_1_AND_2_ARE_WORDS != 0 { if offset + 4 > data.len() { return Err(FontError::MalformedTable("glyf: compound args truncated")); } arg1 = i16::from_be_bytes([data[offset], data[offset + 1]]); arg2 = i16::from_be_bytes([data[offset + 2], data[offset + 3]]); offset += 4; } else { if offset + 2 > data.len() { return Err(FontError::MalformedTable("glyf: compound args truncated")); } if flags & ARGS_ARE_XY_VALUES != 0 { arg1 = data[offset] as i8 as i16; arg2 = data[offset + 1] as i8 as i16; } else { arg1 = data[offset] as i16; arg2 = data[offset + 1] as i16; } offset += 2; } // Read optional scale/transform. let (scale_x, scale_01, scale_10, scale_y): (f32, f32, f32, f32); if flags & WE_HAVE_A_SCALE != 0 { if offset + 2 > data.len() { return Err(FontError::MalformedTable("glyf: compound scale truncated")); } let s = f2dot14(data[offset], data[offset + 1]); scale_x = s; scale_y = s; scale_01 = 0.0; scale_10 = 0.0; offset += 2; } else if flags & WE_HAVE_AN_X_AND_Y_SCALE != 0 { if offset + 4 > data.len() { return Err(FontError::MalformedTable( "glyf: compound xy-scale truncated", )); } scale_x = f2dot14(data[offset], data[offset + 1]); scale_y = f2dot14(data[offset + 2], data[offset + 3]); scale_01 = 0.0; scale_10 = 0.0; offset += 4; } else if flags & WE_HAVE_A_TWO_BY_TWO != 0 { if offset + 8 > data.len() { return Err(FontError::MalformedTable( "glyf: compound 2x2 matrix truncated", )); } scale_x = f2dot14(data[offset], data[offset + 1]); scale_01 = f2dot14(data[offset + 2], data[offset + 3]); scale_10 = f2dot14(data[offset + 4], data[offset + 5]); scale_y = f2dot14(data[offset + 6], data[offset + 7]); offset += 8; } else { scale_x = 1.0; scale_y = 1.0; scale_01 = 0.0; scale_10 = 0.0; } let (dx, dy) = if flags & ARGS_ARE_XY_VALUES != 0 { (arg1, arg2) } else { // Point matching — treat as (0,0) offset (rare, complex to implement fully). (0i16, 0i16) }; // Recursively parse the component glyph. if let Some(component_outline) = parse_glyph_inner(glyph_index, glyf_data, loca, depth + 1)? { let has_transform = scale_x != 1.0 || scale_y != 1.0 || scale_01 != 0.0 || scale_10 != 0.0; for contour in &component_outline.contours { let points = contour .points .iter() .map(|p| { let (px, py) = if has_transform { let fx = p.x as f32; let fy = p.y as f32; let tx = fx * scale_x + fy * scale_10; let ty = fx * scale_01 + fy * scale_y; (tx.round() as i16, ty.round() as i16) } else { (p.x, p.y) }; Point { x: px.saturating_add(dx), y: py.saturating_add(dy), on_curve: p.on_curve, } }) .collect(); contours.push(Contour { points }); } } if flags & MORE_COMPONENTS == 0 { break; } } Ok(GlyphOutline { x_min: header.x_min, y_min: header.y_min, x_max: header.x_max, y_max: header.y_max, contours, }) } /// Decode a 2.14 fixed-point number to f32. fn f2dot14(hi: u8, lo: u8) -> f32 { let raw = i16::from_be_bytes([hi, lo]); raw as f32 / 16384.0 } /// Internal glyph parser that handles both simple and compound glyphs. /// /// Returns `None` for glyphs with no outline data (e.g., space). fn parse_glyph_inner( glyph_id: u16, glyf_data: &[u8], loca: &LocaTable, depth: u8, ) -> Result, FontError> { let (start, end) = match loca.glyph_range(glyph_id) { Some(range) => range, None => return Ok(None), // No outline (e.g., space). }; let start = start as usize; let end = end as usize; if end > glyf_data.len() || start >= end { return Ok(None); } let glyph_bytes = &glyf_data[start..end]; if glyph_bytes.len() < 10 { return Err(FontError::MalformedTable("glyf: glyph header too short")); } let r = Reader::new(glyph_bytes); let number_of_contours = r.i16(0)?; let header = GlyphHeader { x_min: r.i16(2)?, y_min: r.i16(4)?, x_max: r.i16(6)?, y_max: r.i16(8)?, }; if number_of_contours >= 0 { // Simple glyph. let outline = parse_simple_glyph(&glyph_bytes[10..], number_of_contours, &header)?; Ok(Some(outline)) } else { // Compound glyph (number_of_contours == -1). let outline = parse_compound_glyph(&glyph_bytes[10..], &header, glyf_data, loca, depth)?; Ok(Some(outline)) } } /// Parse a glyph outline from the `glyf` table. /// /// Returns `None` for glyphs with no outline data (e.g., space character). /// Returns `Err` for malformed data. pub fn parse_glyph( glyph_id: u16, glyf_data: &[u8], loca: &LocaTable, ) -> Result, FontError> { parse_glyph_inner(glyph_id, glyf_data, loca, 0) }