web engine - experimental web browser

Implement glyf and OS/2 table parsing for glyph outline extraction

Parse TrueType glyph outlines from the glyf table:
- Simple glyphs: contour endpoints, flags, delta-encoded coordinates
- Compound/composite glyphs: recursive component flattening with
translation, scale, and 2x2 matrix transforms
- Public API: Font::glyph_outline(glyph_id) -> Option<GlyphOutline>

Parse OS/2 table for font-wide metrics:
- Typographic ascender/descender/line gap
- Weight class, width class, embedding flags
- Strikeout size/position, sub/superscript offsets
- sxHeight, sCapHeight (version >= 2)

Data structures: Point (x, y, on_curve), Contour, GlyphOutline
7 new tests against real system fonts, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+683
+177
crates/text/src/font/mod.rs
··· 9 9 mod tables; 10 10 11 11 pub use tables::cmap::CmapTable; 12 + pub use tables::glyf::{Contour, GlyphOutline, Point}; 12 13 pub use tables::head::HeadTable; 13 14 pub use tables::hhea::HheaTable; 14 15 pub use tables::hmtx::HmtxTable; 15 16 pub use tables::loca::LocaTable; 16 17 pub use tables::maxp::MaxpTable; 17 18 pub use tables::name::NameTable; 19 + pub use tables::os2::Os2Table; 18 20 19 21 /// Errors that can occur during font parsing. 20 22 #[derive(Debug)] ··· 195 197 LocaTable::parse(data, head.index_to_loc_format, maxp.num_glyphs) 196 198 } 197 199 200 + /// Parse the `OS/2` table. 201 + pub fn os2(&self) -> Result<Os2Table, FontError> { 202 + let data = self 203 + .table_data(b"OS/2") 204 + .ok_or(FontError::MissingTable("OS/2"))?; 205 + Os2Table::parse(data) 206 + } 207 + 198 208 /// Map a Unicode code point to a glyph index using the cmap table. 199 209 pub fn glyph_index(&self, codepoint: u32) -> Result<Option<u16>, FontError> { 200 210 let cmap = self.cmap()?; 201 211 Ok(cmap.glyph_index(codepoint)) 202 212 } 203 213 214 + /// Extract the outline for a glyph by its glyph ID. 215 + /// 216 + /// Returns `None` for glyphs with no outline (e.g., space). 217 + /// Requires the `glyf` and `loca` tables. 218 + pub fn glyph_outline(&self, glyph_id: u16) -> Result<Option<GlyphOutline>, FontError> { 219 + let loca = self.loca()?; 220 + let glyf_data = self 221 + .table_data(b"glyf") 222 + .ok_or(FontError::MissingTable("glyf"))?; 223 + tables::glyf::parse_glyph(glyph_id, glyf_data, &loca) 224 + } 225 + 204 226 /// Returns true if this is a TrueType font (vs CFF/PostScript outlines). 205 227 pub fn is_truetype(&self) -> bool { 206 228 self.sf_version == 0x00010000 || self.sf_version == 0x74727565 ··· 369 391 let font = load_system_font().expect("should load a system font"); 370 392 assert!(!font.tables.is_empty()); 371 393 } 394 + } 395 + 396 + #[test] 397 + fn parse_os2_table() { 398 + let font = test_font(); 399 + let os2 = font.os2().expect("failed to parse OS/2"); 400 + // Weight class should be in valid range (100–900). 401 + assert!( 402 + os2.us_weight_class >= 100 && os2.us_weight_class <= 900, 403 + "weight class out of range: {}", 404 + os2.us_weight_class 405 + ); 406 + // Width class should be 1–9. 407 + assert!( 408 + os2.us_width_class >= 1 && os2.us_width_class <= 9, 409 + "width class out of range: {}", 410 + os2.us_width_class 411 + ); 412 + // Typo ascender should be positive for normal fonts. 413 + assert!( 414 + os2.s_typo_ascender > 0, 415 + "sTypoAscender should be positive: {}", 416 + os2.s_typo_ascender 417 + ); 418 + // Typo descender should be negative or zero. 419 + assert!( 420 + os2.s_typo_descender <= 0, 421 + "sTypoDescender should be <= 0: {}", 422 + os2.s_typo_descender 423 + ); 424 + } 425 + 426 + #[test] 427 + fn parse_os2_version_2_fields() { 428 + let font = test_font(); 429 + let os2 = font.os2().expect("failed to parse OS/2"); 430 + if os2.version >= 2 { 431 + // sxHeight and sCapHeight should be non-negative for version >= 2. 432 + assert!( 433 + os2.sx_height >= 0, 434 + "sxHeight should be >= 0: {}", 435 + os2.sx_height 436 + ); 437 + assert!( 438 + os2.s_cap_height >= 0, 439 + "sCapHeight should be >= 0: {}", 440 + os2.s_cap_height 441 + ); 442 + } 443 + } 444 + 445 + #[test] 446 + fn glyph_outline_simple() { 447 + let font = test_font(); 448 + // Get glyph ID for 'A'. 449 + let gid = font 450 + .glyph_index(0x0041) 451 + .expect("glyph_index failed") 452 + .expect("no glyph for 'A'"); 453 + let outline = font 454 + .glyph_outline(gid) 455 + .expect("glyph_outline failed") 456 + .expect("'A' should have an outline"); 457 + 458 + // 'A' should have at least one contour. 459 + assert!( 460 + !outline.contours.is_empty(), 461 + "'A' should have at least 1 contour" 462 + ); 463 + // Bounding box should be valid. 464 + assert!( 465 + outline.x_max >= outline.x_min, 466 + "x_max ({}) should be >= x_min ({})", 467 + outline.x_max, 468 + outline.x_min 469 + ); 470 + assert!( 471 + outline.y_max >= outline.y_min, 472 + "y_max ({}) should be >= y_min ({})", 473 + outline.y_max, 474 + outline.y_min 475 + ); 476 + // Each contour should have points. 477 + for contour in &outline.contours { 478 + assert!( 479 + !contour.points.is_empty(), 480 + "contour should have at least one point" 481 + ); 482 + } 483 + } 484 + 485 + #[test] 486 + fn glyph_outline_space_has_no_outline() { 487 + let font = test_font(); 488 + // Space should map to a glyph but have no outline. 489 + let gid = font 490 + .glyph_index(0x0020) 491 + .expect("glyph_index failed") 492 + .expect("no glyph for space"); 493 + let outline = font.glyph_outline(gid).expect("glyph_outline failed"); 494 + assert!( 495 + outline.is_none(), 496 + "space should have no outline (got {:?})", 497 + outline 498 + ); 499 + } 500 + 501 + #[test] 502 + fn glyph_outline_multiple_glyphs() { 503 + let font = test_font(); 504 + // Parse outlines for several ASCII characters. 505 + for &cp in &[0x0042u32, 0x0043, 0x004F, 0x0053] { 506 + // B, C, O, S 507 + let gid = font.glyph_index(cp).expect("glyph_index failed"); 508 + if let Some(gid) = gid { 509 + let result = font.glyph_outline(gid); 510 + assert!( 511 + result.is_ok(), 512 + "failed to parse outline for U+{:04X}: {:?}", 513 + cp, 514 + result.err() 515 + ); 516 + } 517 + } 518 + } 519 + 520 + #[test] 521 + fn glyph_outline_has_on_curve_points() { 522 + let font = test_font(); 523 + let gid = font 524 + .glyph_index(0x0041) 525 + .expect("glyph_index failed") 526 + .expect("no glyph for 'A'"); 527 + let outline = font 528 + .glyph_outline(gid) 529 + .expect("glyph_outline failed") 530 + .expect("'A' should have an outline"); 531 + 532 + // Every contour should have at least some on-curve points. 533 + for contour in &outline.contours { 534 + let on_curve_count = contour.points.iter().filter(|p| p.on_curve).count(); 535 + assert!( 536 + on_curve_count > 0, 537 + "contour should have at least one on-curve point" 538 + ); 539 + } 540 + } 541 + 542 + #[test] 543 + fn glyph_outline_notdef() { 544 + let font = test_font(); 545 + // Glyph 0 (.notdef) usually has an outline (a rectangle or empty box). 546 + let result = font.glyph_outline(0); 547 + assert!(result.is_ok(), ".notdef outline parse should not error"); 548 + // .notdef may or may not have an outline — just verify no crash. 372 549 } 373 550 }
+407
crates/text/src/font/tables/glyf.rs
··· 1 + //! `glyf` — Glyph Data table. 2 + //! 3 + //! Parses TrueType glyph outlines: simple glyphs (quadratic Bézier contours) 4 + //! and compound/composite glyphs (assembled from other glyphs). 5 + //! Reference: <https://learn.microsoft.com/en-us/typography/opentype/spec/glyf> 6 + 7 + use crate::font::parse::Reader; 8 + use crate::font::tables::loca::LocaTable; 9 + use crate::font::FontError; 10 + 11 + /// A point on a glyph outline. 12 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 13 + pub struct Point { 14 + /// X coordinate in font design units. 15 + pub x: i16, 16 + /// Y coordinate in font design units. 17 + pub y: i16, 18 + /// Whether this point is on the curve (vs. an off-curve control point). 19 + pub on_curve: bool, 20 + } 21 + 22 + /// A single contour of a glyph outline (a closed path of points). 23 + #[derive(Debug, Clone)] 24 + pub struct Contour { 25 + pub points: Vec<Point>, 26 + } 27 + 28 + /// A complete glyph outline, consisting of one or more contours. 29 + #[derive(Debug, Clone)] 30 + pub struct GlyphOutline { 31 + /// Bounding box: minimum X. 32 + pub x_min: i16, 33 + /// Bounding box: minimum Y. 34 + pub y_min: i16, 35 + /// Bounding box: maximum X. 36 + pub x_max: i16, 37 + /// Bounding box: maximum Y. 38 + pub y_max: i16, 39 + /// The contours that make up this glyph. 40 + pub contours: Vec<Contour>, 41 + } 42 + 43 + // Simple glyph flag bits. 44 + const ON_CURVE_POINT: u8 = 0x01; 45 + const X_SHORT_VECTOR: u8 = 0x02; 46 + const Y_SHORT_VECTOR: u8 = 0x04; 47 + const REPEAT_FLAG: u8 = 0x08; 48 + const X_IS_SAME_OR_POSITIVE: u8 = 0x10; 49 + const Y_IS_SAME_OR_POSITIVE: u8 = 0x20; 50 + 51 + // Compound glyph flag bits. 52 + const ARG_1_AND_2_ARE_WORDS: u16 = 0x0001; 53 + const ARGS_ARE_XY_VALUES: u16 = 0x0002; 54 + const WE_HAVE_A_SCALE: u16 = 0x0008; 55 + const MORE_COMPONENTS: u16 = 0x0020; 56 + const WE_HAVE_AN_X_AND_Y_SCALE: u16 = 0x0040; 57 + const WE_HAVE_A_TWO_BY_TWO: u16 = 0x0080; 58 + 59 + /// Glyph header fields parsed from the first 10 bytes. 60 + struct GlyphHeader { 61 + x_min: i16, 62 + y_min: i16, 63 + x_max: i16, 64 + y_max: i16, 65 + } 66 + 67 + /// Parse a simple glyph from raw bytes (starting after the glyph header). 68 + /// 69 + /// `number_of_contours` must be > 0. 70 + fn parse_simple_glyph( 71 + data: &[u8], 72 + number_of_contours: i16, 73 + header: &GlyphHeader, 74 + ) -> Result<GlyphOutline, FontError> { 75 + let r = Reader::new(data); 76 + let n_contours = number_of_contours as usize; 77 + 78 + // Read endPtsOfContours array. 79 + let mut end_pts = Vec::with_capacity(n_contours); 80 + for i in 0..n_contours { 81 + end_pts.push(r.u16(i * 2)? as usize); 82 + } 83 + 84 + let total_points = match end_pts.last() { 85 + Some(&last) => last + 1, 86 + None => return Err(FontError::MalformedTable("glyf: no contour endpoints")), 87 + }; 88 + 89 + // Skip instructions. 90 + let instructions_offset = n_contours * 2; 91 + let instruction_length = r.u16(instructions_offset)? as usize; 92 + let flags_offset = instructions_offset + 2 + instruction_length; 93 + 94 + // Read flags (with REPEAT_FLAG expansion). 95 + let mut flags = Vec::with_capacity(total_points); 96 + let mut offset = flags_offset; 97 + while flags.len() < total_points { 98 + if offset >= data.len() { 99 + return Err(FontError::MalformedTable("glyf: flags truncated")); 100 + } 101 + let flag = data[offset]; 102 + offset += 1; 103 + flags.push(flag); 104 + 105 + if flag & REPEAT_FLAG != 0 { 106 + if offset >= data.len() { 107 + return Err(FontError::MalformedTable("glyf: repeat count truncated")); 108 + } 109 + let repeat_count = data[offset] as usize; 110 + offset += 1; 111 + for _ in 0..repeat_count { 112 + flags.push(flag); 113 + } 114 + } 115 + } 116 + 117 + // Read X coordinates (delta-encoded). 118 + let mut x_coords = Vec::with_capacity(total_points); 119 + let mut x: i16 = 0; 120 + for &flag in &flags[..total_points] { 121 + if flag & X_SHORT_VECTOR != 0 { 122 + if offset >= data.len() { 123 + return Err(FontError::MalformedTable("glyf: x coordinates truncated")); 124 + } 125 + let dx = data[offset] as i16; 126 + offset += 1; 127 + if flag & X_IS_SAME_OR_POSITIVE != 0 { 128 + x += dx; 129 + } else { 130 + x -= dx; 131 + } 132 + } else if flag & X_IS_SAME_OR_POSITIVE != 0 { 133 + // x is the same as previous (delta = 0). 134 + } else { 135 + if offset + 1 >= data.len() { 136 + return Err(FontError::MalformedTable("glyf: x coordinates truncated")); 137 + } 138 + let dx = i16::from_be_bytes([data[offset], data[offset + 1]]); 139 + offset += 2; 140 + x += dx; 141 + } 142 + x_coords.push(x); 143 + } 144 + 145 + // Read Y coordinates (delta-encoded). 146 + let mut y_coords = Vec::with_capacity(total_points); 147 + let mut y: i16 = 0; 148 + for &flag in &flags[..total_points] { 149 + if flag & Y_SHORT_VECTOR != 0 { 150 + if offset >= data.len() { 151 + return Err(FontError::MalformedTable("glyf: y coordinates truncated")); 152 + } 153 + let dy = data[offset] as i16; 154 + offset += 1; 155 + if flag & Y_IS_SAME_OR_POSITIVE != 0 { 156 + y += dy; 157 + } else { 158 + y -= dy; 159 + } 160 + } else if flag & Y_IS_SAME_OR_POSITIVE != 0 { 161 + // y is the same as previous (delta = 0). 162 + } else { 163 + if offset + 1 >= data.len() { 164 + return Err(FontError::MalformedTable("glyf: y coordinates truncated")); 165 + } 166 + let dy = i16::from_be_bytes([data[offset], data[offset + 1]]); 167 + offset += 2; 168 + y += dy; 169 + } 170 + y_coords.push(y); 171 + } 172 + 173 + // Build contours from endpoints. 174 + let mut contours = Vec::with_capacity(n_contours); 175 + let mut start = 0; 176 + for &end in &end_pts { 177 + let mut points = Vec::with_capacity(end - start + 1); 178 + for i in start..=end { 179 + points.push(Point { 180 + x: x_coords[i], 181 + y: y_coords[i], 182 + on_curve: flags[i] & ON_CURVE_POINT != 0, 183 + }); 184 + } 185 + contours.push(Contour { points }); 186 + start = end + 1; 187 + } 188 + 189 + Ok(GlyphOutline { 190 + x_min: header.x_min, 191 + y_min: header.y_min, 192 + x_max: header.x_max, 193 + y_max: header.y_max, 194 + contours, 195 + }) 196 + } 197 + 198 + /// Parse a compound/composite glyph, flattening component glyphs into a 199 + /// single outline. 200 + /// 201 + /// `glyf_data` is the entire glyf table; `loca` provides offsets for 202 + /// sub-glyph lookups. 203 + fn parse_compound_glyph( 204 + data: &[u8], 205 + header: &GlyphHeader, 206 + glyf_data: &[u8], 207 + loca: &LocaTable, 208 + depth: u8, 209 + ) -> Result<GlyphOutline, FontError> { 210 + // Guard against infinite recursion from malformed fonts. 211 + if depth > 16 { 212 + return Err(FontError::MalformedTable( 213 + "glyf: compound glyph recursion too deep", 214 + )); 215 + } 216 + 217 + let mut contours = Vec::new(); 218 + let mut offset = 0; 219 + 220 + loop { 221 + if offset + 4 > data.len() { 222 + return Err(FontError::MalformedTable( 223 + "glyf: compound component truncated", 224 + )); 225 + } 226 + let flags = u16::from_be_bytes([data[offset], data[offset + 1]]); 227 + let glyph_index = u16::from_be_bytes([data[offset + 2], data[offset + 3]]); 228 + offset += 4; 229 + 230 + // Read translation arguments. 231 + let (arg1, arg2): (i16, i16); 232 + if flags & ARG_1_AND_2_ARE_WORDS != 0 { 233 + if offset + 4 > data.len() { 234 + return Err(FontError::MalformedTable("glyf: compound args truncated")); 235 + } 236 + arg1 = i16::from_be_bytes([data[offset], data[offset + 1]]); 237 + arg2 = i16::from_be_bytes([data[offset + 2], data[offset + 3]]); 238 + offset += 4; 239 + } else { 240 + if offset + 2 > data.len() { 241 + return Err(FontError::MalformedTable("glyf: compound args truncated")); 242 + } 243 + if flags & ARGS_ARE_XY_VALUES != 0 { 244 + arg1 = data[offset] as i8 as i16; 245 + arg2 = data[offset + 1] as i8 as i16; 246 + } else { 247 + arg1 = data[offset] as i16; 248 + arg2 = data[offset + 1] as i16; 249 + } 250 + offset += 2; 251 + } 252 + 253 + // Read optional scale/transform. 254 + let (scale_x, scale_01, scale_10, scale_y): (f32, f32, f32, f32); 255 + if flags & WE_HAVE_A_SCALE != 0 { 256 + if offset + 2 > data.len() { 257 + return Err(FontError::MalformedTable("glyf: compound scale truncated")); 258 + } 259 + let s = f2dot14(data[offset], data[offset + 1]); 260 + scale_x = s; 261 + scale_y = s; 262 + scale_01 = 0.0; 263 + scale_10 = 0.0; 264 + offset += 2; 265 + } else if flags & WE_HAVE_AN_X_AND_Y_SCALE != 0 { 266 + if offset + 4 > data.len() { 267 + return Err(FontError::MalformedTable( 268 + "glyf: compound xy-scale truncated", 269 + )); 270 + } 271 + scale_x = f2dot14(data[offset], data[offset + 1]); 272 + scale_y = f2dot14(data[offset + 2], data[offset + 3]); 273 + scale_01 = 0.0; 274 + scale_10 = 0.0; 275 + offset += 4; 276 + } else if flags & WE_HAVE_A_TWO_BY_TWO != 0 { 277 + if offset + 8 > data.len() { 278 + return Err(FontError::MalformedTable( 279 + "glyf: compound 2x2 matrix truncated", 280 + )); 281 + } 282 + scale_x = f2dot14(data[offset], data[offset + 1]); 283 + scale_01 = f2dot14(data[offset + 2], data[offset + 3]); 284 + scale_10 = f2dot14(data[offset + 4], data[offset + 5]); 285 + scale_y = f2dot14(data[offset + 6], data[offset + 7]); 286 + offset += 8; 287 + } else { 288 + scale_x = 1.0; 289 + scale_y = 1.0; 290 + scale_01 = 0.0; 291 + scale_10 = 0.0; 292 + } 293 + 294 + let (dx, dy) = if flags & ARGS_ARE_XY_VALUES != 0 { 295 + (arg1, arg2) 296 + } else { 297 + // Point matching — treat as (0,0) offset (rare, complex to implement fully). 298 + (0i16, 0i16) 299 + }; 300 + 301 + // Recursively parse the component glyph. 302 + if let Some(component_outline) = parse_glyph_inner(glyph_index, glyf_data, loca, depth + 1)? 303 + { 304 + let has_transform = 305 + scale_x != 1.0 || scale_y != 1.0 || scale_01 != 0.0 || scale_10 != 0.0; 306 + 307 + for contour in &component_outline.contours { 308 + let points = contour 309 + .points 310 + .iter() 311 + .map(|p| { 312 + let (px, py) = if has_transform { 313 + let fx = p.x as f32; 314 + let fy = p.y as f32; 315 + let tx = fx * scale_x + fy * scale_10; 316 + let ty = fx * scale_01 + fy * scale_y; 317 + (tx.round() as i16, ty.round() as i16) 318 + } else { 319 + (p.x, p.y) 320 + }; 321 + Point { 322 + x: px.saturating_add(dx), 323 + y: py.saturating_add(dy), 324 + on_curve: p.on_curve, 325 + } 326 + }) 327 + .collect(); 328 + contours.push(Contour { points }); 329 + } 330 + } 331 + 332 + if flags & MORE_COMPONENTS == 0 { 333 + break; 334 + } 335 + } 336 + 337 + Ok(GlyphOutline { 338 + x_min: header.x_min, 339 + y_min: header.y_min, 340 + x_max: header.x_max, 341 + y_max: header.y_max, 342 + contours, 343 + }) 344 + } 345 + 346 + /// Decode a 2.14 fixed-point number to f32. 347 + fn f2dot14(hi: u8, lo: u8) -> f32 { 348 + let raw = i16::from_be_bytes([hi, lo]); 349 + raw as f32 / 16384.0 350 + } 351 + 352 + /// Internal glyph parser that handles both simple and compound glyphs. 353 + /// 354 + /// Returns `None` for glyphs with no outline data (e.g., space). 355 + fn parse_glyph_inner( 356 + glyph_id: u16, 357 + glyf_data: &[u8], 358 + loca: &LocaTable, 359 + depth: u8, 360 + ) -> Result<Option<GlyphOutline>, FontError> { 361 + let (start, end) = match loca.glyph_range(glyph_id) { 362 + Some(range) => range, 363 + None => return Ok(None), // No outline (e.g., space). 364 + }; 365 + 366 + let start = start as usize; 367 + let end = end as usize; 368 + if end > glyf_data.len() || start >= end { 369 + return Ok(None); 370 + } 371 + 372 + let glyph_bytes = &glyf_data[start..end]; 373 + if glyph_bytes.len() < 10 { 374 + return Err(FontError::MalformedTable("glyf: glyph header too short")); 375 + } 376 + 377 + let r = Reader::new(glyph_bytes); 378 + let number_of_contours = r.i16(0)?; 379 + let header = GlyphHeader { 380 + x_min: r.i16(2)?, 381 + y_min: r.i16(4)?, 382 + x_max: r.i16(6)?, 383 + y_max: r.i16(8)?, 384 + }; 385 + 386 + if number_of_contours >= 0 { 387 + // Simple glyph. 388 + let outline = parse_simple_glyph(&glyph_bytes[10..], number_of_contours, &header)?; 389 + Ok(Some(outline)) 390 + } else { 391 + // Compound glyph (number_of_contours == -1). 392 + let outline = parse_compound_glyph(&glyph_bytes[10..], &header, glyf_data, loca, depth)?; 393 + Ok(Some(outline)) 394 + } 395 + } 396 + 397 + /// Parse a glyph outline from the `glyf` table. 398 + /// 399 + /// Returns `None` for glyphs with no outline data (e.g., space character). 400 + /// Returns `Err` for malformed data. 401 + pub fn parse_glyph( 402 + glyph_id: u16, 403 + glyf_data: &[u8], 404 + loca: &LocaTable, 405 + ) -> Result<Option<GlyphOutline>, FontError> { 406 + parse_glyph_inner(glyph_id, glyf_data, loca, 0) 407 + }
+2
crates/text/src/font/tables/mod.rs
··· 1 1 //! Individual font table parsers. 2 2 3 3 pub mod cmap; 4 + pub mod glyf; 4 5 pub mod head; 5 6 pub mod hhea; 6 7 pub mod hmtx; 7 8 pub mod loca; 8 9 pub mod maxp; 9 10 pub mod name; 11 + pub mod os2;
+97
crates/text/src/font/tables/os2.rs
··· 1 + //! `OS/2` — OS/2 and Windows Metrics table. 2 + //! 3 + //! Contains font-wide metrics used for line spacing, weight classification, 4 + //! and typographic alignment. 5 + //! Reference: <https://learn.microsoft.com/en-us/typography/opentype/spec/os2> 6 + 7 + use crate::font::parse::Reader; 8 + use crate::font::FontError; 9 + 10 + /// Parsed `OS/2` table. 11 + #[derive(Debug)] 12 + pub struct Os2Table { 13 + /// Table version (0–5). 14 + pub version: u16, 15 + /// Average weighted advance width of lowercase letters and space. 16 + pub x_avg_char_width: i16, 17 + /// Visual weight class (100–900). 18 + pub us_weight_class: u16, 19 + /// Visual width class (1–9). 20 + pub us_width_class: u16, 21 + /// Font embedding licensing rights. 22 + pub fs_type: u16, 23 + /// Typographic ascender. 24 + pub s_typo_ascender: i16, 25 + /// Typographic descender (typically negative). 26 + pub s_typo_descender: i16, 27 + /// Typographic line gap. 28 + pub s_typo_line_gap: i16, 29 + /// Strikeout stroke size in font design units. 30 + pub y_strikeout_size: i16, 31 + /// Strikeout stroke position above baseline. 32 + pub y_strikeout_position: i16, 33 + /// Subscript vertical offset. 34 + pub y_subscript_y_offset: i16, 35 + /// Superscript vertical offset. 36 + pub y_superscript_y_offset: i16, 37 + /// Height of lowercase 'x' (version >= 2, else 0). 38 + pub sx_height: i16, 39 + /// Height of uppercase letters (version >= 2, else 0). 40 + pub s_cap_height: i16, 41 + } 42 + 43 + impl Os2Table { 44 + /// Parse the `OS/2` table from raw bytes. 45 + pub fn parse(data: &[u8]) -> Result<Os2Table, FontError> { 46 + let r = Reader::new(data); 47 + 48 + // Minimum OS/2 table is 78 bytes (version 0). 49 + if r.len() < 78 { 50 + return Err(FontError::MalformedTable("OS/2: too short")); 51 + } 52 + 53 + let version = r.u16(0)?; 54 + let x_avg_char_width = r.i16(2)?; 55 + let us_weight_class = r.u16(4)?; 56 + let us_width_class = r.u16(6)?; 57 + let fs_type = r.u16(8)?; 58 + 59 + // Subscript/superscript offsets: offsets 14 and 18 respectively 60 + // ySubscriptXSize(10), ySubscriptYSize(12), ySubscriptXOffset(14) 61 + let y_subscript_y_offset = r.i16(16)?; 62 + // ySuperscriptXSize(18), ySuperscriptYSize(20), ySuperscriptXOffset(22) 63 + let y_superscript_y_offset = r.i16(24)?; 64 + 65 + let y_strikeout_size = r.i16(26)?; 66 + let y_strikeout_position = r.i16(28)?; 67 + 68 + // sTypoAscender is at offset 68, sTypoDescender at 70, sTypoLineGap at 72 69 + let s_typo_ascender = r.i16(68)?; 70 + let s_typo_descender = r.i16(70)?; 71 + let s_typo_line_gap = r.i16(72)?; 72 + 73 + // sxHeight and sCapHeight are available in version >= 2 at offsets 86 and 88. 74 + let (sx_height, s_cap_height) = if version >= 2 && r.len() >= 96 { 75 + (r.i16(86)?, r.i16(88)?) 76 + } else { 77 + (0, 0) 78 + }; 79 + 80 + Ok(Os2Table { 81 + version, 82 + x_avg_char_width, 83 + us_weight_class, 84 + us_width_class, 85 + fs_type, 86 + s_typo_ascender, 87 + s_typo_descender, 88 + s_typo_line_gap, 89 + y_strikeout_size, 90 + y_strikeout_position, 91 + y_subscript_y_offset, 92 + y_superscript_y_offset, 93 + sx_height, 94 + s_cap_height, 95 + }) 96 + } 97 + }