use std::collections::HashMap;
use std::sync::{ Arc, Mutex };
use rustybuzz::ttf_parser::{ Face, GlyphId, OutlineBuilder, RasterImageFormat };
use tiny_skia::{ FillRule, Mask, Path, PathBuilder, Transform };
#[ derive( Debug ) ]
pub struct ColorGlyph
{
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
pub left: f32,
pub bottom: f32,
pub em_width: f32,
pub em_height: f32,
}
#[ derive( Copy, Clone, PartialEq, Debug, Default ) ]
pub struct OutlineBounds
{
pub xmin: f32,
pub ymin: f32,
pub width: f32,
pub height: f32,
}
impl OutlineBounds
{
fn scale( &self, scale: f32 ) -> Self
{
Self
{
xmin: self.xmin * scale,
ymin: self.ymin * scale,
width: self.width * scale,
height: self.height * scale,
}
}
}
#[ derive( Copy, Clone, PartialEq, Debug, Default ) ]
pub struct Metrics
{
pub xmin: i32,
pub ymin: i32,
pub width: usize,
pub height: usize,
pub advance_width: f32,
pub advance_height: f32,
pub bounds: OutlineBounds,
}
#[ derive( Copy, Clone, PartialEq, Debug, Default ) ]
pub struct LineMetrics
{
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
pub new_line_size: f32,
}
impl LineMetrics
{
fn scale( &self, scale: f32 ) -> Self
{
Self
{
ascent: self.ascent * scale,
descent: self.descent * scale,
line_gap: self.line_gap * scale,
new_line_size: self.new_line_size * scale,
}
}
}
struct Outline
{
path: Option<Path>,
bounds: OutlineBounds,
advance_width: f32,
advance_height: f32,
}
pub struct FontFace
{
data: Arc<Vec<u8>>,
index: u32,
units_per_em: f32,
line: LineMetrics,
outlines: Mutex<HashMap<u16, Arc<Outline>>>,
indices: Mutex<HashMap<char, u16>>,
colors: Mutex<HashMap<u16, Option<Arc<ColorGlyph>>>>,
}
impl std::fmt::Debug for FontFace
{
fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::fmt::Result
{
f.debug_struct( "FontFace" )
.field( "index", &self.index )
.field( "units_per_em", &self.units_per_em )
.field( "line", &self.line )
.finish_non_exhaustive()
}
}
impl FontFace
{
pub fn from_bytes( data: Arc<Vec<u8>>, index: u32 ) -> Option<Self>
{
let ( units_per_em, line ) =
{
let face = Face::parse( data.as_slice(), index ).ok()?;
let upem = face.units_per_em() as f32;
if upem <= 0.0 { return None; }
let ascent = face.ascender() as f32;
let descent = face.descender() as f32;
let line_gap = face.line_gap() as f32;
let line = LineMetrics
{
ascent,
descent,
line_gap,
new_line_size: ascent - descent + line_gap,
};
( upem, line )
};
Some( Self
{
data,
index,
units_per_em,
line,
outlines: Mutex::new( HashMap::new() ),
indices: Mutex::new( HashMap::new() ),
colors: Mutex::new( HashMap::new() ),
} )
}
pub fn units_per_em( &self ) -> f32
{
self.units_per_em
}
pub fn data( &self ) -> &Arc<Vec<u8>>
{
&self.data
}
pub fn face_index( &self ) -> u32
{
self.index
}
pub fn scale_factor( &self, px: f32 ) -> f32
{
px / self.units_per_em
}
pub fn lookup_glyph_index( &self, ch: char ) -> u16
{
if let Some( id ) = self.indices.lock().ok().and_then( |m| m.get( &ch ).copied() )
{
return id;
}
let id = self.face()
.and_then( |f| f.glyph_index( ch ) )
.map( |g| g.0 )
.unwrap_or( 0 );
if let Ok( mut m ) = self.indices.lock() { m.insert( ch, id ); }
id
}
pub fn horizontal_line_metrics( &self, px: f32 ) -> Option<LineMetrics>
{
Some( self.line.scale( self.scale_factor( px ) ) )
}
pub fn metrics( &self, ch: char, px: f32 ) -> Metrics
{
self.metrics_indexed( self.lookup_glyph_index( ch ), px )
}
pub fn metrics_indexed( &self, id: u16, px: f32 ) -> Metrics
{
if px <= 0.0 { return Metrics::default(); }
self.raw_metrics( &self.outline( id ), self.scale_factor( px ) ).0
}
pub fn rasterize_indexed( &self, id: u16, px: f32 ) -> ( Metrics, Vec<u8> )
{
if px <= 0.0 { return ( Metrics::default(), Vec::new() ); }
let outline = self.outline( id );
let scale = self.scale_factor( px );
let ( metrics, ox, oy ) = self.raw_metrics( &outline, scale );
if metrics.width == 0 || metrics.height == 0
{
return ( metrics, Vec::new() );
}
let blank = || vec![ 0u8; metrics.width * metrics.height ];
let Some( path ) = outline.path.as_ref() else { return ( metrics, blank() ) };
let Some( mut mask ) = Mask::new( metrics.width as u32, metrics.height as u32 ) else { return ( metrics, blank() ) };
let ts = Transform::from_row(
scale,
0.0,
0.0,
-scale,
ox - metrics.bounds.xmin,
oy + metrics.bounds.ymin + metrics.bounds.height,
);
mask.fill_path( path, FillRule::Winding, true, ts );
( metrics, mask.take() )
}
pub fn color_glyph( &self, id: u16 ) -> Option<Arc<ColorGlyph>>
{
if let Some( hit ) = self.colors.lock().ok().and_then( |c| c.get( &id ).cloned() ) { return hit; }
let glyph = self.decode_color_glyph( id ).map( Arc::new );
if let Ok( mut c ) = self.colors.lock() { c.insert( id, glyph.clone() ); }
glyph
}
fn decode_color_glyph( &self, id: u16 ) -> Option<ColorGlyph>
{
let face = self.face()?;
let raster = face.glyph_raster_image( GlyphId( id ), u16::MAX )?;
if !matches!( raster.format, RasterImageFormat::PNG ) || raster.pixels_per_em == 0 { return None; }
let decoded = image::load_from_memory_with_format( raster.data, image::ImageFormat::Png ).ok()?.to_rgba8();
let ( width, height ) = decoded.dimensions();
let ppem = raster.pixels_per_em as f32;
Some( ColorGlyph
{
rgba: decoded.into_raw(),
width,
height,
left: raster.x as f32 / ppem,
bottom: raster.y as f32 / ppem,
em_width: width as f32 / ppem,
em_height: height as f32 / ppem,
} )
}
fn face( &self ) -> Option<Face<'_>>
{
Face::parse( self.data.as_slice(), self.index ).ok()
}
fn raw_metrics( &self, outline: &Outline, scale: f32 ) -> ( Metrics, f32, f32 )
{
let bounds = outline.bounds.scale( scale );
let mut ox = bounds.xmin.fract();
let mut oy = ( 1.0 - bounds.height.fract() - bounds.ymin.fract() ).fract();
if ox < 0.0 { ox += 1.0; }
if oy < 0.0 { oy += 1.0; }
let metrics = Metrics
{
xmin: bounds.xmin.floor() as i32,
ymin: bounds.ymin.floor() as i32,
width: ( bounds.width + ox ).ceil().max( 0.0 ) as usize,
height: ( bounds.height + oy ).ceil().max( 0.0 ) as usize,
advance_width: scale * outline.advance_width,
advance_height: scale * outline.advance_height,
bounds,
};
( metrics, ox, oy )
}
fn outline( &self, id: u16 ) -> Arc<Outline>
{
if let Some( o ) = self.outlines.lock().ok().and_then( |m| m.get( &id ).cloned() )
{
return o;
}
let built = Arc::new( self.build_outline( id ) );
if let Ok( mut m ) = self.outlines.lock() { m.insert( id, Arc::clone( &built ) ); }
built
}
fn build_outline( &self, id: u16 ) -> Outline
{
let empty = || Outline
{
path: None,
bounds: OutlineBounds::default(),
advance_width: 0.0,
advance_height: 0.0,
};
let Some( face ) = self.face() else { return empty() };
let gid = GlyphId( id );
let advance_width = face.glyph_hor_advance( gid ).unwrap_or( 0 ) as f32;
let advance_height = face.glyph_ver_advance( gid ).unwrap_or( 0 ) as f32;
let mut sink = Outliner::new();
let _ = face.outline_glyph( gid, &mut sink );
let bounds = sink.aabb.finish().unwrap_or_default();
let path = sink.builder.finish();
Outline { path, bounds, advance_width, advance_height }
}
}
struct Aabb
{
xmin: f32,
ymin: f32,
xmax: f32,
ymax: f32,
seen: bool,
}
impl Aabb
{
fn new() -> Self
{
Self { xmin: f32::MAX, ymin: f32::MAX, xmax: f32::MIN, ymax: f32::MIN, seen: false }
}
fn add( &mut self, x: f32, y: f32 )
{
self.add_x( x );
self.add_y( y );
self.seen = true;
}
fn add_x( &mut self, x: f32 )
{
self.xmin = self.xmin.min( x );
self.xmax = self.xmax.max( x );
}
fn add_y( &mut self, y: f32 )
{
self.ymin = self.ymin.min( y );
self.ymax = self.ymax.max( y );
}
fn finish( &self ) -> Option<OutlineBounds>
{
self.seen.then_some( OutlineBounds
{
xmin: self.xmin,
ymin: self.ymin,
width: self.xmax - self.xmin,
height: self.ymax - self.ymin,
} )
}
}
struct Outliner
{
builder: PathBuilder,
aabb: Aabb,
from: ( f32, f32 ),
}
impl Outliner
{
fn new() -> Self
{
Self { builder: PathBuilder::new(), aabb: Aabb::new(), from: ( 0.0, 0.0 ) }
}
}
impl OutlineBuilder for Outliner
{
fn move_to( &mut self, x: f32, y: f32 )
{
self.builder.move_to( x, y );
self.aabb.add( x, y );
self.from = ( x, y );
}
fn line_to( &mut self, x: f32, y: f32 )
{
self.builder.line_to( x, y );
self.aabb.add( x, y );
self.from = ( x, y );
}
fn quad_to( &mut self, x1: f32, y1: f32, x: f32, y: f32 )
{
self.builder.quad_to( x1, y1, x, y );
self.aabb.add( x, y );
let ( x0, y0 ) = self.from;
if let Some( v ) = quad_extremum( x0, x1, x ) { self.aabb.add_x( v ); }
if let Some( v ) = quad_extremum( y0, y1, y ) { self.aabb.add_y( v ); }
self.from = ( x, y );
}
fn curve_to( &mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32 )
{
self.builder.cubic_to( x1, y1, x2, y2, x, y );
self.aabb.add( x, y );
let ( x0, y0 ) = self.from;
for v in cubic_extrema( x0, x1, x2, x ) { self.aabb.add_x( v ); }
for v in cubic_extrema( y0, y1, y2, y ) { self.aabb.add_y( v ); }
self.from = ( x, y );
}
fn close( &mut self )
{
self.builder.close();
}
}
fn quad_extremum( p0: f32, p1: f32, p2: f32 ) -> Option<f32>
{
let denom = p0 - 2.0 * p1 + p2;
if denom.abs() <= f32::EPSILON { return None; }
let t = ( p0 - p1 ) / denom;
if !( t > 0.0 && t < 1.0 ) { return None; }
let mt = 1.0 - t;
Some( mt * mt * p0 + 2.0 * mt * t * p1 + t * t * p2 )
}
fn cubic_extrema( p0: f32, p1: f32, p2: f32, p3: f32 ) -> Vec<f32>
{
let a = -p0 + 3.0 * p1 - 3.0 * p2 + p3;
let b = 2.0 * ( p0 - 2.0 * p1 + p2 );
let c = p1 - p0;
let mut roots: Vec<f32> = Vec::new();
if a.abs() <= f32::EPSILON
{
if b.abs() > f32::EPSILON { roots.push( -c / b ); }
}
else
{
let disc = b * b - 4.0 * a * c;
if disc >= 0.0
{
let root = disc.sqrt();
roots.push( ( -b + root ) / ( 2.0 * a ) );
roots.push( ( -b - root ) / ( 2.0 * a ) );
}
}
roots
.into_iter()
.filter( |t| *t > 0.0 && *t < 1.0 )
.map( |t|
{
let mt = 1.0 - t;
mt * mt * mt * p0
+ 3.0 * mt * mt * t * p1
+ 3.0 * mt * t * t * p2
+ t * t * t * p3
} )
.collect()
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn quadratic_turning_point_is_the_apex()
{
assert_eq!( quad_extremum( 0.0, 10.0, 0.0 ), Some( 5.0 ) );
}
#[ test ]
fn monotone_quadratic_has_no_interior_turning_point()
{
assert_eq!( quad_extremum( 0.0, 5.0, 10.0 ), None );
}
#[ test ]
fn cubic_turning_points_bracket_the_control_hull()
{
let v = cubic_extrema( 0.0, 10.0, -10.0, 0.0 );
assert_eq!( v.len(), 2 );
for x in v { assert!( x.abs() < 10.0 ); }
}
#[ test ]
fn monotone_cubic_has_no_interior_turning_point()
{
assert!( cubic_extrema( 0.0, 1.0, 2.0, 3.0 ).is_empty() );
}
#[ test ]
fn line_metrics_scale_together()
{
let m = LineMetrics { ascent: 800.0, descent: -200.0, line_gap: 0.0, new_line_size: 1000.0 };
let s = m.scale( 0.02 );
assert_eq!( s.ascent, 16.0 );
assert_eq!( s.descent, -4.0 );
assert_eq!( s.new_line_size, 20.0 );
}
}