Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fauntlet - the font gauntlet #679

Merged
merged 10 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"write-fonts",
"otexplorer",
"skrifa",
"fauntlet",
]

[workspace.dependencies]
Expand Down
22 changes: 22 additions & 0 deletions fauntlet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "fauntlet"
version = "0.1.0"
edition = "2021"
license = "MIT/Apache-2.0"
description = "Testing tool for comparing Skrifa and FreeType."
repository = "https://github.com/googlefonts/fontations"
readme = "README.md"
publish = false

[dependencies]
skrifa = { version = "0.12.0", path = "../skrifa" }
freetype-rs = "0.32.0"
drott marked this conversation as resolved.
Show resolved Hide resolved
memmap2 = "0.5.10"
rayon = "1.8.0"
clap = { version = "4.4.7", features = ["derive"] }
wild = "2.2.0"
similar = "2.3.0"

# cargo-release settings
[package.metadata.release]
release = false
42 changes: 42 additions & 0 deletions fauntlet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# fauntlet - the *f*ont g*auntlet*

A tool to compare the output of [Skrifa](https://github.com/googlefonts/fontations/skrifa)
and [FreeType](https://freetype.org).

## Capabilities

This currently compares glyph outlines (in path form) and advance widths for
fixed sets of sizes and locations in variation space. Prior to comparison,
the outlines are "regularized" to account for minor differences in output.
These differences do not affect rendering and exist for the following reasons:

1. All contours are implicitly closed in FreeType. Skrifa emits close
elements to allow our output to be further processed by, e.g. a stroke
converter without requiring modification.
2. The FreeType CFF loader drops degenerate move and line elements but does
so before a final scaling step which may produce additional unused path
commands. Skrifa performs this filtering step after all scaling steps have
been applied, leading to more aggressive removal of degenerates.
3. In unscaled (`FT_LOAD_NO_SCALE`) mode, the FreeType TrueType loader
yields outlines with coordinates in integral font units. Passing these to
`FT_Outline_Decompose` results in truncated coordinates for some implicit
oncurve points (those with odd deltas in either direction). Skrifa produces
fractional values for these midpoints.

## Usage

To compare glyphs for a single font:

```bash
cargo run --release -p fauntlet -- compare-glyphs path/to/font.ttf
```

You can specify multiple font files and each file may use glob syntax. For example:

```bash
cargo run --release -p fauntlet -- compare-glyphs /myfonts/noto/**/hinted/**.ttf otherfonts/*.?tf
```

This will process all fonts in parallel, emitting diagnostics to `stderr` if
mismatches are found. Use the `--exit-on-fail` flag to end the process on the
first failure.
85 changes: 85 additions & 0 deletions fauntlet/src/compare_glyphs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use super::{FreeTypeInstance, InstanceOptions, RecordingPen, RegularizingPen, SkrifaInstance};
use skrifa::GlyphId;
use std::{io::Write, path::Path};

pub fn compare_glyphs(
path: &Path,
options: &InstanceOptions,
(mut ft_instance, mut skrifa_instance): (FreeTypeInstance, SkrifaInstance),
exit_on_fail: bool,
) -> bool {
let glyph_count = skrifa_instance.glyph_count();
let is_scaled = options.ppem != 0;

let mut ft_outline = RecordingPen::default();
let mut skrifa_outline = RecordingPen::default();

let mut ok = true;

for gid in 0..glyph_count {
let gid = GlyphId::new(gid);
let ft_advance = ft_instance.advance(gid);
let skrifa_advance = skrifa_instance.advance(gid);
if ft_advance != skrifa_advance {
writeln!(
std::io::stderr(),
"[{path:?}#{} ppem: {} coords: {:?}] glyph id {} advance doesn't match:\nFreeType: {ft_advance:?}\nSkrifa: {skrifa_advance:?}",
options.index,
options.ppem,
options.coords,
gid.to_u16(),
)
.unwrap();
if exit_on_fail {
std::process::exit(1);
}
}
ft_outline.clear();
ft_instance
.outline(gid, &mut RegularizingPen::new(&mut ft_outline, is_scaled))
.unwrap();
skrifa_outline.clear();
skrifa_instance
.outline(
gid,
&mut RegularizingPen::new(&mut skrifa_outline, is_scaled),
)
.unwrap();
if ft_outline != skrifa_outline {
ok = false;
fn outline_to_string(outline: &RecordingPen) -> String {
outline
.0
.iter()
.map(|cmd| format!("{cmd:?}"))
.collect::<Vec<_>>()
.join("\n")
}
let ft_cmds = outline_to_string(&ft_outline);
let skrifa_cmds = outline_to_string(&skrifa_outline);
let diff = similar::TextDiff::from_lines(&ft_cmds, &skrifa_cmds);
let mut diff_str = String::default();
for change in diff.iter_all_changes() {
let sign = match change.tag() {
similar::ChangeTag::Delete => "-",
similar::ChangeTag::Insert => "+",
similar::ChangeTag::Equal => " ",
};
diff_str.push_str(&format!("{sign} {change}"));
}
write!(
std::io::stderr(),
"[{path:?}#{} ppem: {} coords: {:?}] glyph id {} outline doesn't match:\n{diff_str}",
options.index,
options.ppem,
options.coords,
gid.to_u16(),
)
.unwrap();
if exit_on_fail {
std::process::exit(1);
}
}
}
ok
}
191 changes: 191 additions & 0 deletions fauntlet/src/font/freetype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use freetype::{
face::LoadFlag,
ffi::{FT_Error, FT_Face, FT_Fixed, FT_Int32, FT_Long, FT_UInt, FT_Vector},
Face, Library,
};
use skrifa::{raw::FileRef, scale::Pen, GlyphId};

use std::ffi::{c_int, c_void};

use super::{InstanceOptions, SharedFontData};

pub fn collect_faces(data: &SharedFontData) -> Option<(Library, Vec<Face<SharedFontData>>)> {
let library = Library::init().unwrap();
let mut faces = vec![];
let font_count = match FileRef::new(data.0.as_ref()).ok()? {
FileRef::Font(_) => 1,
FileRef::Collection(collection) => collection.len(),
};
for i in 0..font_count {
faces.push(library.new_memory_face2(data.clone(), i as isize).ok()?);
}
Some((library, faces))
}

pub struct FreeTypeInstance<'a> {
face: &'a mut Face<SharedFontData>,
load_flags: LoadFlag,
}

impl<'a> FreeTypeInstance<'a> {
pub fn new(face: &'a mut Face<SharedFontData>, options: &InstanceOptions) -> Option<Self> {
let mut load_flags = LoadFlag::NO_AUTOHINT | LoadFlag::NO_HINTING | LoadFlag::NO_BITMAP;
if options.ppem != 0 {
face.set_pixel_sizes(options.ppem, options.ppem).ok()?;
} else {
load_flags |= LoadFlag::NO_SCALE;
}
if !options.coords.is_empty() {
let mut ft_coords = vec![];
ft_coords.extend(
options
.coords
.iter()
.map(|c| c.to_fixed().to_bits() as FT_Long),
);
unsafe {
freetype::freetype_sys::FT_Set_Var_Blend_Coordinates(
face.raw_mut() as _,
options.coords.len() as u32,
ft_coords.as_ptr(),
);
}
} else {
unsafe {
// Note the explicit call to set *design* coordinates. Setting
// blend doesn't correctly disable variation processing
freetype::freetype_sys::FT_Set_Var_Design_Coordinates(
face.raw_mut() as _,
0,
std::ptr::null(),
);
}
}
Some(Self { face, load_flags })
}

pub fn glyph_count(&self) -> u16 {
self.face.num_glyphs() as u16
}

pub fn advance(&mut self, glyph_id: GlyphId) -> Option<f32> {
let mut advance: FT_Fixed = 0;
if unsafe {
FT_Get_Advance(
self.face.raw_mut(),
glyph_id.to_u16() as _,
self.load_flags.bits(),
&mut advance as *mut _,
)
} == 0
{
let mut advance = advance as f32;
if !self.load_flags.contains(LoadFlag::NO_SCALE) {
advance /= 65536.0;
}
return Some(advance);
}
None
}

pub fn outline(&mut self, glyph_id: GlyphId, pen: &mut impl Pen) -> Option<()> {
self.face
.load_glyph(glyph_id.to_u16() as u32, self.load_flags)
.ok()?;
let mut ft_pen = FreeTypePen {
inner: pen,
is_scaled: !self.load_flags.contains(LoadFlag::NO_SCALE),
};
let funcs = freetype::freetype_sys::FT_Outline_Funcs {
move_to: ft_move_to,
line_to: ft_line_to,
conic_to: ft_conic_to,
cubic_to: ft_cubic_to,
delta: 0,
shift: 0,
};
unsafe {
freetype::freetype_sys::FT_Outline_Decompose(
&self.face.glyph().raw().outline as *const _ as *mut _,
&funcs,
(&mut ft_pen) as *mut FreeTypePen as *mut _,
);
}
Some(())
}
}

// Since Pen is dyn here which is a fat pointer, we wrap it in a struct
// to pass through the required void* type in FT_Outline_Decompose.
struct FreeTypePen<'a> {
inner: &'a mut dyn Pen,
is_scaled: bool,
}

