use crate::GlyphSource;
use pdfrum_common::{DiagKind, Diagnostics, Severity};
use read_fonts::TableProvider;
use read_fonts::tables::gsub::{Gsub, SingleSubst, SubstitutionLookup};
use read_fonts::types::Tag;
use std::collections::HashMap;
const VERT: Tag = Tag::new(b"vert");
const VRT2: Tag = Tag::new(b"vrt2");
#[derive(Debug, Clone, Default)]
pub(super) struct VerticalSubst {
map: HashMap<u16, u16>,
}
impl VerticalSubst {
pub(super) fn none() -> Self {
Self::default()
}
pub(super) fn parse(glyphs: &GlyphSource, diags: &mut Diagnostics) -> Self {
let Some(face) = super::face_of(glyphs) else {
return Self::none();
};
let Ok(font) = skrifa::FontRef::from_index(face.bytes(), face.index()) else {
return Self::none();
};
let Ok(gsub) = font.gsub() else {
return Self::none();
};
if let Some(map) = collect(&gsub) {
Self { map }
} else {
diags.record(Severity::Suspicious, DiagKind::GsubUnreadable, None);
Self::none()
}
}
pub(super) fn vertical_glyph(&self, gid: u16) -> Option<u16> {
self.map.get(&gid).copied().filter(|g| *g != 0)
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.map.len()
}
}
fn collect(gsub: &Gsub<'_>) -> Option<HashMap<u16, u16>> {
let feature_list = gsub.feature_list().ok()?;
let lookup_list = gsub.lookup_list().ok()?;
let mut wanted: Vec<u16> = Vec::new();
if let Ok(scripts) = gsub.script_list() {
for script in scripts.script_records() {
let Ok(s) = script.script(scripts.offset_data()) else {
continue;
};
let mut lang_systems: Vec<_> = s
.lang_sys_records()
.iter()
.filter_map(|r| r.lang_sys(s.offset_data()).ok())
.collect();
if let Some(Ok(default)) = s.default_lang_sys() {
lang_systems.push(default);
}
for lang in lang_systems {
for &idx in lang.feature_indices() {
let i = idx.get();
if is_vertical_feature(&feature_list, i) && !wanted.contains(&i) {
wanted.push(i);
}
}
}
}
}
if wanted.is_empty() {
for (i, rec) in feature_list.feature_records().iter().enumerate() {
if is_vertical_tag(rec.feature_tag())
&& let Ok(i) = u16::try_from(i)
{
wanted.push(i);
}
}
}
let mut map = HashMap::new();
for feature_index in wanted {
let Some(rec) = feature_list
.feature_records()
.get(usize::from(feature_index))
else {
continue;
};
let Ok(feature) = rec.feature(feature_list.offset_data()) else {
continue;
};
for &lookup_index in feature.lookup_list_indices() {
let Ok(lookup) = lookup_list.lookups().get(usize::from(lookup_index.get())) else {
continue;
};
let SubstitutionLookup::Single(single) = lookup else {
continue;
};
for subtable in single.subtables().iter().flatten() {
collect_single(&subtable, &mut map);
}
}
}
Some(map)
}
fn collect_single(subtable: &SingleSubst<'_>, map: &mut HashMap<u16, u16>) {
match subtable {
SingleSubst::Format1(t) => {
let Ok(coverage) = t.coverage() else { return };
let delta = t.delta_glyph_id();
for gid in coverage.iter() {
let from = gid.to_u16();
map.entry(from)
.or_insert_with(|| from.wrapping_add(delta as u16));
}
}
SingleSubst::Format2(t) => {
let Ok(coverage) = t.coverage() else { return };
let substitutes = t.substitute_glyph_ids();
for (i, gid) in coverage.iter().enumerate() {
let Some(to) = substitutes.get(i) else {
break;
};
map.entry(gid.to_u16()).or_insert_with(|| to.get().to_u16());
}
}
}
}
fn is_vertical_feature(list: &read_fonts::tables::layout::FeatureList<'_>, index: u16) -> bool {
list.feature_records()
.get(usize::from(index))
.is_some_and(|r| is_vertical_tag(r.feature_tag()))
}
fn is_vertical_tag(tag: Tag) -> bool {
tag == VERT || tag == VRT2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_two_vertical_tags_are_vert_and_vrt2() {
assert!(is_vertical_tag(VERT));
assert!(is_vertical_tag(VRT2));
assert!(!is_vertical_tag(Tag::new(b"liga")));
assert!(!is_vertical_tag(Tag::new(b"vkrn")));
}
#[test]
fn a_face_with_no_gsub_substitutes_nothing() {
let mut diags = Diagnostics::default();
let (glyphs, _) = crate::subst::builtin_generic(false);
let v = VerticalSubst::parse(&glyphs, &mut diags);
assert_eq!(v.len(), 0);
assert_eq!(v.vertical_glyph(1), None);
assert!(!diags.contains(&DiagKind::GsubUnreadable));
}
#[test]
fn the_empty_substitution_answers_nothing() {
let v = VerticalSubst::none();
assert_eq!(v.vertical_glyph(0), None);
assert_eq!(v.vertical_glyph(u16::MAX), None);
}
}