2 unstable releases
| 0.2.0 | Sep 11, 2025 |
|---|---|
| 0.1.0 | Sep 11, 2025 |
#443 in Text processing
185KB
3K
SLoC
Grida Canvas Fonts
A high-performance font parsing and selection library for the Grida design tool, providing comprehensive font selection capabilities, italic detection, variable font support, and JSON serialization for WASM communication. This library implements the Blink (Chrome) font selection model for professional-grade font handling.
Overview
This crate implements a complete font selection pipeline that follows the Blink (Chrome) font selection model. It provides reliable font selection and italic classification using structured font table data (OS/2 bits, ital axis, and slnt axis). The library is organized around the concept of "Font Selection" with a clean, modular architecture that supports both high-level UI APIs and low-level font parsing.
Key Features:
- 🎯 Smart Font Selection - Intelligent font matching following Blink (Chrome) model
- 🔤 True Italic Detection - Reliable italic classification using OS/2 bits and variable font axes
- 📊 Variable Font Support - Full support for
fvartable parsing with axes and instances - 🎨 UI-Friendly API - High-level interface designed for design tool integration
- ⚡ High Performance - Zero-copy parsing with minimal allocations
- 🌐 WASM Ready - JSON serialization for web integration
Quick Start
use fonts::{UIFontParser, UIFontFace};
// Create parser and load font data
let parser = UIFontParser::new();
let font_faces = vec![
UIFontFace {
face_id: "Inter-Regular.ttf".to_string(),
data: std::fs::read("Inter-Regular.ttf")?,
user_font_style_italic: Some(false), // User declares this is not italic
},
UIFontFace {
face_id: "Inter-Italic.ttf".to_string(),
data: std::fs::read("Inter-Italic.ttf")?,
user_font_style_italic: Some(true), // User declares this is italic
},
];
// Analyze font family
let result = parser.analyze_family(Some("Inter".to_string()), font_faces)?;
// Check italic capabilities
println!("Family: {}", result.family_name);
println!("Has italic: {}", result.italic_capability.has_italic);
println!("Strategy: {:?}", result.italic_capability.strategy);
// Find closest italic variants
let italic_matches = parser.get_italics(
Some("Inter".to_string()),
font_faces,
None, // No current style specified
Some(3), // Max 3 results
)?;
if let Some(closest) = italic_matches.first() {
println!("Closest italic: {}", closest.recipe.name);
}
Table of Contents
- Features
- Module Structure
- Quick Start
- API Reference
- Font Family Scenarios
- Implementation Status
- Examples
- Installation
- Testing
- Performance
- Dependencies
- Related Documentation
- Contributing
- License
- Changelog
Features
Core Font Parsing
- Font Metadata Extraction: Parse font names, features, and properties using
ttf-parser - Variable Font Support: Full support for
fvartable parsing with axes and instances (Level 1) - STAT Table Support: Parse Style Attributes table for advanced font information
- Font Feature Analysis: Extract and analyze OpenType features from
GSUBtable
Font Selection Pipeline
- User Font Style Declaration: Highest priority explicit user declarations
- OS/2 ITALIC Bit Detection: Reliable detection using OS/2 table bit 0
- Variable Font
italAxis: Support for variable fonts with italic axis - Variable Font
slntAxis: Support for slant-based italic in variable fonts - Scenario 3-1 Support: Variable fonts with
slntaxis and italic instances - Family Aggregation: Intelligent grouping and selection of font families
- Parser Configuration: Configurable trust levels for user declarations
- Style Matching: Find closest italic variants to current text style with axis differences
- Font Selection Pipeline: Complete font selection workflow following Blink (Chrome) model
JSON Serialization (Optional)
- WASM Communication: JSON-friendly structs for WASM transport
- Serialization Only: Focused on output serialization (no deserialization)
- Predictable Ordering: Uses
Vecinstead ofHashMapfor consistent JSON - Clean API: High-level response structures for easy consumption
Module Structure
The library is organized into focused modules following the Selection terminology:
parse: Low-level font parsing functionality using ttf-parserselection: Core font selection logic and classificationselection_italic: Italic-specific selection functionality with legacy compatibilityparse_ui: High-level UI-friendly API for font analysisserde: JSON serialization for WASM communication (optional feature)
API Layers
- High-Level UI API (
parse_ui) - Recommended for most use cases - Core Selection API (
selection) - For advanced font selection logic - Low-Level Parsing API (
parse) - For direct font file analysis - Legacy Compatibility (
selection_italic) - Backward compatibility layer
Quick Start
Font Selection API (Core)
use fonts::{FontSelectionParser, FaceRecord, FontStyle};
// Create font selection parser
let parser = FontSelectionParser::new();
// Create face records (typically from font files)
let face_records = vec![
FaceRecord {
face_id: "Inter-Regular.ttf".to_string(),
ps_name: "Inter-Regular".to_string(),
family_name: "Inter".to_string(),
subfamily_name: "Regular".to_string(),
is_variable: false,
axes: std::collections::HashMap::new(),
os2_italic_bit: false,
weight_class: 400,
width_class: 5,
user_font_style_italic: None,
// ... other fields
},
];
// Build capability map for font selection
let capability_map = parser.build_capability_map(face_records);
// Select a font face based on style requirements
let selection = parser.select_face(&capability_map, 400, 5, FontStyle::Italic);
match selection {
FontSelection::Selected { face_id, vf_recipe, .. } => {
println!("Selected face: {}", face_id);
if let Some(recipe) = vf_recipe {
println!("Variable font recipe: {:?}", recipe.axis_values);
}
}
FontSelection::Unavailable => {
println!("No suitable face found");
}
}
High-Level UI API (Recommended)
use fonts::UIFontParser;
// Create UI parser
let parser = UIFontParser::new();
// Load font family data with user-specified IDs
let font_faces = vec![
UIFontFace {
face_id: "Inter-Regular.ttf".to_string(),
data: std::fs::read("Inter-Regular.ttf")?,
user_font_style_italic: Some(false), // User declares this is not italic
},
UIFontFace {
face_id: "Inter-Italic.ttf".to_string(),
data: std::fs::read("Inter-Italic.ttf")?,
user_font_style_italic: Some(true), // User declares this is italic
},
];
// Analyze entire family
let result = parser.analyze_family(Some("Inter".to_string()), font_faces)?;
// Display family information
println!("Family: {}", result.family_name);
println!("Italic available: {}", result.italic_capability.has_italic);
println!("Strategy: {:?}", result.italic_capability.strategy);
// Show available styles
for recipe in &result.italic_capability.recipes {
println!("Style: {} - {}", recipe.name, recipe.description);
if let Some(vf_recipe) = &recipe.vf_recipe {
println!(" VF Recipe: {:?}", vf_recipe.axis_values);
}
}
// Show variable font axes
if let Some(vf_info) = &result.variable_font_info {
for axis in &vf_info.axes {
println!("Axis: {} ({}): {} to {}", axis.tag, axis.name, axis.min, axis.max);
}
}
Low-Level API (Advanced Usage)
use fonts::{Parser, FontSelectionParser};
// Parse individual font files
let font_data = std::fs::read("font.ttf")?;
let parser = Parser::new(&font_data)?;
let face_record = parser.extract_face_record("font-face-id".to_string(), None)?;
// Manual font selection
let selection_parser = FontSelectionParser::new();
let classification = selection_parser.classify_face(face_record);
// Build capability map
let faces = vec![face_record1, face_record2, face_record3];
let capability_map = selection_parser.build_capability_map(faces);
JSON Serialization (with serde feature)
use fonts::serde::*;
// Enable serde feature: cargo build --features serde
// Convert analysis results to JSON
let response = FontAnalysisResponse {
classifications: vec![FaceClassificationJson::from(classification)],
capability_map: ItalicCapabilityMapJson::from(capability_map),
fvar_data: None,
stat_data: None,
metadata: AnalysisMetadata {
face_count: 1,
has_variable_fonts: false,
timestamp: "2024-01-15T10:30:00Z".to_string(),
engine_version: "0.1.0".to_string(),
},
};
// Serialize to JSON
let json_string = serde_json::to_string(&response)?;
Installation
Add to your Cargo.toml:
[dependencies]
grida-canvas-fonts = "0.1.0"
# Optional: Enable JSON serialization for WASM
[dependencies.grida-canvas-fonts]
version = "0.1.0"
features = ["serde"]
API Reference
High-Level UI API
UIFontParser
Main high-level parser for UI consumption.
pub struct UIFontParser {
// Main analysis method
pub fn analyze_family(
&self,
family_name: Option<String>,
font_faces: Vec<UIFontFace>,
) -> Result<UIFontFamilyResult, String>,
// Style matching methods
pub fn get_italics(
&self,
family_name: Option<String>,
font_faces: Vec<UIFontFace>,
current_style: Option<CurrentTextStyle>,
max_results: Option<usize>,
) -> Result<Vec<ItalicMatch>, String>
pub fn get_romans(
&self,
family_name: Option<String>,
font_faces: Vec<UIFontFace>,
current_style: Option<CurrentTextStyle>,
max_results: Option<usize>,
) -> Result<Vec<ItalicMatch>, String>
pub fn get_faces(
&self,
face_type: FaceType,
family_name: Option<String>,
font_faces: Vec<UIFontFace>,
current_style: Option<CurrentTextStyle>,
max_results: Option<usize>,
) -> Result<Vec<ItalicMatch>, String>
}
UIFontFace
Represents a font face with user-specified ID, data, and optional style declaration.
pub struct UIFontFace {
pub face_id: String, // User-specified identifier (e.g., filename, URL, index)
pub data: Vec<u8>, // Raw font data
pub user_font_style_italic: Option<bool>, // User-declared italic style (highest priority when set)
}
CurrentTextStyle
Represents the current text style being used, with only relevant properties for matching.
pub struct CurrentTextStyle {
pub weight: Option<u16>, // Font weight (100-900)
pub width: Option<u16>, // Font width/stretch (50-200)
pub slant: Option<f32>, // Font slant angle (-90 to 90 degrees)
pub custom_axes: HashMap<String, f32>, // Additional custom axis values
}
Smart Auto-Resolution: The parser intelligently handles duplicated values between explicit properties and custom axes:
- Weight Resolution:
weightproperty takes priority overcustom_axes["wght"]if both are set - Width Resolution:
widthproperty takes priority overcustom_axes["wdth"]if both are set - Slant Resolution:
slantproperty takes priority overcustom_axes["slnt"]if both are set - Axis Seeding: Only resolves to axis values if the target font actually has that axis
- Non-VF Fallback: For non-variable fonts, only explicit properties are used (custom_axes ignored)
This allows flexible input while ensuring consistent, predictable behavior across different font types.
FaceType
Specifies the type of font faces to retrieve using the unified get_faces() interface.
pub enum FaceType {
Italic, // Retrieve italic (slanted) font variants
Roman, // Retrieve roman (upright) font variants
}
ItalicMatch
Represents a matching italic variant with distance and axis differences.
pub struct ItalicMatch {
pub recipe: UIFontItalicRecipe, // The matched italic recipe
pub distance: f32, // Distance score (lower is closer match)
pub axis_diffs: Option<Vec<AxisDiff>>, // Axis differences for variable fonts
}
AxisDiff
Represents a difference in a variable font axis.
pub struct AxisDiff {
pub tag: String, // Axis tag (e.g., "wght", "wdth", "slnt")
pub spec: f32, // Target/specified value
pub diff: f32, // Difference (spec - current)
}
UIFontFamilyResult
Complete family-level analysis result.
pub struct UIFontFamilyResult {
pub family_name: String, // Family name
pub italic_capability: UIFontItalicCapability, // Italic capabilities
pub variable_font_info: Option<UIFontVariableInfo>, // Variable font data
pub face_info: Vec<UIFontFaceInfo>, // Face-level information
}
UIFontItalicCapability
Italic capability analysis for UI consumption.
pub struct UIFontItalicCapability {
pub has_italic: bool, // Whether family has italic variants
pub has_upright: bool, // Whether family has upright variants
pub strategy: UIFontItalicStrategy, // Primary italic strategy
pub recipes: Vec<UIFontItalicRecipe>, // Available italic recipes
pub scenario: FamilyScenario, // Family scenario type
}
UIFontItalicRecipe
Italic recipe for UI consumption.
pub struct UIFontItalicRecipe {
pub name: String, // User-friendly name (e.g., "Bold Italic")
pub description: String, // User-friendly description
pub is_italic: bool, // Whether this recipe produces italic text
pub face_id: String, // Face ID to use for this recipe
pub vf_recipe: Option<VfRecipe>, // Variable font recipe (if applicable)
pub weight_range: (u16, u16), // Weight range (min, max)
pub stretch_range: (u16, u16), // Stretch range (min, max)
}
Low-Level API (Advanced)
Parser
Low-level font parser backed by ttf-parser.
pub struct Parser<'a> {
// Font parsing methods
pub fn new(data: &'a [u8]) -> Result<Self, ttf_parser::FaceParsingError>
pub fn extract_face_record(&self, face_id: String, user_font_style_italic: Option<bool>) -> Result<FaceRecord, String>
pub fn fvar(&self) -> FvarData
pub fn stat(&self) -> StatData
pub fn ffeatures(&self) -> Vec<FontFeature>
}
FontSelectionParser
Core font selection pipeline.
pub struct FontSelectionParser {
pub config: ParserConfig,
}
impl FontSelectionParser {
pub fn new() -> Self
pub fn classify_face(&self, face_record: FaceRecord) -> ClassifiedFace
pub fn build_capability_map(&self, faces: Vec<FaceRecord>) -> FontSelectionCapabilityMap
pub fn select_face(&self, capability_map: &FontSelectionCapabilityMap, weight: u16, stretch: u16, style: FontStyle) -> FontSelection
}
FaceClassification
Result of font classification for a single face.
pub struct FaceClassification {
pub font_style: FontStyle, // Normal or Italic
pub vf_recipe: Option<VfRecipe>, // Variable font recipe
pub weight_key: u16, // Weight for family aggregation
pub stretch_key: u16, // Stretch for family aggregation
pub is_variable: bool, // Whether this is a variable font
pub instance_info: Option<InstanceInfo>, // Instance information (Scenario 3-1)
// Legacy compatibility
pub fn italic_kind(&self) -> FontStyle // Backward compatibility getter
}
FontSelectionCapabilityMap
Family-level font selection capability mapping.
pub struct FontSelectionCapabilityMap {
pub upright_slots: HashMap<(u16, u16), FaceOrVfWithRecipe>, // (weight, stretch) -> face
pub italic_slots: HashMap<(u16, u16), FaceOrVfWithRecipe>, // (weight, stretch) -> face
pub scenario: FamilyScenario, // Family scenario type
}
JSON Types (with serde feature)
FontAnalysisResponse
Complete font analysis results for WASM consumption.
pub struct FontAnalysisResponse {
pub classifications: Vec<FaceClassificationJson>,
pub capability_map: ItalicCapabilityMapJson,
pub fvar_data: Option<FvarDataJson>,
pub stat_data: Option<StatDataJson>,
pub metadata: AnalysisMetadata,
}
Utility Functions
pub mod utils {
pub fn success_response<T: serde::Serialize>(data: T) -> SuccessResponse<T>
pub fn error_response(code: &str, message: &str) -> ErrorResponseWrapper
pub fn error_response_with_details(code: &str, message: &str, details: &str) -> ErrorResponseWrapper
}
Font Family Scenarios
The library handles various font family configurations:
Scenario 1: One Static Font
- Single static font (either normal or italic-only)
- Example:
Allerta-Regular.ttf,Molle-Italic.ttf
Scenario 2: Many Static Fonts
- Multiple static fonts with italic/oblique variants
- Example: PT Serif family (
PTSerif-Regular.ttf,PTSerif-Italic.ttf, etc.)
Scenario 3: One Variable Font with ital Axis
- Single variable font providing both upright and italic
- Uses
italaxis (0 = normal, 1 = italic)
Scenario 3-1: Variable Font with slnt Axis & Italic Instances
- Variable font with
slntaxis and explicit italic instances - Examples: Recursive, Roboto Flex
- Requires italic-named instances in name table
Scenario 4: Two Variable Fonts
- Separate Roman VF and Italic VF
- Examples: Inter family, Noto Sans family
Implementation Status
✅ Current Features (Level 1)
- User Font Style Declaration - Highest priority explicit user declarations
- OS/2 ITALIC Bit Detection - Reliable detection using OS/2 table bit 0
- Variable Font
italAxis - Support for variable fonts with italic axis - Variable Font
slntAxis - Support for slant-based italic in variable fonts - Scenario 3-1 Support - Variable fonts with
slntaxis and italic instances - Family Aggregation - Intelligent grouping and selection of font families
- Style Matching - Find closest italic variants with axis differences
- Parser Configuration - Configurable trust levels for user declarations
🔄 Future Features (Level 2+)
- STAT Table Analysis - Advanced font metadata parsing
- Advanced
fvar.instancesAnalysis - Complex edge cases and malformed fonts - Advanced Name Table Parsing - All name table entries analysis
- Complex Edge Case Handling - Conflicting metadata, malformed fonts
- CJK/Mixed-Script Fallback - Advanced fallback strategies
- Advanced Validation - Comprehensive diagnostics and validation
Examples
Basic Font Analysis
use fonts::{UIFontParser, UIFontFace};
let parser = UIFontParser::new();
let font_faces = vec![UIFontFace {
face_id: "Inter-VariableFont.ttf".to_string(),
data: std::fs::read("Inter-VariableFont.ttf")?,
user_font_style_italic: None, // Let the parser analyze the font metadata
}];
let result = parser.analyze_family(None, font_faces)?;
println!("Family: {}", result.family_name);
println!("Italic available: {}", result.italic_capability.has_italic);
println!("Strategy: {:?}", result.italic_capability.strategy);
Family Analysis
let font_faces = vec![
UIFontFace {
face_id: "Inter-Regular.ttf".to_string(),
data: std::fs::read("Inter-Regular.ttf")?,
user_font_style_italic: Some(false), // User declares this is not italic
},
UIFontFace {
face_id: "Inter-Italic.ttf".to_string(),
data: std::fs::read("Inter-Italic.ttf")?,
user_font_style_italic: Some(true), // User declares this is italic
},
];
let result = parser.analyze_family(Some("Inter".to_string()), font_faces)?;
println!("Family scenario: {:?}", result.italic_capability.scenario);
println!("Upright available: {}", result.italic_capability.has_upright);
println!("Italic available: {}", result.italic_capability.has_italic);
Style Matching
Find the closest italic or roman variants to your current text style:
use fonts::{UIFontParser, UIFontFace, CurrentTextStyle};
use std::collections::HashMap;
let parser = UIFontParser::new();
let font_faces = vec![
UIFontFace {
face_id: "Inter-Regular.ttf".to_string(),
data: std::fs::read("Inter-Regular.ttf")?,
user_font_style_italic: Some(false),
},
UIFontFace {
face_id: "Inter-Italic.ttf".to_string(),
data: std::fs::read("Inter-Italic.ttf")?,
user_font_style_italic: Some(true),
},
];
// Define current text style with smart auto-resolution
let mut custom_axes = HashMap::new();
custom_axes.insert("wght".to_string(), 500.0); // Will be overridden by weight: 400
custom_axes.insert("wdth".to_string(), 120.0); // Will be overridden by width: 100
custom_axes.insert("slnt".to_string(), 5.0); // Will be used since slant is None
let current_style = CurrentTextStyle {
weight: Some(400), // Takes priority over custom_axes["wght"]
width: Some(100), // Takes priority over custom_axes["wdth"]
slant: None, // Falls back to custom_axes["slnt"] if font has slnt axis
custom_axes,
};
// Find closest italic variants
let italic_matches = parser.get_italics(
Some("Inter".to_string()),
font_faces.clone(),
Some(current_style.clone()),
Some(3), // Max 3 results
)?;
// Find closest roman (non-italic) variants
let roman_matches = parser.get_romans(
Some("Inter".to_string()),
font_faces,
Some(current_style),
Some(3), // Max 3 results
)?;
if let Some(closest_italic) = italic_matches.first() {
println!("Closest italic: {}", closest_italic.recipe.name);
println!("Distance: {}", closest_italic.distance);
// Check axis differences for variable fonts
if let Some(axis_diffs) = &closest_italic.axis_diffs {
for diff in axis_diffs {
println!("Axis {}: spec={} (diff: {})",
diff.tag, diff.spec, diff.diff);
}
}
}
if let Some(closest_roman) = roman_matches.first() {
println!("Closest roman: {}", closest_roman.recipe.name);
println!("Distance: {}", closest_roman.distance);
}
Unified Interface
For easier toggling between italic and roman variants, use the unified get_faces() interface:
use fonts::{UIFontParser, UIFontFace, CurrentTextStyle, FaceType};
use std::collections::HashMap;
let parser = UIFontParser::new();
let font_faces = vec![
UIFontFace {
face_id: "Inter-Regular.ttf".to_string(),
data: std::fs::read("Inter-Regular.ttf")?,
user_font_style_italic: Some(false),
},
UIFontFace {
face_id: "Inter-Italic.ttf".to_string(),
data: std::fs::read("Inter-Italic.ttf")?,
user_font_style_italic: Some(true),
},
];
let current_style = CurrentTextStyle {
weight: Some(400),
width: Some(100),
slant: None,
custom_axes: HashMap::new(),
};
// Toggle to italic
let italic_matches = parser.get_faces(
FaceType::Italic,
Some("Inter".to_string()),
font_faces.clone(),
Some(current_style.clone()),
Some(1),
)?;
// Toggle to roman
let roman_matches = parser.get_faces(
FaceType::Roman,
Some("Inter".to_string()),
font_faces,
Some(current_style),
Some(1),
)?;
// Easy toggling in UI code
let toggle_to_italic = parser.get_faces(
FaceType::Italic,
Some("Inter".to_string()),
font_faces,
Some(current_style),
Some(1),
)?;
WASM Communication
// Enable serde feature
use fonts::serde::*;
// Create analysis response
let response = FontAnalysisResponse {
classifications: classifications.into_iter().map(|c| c.into()).collect(),
capability_map: capability_map.into(),
fvar_data: fvar_data.map(|f| f.into()),
stat_data: stat_data.map(|s| s.into()),
metadata: AnalysisMetadata {
face_count: faces.len(),
has_variable_fonts: faces.iter().any(|f| f.is_variable),
timestamp: chrono::Utc::now().to_rfc3339(),
engine_version: env!("CARGO_PKG_VERSION").to_string(),
},
};
// Serialize for WASM
let json = serde_json::to_string(&response)?;
Performance
- Zero-copy parsing: Uses
ttf-parserfor efficient font data access - Minimal allocations: Optimized data structures and conversion paths
- Fast classification: O(1) OS/2 bit checks, O(n) axis iteration
- Efficient aggregation: HashMap-based family grouping
Contributing
- Follow the Level 1 specification in the working group documentation
- Add comprehensive tests for new features
- Update documentation for API changes
- Ensure all tests pass:
cargo test --all-targets --all-features
License
This project is part of the Grida design tool ecosystem. See the main project license for details.
For technical details, testing information, dependencies, and changelog, see AGENTS.md.
Dependencies
~0.8–1.1MB
~25K SLoC