impl<'a> FreeTypePen<'a> {
fn scale_point(&self, p: *const FT_Vector) -> (f32, f32) {
let p = unsafe { &*p };
if self.is_scaled {
const SCALE: f32 = 1.0 / 64.0;
(p.x as f32 * SCALE, p.y as f32 * SCALE)
} else {
(p.x as f32, p.y as f32)
}
}
}

fn ft_pen<'a>(user: *mut c_void) -> &'a mut FreeTypePen<'a> {
// SAFETY: this is wildly unsafe and only works if we make sure to pass
// &mut FreeTypePen as the user parameter to FT_Outline_Decompose
unsafe { &mut *(user as *mut FreeTypePen) }
}

extern "C" fn ft_move_to(to: *const FT_Vector, user: *mut c_void) -> c_int {
let pen = ft_pen(user);
let (x, y) = pen.scale_point(to);
pen.inner.move_to(x, y);
0
}

extern "C" fn ft_line_to(to: *const FT_Vector, user: *mut c_void) -> c_int {
let pen = ft_pen(user);
let (x, y) = pen.scale_point(to);
pen.inner.line_to(x, y);
0
}

extern "C" fn ft_conic_to(
control: *const FT_Vector,
to: *const FT_Vector,
user: *mut c_void,
) -> c_int {
let pen = ft_pen(user);
let (cx0, cy0) = pen.scale_point(control);
let (x, y) = pen.scale_point(to);
pen.inner.quad_to(cx0, cy0, x, y);
0
}

extern "C" fn ft_cubic_to(
control1: *const FT_Vector,
control2: *const FT_Vector,
to: *const FT_Vector,
user: *mut c_void,
) -> c_int {
let pen = ft_pen(user);
let (cx0, cy0) = pen.scale_point(control1);
let (cx1, cy1) = pen.scale_point(control2);
let (x, y) = pen.scale_point(to);
pen.inner.curve_to(cx0, cy0, cx1, cy1, x, y);
0
}

extern "C" {
// freetype-sys doesn't expose this function
pub fn FT_Get_Advance(
face: FT_Face,
gindex: FT_UInt,
load_flags: FT_Int32,
padvance: *mut FT_Fixed,
) -> FT_Error;
}
Loading