use crate::core::{Path, PathCode};
use ttf_parser::OutlineBuilder;
use crate::text::font::{FontSource, horizontal_kerning};
struct PathOutlineBuilder {
vertices: Vec<[f64; 2]>,
codes: Vec<PathCode>,
scale: f64,
offset_x: f64,
offset_y: f64,
}
impl PathOutlineBuilder {
fn map(&self, x: f32, y: f64) -> [f64; 2] {
[
self.offset_x + f64::from(x) * self.scale,
self.offset_y + y * self.scale,
]
}
}
impl OutlineBuilder for PathOutlineBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.vertices.push(self.map(x, f64::from(y)));
self.codes.push(PathCode::MoveTo);
}
fn line_to(&mut self, x: f32, y: f32) {
self.vertices.push(self.map(x, f64::from(y)));
self.codes.push(PathCode::LineTo);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.vertices.push(self.map(x1, f64::from(y1)));
self.codes.push(PathCode::CurveTo3);
self.vertices.push(self.map(x, f64::from(y)));
self.codes.push(PathCode::CurveTo3);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.vertices.push(self.map(x1, f64::from(y1)));
self.codes.push(PathCode::CurveTo4);
self.vertices.push(self.map(x2, f64::from(y2)));
self.codes.push(PathCode::CurveTo4);
self.vertices.push(self.map(x, f64::from(y)));
self.codes.push(PathCode::CurveTo4);
}
fn close(&mut self) {
self.vertices.push([self.offset_x, self.offset_y]);
self.codes.push(PathCode::ClosePoly);
}
}
impl FontSource {
#[must_use]
pub fn text_to_path(&self, text: &str, font_size_px: f64, origin: [f64; 2]) -> Path {
let empty = || Path::new(Vec::new(), Some(Vec::new()));
let Some(face) = self.primary_face() else {
return empty();
};
let units_per_em = f64::from(face.units_per_em());
if units_per_em <= 0.0 {
return empty();
}
let scale = font_size_px / units_per_em;
let mut builder = PathOutlineBuilder {
vertices: Vec::new(),
codes: Vec::new(),
scale,
offset_x: origin[0],
offset_y: origin[1],
};
let mut pen_x = origin[0];
let mut prev_glyph = None;
for ch in text.chars() {
let Some(glyph_id) = face.glyph_index(ch) else {
continue;
};
if let Some(prev) = prev_glyph {
pen_x += horizontal_kerning(&face, prev, glyph_id) * scale;
}
builder.offset_x = pen_x;
let _ = face.outline_glyph(glyph_id, &mut builder);
if let Some(advance) = face.glyph_hor_advance(glyph_id) {
pen_x += f64::from(advance) * scale;
}
prev_glyph = Some(glyph_id);
}
Path::new(builder.vertices, Some(builder.codes))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn source() -> FontSource {
FontSource::dejavu_sans()
}
fn count_code(path: &Path, code: PathCode) -> usize {
path.codes()
.map(|cs| cs.iter().filter(|&&c| c == code).count())
.unwrap_or(0)
}
#[test]
fn empty_string_is_empty_path() {
let path = source().text_to_path("", 12.0, [0.0, 0.0]);
assert!(path.vertices().is_empty());
}
#[test]
fn glyph_has_moveto_and_close() {
let path = source().text_to_path("A", 12.0, [0.0, 0.0]);
assert!(!path.vertices().is_empty(), "A should have an outline");
assert!(
count_code(&path, PathCode::MoveTo) >= 1,
"expected at least one MoveTo"
);
assert!(
count_code(&path, PathCode::ClosePoly) >= 1,
"expected at least one ClosePoly"
);
}
#[test]
fn outline_is_y_up_above_baseline() {
let path = source().text_to_path("A", 12.0, [0.0, 0.0]);
let e = path.get_extents();
assert!(e.ymax() > 0.0, "ymax was {}", e.ymax());
assert!(e.height() > 0.0, "height was {}", e.height());
}
#[test]
fn height_grows_with_font_size() {
let small = source().text_to_path("A", 12.0, [0.0, 0.0]);
let large = source().text_to_path("A", 24.0, [0.0, 0.0]);
let hs = small.get_extents().height();
let hl = large.get_extents().height();
assert!(hs > 0.0, "small height was {hs}");
let ratio = hl / hs;
assert!((ratio - 2.0).abs() < 0.05, "height ratio was {ratio}");
}
#[test]
fn advance_width_matches_measure() {
let size = 16.0;
let path = source().text_to_path("AB", size, [0.0, 0.0]);
let measured = source().measure("AB", size).width;
let path_width = path.get_extents().xmax();
assert!(
path_width <= measured + 1e-6,
"inked width {path_width} should not exceed advance {measured}"
);
assert!(
(measured - path_width).abs() < measured * 0.25,
"inked width {path_width} unexpectedly far from advance {measured}"
);
}
#[test]
fn advance_width_matches_measure_exact() {
let size = 16.0;
let measured = source().measure("AB", size).width;
let measured_plus = source().measure("ABA", size).width;
let ab = source().text_to_path("AB", size, [0.0, 0.0]);
let aba = source().text_to_path("ABA", size, [0.0, 0.0]);
let delta_path = aba.get_extents().xmax() - ab.get_extents().xmax();
let delta_measure = measured_plus - measured;
assert!(
(delta_path - delta_measure).abs() < delta_measure * 0.2,
"path delta {delta_path} vs measure delta {delta_measure}"
);
}
#[test]
fn kerned_pair_path_matches_kerned_measure() {
let size = 100.0;
let src = source();
let ta = src.text_to_path("Ta", size, [0.0, 0.0]);
let measured = src.measure("Ta", size).width;
let t = src.measure("T", size).width;
let a = src.measure("a", size).width;
assert!(ta.get_extents().xmax() <= measured + 1e-6);
assert!(measured < t + a, "'Ta' should kern below T+a");
}
#[test]
fn space_advances_the_pen() {
let ab = source()
.text_to_path("AB", 16.0, [0.0, 0.0])
.get_extents()
.xmax();
let a_b = source()
.text_to_path("A B", 16.0, [0.0, 0.0])
.get_extents()
.xmax();
assert!(a_b > ab, "'A B' ({a_b}) should be wider than 'AB' ({ab})");
}
}