Skip to content

Commit

Permalink
Use fontations for shape planning
Browse files Browse the repository at this point in the history
Replaces the ttf-parser code for script, langsys and feature lookup.

GSUB/GPOS is now fully fontations.
  • Loading branch information
dfrg committed Jul 27, 2024
1 parent cdb55cc commit f5052ba
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 68 deletions.
39 changes: 12 additions & 27 deletions src/hb/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ use core_maths::CoreFloat;

use crate::hb::paint_extents::hb_paint_extents_context_t;
use ttf_parser::gdef::GlyphClass;
use ttf_parser::opentype_layout::LayoutTable;
use ttf_parser::{GlyphId, RgbaColor};

use super::buffer::GlyphPropsFlags;
use super::fonta;
use super::ot_layout::TableIndex;
use super::ot_layout_common::{PositioningTable, SubstitutionTable};
use crate::Variation;

/// A font face handle.
Expand All @@ -21,8 +19,6 @@ pub struct hb_font_t<'a> {
pub(crate) units_per_em: u16,
pixels_per_em: Option<(u16, u16)>,
pub(crate) points_per_em: Option<f32>,
pub(crate) gsub: Option<SubstitutionTable<'a>>,
pub(crate) gpos: Option<PositioningTable<'a>>,
}

impl<'a> AsRef<ttf_parser::Face<'a>> for hb_font_t<'a> {
Expand Down Expand Up @@ -67,28 +63,10 @@ impl<'a> hb_font_t<'a> {
units_per_em: face.units_per_em(),
pixels_per_em: None,
points_per_em: None,
gsub: face.tables().gsub.map(SubstitutionTable::new),
gpos: face.tables().gpos.map(PositioningTable::new),
ttfp_face: face,
})
}

/// Creates a new [`Face`] from [`ttf_parser::Face`].
///
/// Data will be referenced, not owned.
pub fn from_face(face: ttf_parser::Face<'a>) -> Self {
let font = fonta::Font::new(face.raw_face().data, 0).unwrap();
hb_font_t {
font,
units_per_em: face.units_per_em(),
pixels_per_em: None,
points_per_em: None,
gsub: face.tables().gsub.map(SubstitutionTable::new),
gpos: face.tables().gpos.map(PositioningTable::new),
ttfp_face: face,
}
}

// TODO: remove
/// Returns face’s units per EM.
#[inline]
Expand Down Expand Up @@ -338,17 +316,24 @@ impl<'a> hb_font_t<'a> {
}
}

pub(crate) fn layout_table(&self, table_index: TableIndex) -> Option<&LayoutTable<'a>> {
pub(crate) fn layout_table(
&self,
table_index: TableIndex,
) -> Option<fonta::ot::LayoutTable<'a>> {
match table_index {
TableIndex::GSUB => self.gsub.as_ref().map(|table| &table.inner),
TableIndex::GPOS => self.gpos.as_ref().map(|table| &table.inner),
TableIndex::GSUB => Some(fonta::ot::LayoutTable::Gsub(
self.font.ot.gsub.as_ref()?.table.clone(),
)),
TableIndex::GPOS => Some(fonta::ot::LayoutTable::Gpos(
self.font.ot.gpos.as_ref()?.table.clone(),
)),
}
}

