mod error;
#[cfg(not(feature = "rayon"))]
mod norayon;
mod version;
pub mod work;
mod workload;
pub use error::Error;
pub use fontir::orchestration::Flags; use fontra2fontir::source::FontraIrSource;
use glyphs2fontir::source::GlyphsIrSource;
use ufo2fontir::source::DesignSpaceIrSource;
use workload::Workload;
use std::{
ffi::OsStr,
fs,
path::{Path, PathBuf},
};
use fontir::{orchestration::Context as FeContext, source::Source};
use fontbe::orchestration::Context as BeContext;
use log::debug;
#[derive(Debug, Clone)]
pub enum Input {
DesignSpacePath(PathBuf),
GlyphsPath(PathBuf),
FontraPath(PathBuf),
GlyphsMemory(String),
}
impl Input {
pub fn new(path: &Path) -> Result<Self, Error> {
if !path.exists() {
return Err(Error::FileExpected(path.to_path_buf()));
}
let ext = path
.extension()
.and_then(OsStr::to_str)
.ok_or_else(|| Error::UnrecognizedSource(path.to_path_buf()))?;
match ext {
"designspace" => Ok(Input::DesignSpacePath(path.to_path_buf())),
"ufo" => Ok(Input::DesignSpacePath(path.to_path_buf())),
"glyphs" => Ok(Input::GlyphsPath(path.to_path_buf())),
"glyphspackage" => Ok(Input::GlyphsPath(path.to_path_buf())),
"fontra" => Ok(Input::FontraPath(path.to_path_buf())),
_ => Err(Error::UnrecognizedSource(path.to_path_buf())),
}
}
pub fn from_glyphs(source: String) -> Self {
Input::GlyphsMemory(source)
}
#[tracing::instrument(skip_all)]
pub fn create_source(&self) -> Result<Box<dyn Source>, Error> {
match self {
Input::DesignSpacePath(path) => Ok(Box::new(DesignSpaceIrSource::new(path)?)),
Input::GlyphsPath(path) => Ok(Box::new(GlyphsIrSource::new(path)?)),
Input::FontraPath(path) => Ok(Box::new(FontraIrSource::new(path)?)),
Input::GlyphsMemory(source) => Ok(Box::new(GlyphsIrSource::new_from_memory(source)?)),
}
}
}
impl TryFrom<&Path> for Input {
type Error = Error;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
Input::new(path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisableFlags(Flags);
impl Default for DisableFlags {
fn default() -> Self {
DisableFlags(Flags::empty())
}
}
impl DisableFlags {
pub fn empty() -> Self {
DisableFlags(Flags::empty())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<Flags> for DisableFlags {
fn from(flags: Flags) -> Self {
DisableFlags(flags)
}
}
impl std::ops::Not for DisableFlags {
type Output = Flags;
fn not(self) -> Flags {
!self.0
}
}
pub fn version() -> String {
crate::version::version_string(env!("VERGEN_GIT_DESCRIBE"), env!("CARGO_PKG_VERSION"))
}
#[derive(Debug, Default)]
pub struct Options {
pub flags: Flags,
pub flags_to_disable: DisableFlags,
pub skip_features: bool,
pub compile_debg: bool,
pub output_file: Option<PathBuf>,
pub debug_dir: Option<PathBuf>,
}
#[cfg(feature = "cli")]
#[tracing::instrument(skip_all)]
pub fn run(input: Input, options: Options) -> Result<(), Error> {
if options.output_file.is_none() {
return Err(Error::NoOutputFile);
}
let source = input.create_source()?;
let (_fe_root, be_root) = generate_font_internal(source, &options)?;
write_font_file(&options, &be_root)
}
fn merge_compilation_flags(options: &Options, source: &dyn Source) -> Flags {
(options.flags | source.compilation_flags()) & !options.flags_to_disable
}
pub fn generate_font(source: Box<dyn Source>, options: Options) -> Result<Vec<u8>, Error> {
let (_fe_root, be_root) = generate_font_internal(source, &options)?;
Ok(be_root.font.get().to_vec())
}
fn generate_font_internal(
source: Box<dyn Source>,
options: &Options,
) -> Result<(FeContext, BeContext), Error> {
debug!("Running with options {options:#?}");
init_paths(options)?;
let flags = merge_compilation_flags(options, &*source);
let workload = Workload::new(source, options.skip_features)?;
let fe_root = FeContext::new_root(flags);
let be_root = BeContext::new_root(
flags,
Some(version().into()),
options.debug_dir.clone(),
options.compile_debg,
&fe_root,
);
workload.exec(&fe_root, &be_root)?;
Ok((fe_root, be_root))
}
pub fn require_dir(dir: &Path) -> Result<(), Error> {
if dir == Path::new("") {
return Ok(());
}
if dir.exists() && !dir.is_dir() {
return Err(Error::ExpectedDirectory(dir.to_owned()));
}
if !dir.exists() {
debug!("require_dir {dir:?}, creating");
fs::create_dir_all(dir).map_err(|source| Error::FileIo {
path: dir.to_owned(),
source,
})?;
} else {
debug!("require_dir {dir:?}, already exists, nop");
}
Ok(())
}
pub fn init_paths(options: &Options) -> Result<(), Error> {
if let Some(output_file) = options.output_file.as_ref()
&& let Some(parent) = output_file.parent()
{
require_dir(parent)?;
}
if let Some(debug_dir) = options.debug_dir.as_ref() {
require_dir(debug_dir)?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
pub fn write_font_file(options: &Options, be_context: &BeContext) -> Result<(), Error> {
let Some(output_file) = options.output_file.as_ref() else {
return Ok(());
};
fs::write(output_file, &*be_context.font.get()).map_err(|source| Error::FileIo {
path: output_file.clone(),
source,
})
}
#[cfg(test)]
pub fn testdata_dir() -> std::path::PathBuf {
["./resources/testdata", "../resources/testdata"]
.iter()
.map(std::path::PathBuf::from)
.find(|pb| pb.exists())
.unwrap()
}
#[cfg(test)]
mod tests {
use std::{
collections::{HashMap, HashSet},
fs,
path::Path,
sync::Arc,
};
use chrono::{Duration, TimeZone, Utc};
use fontbe::orchestration::{AnyWorkId, Context as BeContext, WorkId as BeWorkIdentifier};
use fontdrasil::{
coords::{NormalizedCoord, NormalizedLocation},
orchestration::Access,
types::{GlyphName, WidthClass},
variations::{Tent, VariationRegion},
};
use fontir::{
ir::{self, GlobalMetric, GlyphOrder, KernGroup, KernPair, KernSide},
orchestration::{Context as FeContext, WorkId as FeWorkIdentifier},
};
use kurbo::{Point, Rect};
use log::info;
use pretty_assertions::assert_eq;
use rstest::rstest;
use skrifa::{MetadataProvider, charmap::Charmap, instance::Size, outline::DrawSettings};
use tempfile::{TempDir, tempdir};
use write_fonts::{
dump_table,
read::{
FontData, FontRead, FontRef, TableProvider,
tables::{
cmap::{Cmap, CmapSubtable},
colr::{Colr, Paint, PaintGlyph},
cpal::ColorRecord,
gasp::GaspRangeBehavior,
glyf::{self, CompositeGlyph, CurvePoint, Glyf},
gpos::{AnchorTable, Gpos, MarkBasePosFormat1, PositionLookup},
gsub::{SingleSubst, SubstitutionLookup},
hmtx::Hmtx,
layout::FeatureParams,
loca::Loca,
name::Name,
os2::SelectionFlags,
post::Post,
variations::{DeltaSetIndexMap, ItemVariationData, ItemVariationStore},
},
},
tables::{
gdef::GlyphClassDef,
glyf::{Bbox, Glyph as RawGlyph},
head,
loca::LocaFormat,
meta::{DataMapRecord, Metadata, ScriptLangTag},
},
types::{F2Dot14, GlyphId, GlyphId16, NameId, Tag, Version16Dot16},
};
use super::*;
use crate::work::AnyAccess;
struct TestCompile {
temp: TempDir,
work_executed: HashSet<AnyWorkId>,
fe_context: FeContext,
be_context: BeContext,
raw_font: Vec<u8>,
options: Options,
workload: Workload,
}
impl Options {
fn for_test(build_dir: &Path) -> Options {
Options {
output_file: Some(build_dir.join("font.ttf")),
..Default::default()
}
}
}
impl TestCompile {
fn new(source_file: &str, adjust_options: impl Fn(Options) -> Options) -> TestCompile {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let temp_dir = tempdir().unwrap();
let options = adjust_options(Options::for_test(temp_dir.path()));
info!("Compile {options:?}");
let input = Input::new(&testdata_dir().join(source_file)).unwrap();
let source = input.create_source().unwrap();
let flags = merge_compilation_flags(&options, &*source);
init_paths(&options).unwrap();
let fe_context = FeContext::new_root(flags);
let be_context = BeContext::new_root(
flags,
Some(crate::version().into()),
options.debug_dir.clone(),
options.compile_debg,
&fe_context.read_only(),
);
let workload = Workload::new(source, options.skip_features).unwrap();
TestCompile {
temp: temp_dir,
work_executed: HashSet::new(),
fe_context,
be_context,
raw_font: Vec::new(),
options,
workload,
}
}
fn run(&mut self) {
let completed = self
.workload
.run_for_test(&self.fe_context, &self.be_context);
self.work_executed = completed;
write_font_file(&self.options, &self.be_context).unwrap();
self.raw_font = self.be_context.font.get().to_vec();
}
fn compile_source(source: &str) -> TestCompile {
TestCompile::compile(source, |options| options)
}
fn compile(source: &str, adjust_options: impl Fn(Options) -> Options) -> TestCompile {
let mut result = Self::new(source, adjust_options);
result.run();
result
}
fn get_glyph_index(&self, name: &str) -> Option<u32> {
self.fe_context
.glyph_order
.get()
.glyph_id(name)
.map(|v| v.to_u16() as u32)
}
fn get_glyph_name(&self, gid: GlyphId16) -> Option<GlyphName> {
self.fe_context
.glyph_order
.get()
.glyph_name(gid.to_u32() as usize)
.cloned()
}
fn contains_glyph(&self, name: &str) -> bool {
self.get_glyph_index(name).is_some()
}
fn get_gid(&self, name: &str) -> GlyphId16 {
let raw = self.get_glyph_index(name).unwrap_or_default();
GlyphId16::new(raw as u16)
}
fn glyphs(&self) -> Glyphs {
Glyphs {
loca_format: *self.be_context.loca_format.get(),
raw_glyf: self.be_context.glyf.get().to_vec(),
raw_loca: self.be_context.loca.get().to_vec(),
}
}
fn font(&self) -> FontRef<'_> {
FontRef::new(&self.raw_font).unwrap()
}
fn read_be_glyph(&self, name: &str) -> RawGlyph {
let id = AnyWorkId::Be(BeWorkIdentifier::GlyfFragment(name.into()));
self.be_context.glyphs.get(&id).data.clone()
}
fn read_ir_glyph(&self, name: &str) -> ir::Glyph {
(*self.fe_context.get_glyph(name)).clone()
}
}
struct Glyphs {
loca_format: LocaFormat,
raw_glyf: Vec<u8>,
raw_loca: Vec<u8>,
}
impl Glyphs {
fn read(&self) -> Vec<Option<glyf::Glyph<'_>>> {
let glyf = Glyf::read(FontData::new(&self.raw_glyf)).unwrap();
let loca = Loca::read(
FontData::new(&self.raw_loca),
self.loca_format == LocaFormat::Long,
)
.unwrap();
(0..loca.len())
.map(|gid| loca.get_glyf(GlyphId16::new(gid as u16).into(), &glyf))
.map(|r| r.unwrap())
.collect()
}
}
fn assert_compile_work(source: &str, glyphs: Vec<&str>) {
let result = TestCompile::compile_source(source);
let mut completed = result.work_executed.iter().cloned().collect::<Vec<_>>();
let mut expected = vec![
AnyWorkId::Fe(FeWorkIdentifier::StaticMetadata),
FeWorkIdentifier::ColorPalettes.into(),
FeWorkIdentifier::GlobalMetrics.into(),
FeWorkIdentifier::PaintGraph.into(),
FeWorkIdentifier::PreliminaryGlyphOrder.into(),
FeWorkIdentifier::GlyphOrder.into(),
FeWorkIdentifier::PreliminaryGdefCategories.into(),
FeWorkIdentifier::Features.into(),
FeWorkIdentifier::KerningLocations.into(),
FeWorkIdentifier::KernInstance(NormalizedLocation::for_pos(&[("wght", 0.0)])).into(),
FeWorkIdentifier::KernInstance(NormalizedLocation::for_pos(&[("wght", 1.0)])).into(),
BeWorkIdentifier::Features.into(),
BeWorkIdentifier::FeaturesAst.into(),
BeWorkIdentifier::Avar.into(),
BeWorkIdentifier::Cmap.into(),
BeWorkIdentifier::Colr.into(),
BeWorkIdentifier::Cpal.into(),
BeWorkIdentifier::Font.into(),
BeWorkIdentifier::Fvar.into(),
BeWorkIdentifier::Gasp.into(),
BeWorkIdentifier::Glyf.into(),
BeWorkIdentifier::Gpos.into(),
BeWorkIdentifier::Gsub.into(),
BeWorkIdentifier::Gdef.into(),
BeWorkIdentifier::Gvar.into(),
BeWorkIdentifier::Head.into(),
BeWorkIdentifier::Hhea.into(),
BeWorkIdentifier::Hmtx.into(),
BeWorkIdentifier::Hvar.into(),
BeWorkIdentifier::GatherIrKerning.into(),
BeWorkIdentifier::KernFragment(0).into(),
BeWorkIdentifier::GatherBeKerning.into(),
BeWorkIdentifier::ExtraFeaTables.into(),
BeWorkIdentifier::Loca.into(),
BeWorkIdentifier::LocaFormat.into(),
BeWorkIdentifier::Marks.into(),
BeWorkIdentifier::Maxp.into(),
BeWorkIdentifier::Meta.into(),
BeWorkIdentifier::Mvar.into(),
BeWorkIdentifier::Name.into(),
BeWorkIdentifier::Os2.into(),
BeWorkIdentifier::Post.into(),
BeWorkIdentifier::Stat.into(),
BeWorkIdentifier::Vhea.into(),
BeWorkIdentifier::Vmtx.into(),
BeWorkIdentifier::Vvar.into(),
];
expected.extend(
glyphs
.iter()
.map(|n| FeWorkIdentifier::Glyph((*n).into()).into()),
);
expected.extend(
glyphs
.iter()
.map(|n| FeWorkIdentifier::Anchor((*n).into()).into()),
);
expected.extend(
glyphs
.iter()
.map(|n| BeWorkIdentifier::GlyfFragment((*n).into()).into()),
);
expected.extend(
glyphs
.iter()
.map(|n| BeWorkIdentifier::GvarFragment((*n).into()).into()),
);
expected.extend(vec![
BeWorkIdentifier::GlyfFragment((".notdef").into()).into(),
BeWorkIdentifier::GvarFragment((".notdef").into()).into(),
]);
completed.sort();
expected.sort();
assert_eq!(expected, completed);
}
#[test]
fn compile_work_for_designspace() {
assert_compile_work("wght_var.designspace", vec!["bar", "plus"])
}
#[test]
fn compile_work_for_glyphs() {
assert_compile_work(
"glyphs3/WghtVar.glyphs",
vec![
"bracketleft",
"bracketright",
"exclam",
"hyphen",
"manual-component",
"space",
],
)
}
fn assert_compiles_with_gpos_and_gsub(
source: &str,
adjust_options: impl Fn(Options) -> Options,
) -> TestCompile {
let result = TestCompile::compile(source, adjust_options);
let font = result.font();
let gpos = font.gpos();
let gsub = font.gsub();
assert!(
gpos.is_ok() && gsub.is_ok(),
"\ngpos: {gpos:?}\ngsub: {gsub:?}"
);
result
}
#[test]
fn compile_fea() {
assert_compiles_with_gpos_and_gsub("static.designspace", |o| o);
}
#[test]
fn compile_fea_with_includes() {
assert_compiles_with_gpos_and_gsub("fea_include.designspace", |o| o);
}
#[test]
fn compile_fea_with_includes_resolved() {
assert_compiles_with_gpos_and_gsub("fea_include_resolve.designspace", |o| o);
}
#[test]
fn compile_debg_emits_debg_table() {
let result = TestCompile::compile("static.designspace", |mut args| {
args.compile_debg = true;
args
});
let font = result.font();
let tags: Vec<_> = font
.table_directory
.table_records()
.iter()
.map(|tr| tr.tag())
.collect();
assert!(
tags.contains(&Tag::new(b"Debg")),
"expected Debg table in font, found: {tags:?}"
);
}
#[test]
fn no_debg_table_by_default() {
let result = TestCompile::compile_source("static.designspace");
let font = result.font();
let tags: Vec<_> = font
.table_directory
.table_records()
.iter()
.map(|tr| tr.tag())
.collect();
assert!(
!tags.contains(&Tag::new(b"Debg")),
"Debg table should not be present by default, found: {tags:?}"
);
}
fn build_contour_and_composite_glyph(prefer_simple_glyphs: bool) -> (TestCompile, ir::Glyph) {
let result = TestCompile::compile("glyphs2/MixedContourComponent.glyphs", |mut args| {
args.flags
.set(Flags::PREFER_SIMPLE_GLYPHS, prefer_simple_glyphs); args
});
let glyph = result
.fe_context
.glyphs
.get(&FeWorkIdentifier::Glyph("contour_and_component".into()));
assert_eq!(1, glyph.sources().len());
(result, (*glyph).clone())
}
#[test]
fn resolve_contour_and_composite_glyph_in_non_legacy_mode() {
let (result, glyph) = build_contour_and_composite_glyph(false);
assert!(glyph.default_instance().contours.is_empty(), "{glyph:?}");
assert_eq!(2, glyph.default_instance().components.len(), "{glyph:?}");
let RawGlyph::Composite(glyph) = result.read_be_glyph(glyph.name.as_str()) else {
panic!("Expected a simple glyph");
};
let raw_glyph = dump_table(&glyph).unwrap();
let glyph = CompositeGlyph::read(FontData::new(&raw_glyph)).unwrap();
assert_eq!(-1, glyph.number_of_contours());
}
#[test]
fn resolve_contour_and_composite_glyph_in_legacy_mode() {
let (result, glyph) = build_contour_and_composite_glyph(true);
assert!(glyph.default_instance().components.is_empty(), "{glyph:?}");
assert_eq!(2, glyph.default_instance().contours.len(), "{glyph:?}");
let RawGlyph::Simple(glyph) = result.read_be_glyph(glyph.name.as_str()) else {
panic!("Expected a simple glyph");
};
assert_eq!(2, glyph.contours.len());
}
#[test]
fn compile_simple_binary_glyph() {
let result = TestCompile::compile_source("static.designspace");
let RawGlyph::Simple(glyph) = result.read_be_glyph("bar") else {
panic!("Expected a simple glyph");
};
assert_eq!(1, glyph.contours.len());
assert_eq!(
4_usize,
glyph
.contours
.iter()
.map(|c| c.iter().count())
.sum::<usize>()
);
assert_eq!(
Bbox {
x_min: 222,
y_min: -241,
x_max: 295,
y_max: 760,
},
glyph.bbox,
);
}
#[test]
fn compile_simple_glyphs_to_glyf_loca() {
let result = TestCompile::compile_source("static.designspace");
assert_eq!(
vec![(8, 2), (0, 0), (4, 1), (12, 1), (0, 0)],
result
.glyphs()
.read()
.iter()
.map(|g| match g {
None => (0, 0),
Some(glyf::Glyph::Simple(glyph)) =>
(glyph.num_points(), glyph.number_of_contours()),
Some(glyf::Glyph::Composite(glyph)) => (0, glyph.number_of_contours()),
})
.collect::<Vec<_>>()
);
}
#[test]
fn respect_lib_when_compiling_direct_from_ufo() {
let result = TestCompile::compile_source("Static-Regular.ufo");
assert!(!result.contains_glyph("skip_me"))
}
#[test]
fn missing_component_does_not_decompose_siblings() {
let result = TestCompile::compile_source("glyphs3/MissingComponent.glyphs");
let b_gid = result.get_gid("B");
let RawGlyph::Composite(glyph) = result.read_be_glyph("A") else {
panic!("'A' should remain a composite glyph, not be decomposed");
};
let comp_gids: Vec<_> = glyph.components().iter().map(|c| c.glyph).collect();
assert_eq!(comp_gids, vec![b_gid], "'A' should reference only 'B'");
}
#[test]
fn compile_sets_xmin_eq_lsb_flag() {
let result = TestCompile::compile_source("fontinfo.designspace");
let head = result.font().head().unwrap();
assert!(head.flags().contains(head::Flags::LSB_AT_X_0));
}
#[test]
fn compile_obeys_family_class() {
let result = TestCompile::compile_source("fontinfo.designspace");
let os2 = result.font().os2().unwrap();
assert_eq!((1 << 8) | 2, os2.s_family_class());
}
#[test]
fn compile_obeys_designspace_gasp_records() {
let result = TestCompile::compile_source("fontinfo.designspace");
let gasp = result.font().gasp().unwrap();
assert_eq!(
vec![
(
7,
GaspRangeBehavior::GASP_DOGRAY | GaspRangeBehavior::GASP_SYMMETRIC_SMOOTHING
),
(
65535,
GaspRangeBehavior::GASP_GRIDFIT
| GaspRangeBehavior::GASP_DOGRAY
| GaspRangeBehavior::GASP_SYMMETRIC_GRIDFIT
| GaspRangeBehavior::GASP_SYMMETRIC_SMOOTHING
),
],
gasp.gasp_ranges()
.iter()
.map(|r| (r.range_max_ppem(), r.range_gasp_behavior()))
.collect::<Vec<_>>(),
)
}
#[test]
fn compile_obeys_glyphs_gasp_records() {
let result = TestCompile::compile_source("glyphs3/WghtVarGasp.glyphs");
let gasp = result.font().gasp().unwrap();
assert_eq!(
vec![
(
8,
GaspRangeBehavior::GASP_DOGRAY | GaspRangeBehavior::GASP_SYMMETRIC_SMOOTHING
),
(
20,
GaspRangeBehavior::GASP_GRIDFIT
| GaspRangeBehavior::GASP_DOGRAY
| GaspRangeBehavior::GASP_SYMMETRIC_GRIDFIT,
),
(
65535,
GaspRangeBehavior::GASP_GRIDFIT
| GaspRangeBehavior::GASP_DOGRAY
| GaspRangeBehavior::GASP_SYMMETRIC_GRIDFIT
| GaspRangeBehavior::GASP_SYMMETRIC_SMOOTHING
),
],
gasp.gasp_ranges()
.iter()
.map(|r| (r.range_max_ppem(), r.range_gasp_behavior()))
.collect::<Vec<_>>(),
)
}
#[test]
fn compile_gasp_only_from_default() {
let result = TestCompile::compile_source("fontinfo_var.designspace");
assert!(result.font().gasp().is_err());
}
#[test]
fn compile_prefers_variable_default_to_fontinfo_value() {
let result = TestCompile::compile_source("fontinfo_var.designspace");
let os2 = result.font().os2().unwrap();
assert_eq!(
100,
os2.us_weight_class(),
"We should use wght default, NOT openTypeOS2WeightClass"
);
}
#[test]
fn compile_variable_simple_glyph_with_implied_oncurves() {
let result = TestCompile::compile_source("glyphs3/Oswald-O.glyphs");
let glyph_data = result.glyphs();
let glyphs = glyph_data.read();
let uppercase_o = &glyphs[result.get_glyph_index("O").unwrap() as usize];
let Some(glyf::Glyph::Simple(glyph)) = uppercase_o else {
panic!("Expected 'O' to be a simple glyph, got {uppercase_o:?}");
};
assert_eq!(2, glyph.number_of_contours());
assert_eq!(35, glyph.num_points());
assert_eq!(
[48, -9, 491, 817],
[glyph.x_min(), glyph.y_min(), glyph.x_max(), glyph.y_max()]
);
assert_eq!(
vec![
CurvePoint::on_curve(270, -9),
CurvePoint::off_curve(188, -9),
CurvePoint::off_curve(90, 55),
CurvePoint::off_curve(48, 174),
CurvePoint::on_curve(48, 254),
],
glyph.points().take(5).collect::<Vec<_>>()
);
}
#[test]
fn compile_composite_glyphs_has_expected_glyph_types() {
let result = TestCompile::compile_source("glyphs2/Component.glyphs");
let glyph_data = result.glyphs();
let all_glyphs = glyph_data.read();
assert!(matches!(
(
all_glyphs[result.get_glyph_index("period").unwrap() as usize]
.as_ref()
.unwrap(),
all_glyphs[result.get_glyph_index("comma").unwrap() as usize]
.as_ref()
.unwrap(),
all_glyphs[result.get_glyph_index("non_uniform_scale").unwrap() as usize]
.as_ref()
.unwrap(),
),
(
glyf::Glyph::Simple(..),
glyf::Glyph::Composite(..),
glyf::Glyph::Composite(..),
),
));
}
#[test]
fn compile_composite_glyphs_to_glyf_loca() {
let result = TestCompile::compile_source("glyphs2/Component.glyphs");
let period_idx = result.get_glyph_index("period").unwrap();
let non_uniform_scale_idx = result.get_glyph_index("non_uniform_scale").unwrap();
let glyph_data = result.glyphs();
let glyphs = glyph_data.read();
let Some(glyf::Glyph::Composite(glyph)) = &glyphs[non_uniform_scale_idx as usize] else {
panic!("Expected a composite\n{glyphs:#?}");
};
let component = glyph.components().next().unwrap();
assert_eq!(period_idx, component.glyph.to_u16() as u32);
assert_component_transform(
&component,
[0.84519, 0.58921, -1.16109, 1.66553, -233.0, -129.0],
);
}
fn assert_component_transform(component: &glyf::Component, expected_transform: [f32; 6]) {
let [xx, yx, xy, yy, dx, dy] = expected_transform;
assert_eq!(
glyf::Anchor::Offset {
x: dx as i16,
y: dy as i16
},
component.anchor
);
assert_eq!(
glyf::Transform {
xx: F2Dot14::from_f32(xx),
yx: F2Dot14::from_f32(yx),
xy: F2Dot14::from_f32(xy),
yy: F2Dot14::from_f32(yy),
},
component.transform
);
}
#[test]
fn compile_composite_glyphs_to_glyf_loca_applies_transforms() {
let result = TestCompile::compile_source("glyphs2/Component.glyphs");
let gid = result.get_glyph_index("simple_transform_again").unwrap();
let glyph_data = result.glyphs();
let glyphs = glyph_data.read();
let Some(glyf::Glyph::Composite(glyph)) = &glyphs[gid as usize] else {
panic!("Expected a composite\n{glyphs:#?}");
};
let components: Vec<_> = glyph.components().collect();
assert_eq!(2, components.len(), "{components:#?}");
assert_component_transform(&components[0], [2.0, 0.0, 0.0, 1.5, 50.0, 50.0]);
assert_component_transform(&components[1], [1.5, 0.0, 0.0, 2.0, 50.0, 50.0]);
assert_eq!(
[425, 125, 800, 250],
[glyph.x_min(), glyph.y_min(), glyph.x_max(), glyph.y_max()]
);
}
#[test]
fn eliminate_2x2_transforms() {
let result = TestCompile::compile("glyphs2/Component.glyphs", |mut args| {
args.flags
.set(Flags::DECOMPOSE_TRANSFORMED_COMPONENTS, true);
args
});
let glyph_data = result.glyphs();
let glyphs = glyph_data.read();
let Some(glyf::Glyph::Simple(..)) =
&glyphs[result.get_glyph_index("simple_transform_again").unwrap() as usize]
else {
panic!("Expected a simple glyph\n{glyphs:#?}");
};
let Some(glyf::Glyph::Composite(..)) =
&glyphs[result.get_glyph_index("translate_only").unwrap() as usize]
else {
panic!("Expected a composite glyph\n{glyphs:#?}");
};
}
#[test]
fn decompose_all_components() {
let result = TestCompile::compile("glyphs2/Component.glyphs", |mut args| {
args.flags.set(Flags::DECOMPOSE_COMPONENTS, true);
args
});
let glyph_data = result.glyphs();
for (i, glyph) in glyph_data.read().iter().enumerate() {
let Some(glyf::Glyph::Simple(_)) = glyph else {
panic!("Expected a simple glyph at index {i}");
};
}
}
#[test]
fn writes_cmap() {
let result = TestCompile::compile_source("glyphs2/Component.glyphs");
let raw_cmap = dump_table(result.be_context.cmap.get().as_ref()).unwrap();
let font_data = FontData::new(&raw_cmap);
let cmap = Cmap::read(font_data).unwrap();
assert!(!cmap.encoding_records().is_empty());
for encoding_record in cmap.encoding_records() {
let CmapSubtable::Format4(cmap4) = encoding_record.subtable(font_data).unwrap() else {
panic!("Wrong subtable {encoding_record:#?}");
};
let cp_and_gid: Vec<(u16, u16)> = cmap4
.start_code()
.iter()
.zip(cmap4.end_code())
.zip(cmap4.id_delta())
.filter_map(|((start, end), id_delta)| {
let start = start.get();
let end = end.get();
if (start, end) == (0xFFFF, 0xFFFF) {
return None;
}
Some((start, end, id_delta.get()))
})
.flat_map(|(start, end, id_delta)| {
(start..=end)
.map(move |cp| (cp, (cp as i32 + id_delta as i32).try_into().unwrap()))
})
.collect();
assert_eq!(
vec![
(0x002C, 2),
(0x002E, 1),
(0x0030, 3),
(0x031, 4),
(0x032, 5),
(0x033, 6)
],
cp_and_gid,
"start {:?}\nend {:?}id_delta {:?}",
cmap4.start_code(),
cmap4.end_code(),
cmap4.id_delta()
);
}
}
#[test]
fn codepoint_mismatch_uses_default_in_cmap() {
let result =
TestCompile::compile_source("codepoint_mismatch/codepoint_mismatch.designspace");
let font = result.font();
let cmap = font.cmap().unwrap();
assert!(
cmap.map_codepoint(0x007Cu32).is_some(),
"U+007C from default master should be in cmap"
);
assert_eq!(
None,
cmap.map_codepoint(0x00A6u32),
"U+00A6 from non-default master should not be in cmap"
);
}
#[test]
fn hmtx_of_one() {
let result = TestCompile::compile_source("glyphs2/NotDef.glyphs");
let raw_hmtx = result.be_context.hmtx.get();
let hmtx = Hmtx::read(FontData::new(&raw_hmtx), 1).unwrap();
assert_eq!(
vec![(600, 250)],
hmtx.h_metrics()
.iter()
.map(|m| (m.advance.get(), m.side_bearing.get()))
.collect::<Vec<_>>()
);
assert!(hmtx.left_side_bearings().is_empty());
}
#[test]
fn metrics_and_limits_of_mono() {
let result = TestCompile::compile_source("glyphs2/Mono.glyphs");
let hhea = result.be_context.hhea.get();
assert_eq!(1, hhea.number_of_h_metrics);
assert_eq!(175, hhea.min_left_side_bearing.to_i16());
assert_eq!(50, hhea.min_right_side_bearing.to_i16());
assert_eq!(375, hhea.x_max_extent.to_i16());
assert_eq!(425, hhea.advance_width_max.to_u16());
let maxp = result.be_context.maxp.get();
assert_eq!(3, maxp.num_glyphs);
assert_eq!(Some(4), maxp.max_points);
assert_eq!(Some(1), maxp.max_contours);
let raw_hmtx = result.be_context.hmtx.get();
let hmtx = Hmtx::read(FontData::new(&raw_hmtx), hhea.number_of_h_metrics).unwrap();
assert_eq!(
vec![(425, 175)],
hmtx.h_metrics()
.iter()
.map(|m| (m.advance.get(), m.side_bearing.get()))
.collect::<Vec<_>>()
);
assert_eq!(
vec![200, 225],
hmtx.left_side_bearings()
.iter()
.map(|m| m.get())
.collect::<Vec<_>>()
);
}
#[test]
fn wght_var_has_expected_tables() {
let result = TestCompile::compile_source("wght_var.designspace");
let font = result.font();
assert_eq!(
vec![
Tag::new(b"GDEF"),
Tag::new(b"GPOS"),
Tag::new(b"GSUB"),
Tag::new(b"HVAR"),
Tag::new(b"MVAR"),
Tag::new(b"OS/2"),
Tag::new(b"STAT"),
Tag::new(b"cmap"),
Tag::new(b"fvar"),
Tag::new(b"glyf"),
Tag::new(b"gvar"),
Tag::new(b"head"),
Tag::new(b"hhea"),
Tag::new(b"hmtx"),
Tag::new(b"loca"),
Tag::new(b"maxp"),
Tag::new(b"name"),
Tag::new(b"post"),
],
font.table_directory
.table_records()
.iter()
.map(|tr| tr.tag())
.collect::<Vec<_>>()
);
}
#[test]
fn compile_mov_xy_and_move_around() {
let result = TestCompile::compile_source("mov_xy.designspace");
let font = result.font();
assert_eq!(
vec!["movx", "movy"],
font.fvar()
.unwrap()
.axes()
.unwrap()
.iter()
.map(|a| a.axis_tag.get().to_string())
.collect::<Vec<_>>()
);
assert_eq!(
vec![
Rect::new(50.0, 50.0, 150.0, 150.0), Rect::new(850.0, 50.0, 950.0, 150.0), Rect::new(50.0, 850.0, 150.0, 950.0), Rect::new(850.0, 850.0, 950.0, 950.0), ],
vec![
cbox_of_char(0x2e, &font, vec![0.0, 0.0]),
cbox_of_char(0x2e, &font, vec![1.0, 0.0]),
cbox_of_char(0x2e, &font, vec![0.0, 1.0]),
cbox_of_char(0x2e, &font, vec![1.0, 1.0]),
]
)
}
fn cbox_of_char(ch: u32, font: &FontRef, coords: Vec<f32>) -> Rect {
let gid = Charmap::new(font).map(ch).unwrap();
let mut bp = CboxPen::new();
let coords = coords
.into_iter()
.map(F2Dot14::from_f32)
.collect::<Vec<_>>();
font.outline_glyphs()
.get(gid)
.unwrap()
.draw(
DrawSettings::unhinted(Size::new(1000.0), coords.as_slice()),
&mut bp,
)
.unwrap();
bp.cbox().unwrap()
}
struct CboxPen {
last_move: Point,
bounds: Option<Rect>,
}
impl CboxPen {
fn new() -> Self {
Self {
last_move: Default::default(),
bounds: None,
}
}
fn cbox(self) -> Option<Rect> {
self.bounds
}
fn update_bounds(&mut self, x: f32, y: f32) {
let pt = Point::new(x as f64, y as f64);
self.bounds = Some(match self.bounds {
None => Rect::from_points(pt, pt),
Some(bounds) => bounds.union_pt(pt),
});
}
}
impl skrifa::outline::OutlinePen for CboxPen {
fn move_to(&mut self, x: f32, y: f32) {
self.last_move = Point::new(x as f64, y as f64);
self.update_bounds(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.update_bounds(x, y);
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.update_bounds(cx0, cy0);
self.update_bounds(x, y);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.update_bounds(cx0, cy0);
self.update_bounds(cx1, cy1);
self.update_bounds(x, y);
}
fn close(&mut self) {
}
}
fn axes(font: &FontRef) -> Vec<(Tag, f32, f32, f32)> {
font.fvar()
.unwrap()
.axes()
.unwrap()
.iter()
.map(|a| {
(
a.axis_tag(),
a.min_value.get().to_f64() as f32,
a.default_value.get().to_f64() as f32,
a.max_value.get().to_f64() as f32,
)
})
.collect::<Vec<_>>()
}
#[test]
fn compile_generates_notdef() {
let result = TestCompile::compile_source("glyphs2/WghtVar.glyphs");
assert!(
!result
.fe_context
.preliminary_glyph_order
.get()
.contains(&GlyphName::NOTDEF)
);
assert_eq!(
Some(GlyphId16::NOTDEF),
result
.fe_context
.glyph_order
.get()
.glyph_id(&GlyphName::NOTDEF)
);
let font = result.font();
assert_eq!(None, font.cmap().unwrap().map_codepoint(0u32));
}
#[test]
fn compile_glyphs_font_with_weight_axis() {
let result = TestCompile::compile_source("glyphs2/WghtVar.glyphs");
let font = result.font();
assert_eq!(vec![(Tag::new(b"wght"), 400.0, 400.0, 700.0)], axes(&font),);
assert_eq!(
vec![
GlyphId16::new(1),
GlyphId16::new(2),
GlyphId16::new(3),
GlyphId16::new(6),
],
[0x20, 0x21, 0x2d, 0x3d]
.iter()
.map(|cp| font.cmap().unwrap().map_codepoint(*cp as u32).unwrap())
.collect::<Vec<_>>()
);
}
#[test]
fn compile_glyphs_font_with_avar() {
let result = TestCompile::compile_source("glyphs2/WghtVar_Avar.glyphs");
let font = result.font();
assert_eq!(vec![(Tag::new(b"wght"), 300.0, 400.0, 700.0)], axes(&font),);
let axis_maps: Vec<_> = font
.avar()
.unwrap()
.axis_segment_maps()
.iter()
.map(|m| m.unwrap())
.map(|m| {
m.axis_value_maps()
.iter()
.map(|e| (e.from_coordinate().to_f32(), e.to_coordinate().to_f32()))
.collect::<Vec<_>>()
})
.collect();
assert_eq!(
vec![vec![
(-1.0, -1.0),
(0.0, 0.0),
(0.333313, 0.666687),
(1.0, 1.0)
]],
axis_maps
);
}
#[test]
fn instance_axis_location_overrides_master() {
let result =
TestCompile::compile_source("glyphs3/WghtVar_AxisLocation_InstanceOverride.glyphs");
let font = result.font();
let axis_maps: Vec<_> = font
.avar()
.unwrap()
.axis_segment_maps()
.iter()
.map(|m| m.unwrap())
.map(|m| {
m.axis_value_maps()
.iter()
.map(|e| (e.from_coordinate().to_f32(), e.to_coordinate().to_f32()))
.collect::<Vec<_>>()
})
.collect();
assert_eq!(
vec![vec![
(-1.0, -1.0),
(0.0, 0.0),
(0.777771, 0.7555542),
(1.0, 1.0)
]],
axis_maps
);
}
#[test]
fn compile_writes_only_the_font() {
let result = TestCompile::compile_source("glyphs2/WghtVar.glyphs");
let outputs = fs::read_dir(result.temp.path())
.unwrap()
.map(|e| {
e.unwrap()
.path()
.strip_prefix(result.temp.path())
.unwrap()
.to_owned()
})
.collect::<Vec<_>>();
assert_eq!(vec![Path::new("font.ttf")], outputs);
}
fn strip_fontc_version_tag(s: &str) -> &str {
s.split_once(";fontc ").map_or(s, |(head, _)| head)
}
fn resolve_name(name: &Name, id: NameId) -> Option<String> {
name.name_record().iter().find_map(|nr| {
(nr.name_id() == id).then(|| {
let resolved = nr
.string(name.string_data())
.unwrap()
.chars()
.collect::<String>();
strip_fontc_version_tag(&resolved).to_string()
})
})
}
#[test]
fn standard_axis_names() {
let result = TestCompile::compile_source("glyphs3/StandardAxisNames.glyphs");
let static_metadata = result.fe_context.static_metadata.get();
assert_eq!(
vec![
"weight".to_string(),
"width".to_string(),
"italic".to_string(),
"slant".to_string(),
"optical".to_string(),
"foobarbaz".to_string(),
],
static_metadata
.axes
.iter()
.map(|axis| axis.name.clone())
.collect::<Vec<_>>()
);
let font = result.font();
let name = font.name().unwrap();
let fvar = font.fvar().unwrap();
assert_eq!(
vec![
"Weight".to_string(),
"Width".to_string(),
"Italic".to_string(),
"Slant".to_string(),
"Optical Size".to_string(),
"foobarbaz".to_string(),
],
fvar.axes()
.unwrap()
.iter()
.map(|axis| resolve_name(&name, axis.axis_name_id()).unwrap())
.collect::<Vec<_>>()
)
}
#[test]
fn custom_english_ui_label_name() {
let result = TestCompile::compile_source("wght_var_axis_label_names.designspace");
let static_metadata = result.fe_context.static_metadata.get();
assert_eq!(
vec!["weight".to_string()],
static_metadata
.axes
.iter()
.map(|axis| axis.name.clone())
.collect::<Vec<_>>()
);
let font = result.font();
let name = font.name().unwrap();
let fvar = font.fvar().unwrap();
assert_eq!(
vec!["Heaviness".to_string()],
fvar.axes()
.unwrap()
.iter()
.map(|axis| resolve_name(&name, axis.axis_name_id()).unwrap())
.collect::<Vec<_>>()
);
let stat = font.stat().unwrap();
assert_eq!(
vec!["Heaviness".to_string()],
stat.design_axes()
.unwrap()
.iter()
.map(|axis| resolve_name(&name, axis.axis_name_id()).unwrap())
.collect::<Vec<_>>()
)
}
fn assert_named_instances(source: &str, expected: Vec<(String, Vec<(&str, f64)>)>) {
let result = TestCompile::compile_source(source);
let font = result.font();
let name = font.name().unwrap();
let fvar = font.fvar().unwrap();
let axis_names: Vec<_> = fvar
.axes()
.unwrap()
.iter()
.map(|axis| resolve_name(&name, axis.axis_name_id()).unwrap())
.collect();
let instances = fvar.instances().unwrap();
assert_eq!(
expected,
instances
.iter()
.map(|i| i.unwrap())
.map(|inst| (
resolve_name(&name, inst.subfamily_name_id)
.unwrap_or_else(|| format!("{:?}", inst.subfamily_name_id)),
inst.coordinates
.iter()
.enumerate()
.map(|(i, coord)| { (axis_names[i].as_str(), coord.get().to_f64()) })
.collect(),
))
.collect::<Vec<_>>()
);
}
#[test]
fn compile_named_instances_from_glyphs() {
assert_named_instances(
"glyphs3/WghtVar_Instances.glyphs",
vec![
("Regular".to_string(), vec![("Weight", 400.0)]),
("Bold".to_string(), vec![("Weight", 700.0)]),
],
);
}
#[test]
fn compile_named_instances_from_designspace() {
assert_named_instances(
"wght_var.designspace",
vec![
("Regular".to_string(), vec![("Weight", 400.0)]),
("Bold".to_string(), vec![("Weight", 700.0)]),
],
);
}
#[test]
fn compile_named_instances_from_designspace_with_stylename_attrs() {
assert_named_instances(
"wght_var_instance_stylename.designspace",
vec![
("Foo".to_string(), vec![("Weight", 400.0)]),
("Bar".to_string(), vec![("Weight", 700.0)]),
],
);
}
fn assert_fs_selection(source: &str, expected: SelectionFlags) {
let result = TestCompile::compile_source(source);
let font = result.font();
let os2 = font.os2().unwrap();
assert_eq!(expected, os2.fs_selection());
}
#[test]
fn wght_var_fs_selection() {
assert_fs_selection(
"glyphs3/WghtVar.glyphs",
SelectionFlags::REGULAR | SelectionFlags::USE_TYPO_METRICS | SelectionFlags::WWS,
);
}
#[test]
fn static_bold_italic_fs_selection() {
assert_fs_selection(
"glyphs3/StaticBoldItalic.glyphs",
SelectionFlags::BOLD | SelectionFlags::ITALIC,
);
}
#[test]
fn populates_unicode_range() {
let result = TestCompile::compile_source("static.designspace");
let font = result.font();
let os2 = font.os2().unwrap();
assert_eq!(
(1, 1 << 6, 0, 0),
(
os2.ul_unicode_range_1(),
os2.ul_unicode_range_2(),
os2.ul_unicode_range_3(),
os2.ul_unicode_range_4()
),
"Should set bit 1, Latin-1 Supplement and bit 38, Math Operators",
);
}
#[test]
fn prefers_explicit_os_x_class() {
let result = TestCompile::compile_source("fontinfo.designspace");
let font = result.font();
let os2 = font.os2().unwrap();
assert_eq!((800, 8), (os2.us_weight_class(), os2.us_width_class(),),);
}
#[test]
fn os2_superscript_reuses_explicit_subscript() {
let result = TestCompile::compile_source("glyphs2/SuperscriptCustomParamFallback.glyphs");
let font = result.font();
let os2 = font.os2().unwrap();
assert_eq!(os2.y_superscript_x_size(), 701);
assert_eq!(os2.y_superscript_y_size(), 659);
assert_eq!(os2.y_superscript_y_offset(), 350); }
#[test]
fn glyphs_app_ir_categories() {
let result = TestCompile::compile_source("glyphs3/Oswald-glyph-categories.glyphs");
let categories = &result.fe_context.gdef_categories.get().categories;
assert_eq!(categories.get("a"), Some(&GlyphClassDef::Base));
assert_eq!(categories.get("acutecomb"), Some(&GlyphClassDef::Mark));
assert_eq!(categories.get("brevecomb"), Some(&GlyphClassDef::Mark));
assert_eq!(
categories.get("brevecomb_acutecomb"),
Some(&GlyphClassDef::Mark)
);
assert_eq!(categories.get("f_a"), Some(&GlyphClassDef::Ligature));
assert_eq!(categories.get(".notdef"), None);
assert_eq!(categories.get("U1C82"), Some(&GlyphClassDef::Base));
}
#[test]
fn mark_spacing_glyph_not_classified_as_base() {
let result = TestCompile::compile_source("glyphs3/MarkSpacingGdef.glyphs");
let categories = &result.fe_context.gdef_categories.get().categories;
assert_eq!(categories.get("tildecomb"), Some(&GlyphClassDef::Mark));
assert_eq!(categories.get("tilde"), None);
}
#[test]
fn glyphs_app_bracket_glyph_categories() {
let result = TestCompile::compile_source("glyphs3/PropagateAnchorsTest.glyphs");
let categories = &result.fe_context.gdef_categories.get().categories;
assert_eq!(
categories.get("A.BRACKET.varAlt01"),
Some(&GlyphClassDef::Base)
);
assert_eq!(
categories.get("Aacute.BRACKET.varAlt01"),
Some(&GlyphClassDef::Base)
);
assert_eq!(categories.get(".notdef"), None);
}
fn build_master_id_to_location<'a>(
expected: &'a glyphs_reader::Font,
static_metadata: &ir::StaticMetadata,
) -> std::collections::HashMap<&'a str, NormalizedLocation> {
use fontdrasil::coords::{DesignCoord, DesignLocation};
expected
.masters
.iter()
.map(|master| {
let design_loc: DesignLocation = expected
.axes
.iter()
.zip(&master.axes_values)
.map(|(axis, &value)| {
let tag: fontdrasil::types::Tag = axis.tag.parse().expect("valid tag");
(tag, DesignCoord::new(value.into_inner()))
})
.collect();
let normalized = design_loc
.to_normalized(&static_metadata.all_source_axes)
.expect("test coordinates should be valid");
(master.id.as_str(), normalized)
})
.collect()
}
fn assert_anchors_match(
our_anchors: &ir::GlyphAnchors,
expected_anchors: &[glyphs_reader::Anchor],
glyph_name: &str,
location: &NormalizedLocation,
) {
assert_eq!(
our_anchors.anchors.len(),
expected_anchors.len(),
"Anchor count mismatch for glyph '{glyph_name}'"
);
for (our_anchor, expected_anchor) in our_anchors.anchors.iter().zip(expected_anchors) {
assert_eq!(
our_anchor.kind.to_name(),
expected_anchor.name,
"Anchor name mismatch for glyph '{glyph_name}'"
);
assert_eq!(
our_anchor.positions.get(location),
Some(&expected_anchor.pos),
"Anchor '{}' position mismatch for glyph '{glyph_name}'",
expected_anchor.name
);
}
}
#[test]
fn propagate_anchors_real_files() {
use glyphs_reader::Font;
let result = TestCompile::compile_source("glyphs3/PropagateAnchorsTest.glyphs");
let expected = Font::load(
&crate::testdata_dir().join("glyphs3/PropagateAnchorsTest-propagated.glyphs"),
)
.expect("Failed to load reference file");
let static_metadata = result.fe_context.static_metadata.get();
let master_id_to_location = build_master_id_to_location(&expected, &static_metadata);
for (glyph_name, expected_glyph) in &expected.glyphs {
if !expected_glyph.has_components() {
continue;
}
let has_expected_anchors = expected_glyph.layers.iter().any(|l| {
master_id_to_location.contains_key(l.layer_id.as_str()) && !l.anchors.is_empty()
});
let our_anchors = result
.fe_context
.anchors
.try_get(&FeWorkIdentifier::Anchor(glyph_name.as_str().into()));
if has_expected_anchors && our_anchors.is_none() {
panic!(
"Missing anchors for composite '{glyph_name}' which has expected anchors in reference"
);
}
let Some(our_anchors) = our_anchors else {
continue; };
for expected_layer in &expected_glyph.layers {
if expected_layer.anchors.is_empty() {
continue;
}
let Some(location) = master_id_to_location.get(expected_layer.layer_id.as_str())
else {
continue; };
assert_anchors_match(&our_anchors, &expected_layer.anchors, glyph_name, location);
}
}
}
#[test]
fn dont_propagate_anchors() {
let result = TestCompile::compile_source("glyphs2/DontPropagateAnchors.glyphs");
let anchors = result
.fe_context
.anchors
.try_get(&FeWorkIdentifier::Anchor("Aacute".into()));
assert!(
anchors.is_none() || anchors.unwrap().anchors.is_empty(),
"Aacute should have no anchors when propagateAnchors is disabled"
);
assert!(
!result.fe_context.flags.contains(Flags::PROPAGATE_ANCHORS),
"PROPAGATE_ANCHORS flag should not be set"
);
}
#[test]
fn propagate_anchors_through_single_layer_smart_component() {
let result = TestCompile::compile_source("glyphs3/SmartComponentSingleLayerAnchors.glyphs");
let anchors = result
.fe_context
.anchors
.try_get(&FeWorkIdentifier::Anchor("composite".into()))
.expect("composite should have propagated anchors");
let bottom = anchors
.anchors
.iter()
.find(|a| a.original_name == "bottom")
.unwrap_or_else(|| {
let names: Vec<_> = anchors.anchors.iter().map(|a| &a.original_name).collect();
panic!("composite should have a 'bottom' anchor (renamed from base's 'top'), found: {names:?}")
});
let pos = bottom.default_pos();
assert_eq!(
(pos.x as i32, pos.y as i32),
(217, 329),
"anchor should be transformed through the full component chain"
);
}
#[test]
fn propagate_anchors_bracket_layers() {
use glyphs_reader::Font;
let result = TestCompile::compile_source("glyphs3/PropagateAnchorsTest.glyphs");
let expected = Font::load(
&crate::testdata_dir().join("glyphs3/PropagateAnchorsTest-propagated.glyphs"),
)
.expect("Failed to load reference file");
let static_metadata = result.fe_context.static_metadata.get();
let master_id_to_location = build_master_id_to_location(&expected, &static_metadata);
let expected_glyph = expected.glyphs.get("Aacute").expect("Aacute should exist");
assert_eq!(
expected_glyph.bracket_layers.len(),
2,
"Aacute should have 2 bracket layers (one per master)"
);
let our_anchors = result
.fe_context
.anchors
.try_get(&FeWorkIdentifier::Anchor("Aacute.BRACKET.varAlt01".into()))
.expect("Aacute.BRACKET.varAlt01 should have anchors");
for bracket_layer in &expected_glyph.bracket_layers {
let master_id = bracket_layer
.associated_master_id
.as_deref()
.expect("bracket layer should have associated master");
let location = master_id_to_location
.get(master_id)
.expect("master id should map to a location");
assert_anchors_match(
&our_anchors,
&bracket_layer.anchors,
"Aacute.BRACKET.varAlt01",
location,
);
}
}
#[test]
fn propagate_anchors_bracket_only_components() {
let result = TestCompile::compile_source("glyphs2/AlumniSans-wononly.glyphs");
let static_metadata = result.fe_context.static_metadata.get();
let anchors = result
.fe_context
.anchors
.try_get(&FeWorkIdentifier::Anchor("won.BRACKET.varAlt01".into()))
.expect("won.BRACKET.varAlt01 should have anchors");
assert_eq!(
anchors.anchors.len(),
2,
"won.BRACKET.varAlt01 should have exactly 2 anchors"
);
for anchor in &anchors.anchors {
assert_eq!(
anchor.positions.len(),
static_metadata.variation_model.locations().count(),
"anchor '{}' should have position at each master",
anchor.kind.to_name()
);
}
}
#[test]
fn ufo_app_ir_categories_matches_glyphs() {
let glyphs = TestCompile::compile_source("glyphs3/Oswald-glyph-categories.glyphs");
let ufo = TestCompile::compile_source("Oswald-glyph-categories/Oswald-Regular.designspace");
assert_eq!(
glyphs.fe_context.gdef_categories.get().categories,
ufo.fe_context.gdef_categories.get().categories
)
}
#[test]
fn captures_vendor_id() {
let result = TestCompile::compile_source("glyphs3/TheBestNames.glyphs");
let font = result.font();
let os2 = font.os2().unwrap();
assert_eq!(Tag::new(b"RODS"), os2.ach_vend_id());
}
fn assert_maxp_composite(
src: &str,
max_component_depth: u16,
max_composite_points: u16,
max_composite_contours: u16,
) {
let result = TestCompile::compile_source(src);
let font = result.font();
let maxp = font.maxp().unwrap();
assert_eq!(
(
max_component_depth,
max_composite_points,
max_composite_contours
),
(
maxp.max_component_depth().unwrap(),
maxp.max_composite_points().unwrap(),
maxp.max_composite_contours().unwrap()
)
);
}
#[test]
fn maxp_with_no_components() {
assert_maxp_composite("glyphs3/StaticBoldItalic.glyphs", 0, 0, 0);
}
#[test]
fn maxp_with_shallow_components() {
assert_maxp_composite("glyphs2/Component.glyphs", 1, 8, 2);
}
#[test]
fn maxp_with_deep_components() {
assert_maxp_composite("glyphs3/NestedComponent.glyphs", 3, 12, 3);
}
fn assert_created_set(source: &str) {
let result = TestCompile::compile_source(source);
let font = result.font();
let head = font.head().unwrap();
let mac_epoch = Utc.with_ymd_and_hms(1904, 1, 1, 0, 0, 0).unwrap();
let created = mac_epoch
.checked_add_signed(Duration::try_seconds(head.created().as_secs()).unwrap())
.unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2023, 5, 5, 15, 11, 55).unwrap(),
created
);
}
#[test]
fn captures_created_from_glyphs() {
assert_created_set("glyphs3/Dated.glyphs");
}
#[test]
fn captures_created_from_designspace() {
assert_created_set("wght_var.designspace");
}
#[test]
fn generates_stat() {
let result = TestCompile::compile_source("wght_var.designspace");
let font = result.font();
let name = font.name().unwrap();
let stat = font.stat().unwrap();
assert_eq!(
("Regular", vec![Tag::new(b"wght"),]),
(
resolve_name(&name, stat.elided_fallback_name_id().unwrap())
.unwrap()
.as_str(),
stat.design_axes()
.unwrap()
.iter()
.map(|ar| ar.axis_tag())
.collect::<Vec<_>>(),
)
);
}
fn assert_simple_kerning(source: &str) {
let result = TestCompile::compile_source(source);
let kerning_locations = result.fe_context.kerning_locations.get();
let expected_groups = vec![
(
KernGroup::Side1("bracketleft_R".into()),
vec!["bracketleft"],
),
(
KernGroup::Side1("bracketright_R".into()),
vec!["bracketright"],
),
(
KernGroup::Side2("bracketleft_L".into()),
vec!["bracketleft"],
),
(
KernGroup::Side2("bracketright_L".into()),
vec!["bracketright"],
),
];
let wght = Tag::new(b"wght");
let mut kerns: HashMap<KernPair, Vec<(String, f64)>> = HashMap::new();
for kern_loc in kerning_locations.locations.iter() {
assert_eq!(
vec![wght],
kern_loc.axis_tags().cloned().collect::<Vec<_>>()
);
let kerns_at = result
.fe_context
.kerning_at
.get(&FeWorkIdentifier::KernInstance(kern_loc.clone()));
let mut groups: Vec<_> = kerns_at
.groups
.iter()
.map(|(name, entries)| {
let mut entries: Vec<_> = entries.iter().map(|e| e.as_str()).collect();
entries.sort();
(name.to_owned(), entries)
})
.collect();
groups.sort();
assert_eq!(groups, expected_groups, "groups at {kern_loc:?}");
for (pair, adjustment) in kerns_at.kerns.iter() {
kerns.entry(pair.clone()).or_default().push((
format!(
"wght {}",
kern_loc.iter().map(|(_, v)| *v).next().unwrap().to_f64()
),
adjustment.0,
));
}
}
let mut kerns: Vec<_> = kerns
.into_iter()
.map(|((left, right), mut values)| {
values.sort_by_key(|(s, _)| s.clone());
(left, right, values)
})
.collect();
kerns.sort_by_key(|(left, right, _)| (left.clone(), right.clone()));
assert_eq!(
kerns,
vec![
(
KernSide::Glyph("bracketleft".into()),
KernSide::Glyph("bracketright".into()),
vec![
("wght 0".to_string(), -300.0),
("wght 1".to_string(), -150.0)
],
),
(
KernSide::Glyph("exclam".into()),
KernSide::Glyph("exclam".into()),
vec![
("wght 0".to_string(), -360.0),
("wght 1".to_string(), -100.0)
],
),
(
KernSide::Glyph("exclam".into()),
KernSide::Glyph("hyphen".into()),
vec![("wght 0".to_string(), 20.0),],
),
(
KernSide::Glyph("exclam".into()),
KernSide::Group(KernGroup::Side2("bracketright_L".into())),
vec![("wght 0".to_string(), -160.0),],
),
(
KernSide::Glyph("hyphen".into()),
KernSide::Glyph("hyphen".into()),
vec![
("wght 0".to_string(), -150.0),
("wght 1".to_string(), -50.0)
],
),
(
KernSide::Group(KernGroup::Side1("bracketleft_R".into())),
KernSide::Glyph("exclam".into()),
vec![("wght 0".to_string(), -165.0),],
),
],
);
}
#[test]
fn kerning_from_glyphs() {
assert_simple_kerning("glyphs3/WghtVar.glyphs");
}
#[test]
fn kerning_from_ufo() {
assert_simple_kerning("designspace_from_glyphs/WghtVar.designspace");
}
#[test]
fn partial_master_kern_exception_is_resolved_per_cell() {
let result = TestCompile::compile_source("PartialKernException.designspace");
assert_eq!(
dump_kern_adjustments(&result),
vec![
"iotadasiaoxia iotadasiaoxia: [30, 50]",
"iotadasiaoxia iotapsilioxia: [30, 30]",
"qhwee-ethiopic ca-ethiopic: [40, 30]",
"qhwee-ethiopic cee-ethiopic: [30, 30]",
"uoA uoX: [30, 70]",
"uoA uoY: [30, 70]",
"@side1.GRK_iotaRight iotapsilioxia: [20, 30]",
"@side1.ethi_qhee ca-ethiopic: [40, 50]",
"@side1.uoLeft uoX: [30, 50]",
"@side1.uoLeft uoY: [30, 50]",
"@side1.GRK_iotaRight @side2.GRK_iotaLeft: [60, 50]",
"@side1.ethi_qhee @side2.ethi_ca: [30, 50]",
"@side1.uoLeft @side2.uoRight: [50, 50]",
]
);
}
fn dump_kern_adjustments(result: &TestCompile) -> Vec<String> {
let all_kerns = result.be_context.all_kerning_pairs.get();
let mut kerns: Vec<(KernPair, Vec<(f64, f64)>)> = all_kerns
.adjustments
.iter()
.map(|(pair, adj)| {
let mut values: Vec<(f64, f64)> = adj
.iter()
.map(|(loc, val)| (loc.iter().map(|(_, v)| v.to_f64()).next().unwrap(), val.0))
.collect();
values.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
(pair.clone(), values)
})
.collect();
kerns.sort_by(|(a, _), (b, _)| {
(a.0.is_group(), a.1.is_group(), a).cmp(&(b.0.is_group(), b.1.is_group(), b))
});
kerns
.iter()
.map(|((first, second), values)| {
let values = values
.iter()
.map(|(_, val)| val.to_string())
.collect::<Vec<_>>()
.join(", ");
format!("{first} {second}: [{values}]")
})
.collect()
}
fn dump_kern_groups(result: &TestCompile) -> Vec<String> {
let all_kerns = result.be_context.all_kerning_pairs.get();
let glyph_order = result.fe_context.glyph_order.get();
all_kerns
.groups
.iter()
.map(|(group, glyphs)| {
let glyphs = glyphs
.iter()
.map(|gid| {
glyph_order
.glyph_name(gid.to_u32() as usize)
.unwrap()
.to_string()
})
.collect::<Vec<_>>()
.join(" ");
format!("@{group} = [{glyphs}]")
})
.collect()
}
type KernGroupsDsl<'a> = &'a [(&'a str, &'a [&'a str])];
type KernPairsDsl<'a> = &'a [(&'a str, &'a [(&'a str, i32)])];
#[derive(Clone, Copy)]
struct KernMaster<'a> {
style: &'a str,
xvalue: u32,
groups: KernGroupsDsl<'a>,
kerning: Option<KernPairsDsl<'a>>,
}
fn kern_plist(body: String) -> String {
format!(
"<?xml version='1.0' encoding='UTF-8'?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n{body}</plist>\n"
)
}
fn kern_groups_plist(groups: KernGroupsDsl) -> String {
let mut body = String::from(" <dict>\n");
for (name, members) in groups {
body.push_str(&format!(" <key>{name}</key>\n <array>\n"));
for member in *members {
body.push_str(&format!(" <string>{member}</string>\n"));
}
body.push_str(" </array>\n");
}
body.push_str(" </dict>\n");
kern_plist(body)
}
fn kern_kerning_plist(pairs: KernPairsDsl) -> String {
let mut body = String::from(" <dict>\n");
for (side1, entries) in pairs {
body.push_str(&format!(" <key>{side1}</key>\n <dict>\n"));
for (side2, value) in *entries {
body.push_str(&format!(
" <key>{side2}</key>\n <integer>{value}</integer>\n"
));
}
body.push_str(" </dict>\n");
}
body.push_str(" </dict>\n");
kern_plist(body)
}
fn kern_glif(name: &str, codepoint: Option<u32>) -> String {
let unicode = codepoint
.map(|cp| format!(" <unicode hex=\"{cp:04X}\"/>\n"))
.unwrap_or_default();
format!(
"<?xml version='1.0' encoding='UTF-8'?>\n\
<glyph name=\"{name}\" format=\"2\">\n\
\x20 <advance width=\"600\"/>\n\
{unicode}\
\x20 <outline>\n\
\x20 <contour>\n\
\x20 <point x=\"50\" y=\"0\" type=\"line\"/>\n\
\x20 <point x=\"550\" y=\"0\" type=\"line\"/>\n\
\x20 <point x=\"550\" y=\"700\" type=\"line\"/>\n\
\x20 <point x=\"50\" y=\"700\" type=\"line\"/>\n\
\x20 </contour>\n\
\x20 </outline>\n\
</glyph>\n"
)
}
fn write_kern_fixture(
dir: &Path,
family: &str,
glyphs: &[&str],
masters: &[KernMaster],
) -> PathBuf {
for master in masters {
let ufo = dir.join(format!("{family}-{}.ufo", master.style));
let glyphs_dir = ufo.join("glyphs");
fs::create_dir_all(&glyphs_dir).unwrap();
fs::write(
ufo.join("metainfo.plist"),
kern_plist(
" <dict>\n\
\x20 <key>creator</key>\n\
\x20 <string>com.github.fonttools.ufoLib</string>\n\
\x20 <key>formatVersion</key>\n\
\x20 <integer>3</integer>\n\
\x20 </dict>\n"
.into(),
),
)
.unwrap();
fs::write(
ufo.join("layercontents.plist"),
kern_plist(
" <array>\n\
\x20 <array>\n\
\x20 <string>public.default</string>\n\
\x20 <string>glyphs</string>\n\
\x20 </array>\n\
\x20 </array>\n"
.into(),
),
)
.unwrap();
fs::write(
ufo.join("fontinfo.plist"),
kern_plist(format!(
" <dict>\n\
\x20 <key>ascender</key>\n\
\x20 <integer>800</integer>\n\
\x20 <key>capHeight</key>\n\
\x20 <integer>700</integer>\n\
\x20 <key>descender</key>\n\
\x20 <integer>-200</integer>\n\
\x20 <key>familyName</key>\n\
\x20 <string>{family}</string>\n\
\x20 <key>styleName</key>\n\
\x20 <string>{}</string>\n\
\x20 <key>unitsPerEm</key>\n\
\x20 <integer>1000</integer>\n\
\x20 <key>xHeight</key>\n\
\x20 <integer>500</integer>\n\
\x20 </dict>\n",
master.style
)),
)
.unwrap();
let mut order = String::from(
" <dict>\n\
\x20 <key>public.glyphOrder</key>\n\
\x20 <array>\n\
\x20 <string>.notdef</string>\n",
);
for glyph in glyphs {
order.push_str(&format!(" <string>{glyph}</string>\n"));
}
order.push_str(" </array>\n </dict>\n");
fs::write(ufo.join("lib.plist"), kern_plist(order)).unwrap();
let mut contents = String::from(
" <dict>\n <key>.notdef</key>\n <string>_notdef.glif</string>\n",
);
for glyph in glyphs {
contents.push_str(&format!(
" <key>{glyph}</key>\n <string>{glyph}_.glif</string>\n"
));
}
contents.push_str(" </dict>\n");
fs::write(glyphs_dir.join("contents.plist"), kern_plist(contents)).unwrap();
fs::write(glyphs_dir.join("_notdef.glif"), kern_glif(".notdef", None)).unwrap();
for glyph in glyphs {
let codepoint = glyph.chars().next().unwrap() as u32;
fs::write(
glyphs_dir.join(format!("{glyph}_.glif")),
kern_glif(glyph, Some(codepoint)),
)
.unwrap();
}
fs::write(ufo.join("groups.plist"), kern_groups_plist(master.groups)).unwrap();
if let Some(pairs) = master.kerning {
fs::write(ufo.join("kerning.plist"), kern_kerning_plist(pairs)).unwrap();
}
}
let mut sources = String::new();
for master in masters {
sources.push_str(&format!(
" <source filename=\"{family}-{style}.ufo\" \
familyname=\"{family}\" stylename=\"{style}\">\n\
\x20 <location>\n\
\x20 <dimension name=\"Weight\" xvalue=\"{x}\"/>\n\
\x20 </location>\n\
\x20 </source>\n",
style = master.style,
x = master.xvalue
));
}
let designspace = dir.join(format!("{family}.designspace"));
fs::write(
&designspace,
format!(
"<?xml version='1.0' encoding='UTF-8'?>\n\
<designspace format=\"5.0\">\n\
\x20 <axes>\n\
\x20 <axis tag=\"wght\" name=\"Weight\" minimum=\"0\" \
maximum=\"1000\" default=\"0\"/>\n\
\x20 </axes>\n\
\x20 <sources>\n\
{sources}\x20 </sources>\n\
</designspace>\n"
),
)
.unwrap();
designspace
}
const DIVERGENT_GLYPHS: &[&str] = &["A", "B", "C", "D", "E", "H", "T", "W", "X", "Y", "Z"];
const DIVERGENT_REGULAR: KernMaster = KernMaster {
style: "Regular",
xvalue: 0,
groups: &[
("public.kern1.in_both", &["C", "D", "E"]),
("public.kern2.in_both", &["X", "W", "Y"]),
],
kerning: Some(&[
("A", &[("public.kern2.in_both", 100)]),
(
"public.kern1.in_both",
&[("T", 200), ("public.kern2.in_both", 300)],
),
]),
};
const DIVERGENT_BOLD: KernMaster = KernMaster {
style: "Bold",
xvalue: 1000,
groups: &[
("public.kern1.bold_only_e", &["E"]),
("public.kern1.in_both", &["C", "D", "H"]),
("public.kern2.bold_only_y", &["Y"]),
("public.kern2.bold_only_z", &["Z"]),
("public.kern2.in_both", &["X", "W"]),
],
kerning: Some(&[
(
"A",
&[
("public.kern2.bold_only_y", -50),
("public.kern2.in_both", 100),
],
),
("B", &[("public.kern2.bold_only_z", -30)]),
(
"public.kern1.bold_only_e",
&[("T", -60), ("public.kern2.bold_only_y", -10)],
),
(
"public.kern1.in_both",
&[("T", 200), ("public.kern2.in_both", 300)],
),
]),
};
#[test]
fn divergent_kern_groups_resolved_against_each_master() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let tmp = tempdir().unwrap();
let designspace = write_kern_fixture(
tmp.path(),
"DivergentKernGroups",
DIVERGENT_GLYPHS,
&[DIVERGENT_REGULAR, DIVERGENT_BOLD],
);
let result = TestCompile::compile_source(designspace.to_str().unwrap());
let kerns = dump_kern_adjustments(&result);
assert_eq!(
kerns,
vec![
"A W: [100, 100]".to_string(),
"A X: [100, 100]".to_string(),
"A Y: [100, -50]".to_string(),
"B Z: [0, -30]".to_string(),
"@side1.bold_only_e T: [200, -60]".to_string(),
"@side1.in_both T: [200, 200]".to_string(),
"@side1.in_both_1 T: [0, 200]".to_string(),
"@side1.bold_only_e @side2.bold_only_y: [300, -10]".to_string(),
"@side1.bold_only_e @side2.in_both: [300, 0]".to_string(),
"@side1.in_both @side2.bold_only_y: [300, 0]".to_string(),
"@side1.in_both @side2.in_both: [300, 300]".to_string(),
"@side1.in_both_1 @side2.in_both: [0, 300]".to_string(),
]
);
assert_eq!(
dump_kern_groups(&result),
vec![
"@side1.bold_only_e = [E]".to_string(),
"@side1.in_both = [C D]".to_string(),
"@side1.in_both_1 = [H]".to_string(),
"@side2.bold_only_y = [Y]".to_string(),
"@side2.bold_only_z = [Z]".to_string(),
"@side2.in_both = [W X]".to_string(),
]
);
}
const DIVERGENT_KERNLESS_MEDIUM: KernMaster = KernMaster {
style: "Medium",
xvalue: 500,
groups: &[
("public.kern1.in_both", &["C"]),
("public.kern1.medium_only_d", &["D"]),
("public.kern2.in_both", &["X", "Y"]),
("public.kern2.medium_only_w", &["W"]),
],
kerning: None,
};
#[test]
fn kernless_master_divergent_groups_do_not_reach_reconciliation() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let tmp = tempdir().unwrap();
let two = write_kern_fixture(
&tmp.path().join("two"),
"DivergentKernGroups",
DIVERGENT_GLYPHS,
&[DIVERGENT_REGULAR, DIVERGENT_BOLD],
);
let three = write_kern_fixture(
&tmp.path().join("three"),
"DivergentKernGroups",
DIVERGENT_GLYPHS,
&[DIVERGENT_REGULAR, DIVERGENT_KERNLESS_MEDIUM, DIVERGENT_BOLD],
);
let two_master = TestCompile::compile_source(two.to_str().unwrap());
let with_kernless = TestCompile::compile_source(three.to_str().unwrap());
assert_eq!(
dump_kern_adjustments(&with_kernless),
dump_kern_adjustments(&two_master),
"kernless divergent-groups master must not change the reconciled adjustments"
);
assert_eq!(
dump_kern_groups(&with_kernless),
dump_kern_groups(&two_master),
"kernless divergent-groups master must not change the reconciled classes"
);
}
#[test]
fn regrouped_kern_groups_refine_by_kerned_signature() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let tmp = tempdir().unwrap();
let designspace = write_kern_fixture(
tmp.path(),
"RegroupedKernGroups",
&["C", "D", "G", "P", "Q", "U", "V", "W"],
&[
KernMaster {
style: "Regular",
xvalue: 0,
groups: &[
("public.kern1.in_both", &["C", "D"]),
("public.kern2.in_both", &["P", "Q", "U", "V", "W"]),
("public.kern2.regular_only_g", &["G"]),
],
kerning: Some(&[(
"public.kern1.in_both",
&[
("public.kern2.in_both", 100),
("public.kern2.regular_only_g", 0),
],
)]),
},
KernMaster {
style: "Bold",
xvalue: 1000,
groups: &[
("public.kern1.in_both", &["C", "D"]),
("public.kern2.bold_only_pq", &["P", "Q"]),
("public.kern2.bold_unkerned_u", &["U"]),
("public.kern2.bold_unkerned_v", &["V"]),
("public.kern2.in_both", &["W"]),
],
kerning: Some(&[(
"public.kern1.in_both",
&[
("P", 5),
("public.kern2.bold_only_pq", -40),
("public.kern2.in_both", 150),
],
)]),
},
],
);
let result = TestCompile::compile_source(designspace.to_str().unwrap());
assert_eq!(
dump_kern_adjustments(&result),
vec![
"@side1.in_both P: [100, 5]".to_string(),
"@side1.in_both @side2.bold_only_pq: [100, -40]".to_string(),
"@side1.in_both @side2.in_both: [100, 150]".to_string(),
"@side1.in_both @side2.in_both_1: [100, 0]".to_string(),
]
);
assert_eq!(
dump_kern_groups(&result),
vec![
"@side1.in_both = [C D]".to_string(),
"@side2.bold_only_pq = [P Q]".to_string(),
"@side2.bold_unkerned_u = [U]".to_string(),
"@side2.bold_unkerned_v = [V]".to_string(),
"@side2.in_both = [W]".to_string(),
"@side2.in_both_1 = [U V]".to_string(),
"@side2.regular_only_g = [G]".to_string(),
]
);
}
fn assert_intermediate_layer(src: &str) {
let result = TestCompile::compile_source(src);
let font = result.font();
assert_eq!(
vec!["CPHT", "wght"],
font.fvar()
.unwrap()
.axes()
.unwrap()
.iter()
.map(|a| a.axis_tag().to_string())
.collect::<Vec<_>>()
);
assert_eq!(
vec![
Rect::new(239.0, 0.0, 364.0, 705.0),
Rect::new(213.0, 0.0, 388.0, 737.0),
Rect::new(191.0, 0.0, 410.0, 761.0),
],
vec![
cbox_of_char(0x69, &font, vec![0.0, 0.0]), cbox_of_char(0x69, &font, vec![0.0, 0.6]), cbox_of_char(0x69, &font, vec![0.0, 1.0]), ]
);
assert_eq!(
vec![
Rect::new(231.0, 0.0, 364.0, 700.0),
Rect::new(195.0, 0.0, 400.0, 700.0),
Rect::new(171.0, 0.0, 424.0, 700.0),
],
vec![
cbox_of_char(0x49, &font, vec![0.0, 0.0]), cbox_of_char(0x49, &font, vec![0.0, 0.6]), cbox_of_char(0x49, &font, vec![0.0, 1.0]), ]
);
assert_eq!(
vec![
Rect::new(231.0, 0.0, 364.0, 600.0),
Rect::new(171.0, 0.0, 424.0, 600.0),
Rect::new(231.0, 0.0, 364.0, 800.0),
Rect::new(171.0, 0.0, 424.0, 800.0),
],
vec![
cbox_of_char(0x49, &font, vec![-1.0, 0.0]), cbox_of_char(0x49, &font, vec![-1.0, 1.0]), cbox_of_char(0x49, &font, vec![1.0, 0.0]), cbox_of_char(0x49, &font, vec![1.0, 1.0]), ]
);
let gdef = font.gdef().unwrap();
let varstore = gdef.item_var_store().unwrap().unwrap();
let regions = varstore
.variation_region_list()
.unwrap()
.variation_regions();
assert_eq!(regions.len(), 1);
let region_coords = regions
.get(0)
.unwrap()
.region_axes()
.iter()
.map(|coords| {
[
coords.start_coord().to_f32(),
coords.peak_coord().to_f32(),
coords.end_coord().to_f32(),
]
})
.collect::<Vec<_>>();
assert_eq!(region_coords, vec![[0.0, 0.0, 0.0], [0.0, 1.0, 1.0]]);
assert_eq!(varstore.item_variation_data_count(), 1, "{varstore:#?}");
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0]);
assert_eq!(vec![vec![-350]], delta_sets(&vardata));
}
#[test]
fn intermediate_layer_in_glyphs2() {
assert_intermediate_layer("glyphs2/IntermediateLayer.glyphs");
}
#[test]
fn intermediate_layer_in_glyphs3() {
assert_intermediate_layer("glyphs3/IntermediateLayer.glyphs");
}
#[test]
fn intermediate_layer_in_designspace() {
assert_intermediate_layer("designspace_from_glyphs/IntermediateLayer.designspace");
}
#[test]
fn empty_glyph_is_zero_length() {
let result = TestCompile::compile_source("glyphs2/WghtVar.glyphs");
let space_idx = result.get_glyph_index("space").unwrap() as usize;
let font = result.font();
let is_long = match font.head().unwrap().index_to_loc_format() {
0 => false,
1 => true,
_ => panic!("Unrecognized loca format"),
};
let loca = font.loca(is_long).unwrap();
assert_eq!(
loca.get_raw(space_idx).unwrap(),
loca.get_raw(space_idx + 1).unwrap(),
"space has no outline and should take 0 bytes of glyf"
);
}
fn assert_no_compile_features(source: &str, table_tags: &[&[u8; 4]]) {
let default_result = TestCompile::compile_source(source);
let nofea_result = TestCompile::compile(source, |mut args| {
args.skip_features = true;
args
});
let font_with_fea = default_result.font();
let font_without_fea = nofea_result.font();
for table_tag in table_tags {
let tag = Tag::new(table_tag);
assert!(
font_with_fea.data_for_tag(tag).is_some(),
"Expected font built from {source} with fea to have {tag}"
);
assert!(
font_without_fea.data_for_tag(tag).is_none(),
"Expected font build from {source} without fea to not have {tag}"
);
}
}
#[test]
fn no_compile_features_from_glyphs_2() {
assert_no_compile_features("glyphs2/WghtVar.glyphs", &[b"GDEF", b"GPOS"]);
}
#[test]
fn no_compile_features_from_glyphs_3() {
assert_no_compile_features("glyphs3/WghtVar.glyphs", &[b"GDEF", b"GPOS"]);
}
#[test]
fn no_compile_features_from_ufo() {
assert_no_compile_features(
"designspace_from_glyphs/WghtVar.designspace",
&[b"GDEF", b"GPOS"],
);
assert_no_compile_features("static.designspace", &[b"GPOS", b"GSUB"]);
}
#[test]
fn compile_simple_glyph_keep_direction() {
let source = "glyphs3/WghtVar.glyphs";
let default_result = TestCompile::compile_source(source);
let ir_glyph = default_result.read_ir_glyph("hyphen");
let ir_default_instance = ir_glyph.default_instance();
assert_eq!(
&ir_default_instance.contours[0].to_svg(),
"M131,330 L131,250 L470,250 L470,330 L131,330 Z"
);
let RawGlyph::Simple(glyph) = default_result.read_be_glyph("hyphen") else {
panic!("Expected a simple glyph");
};
assert_eq!(
vec![
CurvePoint::on_curve(131, 330),
CurvePoint::on_curve(470, 330),
CurvePoint::on_curve(470, 250),
CurvePoint::on_curve(131, 250),
],
glyph
.contours
.iter()
.flat_map(|c| c.iter())
.copied()
.collect::<Vec<_>>()
);
drop(default_result);
let keep_direction_result = TestCompile::compile(source, |mut args| {
args.flags |= Flags::KEEP_DIRECTION;
args
});
let RawGlyph::Simple(glyph) = keep_direction_result.read_be_glyph("hyphen") else {
panic!("Expected a simple glyph");
};
assert_eq!(
vec![
CurvePoint::on_curve(131, 330),
CurvePoint::on_curve(131, 250),
CurvePoint::on_curve(470, 250),
CurvePoint::on_curve(470, 330),
],
glyph
.contours
.iter()
.flat_map(|c| c.iter())
.copied()
.collect::<Vec<_>>()
);
}
fn delta_sets(var_data: &ItemVariationData) -> Vec<Vec<i32>> {
(0..var_data.item_count())
.map(|i| var_data.delta_set(i).collect::<Vec<_>>())
.collect()
}
fn assert_var_regions<'a>(
varstore: &ItemVariationStore<'a>,
expected_regions: &Vec<Vec<[f32; 3]>>,
) {
let regions = varstore
.variation_region_list()
.unwrap()
.variation_regions();
let actual_regions = regions
.iter()
.map(|region| {
region
.expect("To access regions")
.region_axes()
.iter()
.map(|coords| {
[
coords.start_coord().to_f32(),
coords.peak_coord().to_f32(),
coords.end_coord().to_f32(),
]
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
assert_eq!(*expected_regions, actual_regions);
}
fn copy_source(raw_src_dir: &str) -> TempDir {
let mut src_dir = testdata_dir();
src_dir.push(raw_src_dir);
assert!(src_dir.is_dir(), "No {src_dir:?}");
let temp_dir = TempDir::new().unwrap();
let mut frontier = vec![(src_dir, temp_dir.path().to_path_buf())];
while let Some((src, dst)) = frontier.pop() {
if src.is_dir() {
for e in fs::read_dir(&src).unwrap() {
let e = e.unwrap();
let src = e.path();
let mut dst = dst.clone();
dst.push(src.file_name().unwrap());
if src.is_dir() {
fs::create_dir(&dst).unwrap();
}
frontier.push((src, dst));
}
}
if src.is_file() {
fs::copy(src, dst).unwrap();
}
}
temp_dir
}
#[test]
fn no_vvar_without_build_vertical() {
let temp_dir = copy_source("HVVAR/SingleModel_Direct");
let mut src = temp_dir.path().to_path_buf();
let mut fontinfo_file = src.clone();
fontinfo_file.push("SingleModelDirect-Regular.ufo/fontinfo.plist");
let mut fontinfo = fs::read_to_string(&fontinfo_file).unwrap();
fontinfo = fontinfo.replace("openTypeVheaVertTypo", "openTypeHhea");
fs::write(&fontinfo_file, &fontinfo).unwrap();
src.push("SingleModelDirect.designspace");
let mut result = TestCompile::new(src.to_str().unwrap(), |o| o);
result.run();
assert!(
!result.fe_context.static_metadata.get().build_vertical,
"build_vertical is set?!"
);
}
#[test]
fn compile_vvar_single_model_direct_varstore() {
let result =
TestCompile::compile_source("HVVAR/SingleModel_Direct/SingleModelDirect.designspace");
assert!(
result.fe_context.static_metadata.get().build_vertical,
"build_vertical is false?!"
);
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 14);
let vvar = font.vvar().unwrap();
let varstore = vvar.item_variation_store().unwrap();
assert_eq!(
varstore.variation_region_list().unwrap().region_count(),
1,
"Should have 1 variation region for the wght axis"
);
assert_eq!(varstore.item_variation_data_count(), 1);
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0]);
assert_eq!(vardata.item_count(), num_glyphs);
assert_eq!(
vec![
vec![0],
vec![50],
vec![50],
vec![50],
vec![50],
vec![50],
vec![50],
vec![50],
vec![50],
vec![50],
vec![50],
vec![0],
vec![0],
vec![0],
],
delta_sets(&vardata)
);
assert!(
vvar.advance_height_mapping().is_none(),
"Should have NO advance height mapping for direct VarStore"
);
}
#[test]
fn compile_vvar_single_model_indirect_varstore() {
let result = TestCompile::compile_source(
"HVVAR/SingleModel_Indirect/SingleModelIndirect.designspace",
);
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 24);
let vvar = font.vvar().unwrap();
let varstore = vvar.item_variation_store().unwrap();
assert_eq!(
varstore
.variation_region_list()
.unwrap()
.variation_regions()
.len(),
1
);
assert_eq!(varstore.item_variation_data_count(), 1);
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0]);
assert_eq!(vec![vec![0], vec![50],], delta_sets(&vardata));
let Some(Ok(DeltaSetIndexMap::Format0(varidx_map))) = vvar.advance_height_mapping() else {
panic!("Expected advance height mapping with DeltaSetIndexMap::Format0");
};
assert_eq!(varidx_map.map_count(), num_glyphs - 10 + 1);
}
#[test]
fn compile_hvar_single_model_direct_varstore() {
let result =
TestCompile::compile_source("HVVAR/SingleModel_Direct/SingleModelDirect.designspace");
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 14);
let hvar = font.hvar().unwrap();
assert!(hvar.advance_width_mapping().is_none());
let varstore = hvar.item_variation_store().unwrap();
assert_var_regions(&varstore, &vec![vec![[0.0, 1.0, 1.0]]]);
assert_eq!(varstore.item_variation_data_count(), 1, "{varstore:#?}");
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0]);
assert_eq!(
vec![
vec![0],
vec![100],
vec![120],
vec![70],
vec![100],
vec![120],
vec![120],
vec![70],
vec![70],
vec![120],
vec![70],
vec![0],
vec![0],
vec![0],
],
delta_sets(&vardata)
);
}
#[test]
fn compile_hvar_single_model_direct_varstore_from_glyphs2_with_implicit_axes() {
let result = TestCompile::compile_source("glyphs2/WghtVar_ImplicitAxes.glyphs");
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 4);
let hvar = font.hvar().unwrap();
assert!(hvar.advance_width_mapping().is_none());
let varstore = hvar.item_variation_store().unwrap();
assert_var_regions(&varstore, &vec![vec![[0.0, 1.0, 1.0]]]);
assert_eq!(varstore.item_variation_data_count(), 1, "{varstore:#?}");
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0]);
assert_eq!(
vec![vec![0], vec![400], vec![200], vec![200],],
delta_sets(&vardata)
);
}
#[test]
fn compile_hvar_single_model_indirect_varstore() {
let result = TestCompile::compile_source(
"HVVAR/SingleModel_Indirect/SingleModelIndirect.designspace",
);
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 24);
let hvar = font.hvar().unwrap();
let varstore = hvar.item_variation_store().unwrap();
assert_eq!(
varstore
.variation_region_list()
.unwrap()
.variation_regions()
.len(),
1
);
assert_eq!(varstore.item_variation_data_count(), 1);
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0]);
assert_eq!(
vec![vec![0], vec![70], vec![100], vec![120],],
delta_sets(&vardata)
);
let Some(Ok(DeltaSetIndexMap::Format0(varidx_map))) = hvar.advance_width_mapping() else {
panic!("Expected advance width mapping with DeltaSetIndexMap::Format0");
};
assert_eq!(varidx_map.map_count(), num_glyphs - 10 + 1);
}
enum MetricVariationTable<'a> {
Hvar(write_fonts::read::tables::hvar::Hvar<'a>),
Vvar(write_fonts::read::tables::vvar::Vvar<'a>),
}
struct AdvanceDeltaReader<'a> {
table: MetricVariationTable<'a>,
glyph_order: Arc<GlyphOrder>,
}
impl<'a> AdvanceDeltaReader<'a> {
fn from_hvar(
hvar: write_fonts::read::tables::hvar::Hvar<'a>,
glyph_order: Arc<GlyphOrder>,
) -> Self {
Self {
table: MetricVariationTable::Hvar(hvar),
glyph_order,
}
}
fn from_vvar(
vvar: write_fonts::read::tables::vvar::Vvar<'a>,
glyph_order: Arc<GlyphOrder>,
) -> Self {
Self {
table: MetricVariationTable::Vvar(vvar),
glyph_order,
}
}
fn advance_delta(&self, name: &str, coords: &[NormalizedCoord]) -> f64 {
let name = GlyphName::from(name);
let gid = self.glyph_order.glyph_id(&name).unwrap();
let coords: Vec<F2Dot14> = coords
.iter()
.map(|coord| F2Dot14::from_f32(coord.to_f64() as _))
.collect();
match &self.table {
MetricVariationTable::Hvar(hvar) => hvar
.advance_width_delta(gid.into(), &coords)
.unwrap()
.to_f64(),
MetricVariationTable::Vvar(vvar) => vvar
.advance_height_delta(gid.into(), &coords)
.unwrap()
.to_f64(),
}
}
}
#[test]
fn compile_hvar_multi_model_indirect_varstore() {
let result =
TestCompile::compile_source("HVVAR/MultiModel_Indirect/MultiModelIndirect.designspace");
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 5);
let hvar = font.hvar().unwrap();
let varstore = hvar.item_variation_store().unwrap();
assert_var_regions(
&varstore,
&vec![
vec![[0.0, 0.333313, 1.0]],
vec![[0.333313, 1.0, 1.0]],
vec![[0.0, 1.0, 1.0]],
],
);
assert_eq!(varstore.item_variation_data_count(), 1);
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0, 1, 2]);
assert_eq!(vardata.item_count(), 4);
let Some(Ok(varidx_map)) = hvar.advance_width_mapping() else {
panic!("Expected advance width mapping");
};
assert_eq!(
(0..num_glyphs)
.map(|gid| {
let varidx = varidx_map.get(gid as u32).unwrap();
assert_eq!(varidx.outer, 0);
varidx.inner
})
.collect::<Vec<_>>(),
vec![0, 2, 3, 1, 2],
);
let hvar = AdvanceDeltaReader::from_hvar(hvar, result.fe_context.glyph_order.get());
assert_eq!(
hvar.advance_delta(".notdef", &[NormalizedCoord::new(0.0)]),
0.0
);
assert_eq!(
hvar.advance_delta("space", &[NormalizedCoord::new(1.0)]),
100.0
);
assert_eq!(
hvar.advance_delta("space", &[NormalizedCoord::new(0.5)]),
50.0
);
assert_eq!(hvar.advance_delta("A", &[NormalizedCoord::new(1.0)]), 120.0);
assert_eq!(hvar.advance_delta("A", &[NormalizedCoord::new(0.5)]), 83.0);
}
#[test]
fn compile_vvar_multi_model_indirect_varstore() {
let result =
TestCompile::compile_source("HVVAR/MultiModel_Indirect/MultiModelIndirect.designspace");
let font = result.font();
let num_glyphs = font.maxp().unwrap().num_glyphs();
assert_eq!(num_glyphs, 5);
let vvar = font.vvar().unwrap();
let varstore = vvar.item_variation_store().unwrap();
assert_var_regions(
&varstore,
&vec![
vec![[0.0, 0.333313, 1.0]],
vec![[0.333313, 1.0, 1.0]],
vec![[0.0, 1.0, 1.0]],
],
);
assert_eq!(varstore.item_variation_data_count(), 1);
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(vardata.region_indexes(), &[0, 1, 2]);
assert_eq!(vardata.item_count(), 3);
assert_eq!(
vec![vec![0, 0, 0], vec![0, 0, 100], vec![70, 120, 0],],
delta_sets(&vardata)
);
let Some(Ok(varidx_map)) = vvar.advance_height_mapping() else {
panic!("Expected advance height mapping");
};
assert_eq!(
(0..num_glyphs)
.map(|gid| {
let varidx = varidx_map.get(gid as u32).unwrap();
assert_eq!(varidx.outer, 0);
varidx.inner
})
.collect::<Vec<_>>(),
vec![0, 1, 2, 1, 1],
);
let vvar = AdvanceDeltaReader::from_vvar(vvar, result.fe_context.glyph_order.get());
assert_eq!(
vvar.advance_delta(".notdef", &[NormalizedCoord::new(0.0)]),
0.0
);
assert_eq!(
vvar.advance_delta("space", &[NormalizedCoord::new(1.0)]),
100.0
);
assert_eq!(
vvar.advance_delta("space", &[NormalizedCoord::new(0.5)]),
50.0
);
assert_eq!(vvar.advance_delta("A", &[NormalizedCoord::new(1.0)]), 120.0);
assert_eq!(vvar.advance_delta("A", &[NormalizedCoord::new(0.5)]), 83.0);
}
#[test]
fn gvar_simple_glyph_has_vertical_phantom_point_deltas() {
let result =
TestCompile::compile_source("HVVAR/SingleModel_Direct/SingleModelDirect.designspace");
assert!(
result.fe_context.static_metadata.get().build_vertical,
"build_vertical should be true for this test font"
);
let font = result.font();
let gvar = font.gvar().unwrap();
let glyph_order = result.fe_context.glyph_order.get();
let glyph_name = GlyphName::from("I");
let gid = glyph_order.glyph_id(&glyph_name).unwrap();
let glyph_var_data = gvar
.glyph_variation_data(gid.into())
.unwrap()
.expect("Glyph 'I' should have gvar variation data");
let mut tuples = glyph_var_data.tuples();
let first_tuple = tuples
.next()
.expect("Should have at least one tuple variation");
let deltas: Vec<_> = first_tuple.deltas().collect();
assert_eq!(
deltas.len(),
8,
"Should have 8 deltas (4 outline + 4 phantom points)"
);
let top_phantom = deltas[6];
assert_eq!(
(top_phantom.x_delta, top_phantom.y_delta),
(0, 20),
"Top vertical phantom should have Y delta of 20 (vertOrigin: 850 -> 870)"
);
let bottom_phantom = deltas[7];
assert_eq!(
(bottom_phantom.x_delta, bottom_phantom.y_delta),
(0, -30),
"Bottom vertical phantom should have Y delta of -30 (bottom: -100 -> -130)"
);
}
#[test]
fn gvar_composite_has_vertical_phantom_point_deltas() {
let result =
TestCompile::compile_source("HVVAR/SingleModel_Direct/SingleModelDirect.designspace");
assert!(
result.fe_context.static_metadata.get().build_vertical,
"build_vertical should be true for this test font"
);
let font = result.font();
let gvar = font.gvar().unwrap();
let glyf = font.glyf().unwrap();
let head = font.head().unwrap();
let glyph_order = result.fe_context.glyph_order.get();
let glyph_name = GlyphName::from("Aacute");
let gid = glyph_order.glyph_id(&glyph_name).unwrap();
let is_long = head.index_to_loc_format() == 1;
let loca = font.loca(is_long).unwrap();
let glyph_data = loca.get_glyf(gid.into(), &glyf).unwrap().unwrap();
assert!(
matches!(glyph_data, glyf::Glyph::Composite(_)),
"Aacute should be a composite glyph"
);
let glyph_var_data = gvar
.glyph_variation_data(gid.into())
.unwrap()
.expect("Composite glyph 'Aacute' should have gvar variation data");
let mut tuples = glyph_var_data.tuples();
let first_tuple = tuples
.next()
.expect("Should have at least one tuple variation");
let deltas: Vec<_> = first_tuple.deltas().collect();
assert!(
deltas.len() >= 6,
"Should have at least 6 deltas (2 component offsets + 4 phantom points)"
);
let top_phantom = deltas[4];
assert_eq!(
(top_phantom.x_delta, top_phantom.y_delta),
(0, 20),
"Top vertical phantom should have Y delta of 20 (vertOrigin: 880 -> 900)"
);
let bottom_phantom = deltas[5];
assert_eq!(
(bottom_phantom.x_delta, bottom_phantom.y_delta),
(0, -30),
"Bottom vertical phantom should have Y delta of -30 (bottom: -120 -> -150)"
);
}
#[test]
fn no_gvar_vertical_phantom_deltas_without_build_vertical() {
let result = TestCompile::compile_source("wght_var.designspace");
assert!(
!result.fe_context.static_metadata.get().build_vertical,
"build_vertical should be false for this test font"
);
let font = result.font();
let gvar = font.gvar().unwrap();
let glyph_order = result.fe_context.glyph_order.get();
let glyph_name = GlyphName::from("bar");
let gid = glyph_order.glyph_id(&glyph_name).unwrap();
let glyph_var_data = gvar
.glyph_variation_data(gid.into())
.unwrap()
.expect("Glyph 'bar' should have gvar variation data");
let mut tuples = glyph_var_data.tuples();
let first_tuple = tuples.next().unwrap();
let deltas: Vec<_> = first_tuple.deltas().collect();
assert!(
deltas.len() <= 6,
"Should have at most 6 deltas if vertical phantom points are omitted"
);
}
fn anchor_coords(at: AnchorTable) -> (i32, i32) {
match at {
AnchorTable::Format1(at) => (at.x_coordinate() as i32, at.y_coordinate() as i32),
AnchorTable::Format2(at) => (at.x_coordinate() as i32, at.y_coordinate() as i32),
AnchorTable::Format3(at) => (at.x_coordinate() as i32, at.y_coordinate() as i32),
}
}
fn mark_base_lookups<'a>(gpos: &'a Gpos) -> Vec<MarkBasePosFormat1<'a>> {
gpos.lookup_list()
.iter()
.flat_map(|l| l.lookups().iter().map(|l| l.unwrap()))
.filter_map(|l| match l {
PositionLookup::MarkToBase(mark_base) => Some(mark_base),
_ => None,
})
.flat_map(|mb| mb.subtables().iter().map(|s| s.unwrap()))
.collect()
}
#[test]
fn compile_basic_gpos_mark_base() {
let result = TestCompile::compile_source("glyphs3/WghtVar_Anchors.glyphs");
let font = result.font();
let gpos = font.gpos().unwrap();
let base_gid = result.get_gid("A");
let macroncomb_gid = result.get_gid("macroncomb");
let brevecomb_gid = result.get_gid("brevecomb");
let mark_base_lookups = mark_base_lookups(&gpos);
let bases = mark_base_lookups
.iter()
.flat_map(|mb: &MarkBasePosFormat1| {
mb.base_coverage().unwrap().iter().collect::<Vec<_>>()
})
.zip(
mark_base_lookups
.iter()
.flat_map(|mb: &MarkBasePosFormat1| {
let base_array = mb.base_array().unwrap();
let data = base_array.offset_data();
base_array
.base_records()
.iter()
.map(move |r| (data, r.unwrap()))
})
.flat_map(|(data, b)| b.base_anchors(data).iter().map(|b| b.unwrap().unwrap()))
.map(anchor_coords),
)
.collect::<Vec<_>>();
let marks = mark_base_lookups
.iter()
.flat_map(|mb: &MarkBasePosFormat1| {
mb.mark_coverage().unwrap().iter().collect::<Vec<_>>()
})
.zip(
mark_base_lookups
.iter()
.flat_map(|mb: &MarkBasePosFormat1| {
let mark_array = mb.mark_array().unwrap();
let data = mark_array.offset_data();
mb.mark_array()
.unwrap()
.mark_records()
.iter()
.map(move |mr| (data, mr))
})
.map(|(data, mr)| {
(
mr.mark_class(),
anchor_coords(mr.mark_anchor(data).unwrap()),
)
}),
)
.collect::<Vec<_>>();
assert_eq!(
(
vec![(base_gid, (300, 700))],
vec![
(macroncomb_gid, (0, (300, 600))),
(brevecomb_gid, (0, (200, 500)))
]
),
(bases, marks)
);
}
fn assert_noexport(source: &str) {
let result = TestCompile::compile_source(source);
assert!(!result.contains_glyph("hyphen"));
let fe_hyphen_consumer = result
.fe_context
.glyphs
.get(&FeWorkIdentifier::Glyph("manual-component".into()));
assert!(
!fe_hyphen_consumer
.default_instance()
.components
.iter()
.any(|c| c.base == "hyphen"),
"IR glyph should not reference hyphen {fe_hyphen_consumer:?}"
);
let be_hyphen_consumer = result
.be_context
.glyphs
.get(&BeWorkIdentifier::GlyfFragment("manual-component".into()).into());
assert!(
be_hyphen_consumer.is_simple(),
"{be_hyphen_consumer:?} should be a simple glyph"
);
}
#[test]
fn compile_obeys_no_export_glyphs() {
assert_noexport("glyphs3/WghtVar_NoExport.glyphs");
}
#[test]
fn compile_obeys_no_export_designspace() {
assert_noexport("designspace_from_glyphs/WghtVar_NoExport.designspace");
}
fn gvar_tuple_count(result: &TestCompile, glyph_name: &str) -> usize {
let font = result.font();
let gvar = font.gvar().unwrap();
gvar.glyph_variation_data(result.get_gid(glyph_name).into())
.unwrap()
.unwrap()
.tuples()
.count()
}
fn hvar_region_count(result: &TestCompile, glyph_name: &str) -> usize {
let font = result.font();
let hvar = font.hvar().unwrap();
let mapping = hvar.advance_width_mapping().unwrap().unwrap();
let delta_set_index = mapping.get(result.get_gid(glyph_name).to_u32()).unwrap();
let varstore = hvar.item_variation_store().unwrap();
let vardata = varstore
.item_variation_data()
.get(delta_set_index.outer as usize)
.unwrap()
.unwrap();
vardata.region_indexes().len()
}
#[test]
fn non_export_flatten_does_not_leak_brace_layer_sources_into_gvar() {
let result = TestCompile::compile_source("glyphs3/NonExportWithBraceLayer.glyphs");
assert_eq!(gvar_tuple_count(&result, "A"), 2);
assert_eq!(gvar_tuple_count(&result, "acutecomb"), 1);
assert_eq!(gvar_tuple_count(&result, "Aacute"), 1);
assert_eq!(hvar_region_count(&result, "Aacute"), 1);
}
#[test]
fn non_export_composite_brace_layer_preserved_in_gvar() {
let result = TestCompile::compile_source("glyphs3/NonExportWithBraceLayer.glyphs");
assert_eq!(gvar_tuple_count(&result, "Agraveacute"), 2);
assert_eq!(hvar_region_count(&result, "Agraveacute"), 2);
}
#[test]
fn non_export_flatten_plus_decompose_adds_brace_intermediate() {
let result = TestCompile::compile_source("glyphs3/NonExportWithBraceLayer.glyphs");
let font = result.font();
let gid = result.get_gid("Adot");
let is_long = font.head().unwrap().index_to_loc_format() == 1;
let glyf = font.glyf().unwrap();
let loca = font.loca(is_long).unwrap();
let glyph_data = loca.get_glyf(gid.into(), &glyf).unwrap().unwrap();
assert!(
matches!(glyph_data, glyf::Glyph::Simple(_)),
"Adot should be decomposed to a simple glyph (mixed content + non-export flatten)"
);
assert_eq!(gvar_tuple_count(&result, "Adot"), 2);
assert_eq!(hvar_region_count(&result, "Adot"), 2);
}
#[test]
fn compile_do_not_decompose_nested_no_export_glyphs() {
let result = TestCompile::compile("glyphs3/NestedNoExportComponent.glyphs", |mut args| {
args.flags.set(Flags::FLATTEN_COMPONENTS, true);
args
});
assert!(!result.contains_glyph("_glyph"));
for name in ["glyph2", "glyph3"] {
let be_glyph = result
.be_context
.glyphs
.get(&BeWorkIdentifier::GlyfFragment(name.into()).into());
assert!(
be_glyph.is_composite(),
"{be_glyph:?} should be a composite glyph"
);
let fe_glyph = result
.fe_context
.glyphs
.get(&FeWorkIdentifier::Glyph(name.into()));
assert!(
fe_glyph
.default_instance()
.components
.iter()
.all(|c| c.base == "glyph1"),
"IR glyph should reference glyph1 {fe_glyph:?}"
);
}
}
fn assert_fs_type(source: &str, expected_fs_type: u16) {
let compile = TestCompile::compile_source(source);
let os2 = compile.font().os2().unwrap();
assert_eq!(expected_fs_type, os2.fs_type());
}
#[test]
fn default_fs_type_glyphs() {
assert_fs_type("glyphs3/WghtVar.glyphs", 1 << 3);
}
#[test]
fn default_fs_type_designspace() {
assert_fs_type("wght_var.designspace", 1 << 2);
}
#[test]
fn os2_weight_class_matches_default_wght() {
let compile = TestCompile::compile_source("glyphs3/WghtVar_3master_CustomOrigin.glyphs");
let font = compile.font();
assert_eq!(vec![(Tag::new(b"wght"), 200.0, 700.0, 700.0)], axes(&font),);
assert_eq!(700, font.os2().unwrap().us_weight_class());
}
#[test]
fn os2_width_class_matches_default_wdth_glyphs2() {
let compile = TestCompile::compile_source("glyphs2/WdthVar.glyphs");
let font = compile.font();
assert_eq!(vec![(Tag::new(b"wdth"), 50.0, 50.0, 200.0)], axes(&font),);
assert_eq!(
WidthClass::UltraCondensed as u16,
font.os2().unwrap().us_width_class()
);
}
#[test]
fn os2_width_class_matches_default_wdth_glyphs3() {
let compile = TestCompile::compile_source("glyphs3/WdthVar.glyphs");
let font = compile.font();
assert_eq!(vec![(Tag::new(b"wdth"), 50.0, 50.0, 200.0)], axes(&font),);
assert_eq!(
WidthClass::UltraCondensed as u16,
font.os2().unwrap().us_width_class()
);
}
#[test]
fn one_lookup_per_group() {
let compile = TestCompile::compile_source("glyphs3/Oswald-AE-comb.glyphs");
let gpos = compile.font().gpos().unwrap();
assert_eq!(1, mark_base_lookups(&gpos).len());
}
fn assert_post_italic_angle(source: &str, expected_angle: f32) {
let compile = TestCompile::compile_source(source);
let post = compile.font().post().unwrap();
assert_eq!(expected_angle, post.italic_angle().to_f32());
}
#[test]
fn italic_angle_in_glyphs2() {
assert_post_italic_angle("glyphs2/SlantedFont.glyphs", -12.0);
}
#[test]
fn italic_angle_in_glyphs3() {
assert_post_italic_angle("glyphs3/SlantedFont.glyphs", -12.0);
}
#[test]
fn italic_angle_in_designspace() {
assert_post_italic_angle("designspace_from_glyphs/SlantedFont.designspace", -12.0);
}
fn assert_hhea_caret_slope(source: &str, expected_slope_rise: i16, expected_slope_run: i16) {
let compile = TestCompile::compile_source(source);
let hhea = compile.font().hhea().unwrap();
assert_eq!(expected_slope_rise, hhea.caret_slope_rise());
assert_eq!(expected_slope_run, hhea.caret_slope_run());
assert_eq!(0, hhea.caret_offset());
}
#[test]
fn default_caret_slope_in_glyphs2() {
assert_hhea_caret_slope("glyphs2/SlantedFont.glyphs", 1000, 213);
}
#[test]
fn default_caret_slope_in_glyphs3() {
assert_hhea_caret_slope("glyphs3/SlantedFont.glyphs", 1000, 213);
}
#[test]
fn default_caret_slope_in_designspace() {
assert_hhea_caret_slope("designspace_from_glyphs/SlantedFont.designspace", 1000, 213);
}
fn assert_mvar(
mvar: write_fonts::read::tables::mvar::Mvar,
expected_value_records: Vec<(Tag, u16)>,
expected_delta_sets: Vec<Vec<i32>>,
) {
let actual_records = mvar
.value_records()
.iter()
.map(|rec| {
assert_eq!(rec.delta_set_outer_index(), 0);
(rec.value_tag(), rec.delta_set_inner_index())
})
.collect::<Vec<_>>();
assert_eq!(actual_records, expected_value_records);
let Some(Ok(varstore)) = mvar.item_variation_store() else {
panic!("MVAR has no ItemVariationStore?!");
};
assert!(!expected_delta_sets.is_empty());
let expected_num_regions = expected_delta_sets[0].len() as u16;
assert!(expected_num_regions > 0);
assert!(
expected_delta_sets
.iter()
.all(|row| row.len() as u16 == expected_num_regions)
);
assert_eq!(
varstore.variation_region_list().unwrap().region_count(),
expected_num_regions
);
assert_eq!(varstore.item_variation_data_count(), 1);
let vardata = varstore.item_variation_data().get(0).unwrap().unwrap();
assert_eq!(
vardata.region_indexes(),
(0..expected_num_regions).collect::<Vec<_>>()
);
assert_eq!(delta_sets(&vardata), expected_delta_sets);
}
#[test]
fn mvar_from_designspace() {
let compile = TestCompile::compile_source("MVAR.designspace");
assert_mvar(
compile.font().mvar().unwrap(),
vec![
(Tag::new(b"cpht"), 0),
(Tag::new(b"hasc"), 0),
(Tag::new(b"hcla"), 0),
(Tag::new(b"hcld"), 0),
(Tag::new(b"hcof"), 0),
(Tag::new(b"hcrn"), 0),
(Tag::new(b"hcrs"), 0),
(Tag::new(b"hdsc"), 0),
(Tag::new(b"hlgp"), 0),
(Tag::new(b"sbxo"), 0),
(Tag::new(b"sbxs"), 0),
(Tag::new(b"sbyo"), 0),
(Tag::new(b"sbys"), 0),
(Tag::new(b"spxo"), 0),
(Tag::new(b"spxs"), 0),
(Tag::new(b"spyo"), 0),
(Tag::new(b"spys"), 0),
(Tag::new(b"stro"), 0),
(Tag::new(b"strs"), 0),
(Tag::new(b"undo"), 0),
(Tag::new(b"unds"), 0),
(Tag::new(b"xhgt"), 0),
],
vec![vec![5, -5]],
);
}
fn assert_mvar_from_glyphs(mvar: write_fonts::read::tables::mvar::Mvar) {
assert_mvar(
mvar,
vec![
(Tag::new(b"cpht"), 0),
(Tag::new(b"hasc"), 0),
(Tag::new(b"hcla"), 0),
(Tag::new(b"hcld"), 0),
(Tag::new(b"hcrn"), 1),
(Tag::new(b"hdsc"), 0),
(Tag::new(b"hlgp"), 0),
(Tag::new(b"sbxo"), 0),
(Tag::new(b"sbxs"), 0),
(Tag::new(b"sbyo"), 0),
(Tag::new(b"sbys"), 0),
(Tag::new(b"spxo"), 0),
(Tag::new(b"spxs"), 0),
(Tag::new(b"spyo"), 0),
(Tag::new(b"spys"), 0),
(Tag::new(b"stro"), 0),
(Tag::new(b"strs"), 0),
(Tag::new(b"undo"), 0),
(Tag::new(b"unds"), 0),
(Tag::new(b"xhgt"), 0),
],
vec![
vec![5, -5], vec![87, -87], ],
);
}
#[test]
fn mvar_from_glyphs2() {
let compile = TestCompile::compile_source("glyphs2/MVAR.glyphs");
assert_mvar_from_glyphs(compile.font().mvar().unwrap());
}
#[test]
fn mvar_from_glyphs3() {
let compile = TestCompile::compile_source("glyphs3/MVAR.glyphs");
assert_mvar_from_glyphs(compile.font().mvar().unwrap());
}
#[test]
fn strikeout_size_fallback() {
let compile =
TestCompile::compile_source("glyphs3/GlobalMetrics_font_customParameters.glyphs");
let strikeout_variation = compile
.fe_context
.global_metrics
.get()
.deltas(GlobalMetric::StrikeoutSize)
.clone();
let mut light = VariationRegion::default();
light.insert(Tag::new(b"wght"), Tent::zeroes());
let mut elsewhere = VariationRegion::default();
elsewhere.insert(
Tag::new(b"wght"),
Tent::new(
NormalizedCoord::new(0),
NormalizedCoord::MAX,
NormalizedCoord::MAX,
),
);
assert_eq!(strikeout_variation.len(), 2);
assert_eq!(
strikeout_variation
.iter()
.find_map(|(region, deltas)| (*region == light).then_some(deltas)),
Some(&vec![40.0])
);
assert_eq!(
strikeout_variation
.iter()
.find_map(|(region, deltas)| (*region == elsewhere).then_some(deltas)),
Some(&vec![2.0])
);
}
#[test]
fn compile_ufo() {
let result = TestCompile::compile_source("WghtVar-Bold.ufo");
let font = result.font();
let name = font.name().unwrap();
assert_eq!(
vec![
(NameId::FAMILY_NAME, "Wght Var".to_string()),
(NameId::SUBFAMILY_NAME, "Regular".to_string()),
(NameId::UNIQUE_ID, "0.000;NONE;WghtVar-Regular".to_string()),
(NameId::FULL_NAME, "Wght Var Regular".to_string()),
(NameId::VERSION_STRING, "Version 0.000".to_string()),
(NameId::POSTSCRIPT_NAME, "WghtVar-Regular".to_string()),
],
name.name_record()
.iter()
.map(|nr| (nr.name_id.get(), resolve_name(&name, nr.name_id()).unwrap()))
.collect::<Vec<_>>()
);
}
#[test]
fn variation_regions_reach_end_of_axis() {
let result = TestCompile::compile_source("wght_var_wide_axis.designspace");
let font = result.font();
let gvar = font.gvar().unwrap();
let var_data = gvar
.glyph_variation_data(result.get_gid("bar").into())
.unwrap()
.expect("'bar' varies at every master");
let regions = var_data
.tuples()
.map(|tuple| {
let peak = tuple.peak().get(0).unwrap();
let start = tuple
.intermediate_start()
.map(|t| t.get(0).unwrap())
.unwrap_or_else(|| peak.min(F2Dot14::ZERO));
let end = tuple
.intermediate_end()
.map(|t| t.get(0).unwrap())
.unwrap_or_else(|| peak.max(F2Dot14::ZERO));
(start, peak, end)
})
.collect::<Vec<_>>();
let f2dot14 = |v| F2Dot14::from_f32(v);
assert_eq!(
vec![
(f2dot14(0.0), f2dot14(0.4), f2dot14(1.0)),
(f2dot14(0.4), f2dot14(0.6), f2dot14(1.0)),
],
regions
);
}
#[test]
fn compile_empty_gvar_with_correct_axis_count() {
let result = TestCompile::compile_source("varpos.designspace");
let font = result.font();
let fvar = font.fvar().unwrap();
let gvar = font.gvar().unwrap();
assert_eq!(fvar.axis_count(), 1);
assert_eq!(fvar.axis_count(), gvar.axis_count());
let glyph_count = font.maxp().unwrap().num_glyphs();
assert_eq!(gvar.glyph_count(), glyph_count);
assert!((0..glyph_count).all(|gid| {
gvar.glyph_variation_data(GlyphId16::new(gid).into())
.unwrap()
.is_none()
}));
}
#[test]
fn gdef_glyph_categories_without_any_layout() {
let compile = TestCompile::compile_source("glyphs3/gdef_categories_no_layout.glyphs");
let font = compile.font();
let gdef = font.gdef().unwrap();
let classdef = gdef.glyph_class_def().unwrap().unwrap();
assert_eq!(classdef.iter().count(), 1)
}
#[test]
fn no_gdef_when_mark_glyphs_are_non_exported() {
let compile =
TestCompile::compile_source("glyphs3/gdef_categories_non_exported_mark.glyphs");
let font = compile.font();
assert!(
font.gdef().is_err(),
"expected no GDEF when all mark glyphs are non-exported"
);
}
#[test]
fn no_names_for_instances_in_a_static_font() {
let compile = TestCompile::compile_source("glyphs3/StaticWithInstance.glyphs");
let font = compile.font();
let name = font.name().unwrap();
let font_specific_names = name
.name_record()
.iter()
.filter_map(|nr| {
if nr.name_id().to_u16() > 255 {
Some((nr.name_id(), nr.string(name.string_data()).unwrap()))
} else {
None
}
})
.collect::<Vec<_>>();
assert!(
font_specific_names.is_empty(),
"Should have no font specific names, got {font_specific_names:?}"
);
}
#[test]
fn duplicate_components_count_in_maxp() {
let compile = TestCompile::compile_source("glyphs3/PixelRef.glyphs");
let font = compile.font();
let maxp = font.maxp().unwrap();
assert_eq!(
(12, 3),
(
maxp.max_composite_points().unwrap(),
maxp.max_composite_contours().unwrap()
)
);
}
fn assert_expected_unicode_ranges(source: &str) {
let compile = TestCompile::compile_source(source);
let font = compile.font();
let os2 = font.os2().unwrap();
assert_eq!(
[1 | (1 << 28), (1 << 20) | (1 << 24), 0, 1 << 26],
[
os2.ul_unicode_range_1(),
os2.ul_unicode_range_2(),
os2.ul_unicode_range_3(),
os2.ul_unicode_range_4()
]
);
}
#[test]
fn obeys_source_unicode_ranges_glyphs() {
assert_expected_unicode_ranges("glyphs3/WghtVar_OS2.glyphs");
}
#[test]
fn obeys_source_unicode_ranges_designspace() {
assert_expected_unicode_ranges("designspace_from_glyphs/WghtVarOS2.designspace");
}
fn assert_expected_codepage_ranges(source: &str) {
let compile = TestCompile::compile_source(source);
let font = compile.font();
let os2 = font.os2().unwrap();
assert_eq!(
[Some(1 | (1 << 1) | (1 << 19)), Some(1 << 31)],
[os2.ul_code_page_range_1(), os2.ul_code_page_range_2(),]
);
}
#[test]
fn obeys_source_codepage_ranges_glyphs() {
assert_expected_codepage_ranges("glyphs3/WghtVar_OS2.glyphs");
}
#[test]
fn obeys_source_codepage_ranges_designspace() {
assert_expected_codepage_ranges("designspace_from_glyphs/WghtVarOS2.designspace");
}
#[test]
fn bbox_of_nested_components() {
let compile = TestCompile::compile_source("glyphs2/MatrixComponent.glyphs");
let font = compile.font();
let glyf = font.glyf().unwrap();
let loca = font.loca(false).unwrap();
let expected_rot30_bbox = Rect::new(75.0, 107.0, 359.0, 300.0);
let expected_rot60more_bbox = Rect::new(50.0, 149.0, 150.0, 449.0);
let gids = ["rot30", "rot60more"]
.into_iter()
.map(|gn| {
compile
.fe_context
.glyph_order
.get()
.glyph_id(gn)
.map(|gid16| GlyphId::new(gid16.to_u32()))
.unwrap()
})
.collect::<Vec<_>>();
let boxes = gids
.iter()
.map(|gid| loca.get_glyf(*gid, &glyf).unwrap().unwrap())
.map(|glyf| {
Rect::new(
glyf.x_min() as f64,
glyf.y_min() as f64,
glyf.x_max() as f64,
glyf.y_max() as f64,
)
})
.collect::<Vec<_>>();
assert_eq!(vec![expected_rot30_bbox, expected_rot60more_bbox], boxes);
}
#[test]
fn mostly_unique_names() {
let compile = TestCompile::compile_source("glyphs3/DuplicateNames.glyphs");
let font = compile.font();
let name = font.name().unwrap();
let fvar = font.fvar().unwrap();
let stat = font.stat().unwrap();
let axes = fvar.axes().unwrap().iter().collect::<Vec<_>>();
let default_location = axes.iter().map(|a| a.default_value()).collect::<Vec<_>>();
let named_instances = fvar
.instances()
.unwrap()
.iter()
.map(|ni| ni.unwrap())
.collect::<Vec<_>>();
let default_instance_name_id = named_instances
.iter()
.find_map(|ni| (ni.coordinates == default_location).then_some(ni.subfamily_name_id))
.unwrap();
assert_eq!(NameId::SUBFAMILY_NAME, default_instance_name_id);
assert_eq!(
"Regular".to_string(),
resolve_name(&name, default_instance_name_id).unwrap()
);
let min_font_specific_name_id = NameId::new(256);
let non_default_instance_name_id = named_instances
.iter()
.find_map(|ni| (ni.coordinates != default_location).then_some(ni.subfamily_name_id))
.unwrap();
assert!(non_default_instance_name_id >= min_font_specific_name_id);
assert_eq!(
"Bold".to_string(),
resolve_name(&name, non_default_instance_name_id).unwrap()
);
let axis_name_id = axes.first().map(|a| a.axis_name_id()).unwrap();
assert!(axis_name_id >= min_font_specific_name_id);
assert_eq!(
"Regular".to_string(),
resolve_name(&name, axis_name_id).unwrap()
);
let design_axis_name_id = stat
.design_axes()
.unwrap()
.first()
.map(|a| a.axis_name_id())
.unwrap();
assert_eq!(design_axis_name_id, axis_name_id);
}
#[test]
fn allow_duplicate_components() {
TestCompile::compile_source("DoubleComponentError/OuterInner.designspace");
}
#[test]
fn merge_stylistic_set_names() {
let result = TestCompile::compile_source("glyphs3/WghtVarWithStylisticSet.glyphs");
let font = result.font();
let name = font.name().unwrap();
let names = name
.name_record()
.iter()
.map(|rec| {
let value = rec.string(name.string_data()).unwrap().to_string();
(
rec.name_id(),
rec.language_id(),
strip_fontc_version_tag(&value).to_string(),
)
})
.collect::<Vec<_>>();
assert_eq!(
names,
[
(NameId::COPYRIGHT_NOTICE, 0x0409, "Copy!".to_owned()),
(NameId::FAMILY_NAME, 0x0409, "WghtVar".to_owned()),
(NameId::SUBFAMILY_NAME, 0x0409, "Regular".to_owned()),
(
NameId::UNIQUE_ID,
0x0409,
"1.234;NONE;WghtVar-Regular".to_owned()
),
(NameId::FULL_NAME, 0x0409, "WghtVar Regular".to_owned()),
(NameId::VERSION_STRING, 0x0409, "Version 1.234".to_owned()),
(
NameId::POSTSCRIPT_NAME,
0x0409,
"WghtVar-Regular".to_owned()
),
(
NameId::DESCRIPTION,
0x0409,
"The greatest weight var".to_owned()
),
(
NameId::LICENSE_URL,
0x0409,
"https://example.com/my/font/license".to_owned()
),
(NameId::new(256), 0x0409, "Weight".to_owned()),
(NameId::new(257), 0x0409, "my fun feature".to_owned()),
(NameId::FAMILY_NAME, 0x0C0A, "SpanishWghtVar".to_owned()),
(
NameId::DESCRIPTION,
0x0C0A,
"El mayor peso variable".to_owned()
),
]
);
let gsub = font.gsub().unwrap();
let feature_list = gsub.feature_list().unwrap();
assert_eq!(feature_list.feature_records().len(), 2);
let feature = feature_list.feature_records()[0]
.feature(feature_list.offset_data())
.unwrap();
let params = feature.feature_params().unwrap().unwrap();
let FeatureParams::StylisticSet(params) = params else {
panic!("wrong feature params type");
};
assert_eq!(params.ui_name_id().to_u16(), 257);
}
#[test]
fn skip_empty_stylistic_set_names() {
let result = TestCompile::compile_source("glyphs3/WghtVarWithStylisticSet.glyphs");
let font = result.font();
let gsub = font.gsub().unwrap();
let feature_list = gsub.feature_list().unwrap();
assert_eq!(feature_list.feature_records().len(), 2);
let ss02 = feature_list.feature_records()[1]
.feature(feature_list.offset_data())
.unwrap();
assert!(ss02.feature_params().is_none());
}
#[test]
fn merge_name_table_from_fea() {
let result = TestCompile::compile_source("CustomNameTableInFea.ufo");
let font = result.font();
let name = font.name().unwrap();
let names = name
.name_record()
.iter()
.map(|rec| (rec.name_id(), resolve_name(&name, rec.name_id()).unwrap()))
.collect::<Vec<_>>();
assert_eq!(
names,
[
(NameId::FAMILY_NAME, "Wght Var".to_string()),
(NameId::SUBFAMILY_NAME, "Regular".to_string()),
(NameId::UNIQUE_ID, "0.000;NONE;WghtVar-Regular".to_string()),
(NameId::FULL_NAME, "set from FEA name table".to_string()),
(NameId::VERSION_STRING, "Version 0.000".to_string()),
(NameId::POSTSCRIPT_NAME, "WghtVar-Regular".to_string()),
(NameId::DESIGNER, "Joachim Müller-Lancé".to_string()),
]
);
}
#[test]
fn remap_nameids_for_gpos_featureparam() {
let result = TestCompile::compile_source("glyphs3/GposWithFeatureParamName.glyphs");
let font = result.font();
let gpos = font.gpos().unwrap();
let name = font.name().unwrap();
let features = gpos.feature_list().unwrap();
assert_eq!(features.feature_count(), 1);
let feat = features
.feature_records()
.first()
.unwrap()
.feature(features.offset_data())
.unwrap();
let FeatureParams::StylisticSet(params) = feat.feature_params().unwrap().unwrap() else {
panic!("not a stylistic set");
};
let name_id = params.ui_name_id();
let matching_ids = name
.name_record()
.iter()
.filter(|rec| rec.name_id() == name_id)
.collect::<Vec<_>>();
assert_eq!(matching_ids.len(), 1);
let name = matching_ids[0].string(name.string_data()).unwrap();
assert_eq!(name.to_string(), "An extremely stylish set");
}
#[test]
fn use_base_table_from_fea() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("CustomBaseTableInFea.ufo");
let font = result.font();
let base = font.base().unwrap();
let horiz = base.horiz_axis().unwrap().unwrap();
let horiz_tags = horiz.base_tag_list().unwrap().unwrap().baseline_tags();
assert_eq!(horiz_tags, [Tag::new(b"ideo")]);
let horiz_scripts = horiz.base_script_list().unwrap();
assert_eq!(horiz_scripts.base_script_count(), 1);
let vert = base.vert_axis().unwrap().unwrap();
let vert_tags = vert.base_tag_list().unwrap().unwrap().baseline_tags();
assert_eq!(vert_tags, [Tag::new(b"romn")]);
let vert_scripts = vert.base_script_list().unwrap();
assert_eq!(vert_scripts.base_script_count(), 2);
}
#[test]
fn use_stat_table_from_fea() {
let result = TestCompile::compile_source("CustomStatInFea.ufo");
let font = result.font();
let name = font.name().unwrap();
let names = name
.name_record()
.iter()
.map(|rec| (rec.name_id(), resolve_name(&name, rec.name_id()).unwrap()))
.collect::<Vec<_>>();
assert_eq!(
names,
[
(NameId::FAMILY_NAME, "New Font".to_string()),
(NameId::SUBFAMILY_NAME, "Regular".to_string()),
(NameId::UNIQUE_ID, "0.000;NONE;NewFont-Regular".to_string()),
(NameId::FULL_NAME, "New Font Regular".to_string()),
(NameId::VERSION_STRING, "Version 0.000".to_string()),
(NameId::POSTSCRIPT_NAME, "NewFont-Regular".to_string()),
(NameId::new(256), "Regular".to_string()),
(NameId::new(257), "Italic".to_string()),
(NameId::new(258), "Upright".to_string()),
(NameId::new(259), "Italic".to_string()),
]
);
let stat = font.stat().unwrap();
let design_axes = stat.design_axes().unwrap();
assert_eq!(
design_axes
.iter()
.map(|axis| axis.axis_tag())
.collect::<Vec<_>>(),
[Tag::new(b"ital")]
);
let axis_values = stat.offset_to_axis_values().unwrap().unwrap();
assert_eq!(axis_values.axis_values().len(), 2);
}
#[test]
fn generate_meta_table() {
let result = TestCompile::compile_source("glyphs3/MetaTable.glyphs");
let meta = result.be_context.meta.get();
assert_eq!(
meta.data_maps,
[
DataMapRecord::new(
Tag::new(b"dlng"),
Metadata::ScriptLangTags(vec![ScriptLangTag::new("latn-en".into()).unwrap()]),
),
DataMapRecord::new(
Tag::new(b"slng"),
Metadata::ScriptLangTags(vec![
ScriptLangTag::new("latn".into()).unwrap(),
ScriptLangTag::new("derp".into()).unwrap()
]),
)
]
);
}
#[test]
fn dont_generate_meta_table_if_no_glyphs_param() {
let result = TestCompile::compile_source("glyphs3/NoMetaTable.glyphs");
assert!(result.be_context.meta.try_get().is_none())
}
#[test]
fn dont_generate_meta_table_if_empty_glyphs_param() {
let result = TestCompile::compile_source("glyphs3/EmptyMetaTable.glyphs");
assert!(result.be_context.meta.try_get().is_none())
}
#[test]
fn cpal_solid() {
let result = TestCompile::compile_source("glyphs3/COLRv1-solid.glyphs");
let cpal = result.font().cpal().unwrap();
assert_eq!(
(
1,
2,
[
ColorRecord {
red: 177,
green: 216,
blue: 243,
alpha: 255
},
ColorRecord {
red: 42,
green: 43,
blue: 44,
alpha: 255
},
]
.as_slice()
),
(
cpal.num_palettes(),
cpal.num_palette_entries(),
cpal.color_records_array().unwrap().unwrap()
)
);
}
#[test]
fn cpal_prefer_master() {
let result = TestCompile::compile_source("glyphs3/COLRv0-palette-redefinition.glyphs");
let cpal = result.font().cpal().unwrap();
assert_eq!(
(
2,
2,
[
ColorRecord {
red: 20,
green: 21,
blue: 22,
alpha: 23
},
ColorRecord {
red: 24,
green: 25,
blue: 26,
alpha: 27
},
ColorRecord {
red: 28,
green: 29,
blue: 30,
alpha: 31
},
ColorRecord {
red: 32,
green: 33,
blue: 34,
alpha: 35
},
]
.as_slice()
),
(
cpal.num_palettes(),
cpal.num_palette_entries(),
cpal.color_records_array().unwrap().unwrap()
)
);
}
#[test]
fn cpal_grayscale() {
let result = TestCompile::compile_source("glyphs3/COLRv1-grayscale.glyphs");
let cpal = result.font().cpal().unwrap();
assert_eq!(
(
1,
2,
[
ColorRecord {
red: 64,
green: 64,
blue: 64,
alpha: 255
},
ColorRecord {
red: 0,
green: 0,
blue: 0,
alpha: 255
},
]
.as_slice()
),
(
cpal.num_palettes(),
cpal.num_palette_entries(),
cpal.color_records_array().unwrap().unwrap()
)
);
}
#[test]
fn cpal_gradients() {
let result = TestCompile::compile_source("glyphs3/COLRv1-gradient.glyphs");
let cpal = result.font().cpal().unwrap();
assert_eq!(
(
1,
2,
[
ColorRecord {
red: 255,
green: 0,
blue: 0,
alpha: 255
},
ColorRecord {
red: 0,
green: 0,
blue: 255,
alpha: 255
}
]
.as_slice()
),
(
cpal.num_palettes(),
cpal.num_palette_entries(),
cpal.color_records_array().unwrap().unwrap()
)
);
}
#[test]
fn cpal_from_declared_palettes() {
let result = TestCompile::compile_source("glyphs3/COLRv0-1layer.glyphs");
let cpal = result.font().cpal().unwrap();
assert_eq!(
(
2,
2,
[
ColorRecord {
red: 1,
green: 2,
blue: 3,
alpha: 4
},
ColorRecord {
red: 5,
green: 6,
blue: 7,
alpha: 8
},
ColorRecord {
red: 9,
green: 10,
blue: 11,
alpha: 12
},
ColorRecord {
red: 13,
green: 14,
blue: 15,
alpha: 16
},
]
.as_slice()
),
(
cpal.num_palettes(),
cpal.num_palette_entries(),
cpal.color_records_array().unwrap().unwrap()
)
);
}
#[test]
fn color_me_not() {
let compile = TestCompile::compile_source("glyphs3/WghtVar.glyphs");
assert!(compile.font().cpal().is_err());
assert!(compile.font().colr().is_err());
}
#[test]
fn color_only_if_explicitly_color() {
let compile = TestCompile::compile_source("glyphs3/COLRv1-solidnoncolor.glyphs");
assert!(compile.font().cpal().is_err());
assert!(compile.font().colr().is_err());
}
#[test]
fn colr_grayscale() {
let result = TestCompile::compile_source("glyphs3/COLRv1-grayscale.glyphs");
result.font().colr().unwrap(); }
#[test]
fn colrv0_sorted_structures() {
let result = TestCompile::compile_source("glyphs3/COLRv0-reordered.glyphs");
let colr = result.font().colr().unwrap();
for pair in colr.base_glyph_records().unwrap().unwrap().windows(2) {
assert!(
pair[0].glyph_id().to_u32() < pair[1].glyph_id().to_u32(),
"Not sorted: {pair:#?}"
);
}
}
#[test]
fn colrv1_sorted_structures() {
let result = TestCompile::compile_source("glyphs3/COLRv1-reordered.glyphs");
let colr = result.font().colr().unwrap();
for pair in colr
.base_glyph_list()
.unwrap()
.unwrap()
.base_glyph_paint_records()
.windows(2)
{
assert!(
pair[0].glyph_id().to_u32() < pair[1].glyph_id().to_u32(),
"Not sorted: {pair:#?}"
);
}
let clips = colr.clip_list().unwrap().unwrap().clips();
for clip in clips {
assert!(
clip.start_glyph_id().to_u32() <= clip.end_glyph_id().to_u32(),
"Bad start/end: {clip:#?}"
);
}
for pair in clips.windows(2) {
assert!(
pair[0].end_glyph_id() < pair[1].start_glyph_id(),
"Not sorted: {pair:#?}"
);
}
}
fn root_paint<'a>(compile: &TestCompile, colr: &Colr<'a>, glyph_name: &str) -> Paint<'a> {
let gid = compile.get_gid(glyph_name);
let base_glyph_list = colr.base_glyph_list().unwrap().unwrap();
let base_glyphs = base_glyph_list.base_glyph_paint_records();
let base_glyph = base_glyphs.iter().find(|bg| bg.glyph_id() == gid).unwrap();
base_glyph
.paint(base_glyph_list.offset_data())
.expect("A valid paint")
}
fn root_paint_glyph<'a>(
compile: &TestCompile,
colr: &Colr<'a>,
glyph_name: &str,
) -> PaintGlyph<'a> {
let paint = root_paint(compile, colr, glyph_name);
let Paint::Glyph(paint) = paint else {
panic!("Expected a PaintGlyph for {glyph_name}, got {paint:#?}");
};
paint
}
#[test]
fn colr_gradient_linear() {
let result = TestCompile::compile_source("glyphs3/COLRv1-grayscale.glyphs");
let colr = result.font().colr().expect("COLR");
let Paint::LinearGradient(grad) = root_paint_glyph(&result, &colr, "A")
.paint()
.expect("Valid paint")
else {
panic!("Expected LinearGradient");
};
let coords = (
(grad.x0().to_i16(), grad.y0().to_i16()),
(grad.x1().to_i16(), grad.y1().to_i16()),
(grad.x2().to_i16(), grad.y2().to_i16()),
);
assert_eq!(coords, ((111, 50), (494, 450), (511, -333)), "p0, p1, p2");
}
#[test]
fn colr_gradient_radial() {
let result = TestCompile::compile_source("glyphs3/COLRv1-gradient.glyphs");
let colr = result.font().colr().expect("COLR");
let Paint::RadialGradient(grad) = root_paint_glyph(&result, &colr, "K")
.paint()
.expect("Valid paint")
else {
panic!("Expected RadialGradient");
};
let values = (
(grad.x0().to_i16(), grad.y0().to_i16()),
grad.radius0().to_u16(),
(grad.x1().to_i16(), grad.y1().to_i16()),
grad.radius1().to_u16(),
);
assert_eq!(values, ((303, 250), 0, (303, 250), 346), "p0, r0, p1, r1");
}
#[test]
fn colr_gradient_radial_outside_bbox() {
let result = TestCompile::compile_source("glyphs3/COLRv1-gradient.glyphs");
let colr = result.font().colr().expect("COLR");
let Paint::RadialGradient(grad) = root_paint_glyph(&result, &colr, "L")
.paint()
.expect("Valid paint")
else {
panic!("Expected RadialGradient");
};
let values = (
(grad.x0().to_i16(), grad.y0().to_i16()),
grad.radius0().to_u16(),
(grad.x1().to_i16(), grad.y1().to_i16()),
grad.radius1().to_u16(),
);
assert_eq!(values, ((-33, 250), 0, (-33, 250), 627), "p0, r0, p1, r1");
}
#[test]
fn colr_clipbox_quantization() {
use write_fonts::read::tables::colr::ClipBox;
let result = TestCompile::compile_source("glyphs3/COLRv1-gradient.glyphs");
let colr = result.font().colr().expect("COLR");
let clip_list = colr
.clip_list()
.expect("offset")
.expect("ClipList should exist");
for clip in clip_list.clips() {
let clip_box = clip.clip_box(clip_list.offset_data()).expect("ClipBox");
let (x_min, y_min, x_max, y_max) = match clip_box {
ClipBox::Format1(cb) => (cb.x_min(), cb.y_min(), cb.x_max(), cb.y_max()),
ClipBox::Format2(cb) => (cb.x_min(), cb.y_min(), cb.x_max(), cb.y_max()),
};
let coords = (
x_min.to_i16(),
y_min.to_i16(),
x_max.to_i16(),
y_max.to_i16(),
);
assert_eq!(coords, (0, 0, 600, 500), "quantized ClipBox coordinates");
}
}
#[test]
fn colr_cliprun() {
let result = TestCompile::compile_source("glyphs3/COLRv1-cliprun.glyphs");
let colr = result.font().colr().unwrap();
assert_eq!(
vec![(1, 2)],
colr.clip_list()
.unwrap()
.unwrap()
.clips()
.iter()
.map(|c| (c.start_glyph_id().to_u32(), c.end_glyph_id().to_u32()))
.collect::<Vec<_>>()
)
}
#[test]
fn colr_split_not_required() {
let result = TestCompile::compile_source("glyphs3/COLRv1-grayscale.glyphs");
assert_eq!(
vec![".notdef", "A"],
result
.fe_context
.glyph_order
.get()
.names()
.map(|gn| gn.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn colr_split_based_on_paint_type() {
let result = TestCompile::compile_source("glyphs3/COLRv1-gradientsolid.glyphs");
assert_eq!(
vec![".notdef", "A", "A.color0", "A.color1"],
result
.fe_context
.glyph_order
.get()
.names()
.map(|gn| gn.as_str())
.collect::<Vec<_>>()
);
let colr = result.font().colr().unwrap();
assert!(matches!(
root_paint(&result, &colr, "A"),
Paint::ColrLayers(_)
));
let layer_list = colr
.layer_list()
.expect("A layer list")
.expect("A valid layer list");
let layers = layer_list
.paints()
.iter()
.map(|p| {
let Ok(Paint::Glyph(paint_glyph)) = p else {
panic!("Bad paint {p:?}");
};
paint_glyph.paint().unwrap()
})
.collect::<Vec<_>>();
assert!(
matches!(
layers.as_slice(),
[Paint::LinearGradient(_), Paint::Solid(_),]
),
"{layers:#?}"
);
}
#[test]
fn colr_split_based_on_paint_same_paint() {
let result = TestCompile::compile_source("glyphs3/COLRv1-manyshapes-per-glyph.glyphs");
assert_eq!(
vec![
".notdef", "A", "B", "A.color0", "A.color1", "B.color0", "B.color1"
],
result
.fe_context
.glyph_order
.get()
.names()
.map(|gn| gn.as_str())
.collect::<Vec<_>>()
);
let colr = result.font().colr().unwrap();
let Paint::ColrLayers(a_layers) = root_paint(&result, &colr, "A") else {
panic!("Expected ColrLayers for glyph A");
};
let Paint::ColrLayers(b_layers) = root_paint(&result, &colr, "B") else {
panic!("Expected ColrLayers for glyph B");
};
assert_eq!(
(a_layers.first_layer_index(), a_layers.num_layers()),
(0, 2),
"Glyph A: FirstLayerIndex, NumLayers"
);
assert_eq!(
(b_layers.first_layer_index(), b_layers.num_layers()),
(2, 2),
"Glyph B: FirstLayerIndex, NumLayers"
);
let layer_list = colr
.layer_list()
.expect("A layer list")
.expect("A valid layer list");
let layers = layer_list
.paints()
.iter()
.map(|p| {
let Ok(Paint::Glyph(paint_glyph)) = p else {
panic!("Bad paint {p:?}");
};
paint_glyph.paint().unwrap()
})
.collect::<Vec<_>>();
let [
Paint::LinearGradient(g0),
Paint::LinearGradient(g1),
Paint::LinearGradient(g2),
Paint::LinearGradient(g3),
] = layers.as_slice()
else {
panic!("Expected 4 LinearGradients, got {layers:#?}");
};
let coords = [g0, g1, g2, g3].map(|g| {
(
(g.x0().to_i16(), g.y0().to_i16()),
(g.x1().to_i16(), g.y1().to_i16()),
(g.x2().to_i16(), g.y2().to_i16()),
)
});
assert_eq!(
coords,
[
((291, 50), (514, 450), (691, -173)),
((91, 30), (314, 270), (331, -193)),
((665, 71), (537, 421), (1015, 200)),
((91, 30), (314, 270), (331, -193)),
],
"p0, p1, p2 for gradients A.color0, A.color1, B.color0, B.color1"
);
}
fn assert_colr0(compile: &TestCompile, expected: &[(&str, &[(&str, u16)])]) {
let colr = compile.font().colr().unwrap();
let layers = colr
.layer_records()
.expect("A layer list")
.expect("A valid layer list");
assert_eq!(0, colr.version());
let actual: Vec<(String, Vec<(String, u16)>)> = colr
.base_glyph_records()
.expect("v0 records")
.expect("Valid v0 records")
.iter()
.map(|g| {
(
compile
.get_glyph_name(g.glyph_id())
.expect("Valid gids")
.to_string(),
(g.first_layer_index()..(g.first_layer_index() + g.num_layers()))
.map(|i| layers[i as usize])
.map(|l| {
(
compile
.get_glyph_name(l.glyph_id())
.expect("Valid gids")
.to_string(),
l.palette_index(),
)
})
.collect::<Vec<_>>(),
)
})
.collect();
let expected: Vec<(String, Vec<(String, u16)>)> = expected
.iter()
.map(|(name, layers)| {
(
name.to_string(),
layers.iter().map(|(n, i)| (n.to_string(), *i)).collect(),
)
})
.collect();
assert_eq!(expected, actual);
}
#[test]
fn colr0_1layer() {
let result = TestCompile::compile_source("glyphs3/COLRv0-1layer.glyphs");
assert_colr0(&result, &[("A", &[("A.color0", 1)])]);
}
#[test]
fn colr0_2layers() {
let result = TestCompile::compile_source("glyphs3/COLRv0-2layers.glyphs");
assert_colr0(&result, &[("A", &[("A.color0", 1), ("A.color1", 0)])]);
}
#[test]
fn colr0_from_ufo() {
let result = TestCompile::compile_source("COLRv0-var/COLRv0-Regular.ufo");
assert_colr0(&result, &[("a", &[("a.color0", 0), ("a.color1", 1)])]);
}
#[test]
fn colr0_from_variable_designspace() {
let result = TestCompile::compile_source("COLRv0-var/COLRv0-var.designspace");
assert_colr0(&result, &[("a", &[("a.color0", 0), ("a.color1", 1)])]);
let font = result.font();
font.fvar().expect("fvar table for variable font");
font.gvar().expect("gvar table for outline variations");
}
#[test]
fn colr0_from_ufo_multi_palette() {
let result = TestCompile::compile_source("COLRv0-multi-palette.ufo");
assert_colr0(
&result,
&[
("a", &[("a.color0", 0), ("a.color1", 1)]),
("b", &[("b.color0", 0)]),
],
);
let cpal = result.font().cpal().expect("CPAL table");
assert_eq!(cpal.num_palettes(), 2);
}
#[rstest]
#[case("glyphs2/IncompatibleUnexported.glyphs")]
#[case("glyphs3/IncompatibleUnexported.glyphs")]
#[case("designspace_from_glyphs/IncompatibleUnexported.designspace")]
fn skip_unexported_glyph_anchors(#[case] source: &str) {
let result = TestCompile::compile_source(source);
assert!(result.contains_glyph("A"));
assert!(result.contains_glyph("space"));
assert!(!result.contains_glyph("doesnt_matter"));
}
#[test]
fn feature_variation_lookup_order_like_fontmake() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("dspace_rules/Basic.designspace");
let gsub = result.font().gsub().unwrap();
let features = gsub.feature_list().unwrap();
let all_referenced_lookups = features
.feature_records()
.iter()
.flat_map(|rec| {
rec.feature(features.offset_data())
.unwrap()
.lookup_list_indices()
})
.map(|x| x.get())
.collect::<HashSet<_>>();
let lookups = gsub.lookup_list().unwrap();
assert!(!all_referenced_lookups.contains(&0));
assert!(!all_referenced_lookups.contains(&1));
let one = lookups.lookups().get(0).unwrap();
let two = lookups.lookups().get(1).unwrap();
fn get_first_gid_or_die_trying(lookup: SubstitutionLookup) -> GlyphId16 {
match lookup {
SubstitutionLookup::Single(sub) => match sub.subtables().get(0).unwrap() {
SingleSubst::Format1(sub) => sub.coverage().unwrap().iter().next().unwrap(),
SingleSubst::Format2(sub) => sub.coverage().unwrap().iter().next().unwrap(),
},
_ => panic!("oops"),
}
}
let one_gid = get_first_gid_or_die_trying(one);
let two_gid = get_first_gid_or_die_trying(two);
assert_eq!(result.get_gid("bar"), one_gid);
assert_eq!(result.get_gid("plus"), two_gid);
}
#[test]
fn rvrn_aalt_lookup_order() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("dspace_rules/Basic.designspace");
let gsub = result.font().gsub().unwrap();
let feature_list = gsub.feature_list().unwrap();
assert_eq!(feature_list.feature_records().len(), 3);
assert_eq!(
feature_list
.feature_records()
.iter()
.map(|rec| rec.feature_tag())
.collect::<Vec<_>>(),
["aalt", "rvrn", "salt"]
);
let salt = feature_list.feature_records()[2];
let aalt = feature_list.feature_records()[0];
assert_eq!(
aalt.feature(feature_list.offset_data())
.unwrap()
.lookup_list_indices(),
[2]
);
assert_eq!(
salt.feature(feature_list.offset_data())
.unwrap()
.lookup_list_indices(),
[3]
);
}
fn get_first_feature_substitution(
gsub: write_fonts::read::tables::gsub::Gsub,
expected_sub_idx: usize,
) -> write_fonts::read::tables::layout::Feature {
let featvars = gsub.feature_variations().unwrap().unwrap();
let var_rec = featvars.feature_variation_records()[0];
let feature_substitution = var_rec
.feature_table_substitution(featvars.offset_data())
.unwrap()
.unwrap();
let sub_record = feature_substitution.substitutions()[0];
assert_eq!(sub_record.feature_index() as usize, expected_sub_idx);
sub_record
.alternate_feature(feature_substitution.offset_data())
.unwrap()
}
#[test]
fn designspace_rvrn_feature_variation() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("dspace_rules/Basic.designspace");
let gsub = result.font().gsub().unwrap();
let feature_list = gsub.feature_list().unwrap();
let rvrn_idx = feature_list
.feature_records()
.iter()
.position(|rec| rec.feature_tag() == "rvrn")
.unwrap();
let rvrn = feature_list.feature_records()[rvrn_idx];
assert!(
rvrn.feature(feature_list.offset_data())
.unwrap()
.lookup_list_indices()
.is_empty()
);
let rvrn_replacement = get_first_feature_substitution(gsub, rvrn_idx);
assert_eq!(rvrn_replacement.lookup_list_indices(), [0, 1]);
}
#[test]
fn glyphs_feature_variations() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("glyphs3/LibreFranklin-bracketlayer.glyphs");
let gsub = result.font().gsub().unwrap();
let lookup_list = gsub.lookup_list().unwrap();
assert_eq!(lookup_list.lookup_count(), 1);
let feature_list = gsub.feature_list().unwrap();
let rvrn_idx = feature_list
.feature_records()
.iter()
.position(|rec| rec.feature_tag() == "rvrn")
.unwrap();
let rvrn = feature_list.feature_records()[rvrn_idx];
assert!(
rvrn.feature(feature_list.offset_data())
.unwrap()
.lookup_list_indices()
.is_empty()
);
let rvrn_replacement = get_first_feature_substitution(gsub, rvrn_idx);
assert_eq!(rvrn_replacement.lookup_list_indices(), [0]);
}
#[test]
fn glyphs_feature_variations_custom_feature() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("glyphs2/WorkSans-minimal-bracketlayer.glyphs");
let gsub = result.font().gsub().unwrap();
let lookup_list = gsub.lookup_list().unwrap();
assert_eq!(lookup_list.lookup_count(), 1);
let feature_list = gsub.feature_list().unwrap();
assert_eq!(feature_list.feature_records().len(), 1);
assert_eq!(feature_list.feature_records()[0].feature_tag(), "rclt");
let replacement = get_first_feature_substitution(gsub, 0);
assert_eq!(replacement.lookup_list_indices(), [0]);
}
#[test]
fn glyphs_fea_include_file() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let result = TestCompile::compile_source("glyphs_fea_include/glyphs_include.glyphs");
let gsub = result.font().gsub().unwrap();
let feature_list = gsub.feature_list().unwrap();
assert_eq!(feature_list.feature_records().len(), 1);
assert_eq!(feature_list.feature_records()[0].feature_tag(), "test");
}
#[rstest]
#[case("glyphs3/InstancePostscript.glyphs")]
#[case("glyphs2/InstancePostscript.glyphs")]
fn compile_instance_postscript_names(#[case] source: &str) {
let result = TestCompile::compile_source(source);
let font = result.font();
let fvar = font.fvar().unwrap();
let name = font.name().unwrap();
let actual = fvar
.instances()
.unwrap()
.iter()
.map(|instance| {
instance
.unwrap()
.post_script_name_id
.map(|id| resolve_name(&name, id).unwrap())
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![
None,
Some("PostMedium".to_string()),
Some("PostBold".to_string())
]
);
}
#[test]
fn dont_compile_instance_postscript_names_if_none() {
let result = TestCompile::compile_source("glyphs3/InstanceNoPostscript.glyphs");
let font = result.font();
let fvar = font.fvar().unwrap();
assert!(
fvar.instances()
.unwrap()
.iter()
.all(|instance| instance.unwrap().post_script_name_id.is_none())
);
}
#[rstest]
#[case::yes(true)]
#[case::no(false)]
fn rename_glyphs_to_production_names(#[case] use_adobe_style_post_names: bool) {
let result = TestCompile::compile("glyphs3/ProductionNames.glyphs", |mut args| {
args.flags
.set(Flags::PRODUCTION_NAMES, use_adobe_style_post_names);
args
});
let raw_post = dump_table(result.be_context.post.get().as_ref()).unwrap();
let post = Post::read(FontData::new(&raw_post)).unwrap();
if use_adobe_style_post_names {
assert_eq!(post.glyph_name(result.get_gid("A")), Some("A"));
assert_eq!(post.glyph_name(result.get_gid("idotless")), Some("uni0131"));
assert_eq!(post.glyph_name(result.get_gid("nbspace")), Some("uni00A0"));
assert_eq!(
post.glyph_name(result.get_gid("invalid_prod_name")),
Some("uni99")
);
assert_eq!(
post.glyph_name(result.get_gid("foobar-cy")),
Some("foobarcy")
);
assert_eq!(
post.glyph_name(result.get_gid("foobarcy")),
Some("foobarcy.1")
);
assert_eq!(
post.glyph_name(result.get_gid("foobarcy.1")),
Some("foobarcy.1.1")
);
assert_eq!(
post.glyph_name(result.get_gid("duplicate.1")),
Some("uniF8FF")
);
assert_eq!(
post.glyph_name(result.get_gid("duplicate.2")),
Some("uniF8FF.1")
);
assert_eq!(
post.glyph_name(result.get_gid("A_nbspace_idotless")),
Some("uni004100A00131")
);
assert_eq!(
post.glyph_name(result.get_gid("A_nbspace_idotless.ss01")),
Some("uni004100A00131.ss01")
);
} else {
let glyph_order = result.fe_context.glyph_order.get();
for name in glyph_order.iter().map(|(_, name)| name.as_str()) {
assert_eq!(post.glyph_name(result.get_gid(name)), Some(name));
}
}
}
#[rstest]
#[case("glyphs3/DontUseProductionNames.glyphs")]
#[case("glyphs2/DontUseProductionNames.glyphs")]
#[case("designspace_from_glyphs/DontuseProductionNames.designspace")]
fn dont_use_production_names_custom_parameter(#[case] source: &str) {
let result = TestCompile::compile_source(source);
let font = result.font();
let post = font.post().unwrap();
assert_eq!(post.glyph_name(result.get_gid("nbspace")), Some("nbspace"));
}
#[rstest]
#[case("glyphs2/Vertical.glyphs")]
#[case("Vertical.ufo")]
fn compile_vertical_typesetting_header(#[case] source: &str) {
let result = TestCompile::compile_source(source);
let font = result.font();
let maxp = font.maxp().unwrap();
let vhea = font.vhea().expect("should include vertical table");
assert_eq!(vhea.version(), Version16Dot16::VERSION_1_1);
assert_eq!(vhea.ascender().to_i16(), 3456);
assert_eq!(vhea.descender().to_i16(), -789);
assert_eq!(vhea.line_gap().to_i16(), 12);
assert_eq!(vhea.caret_slope_rise(), 500);
assert_eq!(vhea.caret_slope_run(), 1000);
assert_eq!(vhea.caret_offset(), 250);
assert_eq!(vhea.number_of_long_ver_metrics(), maxp.num_glyphs() - 1);
let big_advance = 3333;
let big_v_origin = 2500;
let (big_y_max, big_y_min) = (8000, -4000);
assert_eq!(vhea.advance_height_max().to_u16(), big_advance);
assert_eq!(
vhea.min_top_side_bearing().to_i16(),
big_v_origin - big_y_max
);
assert_eq!(
vhea.min_bottom_side_bearing().to_i16(),
big_y_min - (big_v_origin - big_advance as i16)
);
assert_eq!(vhea.y_max_extent().to_i16(), big_v_origin - big_y_min);
}
#[rstest]
#[case("glyphs2/Vertical.glyphs")]
#[case("Vertical.ufo")]
fn compile_vertical_typesetting_metrics(#[case] source: &str) {
let result = TestCompile::compile_source(source);
let font = result.font();
let vmtx = font.vmtx().expect("should include vertical table");
let (ascender, descender) = (1234, -567);
let typo_asc = 2345;
let notdef_height = (ascender - descender) as u16;
let notdef_y_max = ascender;
let default_vert_origin = typo_asc;
let empty_y_max = 0;
assert_eq!(
vmtx.v_metrics()
.iter()
.map(|long_metric| (long_metric.advance(), long_metric.side_bearing()))
.collect::<Vec<_>>(),
vec![
(notdef_height, default_vert_origin - notdef_y_max),
(3333, 2500 - 8000),
(0, default_vert_origin - empty_y_max),
(2000, default_vert_origin - empty_y_max)
]
);
assert_eq!(
vmtx.top_side_bearings()
.iter()
.map(|value| value.get())
.collect::<Vec<_>>(),
vec![
default_vert_origin - empty_y_max
]
);
}
#[test]
fn bracket_glyphs() {
let result = TestCompile::compile_source("glyphs3/LibreFranklin-bracketlayer.glyphs");
let glyph_data = result.glyphs();
let glyphs = glyph_data.read();
assert_eq!(glyphs.len(), 5);
let yen_gid = result.get_gid("yen");
let yen_bracket_gid = result.get_gid("yen.BRACKET.varAlt01");
let peso_gid = result.get_gid("peso");
let peso_bracket_gid = result.get_gid("peso.BRACKET.varAlt01");
assert!(
[yen_gid, yen_bracket_gid, peso_gid, peso_bracket_gid]
.iter()
.all(|gid| *gid > GlyphId16::NOTDEF)
);
fn get_component_gids(glyph: &glyf::Glyph) -> Vec<GlyphId16> {
match glyph {
glyf::Glyph::Simple(_) => panic!("not a composite"),
glyf::Glyph::Composite(g) => g.components().map(|comp| comp.glyph).collect(),
}
}
let yen = glyphs[yen_gid.to_u32() as usize].as_ref().unwrap();
let yen_bracket = glyphs[yen_bracket_gid.to_u32() as usize].as_ref().unwrap();
assert_eq!(get_component_gids(yen), [peso_gid]);
assert_eq!(get_component_gids(yen_bracket), [peso_bracket_gid]);
}
#[test]
fn glyf_loca_work_waits_for_dynamic_notdef() {
let mut test = TestCompile::new("static.designspace", |options| options);
let glyf_loca_job = test
.workload
.jobs_pending
.get(&AnyWorkId::Be(BeWorkIdentifier::Glyf))
.expect("GlyfLocaWork should exist");
assert_eq!(
glyf_loca_job.read_access,
AnyAccess::Be(Access::Unknown),
"GlyfLocaWork.read_access should start with Access::Unknown"
);
test.run();
let completed = test.work_executed.iter().cloned().collect::<HashSet<_>>();
assert!(completed.contains(&AnyWorkId::Be(BeWorkIdentifier::Glyf)));
assert!(completed.contains(&AnyWorkId::Be(BeWorkIdentifier::Loca)));
}
#[test]
fn gvar_work_waits_for_dynamic_notdef() {
let mut test = TestCompile::new("wght_var.designspace", |options| options);
let gvar_job = test
.workload
.jobs_pending
.get(&AnyWorkId::Be(BeWorkIdentifier::Gvar))
.expect("GvarWork should exist");
assert_eq!(
gvar_job.read_access,
AnyAccess::Be(Access::Unknown),
"GvarWork.read_access should start with Access::Unknown"
);
test.run();
let completed = test.work_executed.iter().cloned().collect::<HashSet<_>>();
assert!(completed.contains(&AnyWorkId::Be(BeWorkIdentifier::Gvar)));
}
#[test]
fn component_point_rounding() {
let result = TestCompile::compile_source("glyphs3/ComponentPointRounding.glyphs");
let deltas =
result
.be_context
.gvar_fragments
.get(&AnyWorkId::Be(BeWorkIdentifier::GvarFragment(
"Reversedzecyr".into(),
)));
assert_eq!(deltas.deltas.len(), 2);
let defaults = deltas.deltas[0].1.as_slice();
let deltas = deltas.deltas[1].1.as_slice();
assert_eq!(
defaults.iter().map(|d| d.x).collect::<Vec<_>>(),
[573, 0, 578, 0, 0]
);
assert_eq!(
deltas.iter().map(|d| d.x).collect::<Vec<_>>(),
[3, 0, 0, 0, 0]
)
}
fn has_use_my_metrics_flag(font_file: &str, composite_glyph_name: &str) -> bool {
let result = TestCompile::compile_source(font_file);
let glyph_data = result.glyphs();
let glyphs = glyph_data.read();
let gid = result.get_glyph_index(composite_glyph_name).unwrap();
let Some(glyf::Glyph::Composite(glyph)) = &glyphs[gid as usize] else {
panic!(
"Expected '{}' to be a composite glyph",
composite_glyph_name
);
};
glyph.components().any(|comp| {
comp.flags
.contains(glyf::CompositeGlyphFlags::USE_MY_METRICS)
})
}
#[test]
fn static_font_sets_use_my_metrics() {
assert!(
has_use_my_metrics_flag("glyphs3/StaticComposite.glyphs", "equal"),
"Static composite glyph should have USE_MY_METRICS set on at least one component"
);
}
#[test]
fn variable_font_does_not_set_use_my_metrics() {
assert!(
!has_use_my_metrics_flag("glyphs3/WghtVarComposite.glyphs", "equal"),
"Variable composite glyph should NOT have USE_MY_METRICS set on any component"
);
}
#[test]
fn ufo_with_open_corners() {
let result = TestCompile::compile_source("OpenCorners.ufo");
let glyphs = result.glyphs();
let idx = result.get_glyph_index("ordfeminine").unwrap();
let glyph_array = glyphs.read();
let Some(glyf::Glyph::Simple(glyph)) = glyph_array[idx as usize].as_ref() else {
panic!("not a simple glyph");
};
assert_eq!(glyph.num_points(), 3); }
#[test]
fn generate_variable_notdef() {
let result = TestCompile::compile_source("glyphs3/empty_font_needs_variable_notdef.glyphs");
let ir_notdef = result.fe_context.get_glyph(".notdef");
assert_eq!(ir_notdef.sources().len(), 2);
assert_eq!(
ir_notdef
.sources()
.values()
.map(|inst| inst.height.unwrap_or(0.) as u32)
.collect::<HashSet<_>>(),
HashSet::from([1000, 1100])
);
}
#[test]
fn ufo2ftfilters() {
let result = TestCompile::compile_source("glyphs3/UfoFilters.glyphs");
assert!(result.fe_context.flags.contains(Flags::FLATTEN_COMPONENTS));
assert!(!result.fe_context.flags.contains(Flags::ERASE_OPEN_CORNERS));
assert!(result.fe_context.flags.contains(Flags::PROPAGATE_ANCHORS));
assert!(
result
.fe_context
.flags
.contains(Flags::DECOMPOSE_TRANSFORMED_COMPONENTS),
"decomposeTransformedComponents filter in userData should set DECOMPOSE_TRANSFORMED_COMPONENTS flag"
);
}
#[test]
fn color_base_glyphs() {
let result = TestCompile::compile_source("glyphs3/COLRv1-gradient.glyphs");
let colr = result.font().colr().unwrap();
assert_eq!(
8,
colr.base_glyph_list()
.expect("A base glyph list")
.expect("A valid base glyph list")
.base_glyph_paint_records()
.iter()
.count()
);
}
#[test]
fn compile_in_memory() {
let result = TestCompile::compile("glyphs3/WghtVar.glyphs", |mut args| {
args.debug_dir = None;
args.output_file = None;
args
});
assert_ne!(
0,
result.be_context.font.get().len(),
"We should still produce a font"
);
let temp_files = result.temp.path().read_dir().unwrap().collect::<Vec<_>>();
assert_eq!(
0,
temp_files.len(),
"We still write to disk?!\n{}",
temp_files
.iter()
.map(|p| format!("{p:?}"))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn fea_table_overrides_head() {
let result = TestCompile::compile_source("FeaTableOverrides.ufo");
let head = result.font().head().unwrap();
assert_eq!(
write_fonts::types::Fixed::from_f64(2.5),
head.font_revision()
);
}
#[test]
fn fea_table_overrides_hhea() {
let result = TestCompile::compile_source("FeaTableOverrides.ufo");
let hhea = result.font().hhea().unwrap();
assert_eq!(write_fonts::types::FWord::new(950), hhea.ascender());
assert_eq!(write_fonts::types::FWord::new(-250), hhea.descender());
assert_eq!(write_fonts::types::FWord::new(0), hhea.line_gap());
assert_eq!(20, hhea.caret_offset());
}
#[test]
fn fea_table_overrides_os2() {
let result = TestCompile::compile_source("FeaTableOverrides.ufo");
let os2 = result.font().os2().unwrap();
assert_eq!(0, os2.fs_type());
assert_eq!(950, os2.s_typo_ascender());
assert_eq!(-250, os2.s_typo_descender());
assert_eq!(0, os2.s_typo_line_gap());
assert_eq!(1100, os2.us_win_ascent());
assert_eq!(250, os2.us_win_descent());
assert_eq!(700, os2.us_weight_class());
}
fn gpos_feature_tags(font: &FontRef) -> Vec<Tag> {
let gpos = font.gpos().unwrap();
let feature_list = gpos.feature_list().unwrap();
feature_list
.feature_records()
.iter()
.map(|r| r.feature_tag())
.collect()
}
#[test]
fn feature_writers_absent_generates_defaults() {
let compile = TestCompile::compile_source("glyphs3/FeatureWritersDefault.glyphs");
let font = compile.font();
let tags = gpos_feature_tags(&font);
assert!(tags.contains(&Tag::new(b"kern")), "{tags:?}");
assert!(tags.contains(&Tag::new(b"mark")), "{tags:?}");
let gdef = font.gdef().unwrap();
let classdef = gdef.glyph_class_def().unwrap().unwrap();
let gravecomb = compile.get_gid("gravecomb");
assert_eq!(
classdef.get(gravecomb),
GlyphClassDef::Mark as u16,
"the combining mark should get GDEF class Mark"
);
assert!(
gdef.lig_caret_list().is_some(),
"GdefFeatureWriter should emit a ligature caret list"
);
}
#[test]
fn feature_writers_empty_list_disables_everything() {
let compile = TestCompile::compile_source("glyphs3/FeatureWritersEmpty.glyphs");
let font = compile.font();
assert!(font.gpos().is_err(), "expected no GPOS");
assert!(font.gdef().is_err(), "expected no GDEF");
}
#[test]
fn feature_writers_curs_from_glyphs_user_data() {
let compile = TestCompile::compile_source("glyphs3/FeatureWritersCurs.glyphs");
let font = compile.font();
let tags = gpos_feature_tags(&font);
assert!(tags.contains(&Tag::new(b"curs")), "{tags:?}");
assert!(tags.contains(&Tag::new(b"kern")), "{tags:?}");
}
#[test]
fn feature_writers_kern_append_keeps_source_kerning() {
let compile = TestCompile::compile_source("fw_append.designspace");
let font = compile.font();
let gpos = font.gpos().unwrap();
assert_eq!(
gpos.lookup_list().unwrap().lookup_count(),
2,
"manual plus appended source kern lookups"
);
let feature_list = gpos.feature_list().unwrap();
let appended = feature_list.feature_records().iter().any(|r| {
r.feature_tag() == Tag::new(b"kern")
&& r.feature(feature_list.offset_data())
.unwrap()
.lookup_list_indices()
.iter()
.any(|i| i.get() == 1)
});
assert!(
appended,
"the source-derived kern lookup should be appended to the kern feature"
);
}
}