use std::collections::{HashMap, HashSet};
use ttf_parser::{
gpos, gsub,
opentype_layout::{Coverage, LookupSubtable},
Face, Tag,
};
#[derive(Debug, Clone)]
pub struct FvarAxis {
pub tag: String,
pub min: f32,
pub def: f32,
pub max: f32,
pub flags: u16,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct FvarInstance {
pub name: String,
pub coordinates: HashMap<String, f32>,
pub flags: u16,
pub postscript_name: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct FvarData {
pub axes: HashMap<String, FvarAxis>,
pub instances: Vec<FvarInstance>,
}
#[derive(Debug, Clone)]
pub struct StatAxisValue {
pub name: String,
pub value: f32,
pub linked_value: Option<f32>,
pub range_min_value: Option<f32>,
pub range_max_value: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct StatAxis {
pub tag: String,
pub name: String,
pub values: Vec<StatAxisValue>,
}
#[derive(Debug, Clone)]
pub struct StatCombination {
pub name: String,
pub values: Vec<(String, f32)>,
}
#[derive(Debug, Clone, Default)]
pub struct StatData {
pub axes: Vec<StatAxis>,
pub combinations: Vec<StatCombination>,
pub elided_fallback_name: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct FontFeature {
pub tag: String,
pub name: String,
pub tooltip: Option<String>,
pub sample_text: Option<String>,
pub glyphs: Vec<String>,
pub param_labels: Vec<String>,
pub lookup_indices: Vec<u16>,
}
pub struct Parser<'a> {
face: Face<'a>,
}
impl<'a> Parser<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, ttf_parser::FaceParsingError> {
let face = Face::parse(data, 0)?;
Ok(Self { face })
}
pub fn fvar(&self) -> FvarData {
let table = match self.face.raw_face().table(Tag::from_bytes(b"fvar")) {
Some(t) => t,
None => return FvarData::default(),
};
parse_fvar(&self.face, table)
}
pub fn stat(&self) -> StatData {
let table = match self.face.raw_face().table(Tag::from_bytes(b"STAT")) {
Some(t) => t,
None => return StatData::default(),
};
parse_stat(&self.face, table)
}
pub fn ffeatures(&self) -> Vec<FontFeature> {
let mut features = Vec::new();
if let Some(data) = self.face.raw_face().table(Tag::from_bytes(b"GSUB")) {
features.extend(parse_gsub_features_raw(&self.face, data));
}
if let Some(gpos_table) = self.face.tables().gpos {
features.extend(parse_gpos_features(&self.face, gpos_table));
}
features
}
pub fn extract_face_record(
&self,
face_id: String,
user_font_style_italic: Option<bool>,
) -> Result<crate::selection::FaceRecord, String> {
crate::selection::extract_face_record(&self.face, face_id, user_font_style_italic)
}
pub fn is_variable(&self) -> bool {
self.face.is_variable()
}
}
fn parse_fvar(face: &Face<'_>, data: &[u8]) -> FvarData {
if data.len() < 16 {
return FvarData::default();
}
let axis_offset = be_u16(data, 4) as usize;
let axis_count = be_u16(data, 8) as usize;
let axis_size = be_u16(data, 10) as usize;
let instance_count = be_u16(data, 12) as usize;
let instance_size = be_u16(data, 14) as usize;
let mut axes: HashMap<String, FvarAxis> = HashMap::new();
let mut axis_tags: Vec<String> = Vec::new();
for i in 0..axis_count {
let off = axis_offset + i * axis_size;
if off + axis_size > data.len() {
break;
}
let tag = tag_to_string(&data[off..off + 4]);
let min = be_fixed(data, off + 4);
let def = be_fixed(data, off + 8);
let max = be_fixed(data, off + 12);
let flags = be_u16(data, off + 16);
let name_id = be_u16(data, off + 18);
let name = lookup_name(face, name_id).unwrap_or_default();
axis_tags.push(tag.clone());
axes.insert(
tag.clone(),
FvarAxis {
tag,
min,
def,
max,
flags,
name,
},
);
}
let mut instances: Vec<FvarInstance> = Vec::new();
let mut inst_off = axis_offset + axis_count * axis_size;
for _ in 0..instance_count {
if inst_off + instance_size > data.len() {
break;
}
let name_id = be_u16(data, inst_off);
let flags = be_u16(data, inst_off + 2);
let mut coords = HashMap::new();
let mut coord_off = inst_off + 4;
for tag in &axis_tags {
if coord_off + 4 > data.len() {
break;
}
let v = be_fixed(data, coord_off);
coords.insert(tag.clone(), v);
coord_off += 4;
}
let mut postscript_name = None;
if instance_size >= 4 + axis_tags.len() * 4 + 2 {
let ps_id = be_u16(data, inst_off + instance_size - 2);
if ps_id != 0 && ps_id != 0xFFFF {
postscript_name = lookup_name(face, ps_id);
}
}
let name = lookup_name(face, name_id).unwrap_or_default();
instances.push(FvarInstance {
name,
coordinates: coords,
flags,
postscript_name,
});
inst_off += instance_size;
}
FvarData { axes, instances }
}
fn parse_stat(face: &Face<'_>, data: &[u8]) -> StatData {
let table = match ttf_parser::stat::Table::parse(data) {
Some(t) => t,
None => return StatData::default(),
};
let mut axes: Vec<StatAxis> = Vec::new();
let mut tags: Vec<String> = Vec::new();
for record in table.axes.clone() {
let tag = tag_to_string(&record.tag.to_bytes());
let name = lookup_name(face, record.name_id).unwrap_or_default();
tags.push(tag.clone());
axes.push(StatAxis {
tag,
name,
values: Vec::new(),
});
}
let mut combinations: Vec<StatCombination> = Vec::new();
for sub in table.subtables() {
match sub {
ttf_parser::stat::AxisValueSubtable::Format1(v) => {
if let Some(axis) = axes.get_mut(v.axis_index as usize) {
let name = lookup_name(face, v.value_name_id).unwrap_or_default();
axis.values.push(StatAxisValue {
name,
value: v.value.0,
linked_value: None,
range_min_value: None,
range_max_value: None,
});
}
}
ttf_parser::stat::AxisValueSubtable::Format2(v) => {
if let Some(axis) = axes.get_mut(v.axis_index as usize) {
let name = lookup_name(face, v.value_name_id).unwrap_or_default();
axis.values.push(StatAxisValue {
name,
value: v.nominal_value.0,
linked_value: None,
range_min_value: Some(v.range_min_value.0),
range_max_value: Some(v.range_max_value.0),
});
}
}
ttf_parser::stat::AxisValueSubtable::Format3(v) => {
if let Some(axis) = axes.get_mut(v.axis_index as usize) {
let name = lookup_name(face, v.value_name_id).unwrap_or_default();
axis.values.push(StatAxisValue {
name,
value: v.value.0,
linked_value: Some(v.linked_value.0),
range_min_value: None,
range_max_value: None,
});
}
}
ttf_parser::stat::AxisValueSubtable::Format4(v) => {
let name = lookup_name(face, v.value_name_id).unwrap_or_default();
let mut values = Vec::new();
for av in v.values {
if let Some(tag) = tags.get(av.axis_index as usize) {
values.push((tag.clone(), av.value.0));
}
}
combinations.push(StatCombination { name, values });
}
}
}
let elided_fallback_name = table.fallback_name_id.and_then(|id| lookup_name(face, id));
StatData {
axes,
combinations,
elided_fallback_name,
}
}
fn parse_gsub_features_raw(face: &Face<'_>, data: &[u8]) -> Vec<FontFeature> {
if data.len() < 10 {
return Vec::new();
}
let feature_list_offset = be_u16(data, 6) as usize;
let lookup_list_offset = be_u16(data, 8) as usize;
let glyph_map = build_glyph_map(face);
if feature_list_offset >= data.len() {
return Vec::new();
}
let fl_base = feature_list_offset;
let feature_count = be_u16(data, fl_base) as usize;
let mut features: Vec<FontFeature> = Vec::new();
for i in 0..feature_count {
let rec_off = fl_base + 2 + i * 6;
if rec_off + 6 > data.len() {
break;
}
let tag = tag_to_string(&data[rec_off..rec_off + 4]);
let feature_off = fl_base + be_u16(data, rec_off + 4) as usize;
if feature_off + 4 > data.len() {
continue;
}
let params_offset = be_u16(data, feature_off) as usize;
let lookup_count = be_u16(data, feature_off + 2) as usize;
let mut lookup_indices = Vec::new();
for j in 0..lookup_count {
let li_off = feature_off + 4 + j * 2;
if li_off + 2 > data.len() {
break;
}
lookup_indices.push(be_u16(data, li_off));
}
let mut name = None;
let mut tooltip = None;
let mut sample_text = None;
let mut param_labels: Vec<String> = Vec::new();
if params_offset != 0 && feature_off + params_offset + 2 <= data.len() {
let po = feature_off + params_offset;
if tag.starts_with("ss") {
if po + 6 <= data.len() {
let ui = be_u16(data, po + 2);
let sample = be_u16(data, po + 4);
name = lookup_name(face, ui);
sample_text = lookup_name(face, sample);
}
} else if tag.starts_with("cv") {
if po + 12 <= data.len() {
let ui = be_u16(data, po + 2);
let ti = be_u16(data, po + 4);
let si = be_u16(data, po + 6);
let pcnt = be_u16(data, po + 8) as usize;
let first = be_u16(data, po + 10);
name = lookup_name(face, ui);
tooltip = lookup_name(face, ti);
sample_text = lookup_name(face, si);
for k in 0..pcnt {
if let Some(label) = lookup_name(face, first + k as u16) {
param_labels.push(label);
}
}
}
}
}
let mut glyph_set: HashSet<u16> = HashSet::new();
for &lookup_index in &lookup_indices {
glyph_set.extend(parse_lookup_glyphs(data, lookup_list_offset, lookup_index));
}
let glyphs: Vec<String> = glyph_set
.into_iter()
.filter_map(|gid| glyph_map.get(&gid).copied())
.map(|c| c.to_string())
.collect();
let feature_name = name
.clone()
.unwrap_or_else(|| get_feature_name_from_font(face, &tag));
features.push(FontFeature {
tag: tag.clone(),
name: feature_name,
tooltip,
sample_text,
glyphs,
param_labels,
lookup_indices,
});
}
features
}
fn parse_lookup_glyphs(data: &[u8], lookup_list_offset: usize, lookup_index: u16) -> Vec<u16> {
let mut glyphs = Vec::new();
let ll_offset = lookup_list_offset;
if ll_offset + 2 > data.len() {
return glyphs;
}
let lookup_count = be_u16(data, ll_offset);
if lookup_index as usize >= lookup_count as usize {
return glyphs;
}
let lookup_offset_pos = ll_offset + 2 + lookup_index as usize * 2;
if lookup_offset_pos + 2 > data.len() {
return glyphs;
}
let lookup_offset = ll_offset + be_u16(data, lookup_offset_pos) as usize;
if lookup_offset + 6 > data.len() {
return glyphs;
}
let lookup_type = be_u16(data, lookup_offset);
let sub_count = be_u16(data, lookup_offset + 4) as usize;
for i in 0..sub_count {
let sub_off_pos = lookup_offset + 6 + i * 2;
if sub_off_pos + 2 > data.len() {
break;
}
let sub_off = lookup_offset + be_u16(data, sub_off_pos) as usize;
if sub_off > data.len() {
continue;
}
if let Some(subtable) = gsub::SubstitutionSubtable::parse(&data[sub_off..], lookup_type) {
match subtable {
gsub::SubstitutionSubtable::Single(s) => {
glyphs.extend(coverage_glyphs(s.coverage()));
}
gsub::SubstitutionSubtable::Ligature(l) => {
let sets = l.ligature_sets;
for si in 0..sets.len() {
if let Some(set) = sets.get(si) {
for li in 0..set.len() {
if let Some(lig) = set.get(li) {
glyphs.push(lig.glyph.0);
}
}
}
}
}
_ => {}
}
}
}
glyphs
}
fn coverage_glyphs(cov: Coverage) -> Vec<u16> {
match cov {
Coverage::Format1 { glyphs } => (0..glyphs.len())
.filter_map(|i| glyphs.get(i))
.map(|g| g.0)
.collect(),
Coverage::Format2 { records } => {
let mut res = Vec::new();
for i in 0..records.len() {
if let Some(rec) = records.get(i) {
for g in rec.start.0..=rec.end.0 {
res.push(g);
}
}
}
res
}
}
}
fn build_glyph_map(face: &Face<'_>) -> HashMap<u16, char> {
let mut map = HashMap::new();
if let Some(cmap) = face.tables().cmap {
for sub in cmap.subtables.into_iter() {
if sub.is_unicode() {
sub.codepoints(|cp| {
if let Some(gid) = sub.glyph_index(cp) {
if gid.0 != 0 && !map.contains_key(&gid.0) {
if let Some(ch) = char::from_u32(cp) {
map.insert(gid.0, ch);
}
}
}
});
}
}
}
map
}
fn lookup_name(face: &Face<'_>, id: u16) -> Option<String> {
face.names()
.into_iter()
.find(|n| n.name_id == id && n.is_unicode())
.and_then(|n| n.to_string())
}
fn tag_to_string(bytes: &[u8]) -> String {
std::str::from_utf8(bytes).unwrap_or("").to_string()
}
fn be_u16(data: &[u8], offset: usize) -> u16 {
let b = [data[offset], data[offset + 1]];
u16::from_be_bytes(b)
}
fn be_fixed(data: &[u8], offset: usize) -> f32 {
let b = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
let v = i32::from_be_bytes(b);
v as f32 / 65536.0
}
fn parse_gpos_features(
face: &Face<'_>,
gpos_table: ttf_parser::opentype_layout::LayoutTable<'_>,
) -> Vec<FontFeature> {
let mut features = Vec::new();
for feature in gpos_table.features {
let tag = feature.tag.to_string();
let lookup_indices: Vec<u16> = feature.lookup_indices.into_iter().collect();
let glyphs = analyze_gpos_feature(&gpos_table, &lookup_indices, face);
let name = get_feature_name_from_font(face, &tag);
features.push(FontFeature {
tag,
name,
tooltip: None,
sample_text: None,
glyphs,
param_labels: Vec::new(),
lookup_indices,
});
}
features
}
fn analyze_gpos_feature(
gpos_table: &ttf_parser::opentype_layout::LayoutTable<'_>,
lookup_indices: &[u16],
face: &Face<'_>,
) -> Vec<String> {
let mut covered_glyphs = std::collections::HashSet::new();
for &lookup_index in lookup_indices {
if let Some(lookup) = gpos_table.lookups.get(lookup_index) {
if let Some(pos_subtable) = lookup.subtables.get::<gpos::PositioningSubtable>(0) {
match pos_subtable {
gpos::PositioningSubtable::Single(single_adj) => {
extract_coverage_glyphs(&single_adj.coverage(), &mut covered_glyphs);
}
gpos::PositioningSubtable::Pair(pair_adj) => {
extract_coverage_glyphs(&pair_adj.coverage(), &mut covered_glyphs);
}
gpos::PositioningSubtable::MarkToBase(mark_to_base) => {
extract_coverage_glyphs(&mark_to_base.mark_coverage, &mut covered_glyphs);
}
gpos::PositioningSubtable::MarkToMark(mark_to_mark) => {
extract_coverage_glyphs(&mark_to_mark.mark1_coverage, &mut covered_glyphs);
}
gpos::PositioningSubtable::Cursive(cursive_adj) => {
extract_coverage_glyphs(&cursive_adj.coverage, &mut covered_glyphs);
}
gpos::PositioningSubtable::MarkToLigature(mark_to_lig) => {
extract_coverage_glyphs(&mark_to_lig.mark_coverage, &mut covered_glyphs);
}
gpos::PositioningSubtable::Context(ctx) => {
extract_coverage_glyphs(&ctx.coverage(), &mut covered_glyphs);
}
gpos::PositioningSubtable::ChainContext(chain_ctx) => {
extract_coverage_glyphs(&chain_ctx.coverage(), &mut covered_glyphs);
}
}
}
}
}
let glyph_map = build_glyph_map(face);
covered_glyphs
.into_iter()
.filter_map(|gid_str| {
gid_str
.parse::<u16>()
.ok()
.and_then(|gid| glyph_map.get(&gid).copied())
.map(|c| c.to_string())
})
.collect()
}
fn extract_coverage_glyphs(
coverage: &ttf_parser::opentype_layout::Coverage<'_>,
covered_glyphs: &mut std::collections::HashSet<String>,
) {
match coverage {
ttf_parser::opentype_layout::Coverage::Format1 { glyphs } => {
for i in 0..glyphs.len() {
if let Some(glyph_id) = glyphs.get(i) {
covered_glyphs.insert(format!("{}", glyph_id.0));
}
}
}
ttf_parser::opentype_layout::Coverage::Format2 { records } => {
for i in 0..records.len() {
if let Some(record) = records.get(i) {
for glyph_id in record.start.0..=record.end.0 {
covered_glyphs.insert(format!("{}", glyph_id));
}
}
}
}
}
}
fn get_feature_name_from_font(face: &Face<'_>, tag: &str) -> String {
if let Some(feat_table) = face.tables().feat {
for i in 0..feat_table.names.len() {
if let Some(feature_name) = feat_table.names.get(i) {
if let Some(name) = lookup_name(face, feature_name.name_index) {
if name.to_lowercase().contains(&tag.to_lowercase())
|| tag.to_lowercase().contains(&name.to_lowercase())
{
return name;
}
}
}
}
for feature_name in feat_table.names {
if let Some(name) = lookup_name(face, feature_name.name_index) {
return name;
}
}
}
tag.to_string()
}