pub(crate) fn layout_tables(
&self,
) -> impl Iterator<Item = (TableIndex, &LayoutTable<'a>)> + '_ {
TableIndex::iter().filter_map(move |idx| self.layout_table(idx).map(|table| (idx, table)))
) -> impl Iterator<Item = (TableIndex, fonta::ot::LayoutTable<'a>)> + '_ {
TableIndex::iter().filter_map(move |idx| Some((idx, self.layout_table(idx)?)))
}
}

Expand Down
12 changes: 5 additions & 7 deletions src/hb/fonta/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,12 @@ impl<'a> Font<'a> {
pub(crate) fn set_coords(&mut self, coords: &[NormalizedCoordinate]) {
self.coords.clear();
if !coords.is_empty() && !coords.iter().all(|coord| coord.get() == 0) {
self.coords.extend(
coords
.iter()
.map(|coord| NormalizedCoord::from_bits(coord.get())),
);
let ivs = self.ivs.take().or_else(|| self.ot.item_variation_store());
if ivs.is_some() {
self.coords.extend(
coords
.iter()
.map(|coord| NormalizedCoord::from_bits(coord.get())),
);
}
self.ivs = ivs;
} else {
self.ivs = None;
Expand Down
261 changes: 259 additions & 2 deletions src/hb/fonta/ot/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use crate::hb::{
hb_font_t,
common::TagExt,
hb_font_t, hb_tag_t,
ot_layout::LayoutLookup,
ot_layout_gsubgpos::{Apply, WouldApply, WouldApplyContext, OT::hb_ot_apply_context_t},
set_digest::hb_set_digest_ext,
};
use skrifa::raw::{
tables::{
gdef::Gdef,
layout::{ClassDef, CoverageTable},
gpos::Gpos,
gsub::{FeatureList, FeatureVariations, Gsub, ScriptList},
layout::{ClassDef, Condition, CoverageTable, Feature, LangSys, Script},
variations::ItemVariationStore,
},
types::{F2Dot14, Scalar},
ReadError, TableProvider,
};
use ttf_parser::GlyphId;
Expand Down Expand Up @@ -159,6 +163,259 @@ impl LookupInfo {
}
}

pub enum LayoutTable<'a> {
Gsub(Gsub<'a>),
Gpos(Gpos<'a>),
}

fn conv_tag(tag: hb_tag_t) -> skrifa::raw::types::Tag {
skrifa::raw::types::Tag::from_u32(tag.0)
}

impl<'a> LayoutTable<'a> {
fn script_list(&self) -> Option<ScriptList<'a>> {
match self {
Self::Gsub(gsub) => gsub.script_list().ok(),
Self::Gpos(gpos) => gpos.script_list().ok(),
}
}

fn feature_list(&self) -> Option<FeatureList<'a>> {
match self {
Self::Gsub(gsub) => gsub.feature_list().ok(),
Self::Gpos(gpos) => gpos.feature_list().ok(),
}
}

fn feature_variations(&self) -> Option<FeatureVariations<'a>> {
match self {
Self::Gsub(gsub) => gsub.feature_variations(),
Self::Gpos(gpos) => gpos.feature_variations(),
}
.transpose()
.ok()
.flatten()
}

fn script_index(&self, tag: hb_tag_t) -> Option<u16> {
let list = self.script_list()?;
let tag = conv_tag(tag);
list.script_records()
.binary_search_by_key(&tag, |rec| rec.script_tag())
.map(|index| index as u16)
.ok()
}

fn script(&self, index: u16) -> Option<Script<'a>> {
let list = self.script_list()?;
let record = list.script_records().get(index as usize)?;
record.script(list.offset_data()).ok()
}

fn langsys_index(&self, script_index: u16, tag: hb_tag_t) -> Option<u16> {
let script = self.script(script_index)?;
let tag = conv_tag(tag);
script
.lang_sys_records()
.binary_search_by_key(&tag, |rec| rec.lang_sys_tag())
.map(|index| index as u16)
.ok()
}

fn langsys(&self, script_index: u16, langsys_index: Option<u16>) -> Option<LangSys<'a>> {
let script = self.script(script_index)?;
if let Some(index) = langsys_index {
let record = script.lang_sys_records().get(index as usize)?;
record.lang_sys(script.offset_data()).ok()
} else {
script.default_lang_sys().transpose().ok().flatten()
}
}

pub(crate) fn feature(&self, index: u16) -> Option<Feature<'a>> {
let list = self.feature_list()?;
let record = list.feature_records().get(index as usize)?;
record.feature(list.offset_data()).ok()
}

fn feature_tag(&self, index: u16) -> Option<hb_tag_t> {
let list = self.feature_list()?;
let record = list.feature_records().get(index as usize)?;
Some(hb_tag_t(u32::from_be_bytes(record.feature_tag().to_raw())))
}

pub(crate) fn feature_variation_index(&self, coords: &[F2Dot14]) -> Option<u32> {
let feature_variations = self.feature_variations()?;
for (index, rec) in feature_variations
.feature_variation_records()
.iter()
.enumerate()
{
// If the ConditionSet offset is 0, this is treated as the
// universal condition: all contexts are matched.
if rec.condition_set_offset().is_null() {
return Some(index as u32);
}
let Some(Ok(condition_set)) = rec.condition_set(feature_variations.offset_data())
else {
continue;
};
// Otherwise, all conditions must be satisfied.
if condition_set
.conditions()
.iter()
// .. except we ignore errors
.filter_map(|cond| cond.ok())
.all(|cond| match cond {
Condition::Format1AxisRange(format1) => {
let coord = coords
.get(format1.axis_index() as usize)
.copied()
.unwrap_or_default();
coord >= format1.filter_range_min_value()
&& coord <= format1.filter_range_max_value()
}
_ => false,
})
{
return Some(index as u32);
}
}
None
}

