use crate::{
constants::{OutlineType, RIBBI_STYLE_NAMES, STATIC_STYLE_NAMES},
error::FontspectorError,
filetype::FileTypeConvert,
Context, FileType, Testable,
};
use fontations::{
read::{tables::name::NameString, TopLevelTable},
skrifa::{
font::FontRef,
outline::{DrawSettings, OutlinePen},
prelude::Size,
raw::{
tables::{
gdef::GlyphClassDef,
glyf::Glyph,
gpos::{PairPos, PairPosFormat1, PairPosFormat2, PositionSubtables},
head::MacStyle,
layout::{Feature, FeatureRecord},
os2::SelectionFlags,
},
ReadError, TableProvider,
},
setting::VariationSetting,
string::StringId,
GlyphId, GlyphId16, GlyphNames, MetadataProvider, Tag,
},
write::{validate::Validate, FontWrite},
};
use itertools::Either;
use std::{
collections::{BTreeMap, HashMap, HashSet},
error::Error,
fmt::{Debug, Formatter},
path::{Path, PathBuf},
};
pub struct TestFont<'a> {
pub filename: PathBuf,
font_data: &'a [u8],
pub glyph_count: usize,
}
impl Debug for TestFont<'_> {
fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
write!(f, "<TestFont:{}>", self.filename.display())
}
}
pub const TTF: FileType = FileType {
pattern: "*.[ot]tf",
};
impl<'a> FileTypeConvert<'a, TestFont<'a>> for FileType<'a> {
fn from_testable(&self, t: &'a Testable) -> Option<TestFont<'a>> {
self.applies(t)
.then(|| TestFont::new_from_data(&t.filename, &t.contents))
.transpose()
.unwrap_or(None)
}
}
impl TestFont<'_> {
pub fn new_from_data<'a>(
filename: &Path,
font_data: &'a [u8],
) -> Result<TestFont<'a>, Box<dyn Error>> {
let font = FontRef::new(font_data)?;
let glyph_count = font.maxp()?.num_glyphs().into();
Ok(TestFont {
filename: filename.to_path_buf(),
font_data,
glyph_count,
})
}
pub fn font(&self) -> FontRef<'_> {
#[allow(clippy::expect_used)] FontRef::new(self.font_data).expect("Can't happen")
}
pub fn style(&self) -> Option<&str> {
if self.is_variable_font() {
if let Some(default_location) = self.default_location() {
if default_location.get("wght") == Some(&700.0) {
if self.filename.to_str()?.contains("Italic") {
return Some("BoldItalic");
} else {
return Some("Bold");
}
} else {
if self.filename.to_str()?.contains("Italic") {
return Some("Italic");
}
return Some("Regular");
}
}
}
if let Some(style_part) = self.filename.file_stem()?.to_str()?.split('-').next_back() {
for styles in STATIC_STYLE_NAMES.iter() {
if style_part == styles.replace(" ", "") {
return Some(style_part);
}
}
}
None
}
pub fn is_ribbi(&self) -> bool {
self.style()
.is_some_and(|s| RIBBI_STYLE_NAMES.iter().any(|r| r == &s))
}
pub fn is_italic(&self) -> Result<bool, ReadError> {
let font = self.font();
let os2 = font.os2()?;
if os2.fs_selection().contains(SelectionFlags::ITALIC) {
return Ok(true);
}
let head = font.head()?;
if head.mac_style().contains(MacStyle::ITALIC) {
return Ok(true);
}
if self
.get_name_entry_strings(StringId::FULL_NAME)
.any(|x| x.to_lowercase().contains("italic"))
{
return Ok(true);
}
let post = font.post()?;
if post.italic_angle().to_f32() != 0.0 {
return Ok(true);
}
Ok(false)
}
pub fn has_table(&self, table: &[u8; 4]) -> bool {
self.font().table_data(Tag::new(table)).is_some()
}
pub fn gdef_class(&self, glyph_id: impl Into<GlyphId>) -> GlyphClassDef {
if let Some(Ok(class_def)) = self
.font()
.gdef()
.ok()
.and_then(|gdef| gdef.glyph_class_def())
{
GlyphId16::try_from(glyph_id.into())
.map(|gid| class_def.get(gid))
.map_or(GlyphClassDef::Unknown, GlyphClassDef::new)
} else {
GlyphClassDef::Unknown
}
}
pub fn get_os2_fsselection(&self) -> Result<SelectionFlags, FontspectorError> {
let os2 = self.font().os2()?;
Ok(os2.fs_selection())
}
pub fn get_name_entry_strings(&self, name_id: StringId) -> impl Iterator<Item = String> + '_ {
self.font()
.localized_strings(name_id)
.map(|s| s.to_string())
}
fn glyph_name_for_id_impl(&self, gid: impl Into<GlyphId>, synthesize: bool) -> Option<String> {
let names = GlyphNames::new(&self.font());
let proposed_name = names.get(gid.into());
proposed_name.and_then(|name| {
if name.is_synthesized() && !synthesize {
None
} else {
Some(name.as_str().to_string())
}
})
}
pub fn glyph_name_for_id(&self, gid: impl Into<GlyphId>) -> Option<String> {
self.glyph_name_for_id_impl(gid, false)
}
pub fn glyph_name_for_id_synthesise(&self, gid: impl Into<GlyphId>) -> String {
#[allow(clippy::unwrap_used)]
self.glyph_name_for_id_impl(gid, true).unwrap()
}
fn glyph_name_for_unicode_impl(&self, u: impl Into<u32>, synthesize: bool) -> Option<String> {
self.font()
.charmap()
.map(u)
.and_then(|gid| self.glyph_name_for_id_impl(gid, synthesize))
}
pub fn glyph_name_for_unicode(&self, u: impl Into<u32>) -> Option<String> {
self.glyph_name_for_unicode_impl(u, false)
}
pub fn glyph_name_for_unicode_synthesise(&self, u: impl Into<u32>) -> String {
#[allow(clippy::unwrap_used)]
self.glyph_name_for_unicode_impl(u, true).unwrap()
}
pub fn get_glyf_glyph(&self, gid: GlyphId) -> Result<Option<Glyph<'_>>, ReadError> {
let loca = self.font().loca(None)?;
let glyf = self.font().glyf()?;
loca.get_glyf(gid, &glyf)
}
pub fn is_variable_font(&self) -> bool {
self.has_table(b"fvar")
}
pub fn outline_type(&self) -> OutlineType {
if self.has_table(b"glyf") {
OutlineType::TrueType
} else {
OutlineType::CFF
}
}
pub fn has_axis(&self, axis: &str) -> bool {
self.is_variable_font() && self.font().axes().iter().any(|a| a.tag() == axis)
}
pub fn default_location(&self) -> Option<HashMap<String, f32>> {
Some(
self.font()
.fvar()
.ok()?
.axes()
.ok()?
.iter()
.map(|axis| {
let tag = axis.axis_tag().to_string();
let default = axis.default_value().to_f32();
(tag, default)
})
.collect(),
)
}
pub fn codepoints(&self, context: Option<&Context>) -> HashSet<u32> {
let get_codepoints = || {
Ok(self
.font()
.charmap()
.mappings()
.map(|(u, _gid)| u)
.collect::<HashSet<u32>>())
};
if let Some(context) = context {
let key = "codepoints:".to_string() + &self.filename.to_string_lossy();
#[allow(clippy::unwrap_used)] context
.cached_question(
&key,
get_codepoints,
|hashset| serde_json::to_value(hashset).unwrap(),
|value| {
serde_json::from_value(value.clone())
.map_err(|e| FontspectorError::CacheSerialization(e.to_string()))
},
)
.unwrap_or_default()
} else {
get_codepoints().unwrap_or_default()
}
}
pub fn named_instances(&self) -> impl Iterator<Item = (String, BTreeMap<String, f32>)> + '_ {
self.font().named_instances().iter().map(|ni| {
let instance_name = self
.font()
.localized_strings(ni.subfamily_name_id())
.english_or_first()
.map(|s| s.chars().collect::<String>())
.unwrap_or("Unnamed".to_string());
let coords = ni
.user_coords()
.zip(self.font().axes().iter())
.map(|(coord, axis)| (axis.tag().to_string(), coord));
(instance_name, coords.collect())
})
}
pub fn axis_ranges(&self) -> impl Iterator<Item = (String, f32, f32, f32)> + '_ {
self.font().axes().iter().map(|axis| {
let tag = axis.tag().to_string();
let min = axis.min_value();
let max = axis.max_value();
let def = axis.default_value();
(tag, min, def, max)
})
}
pub fn draw_glyph<I>(
&self,
gid: GlyphId,
pen: &mut impl OutlinePen,
settings: I,
) -> Result<(), FontspectorError>
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
let glyph = self
.font()
.outline_glyphs()
.get(gid)
.ok_or_else(|| FontspectorError::skip("no-H", "No H glyph in font"))?;
let location = self.font().axes().location(settings);
let settings = DrawSettings::unhinted(Size::unscaled(), &location);
glyph.draw(settings, pen)?;
Ok(())
}
pub fn feature_records(
&self,
gsub_only: bool,
) -> impl Iterator<Item = (&FeatureRecord, Result<Feature<'_>, ReadError>)> {
let gsub_featurelist = self
.font()
.gsub()
.ok()
.and_then(|gsub| gsub.feature_list().ok());
let gpos_feature_list = self
.font()
.gpos()
.ok()
.and_then(|gpos| gpos.feature_list().ok());
let gsub_feature_and_data = gsub_featurelist.map(|list| {
list.feature_records()
.iter()
.map(move |feature| (feature, feature.feature(list.offset_data())))
});
let gpos_feature_and_data = gpos_feature_list.map(|list| {
list.feature_records()
.iter()
.map(move |feature| (feature, feature.feature(list.offset_data())))
});
let iter = gsub_feature_and_data.into_iter().flatten();
if gsub_only {
Either::Left(iter)
} else {
Either::Right(iter.chain(gpos_feature_and_data.into_iter().flatten()))
}
}
pub fn has_feature(&self, gsub_only: bool, tag: &str) -> bool {
self.feature_records(gsub_only)
.any(|(f, _)| f.feature_tag() == tag)
}
pub fn all_glyphs(&self) -> impl Iterator<Item = GlyphId> {
(0..self.glyph_count as u32).map(GlyphId::from)
}
pub fn cjk_codepoints(&self, context: Option<&Context>) -> impl Iterator<Item = u32> {
self.codepoints(context)
.into_iter()
.filter(|&cp| is_cjk(cp))
}
pub fn is_cjk_font(&self, context: Option<&Context>) -> bool {
self.cjk_codepoints(context).count() > 150
}
pub fn process_kerning<T>(
&self,
format1_func: &dyn Fn(PairPosFormat1) -> Result<Vec<T>, ReadError>,
format2_func: &dyn Fn(PairPosFormat2) -> Result<Vec<T>, ReadError>,
) -> Result<Vec<T>, ReadError> {
let gpos = self.font().gpos()?;
Ok(
gpos.lookup_list()?
.lookups()
.iter()
.flatten()
.flat_map(|l| l.subtables())
.filter_map(|s| match s {
PositionSubtables::Pair(p) => Some(p),
_ => None,
})
.flat_map(|p| p.iter())
.flatten()
.map(|pp| match pp {
PairPos::Format1(pp1) => format1_func(pp1),
PairPos::Format2(pp2) => format2_func(pp2),
})
.flat_map(|v| v.into_iter())
.flatten()
.collect(), )
}
pub fn get_best_name(&self, ids: &[StringId]) -> Option<String> {
for id in ids {
if let Some(name) = self.font().localized_strings(*id).english_or_first() {
return Some(name.chars().collect());
}
}
None
}
pub fn best_familyname(&self) -> Option<String> {
self.get_best_name(&[
StringId::WWS_FAMILY_NAME,
StringId::TYPOGRAPHIC_FAMILY_NAME,
StringId::FAMILY_NAME,
])
}
pub fn best_subfamilyname(&self) -> Option<String> {
self.get_best_name(&[
StringId::WWS_SUBFAMILY_NAME,
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
StringId::SUBFAMILY_NAME,
])
}
pub fn vertical_metrics(&self) -> Result<VerticalMetrics, FontspectorError> {
Ok(VerticalMetrics {
upm: self.font().head()?.units_per_em(),
os2_typo_ascender: self.font().os2()?.s_typo_ascender(),
os2_typo_descender: self.font().os2()?.s_typo_descender(),
os2_typo_linegap: self.font().os2()?.s_typo_line_gap(),
hhea_ascent: self.font().hhea()?.ascender().to_i16(),
hhea_descent: self.font().hhea()?.descender().to_i16(),
hhea_linegap: self.font().hhea()?.line_gap().to_i16(),
os2_win_ascent: self.font().os2()?.us_win_ascent(),
os2_win_descent: self.font().os2()?.us_win_descent(),
})
}
pub fn use_typo_metrics(&self) -> Result<bool, FontspectorError> {
Ok(self
.font()
.os2()?
.fs_selection()
.intersects(SelectionFlags::USE_TYPO_METRICS))
}
pub fn rebuild_with_new_table<T: FontWrite + Validate + TopLevelTable>(
&self,
table: &T,
) -> Result<Vec<u8>, FontspectorError> {
let mut new_font = fontations::write::FontBuilder::new();
new_font.add_table(table)?;
new_font.copy_missing_tables(self.font());
Ok(new_font.build())
}
}
fn is_cjk(cp: u32) -> bool {
crate::constants::CJK_UNICODE_RANGES
.iter()
.any(|range| range.contains(&cp))
}
pub const DEFAULT_LOCATION: &[VariationSetting] = &[];
pub struct VerticalMetrics {
pub upm: u16,
pub os2_typo_ascender: i16,
pub os2_typo_descender: i16,
pub os2_typo_linegap: i16,
pub os2_win_ascent: u16,
pub os2_win_descent: u16,
pub hhea_ascent: i16,
pub hhea_descent: i16,
pub hhea_linegap: i16,
}
impl VerticalMetrics {
pub fn scale_to_upm(&self, other_upm: u16) -> VerticalMetrics {
let scaled_upm = other_upm as f32 / self.upm as f32;
VerticalMetrics {
upm: other_upm,
os2_typo_ascender: (self.os2_typo_ascender as f32 * scaled_upm).ceil() as i16,
os2_typo_descender: (self.os2_typo_descender as f32 * scaled_upm).ceil() as i16,
os2_typo_linegap: (self.os2_typo_linegap as f32 * scaled_upm).ceil() as i16,
os2_win_ascent: (self.os2_win_ascent as f32 * scaled_upm).ceil() as u16,
os2_win_descent: (self.os2_win_descent as f32 * scaled_upm).ceil() as u16,
hhea_ascent: (self.hhea_ascent as f32 * scaled_upm).ceil() as i16,
hhea_descent: (self.hhea_descent as f32 * scaled_upm).ceil() as i16,
hhea_linegap: (self.hhea_linegap as f32 * scaled_upm).ceil() as i16,
}
}
}
pub struct PlatformSelector {
pub platform_id: u16,
pub encoding_id: u16,
pub language_id: u16,
}
pub fn get_name_entry_string<'a>(
font: &'a FontRef,
selector: PlatformSelector,
name_id: StringId,
) -> Option<NameString<'a>> {
let name = font.name().ok();
let mut records = name
.as_ref()
.map(|name| name.name_record().iter())
.unwrap_or([].iter());
records.find_map(|record| {
if record.platform_id() == selector.platform_id
&& record.encoding_id() == selector.encoding_id
&& record.language_id() == selector.language_id
&& record.name_id() == name_id
{
let name_table = name.as_ref()?;
record.string(name_table.string_data()).ok()
} else {
None
}
})
}
pub fn get_name_platform_tuples(font: FontRef) -> HashSet<(u16, u16, u16)> {
let name_table = font.name().ok();
let mut codes = HashSet::new();
if let Some(name_table) = name_table {
for rec in name_table.name_record().iter() {
let code = (rec.platform_id(), rec.encoding_id(), rec.language_id());
codes.insert(code);
}
}
codes
}