mod bezier_flat;
pub use bezier_flat::{shape_and_flatten_text, shape_and_flatten_text_via_agg};
use std::sync::Arc;
use agg_rust::basics::{
is_end_poly, is_move_to, is_stop, VertexSource, PATH_CMD_LINE_TO, PATH_FLAGS_NONE,
};
use agg_rust::conv_contour::ConvContour;
use agg_rust::conv_curve::ConvCurve;
use agg_rust::conv_transform::ConvTransform;
use agg_rust::path_storage::PathStorage;
use agg_rust::trans_affine::TransAffine;
#[derive(Debug, Clone, Copy, Default)]
pub struct TextMetrics {
pub width: f64,
pub ascent: f64,
pub descent: f64,
pub line_height: f64,
}
impl TextMetrics {
pub fn centered_baseline_y(&self, height: f64) -> f64 {
(height - (self.ascent - self.descent)) * 0.5
}
}
pub struct Font {
pub(crate) data: Arc<Vec<u8>>,
index: u32,
units_per_em: u16,
ascender: i16,
descender: i16,
line_gap: i16,
pub(crate) fallback: Option<Arc<Font>>,
}
impl Font {
pub fn from_bytes(data: Vec<u8>) -> Result<Self, &'static str> {
let face = ttf_parser::Face::parse(&data, 0).map_err(|_| "failed to parse font")?;
Ok(Self {
units_per_em: face.units_per_em(),
ascender: face.ascender(),
descender: face.descender(),
line_gap: face.line_gap(),
data: Arc::new(data),
index: 0,
fallback: None,
})
}
pub fn from_slice(data: &[u8]) -> Result<Self, &'static str> {
Self::from_bytes(data.to_vec())
}
pub fn with_fallback(mut self, fallback: Arc<Font>) -> Self {
self.fallback = Some(fallback);
self
}
pub fn units_per_em(&self) -> u16 {
self.units_per_em
}
pub fn ascender_px(&self, size: f64) -> f64 {
self.ascender as f64 * size / self.units_per_em as f64
}
pub fn descender_px(&self, size: f64) -> f64 {
self.descender.unsigned_abs() as f64 * size / self.units_per_em as f64
}
pub fn line_height_px(&self, size: f64) -> f64 {
let total = (self.ascender - self.descender + self.line_gap) as f64;
total * size / self.units_per_em as f64
}
pub(crate) fn with_rb_face<F, R>(&self, f: F) -> R
where
F: FnOnce(&rustybuzz::Face<'_>) -> R,
{
let face = rustybuzz::Face::from_slice(&self.data, self.index)
.expect("font was validated at construction");
f(&face)
}
pub(crate) fn with_ttf_face<F, R>(&self, f: F) -> R
where
F: FnOnce(&ttf_parser::Face<'_>) -> R,
{
let face = ttf_parser::Face::parse(&self.data, self.index)
.expect("font was validated at construction");
f(&face)
}
}
pub(crate) struct GlyphPathBuilder {
pub path: PathStorage,
ox: f64,
oy: f64,
scale: f64,
width_scale: f64,
italic_shear: f64,
pub has_outline: bool,
}
impl GlyphPathBuilder {
pub fn new(ox: f64, oy: f64, scale: f64) -> Self {
Self {
path: PathStorage::new(),
ox,
oy,
scale,
width_scale: 1.0,
italic_shear: 0.0,
has_outline: false,
}
}
#[allow(dead_code)]
pub fn with_style(mut self, width: f64, italic: f64) -> Self {
self.width_scale = width;
self.italic_shear = italic;
self
}
#[inline]
fn x(&self, v: f32, y_raw: f32) -> f64 {
let base_x = self.ox + v as f64 * self.scale * self.width_scale;
let shear = y_raw as f64 * self.scale * self.italic_shear;
base_x + shear
}
#[inline]
fn y(&self, v: f32) -> f64 {
self.oy + v as f64 * self.scale
}
}
impl ttf_parser::OutlineBuilder for GlyphPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to(self.x(x, y), self.y(y));
self.has_outline = true;
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to(self.x(x, y), self.y(y));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.path
.curve3(self.x(x1, y1), self.y(y1), self.x(x, y), self.y(y));
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.path.curve4(
self.x(x1, y1),
self.y(y1),
self.x(x2, y2),
self.y(y2),
self.x(x, y),
self.y(y),
);
}
fn close(&mut self) {
self.path.close_polygon(PATH_FLAGS_NONE);
}
}
fn apply_faux_weight(path: PathStorage, weight_px: f64) -> PathStorage {
if weight_px.abs() < 1e-4 {
return path;
}
let mut src = path;
let mut curves = ConvCurve::new(&mut src);
let zoom_in = TransAffine::new_scaling(1.0, 100.0);
let mut zoomed_in = ConvTransform::new(&mut curves, zoom_in);
let mut contour = ConvContour::new(&mut zoomed_in);
contour.set_auto_detect_orientation(false);
contour.set_width(weight_px);
let zoom_out = TransAffine::new_scaling(1.0, 1.0 / 100.0);
let mut out = ConvTransform::new(&mut contour, zoom_out);
let mut result = PathStorage::new();
out.rewind(0);
loop {
let (mut vx, mut vy) = (0.0_f64, 0.0_f64);
let cmd = out.vertex(&mut vx, &mut vy);
if is_stop(cmd) {
break;
}
if is_move_to(cmd) {
result.move_to(vx, vy);
} else if cmd == PATH_CMD_LINE_TO {
result.line_to(vx, vy);
} else if is_end_poly(cmd) {
result.close_polygon(PATH_FLAGS_NONE);
}
}
result
}
pub(crate) fn shape_text(
font: &Font,
text: &str,
size: f64,
x: f64,
y: f64,
) -> (Vec<PathStorage>, f64) {
let shaped = shape_glyphs(font, text, size);
let width_scale = crate::font_settings::current_width();
let italic_shear = crate::font_settings::current_faux_italic() / 3.0;
let hint_y = crate::font_settings::hinting_enabled();
let interval_em = crate::font_settings::current_interval();
let interval_px = interval_em * size;
let faux_weight = crate::font_settings::current_faux_weight();
let weight_px = if faux_weight.abs() < 0.05 {
0.0 } else {
-faux_weight * size / 15.0
};
let mut paths = Vec::new();
let mut pen_x = x;
let mut total_advance = 0.0;
for g in &shaped {
let gx = pen_x + g.x_offset;
let gy_unsnapped = y + g.y_offset;
let gy = if hint_y {
(gy_unsnapped + 0.5).floor()
} else {
gy_unsnapped
};
let render_font = g.fallback_font.as_deref().unwrap_or(font);
let scale = size / render_font.units_per_em() as f64;
let mut builder =
GlyphPathBuilder::new(gx, gy, scale).with_style(width_scale, italic_shear);
let has_outline = render_font.with_ttf_face(|face| {
face.outline_glyph(ttf_parser::GlyphId(g.glyph_id), &mut builder)
.is_some()
});
if has_outline && builder.has_outline {
let path = apply_faux_weight(builder.path, weight_px);
paths.push(path);
}
let advance = g.x_advance + interval_px;
pen_x += advance;
total_advance += advance;
}
(paths, total_advance)
}
#[derive(Clone)]
pub struct ShapedGlyph {
pub glyph_id: u16,
pub x_advance: f64,
pub x_offset: f64,
pub y_offset: f64,
pub fallback_font: Option<Arc<Font>>,
}
pub fn shape_glyphs(font: &Font, text: &str, size: f64) -> Vec<ShapedGlyph> {
let font_key = Arc::as_ptr(&font.data) as usize;
let size_key = size.to_bits();
SHAPE_CACHE.with(|cache| {
{
let c = cache.borrow();
if let Some(cached) = c.get(&(font_key, text.to_owned(), size_key)) {
return cached.clone();
}
}
let scale = size / font.units_per_em() as f64;
let glyphs = font.with_rb_face(|face| {
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(text);
let output = rustybuzz::shape(face, &[], buffer);
output
.glyph_infos()
.iter()
.zip(output.glyph_positions().iter())
.map(|(info, pos)| {
let glyph_id = info.glyph_id as u16;
let x_advance = pos.x_advance as f64 * scale;
let x_offset = pos.x_offset as f64 * scale;
let y_offset = pos.y_offset as f64 * scale;
if glyph_id == 0 {
let byte_off = info.cluster as usize;
if let Some(ch) = text.get(byte_off..).and_then(|s| s.chars().next()) {
let mut cur_fb = font.fallback.as_ref();
while let Some(fb) = cur_fb {
let fb_id = fb
.with_ttf_face(|f| f.glyph_index(ch).map(|g| g.0).unwrap_or(0));
if fb_id != 0 {
let fb_scale = size / fb.units_per_em() as f64;
let fb_adv = fb.with_ttf_face(|f| {
f.glyph_hor_advance(ttf_parser::GlyphId(fb_id))
.map(|a| a as f64 * fb_scale)
.unwrap_or(0.0)
});
return ShapedGlyph {
glyph_id: fb_id,
x_advance: fb_adv,
x_offset,
y_offset,
fallback_font: Some(Arc::clone(fb)),
};
}
cur_fb = fb.fallback.as_ref();
}
}
}
ShapedGlyph {
glyph_id,
x_advance,
x_offset,
y_offset,
fallback_font: None,
}
})
.collect::<Vec<_>>()
});
cache
.borrow_mut()
.insert((font_key, text.to_owned(), size_key), glyphs.clone());
glyphs
})
}
pub fn flatten_glyph_at_origin(
font: &Font,
glyph_id: u16,
size: f64,
) -> Option<Vec<Vec<[f32; 2]>>> {
let scale = size / font.units_per_em() as f64;
font.with_rb_face(|face| {
let gid = ttf_parser::GlyphId(glyph_id);
let mut builder = GlyphPathBuilder::new(0.0, 0.0, scale);
let has_outline = face.outline_glyph(gid, &mut builder).is_some();
if !has_outline || !builder.has_outline {
return None;
}
let mut curves = ConvCurve::new(builder.path);
curves.rewind(0);
let mut contours: Vec<Vec<[f32; 2]>> = Vec::new();
let mut current: Vec<[f32; 2]> = Vec::new();
loop {
let (mut cx, mut cy) = (0.0_f64, 0.0_f64);
let cmd = curves.vertex(&mut cx, &mut cy);
if is_stop(cmd) {
break;
}
if is_move_to(cmd) {
if current.len() >= 3 {
contours.push(std::mem::take(&mut current));
} else {
current.clear();
}
current.push([cx as f32, cy as f32]);
} else if cmd == PATH_CMD_LINE_TO {
current.push([cx as f32, cy as f32]);
} else if is_end_poly(cmd) {
if current.len() >= 3 {
contours.push(std::mem::take(&mut current));
} else {
current.clear();
}
}
}
if current.len() >= 3 {
contours.push(current);
}
if contours.is_empty() {
None
} else {
Some(contours)
}
})
}
pub fn measure_text_metrics(font: &Font, text: &str, size: f64) -> TextMetrics {
TextMetrics {
width: measure_advance(font, text, size),
ascent: font.ascender_px(size),
descent: font.descender_px(size),
line_height: font.line_height_px(size),
}
}
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static SHAPE_CACHE: RefCell<HashMap<(usize, String, u64), Vec<ShapedGlyph>>> =
RefCell::new(HashMap::new());
}
pub fn measure_advance(font: &Font, text: &str, size: f64) -> f64 {
let shaped = shape_glyphs(font, text, size);
let interval_px = crate::font_settings::current_interval() * size;
shaped.iter().map(|g| g.x_advance + interval_px).sum()
}
#[cfg(test)]
mod tests;