pub(crate) fn feature_substitution(
&self,
variation_index: u32,
feature_index: u16,
) -> Option<Feature<'a>> {
let feature_variations = self.feature_variations()?;
let record = feature_variations
.feature_variation_records()
.get(variation_index as usize)?;
let subst_table = record
.feature_table_substitution(feature_variations.offset_data())?
.ok()?;
let subst_records = subst_table.substitutions();
match subst_records.binary_search_by_key(&feature_index, |subst| subst.feature_index()) {
Ok(ix) => Some(
subst_records
.get(ix)?
.alternate_feature(subst_table.offset_data())
.ok()?,
),
_ => None,
}
}

pub(crate) fn feature_index(&self, tag: hb_tag_t) -> Option<u16> {
let list = self.feature_list()?;
let tag = conv_tag(tag);
for (index, feature) in list.feature_records().iter().enumerate() {
if feature.feature_tag() == tag {
return Some(index as u16);
}
}
None
}

pub(crate) fn lookup_count(&self) -> u16 {
match self {
Self::Gsub(gsub) => gsub
.lookup_list()
.map(|list| list.lookup_count())
.unwrap_or_default(),
Self::Gpos(gpos) => gpos
.lookup_list()
.map(|list| list.lookup_count())
.unwrap_or_default(),
}
}
}

impl crate::hb::ot_layout::LayoutTableExt for LayoutTable<'_> {
// hb_ot_layout_table_select_script
/// Returns true + index and tag of the first found script tag in the given GSUB or GPOS table
/// or false + index and tag if falling back to a default script.
fn select_script(&self, script_tags: &[hb_tag_t]) -> Option<(bool, u16, hb_tag_t)> {
for &tag in script_tags {
if let Some(index) = self.script_index(tag) {
return Some((true, index, tag));
}
}

for &tag in &[
// try finding 'DFLT'
hb_tag_t::default_script(),
// try with 'dflt'; MS site has had typos and many fonts use it now :(
hb_tag_t::default_language(),
// try with 'latn'; some old fonts put their features there even though
// they're really trying to support Thai, for example :(
hb_tag_t::from_bytes(b"latn"),
] {
if let Some(index) = self.script_index(tag) {
return Some((false, index, tag));
}
}

None
}

// hb_ot_layout_script_select_language
/// Returns the index of the first found language tag in the given GSUB or GPOS table,
/// underneath the specified script index.
fn select_script_language(&self, script_index: u16, lang_tags: &[hb_tag_t]) -> Option<u16> {
for &tag in lang_tags {
if let Some(index) = self.langsys_index(script_index, tag) {
return Some(index);
}
}

// try finding 'dflt'
if let Some(index) = self.langsys_index(script_index, hb_tag_t::default_language()) {
return Some(index);
}

None
}

// hb_ot_layout_language_get_required_feature
/// Returns the index and tag of a required feature in the given GSUB or GPOS table,
/// underneath the specified script and language.
fn get_required_language_feature(
&self,
script_index: u16,
lang_index: Option<u16>,
) -> Option<(u16, hb_tag_t)> {
let sys = self.langsys(script_index, lang_index)?;
let idx = sys.required_feature_index();
if idx == 0xFFFF {
return None;
}
let tag = self.feature_tag(idx)?;
Some((idx, tag))
}

// hb_ot_layout_language_find_feature
/// Returns the index of a given feature tag in the given GSUB or GPOS table,
/// underneath the specified script and language.
fn find_language_feature(
&self,
script_index: u16,
lang_index: Option<u16>,
feature_tag: hb_tag_t,
) -> Option<u16> {
let sys = self.langsys(script_index, lang_index)?;
for index in sys.feature_indices() {
let index = index.get();
if self.feature_tag(index) == Some(feature_tag) {
return Some(index);
}
}
None
}
}

fn coverage_index(coverage: Result<CoverageTable, ReadError>, gid: GlyphId) -> Option<u16> {
let gid = skrifa::GlyphId16::new(gid.0);
coverage.ok().and_then(|coverage| coverage.get(gid))
Expand Down
Loading

0 comments on commit f5052ba

Please sign in to comment.