blob: 868357d171a7cb9d2bfe8dbab9a1d238a1d0b3dc [file] [log] [blame]
//! Shaping support for autohinting.
use super::style::{GlyphStyle, StyleClass};
use crate::{charmap::Charmap, collections::SmallVec, FontRef, GlyphId, MetadataProvider};
use core::ops::Range;
use raw::{
tables::{
gsub::{
ChainedSequenceContext, Gsub, SequenceContext, SingleSubst, SubstitutionLookupList,
SubstitutionSubtables,
},
layout::{Feature, ScriptTags},
varc::CoverageTable,
},
types::Tag,
ReadError, TableProvider,
};
// To prevent infinite recursion in contextual lookups. Matches HB
// <https://github.com/harfbuzz/harfbuzz/blob/c7ef6a2ed58ae8ec108ee0962bef46f42c73a60c/src/hb-limits.hh#L53>
const MAX_NESTING_DEPTH: usize = 64;
/// Determines the fidelity with which we apply shaping in the
/// autohinter.
///
/// Shaping only affects glyph style classification and the glyphs that
/// are chosen for metrics computations. We keep the `Nominal` mode around
/// to enable validation of internal algorithms against a configuration that
/// is known to match FreeType. The `BestEffort` mode should always be
/// used for actual rendering.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ShaperMode {
/// Characters are mapped to nominal glyph identifiers and layout tables
/// are not used for style coverage.
///
/// This matches FreeType when HarfBuzz support is not enabled.
Nominal,
/// Simple substitutions are applied according to script rules and layout
/// tables are used to extend style coverage beyond the character map.
#[allow(unused)]
BestEffort,
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct ShapedGlyph {
pub id: GlyphId,
/// This may be used for computing vertical alignment zones, particularly
/// for glyphs like super/subscripts which might have adjustments in GPOS.
///
/// Note that we don't do the same in the horizontal direction which
/// means that we don't care about the x-offset.
pub y_offset: i32,
}
/// Arbitrarily chosen to cover our max input size plus some extra to account
/// for expansion from multiple substitution tables.
const SHAPED_CLUSTER_INLINE_SIZE: usize = 16;
/// Container for storing the result of shaping a cluster.
///
/// Some of our input "characters" for metrics computations are actually
/// multi-character [grapheme clusters](https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)
/// that may expand to multiple glyphs.
pub(crate) type ShapedCluster = SmallVec<ShapedGlyph, SHAPED_CLUSTER_INLINE_SIZE>;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ShaperCoverageKind {
/// Shaper coverage that traverses a specific script.
Script,
/// Shaper coverage that also includes the `Dflt` script.
///
/// This is used as a catch all after all styles are processed.
Default,
}
/// Maps characters to glyphs and handles extended style coverage beyond
/// glyphs that are available in the character map.
///
/// Roughly covers the functionality in <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c>.
pub(crate) struct Shaper<'a> {
font: FontRef<'a>,
#[allow(unused)]
mode: ShaperMode,
charmap: Charmap<'a>,
gsub: Option<Gsub<'a>>,
}
impl<'a> Shaper<'a> {
pub fn new(font: &FontRef<'a>, mode: ShaperMode) -> Self {
let charmap = font.charmap();
let gsub = (mode != ShaperMode::Nominal)
.then(|| font.gsub().ok())
.flatten();
Self {
font: font.clone(),
mode,
charmap,
gsub,
}
}
pub fn font(&self) -> &FontRef<'a> {
&self.font
}
pub fn charmap(&self) -> &Charmap<'a> {
&self.charmap
}
pub fn lookup_count(&self) -> u16 {
self.gsub
.as_ref()
.and_then(|gsub| gsub.lookup_list().ok())
.map(|list| list.lookup_count())
.unwrap_or_default()
}
pub fn cluster_shaper(&'a self, style: &StyleClass) -> ClusterShaper<'a> {
if self.mode == ShaperMode::BestEffort {
// For now, only apply substitutions for styles with an associated
// feature
if let Some(feature_tag) = style.feature {
if let Some((lookup_list, feature)) = self.gsub.as_ref().and_then(|gsub| {
let script_list = gsub.script_list().ok()?;
let selected_script =
script_list.select(&ScriptTags::from_unicode(style.script.tag))?;
let script = script_list.get(selected_script.index).ok()?;
let lang_sys = script.default_lang_sys()?.ok()?;
let feature_list = gsub.feature_list().ok()?;
let feature_ix = lang_sys.feature_index_for_tag(&feature_list, feature_tag)?;
let feature = feature_list.get(feature_ix).ok()?.element;
let lookup_list = gsub.lookup_list().ok()?;
Some((lookup_list, feature))
}) {
return ClusterShaper {
shaper: self,
lookup_list: Some(lookup_list),
kind: ClusterShaperKind::SingleFeature(feature),
};
}
}
}
ClusterShaper {
shaper: self,
lookup_list: None,
kind: ClusterShaperKind::Nominal,
}
}
/// Uses layout tables to compute coverage for the given style.
///
/// Returns `true` if any glyph styles were updated for this style.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L99>
pub(crate) fn compute_coverage(
&self,
style: &StyleClass,
coverage_kind: ShaperCoverageKind,
glyph_styles: &mut [GlyphStyle],
visited_set: &mut VisitedLookupSet<'_>,
) -> bool {
let Some(gsub) = self.gsub.as_ref() else {
return false;
};
let (Ok(script_list), Ok(feature_list), Ok(lookup_list)) =
(gsub.script_list(), gsub.feature_list(), gsub.lookup_list())
else {
return false;
};
let mut script_tags: [Option<Tag>; 3] = [None; 3];
for (a, b) in script_tags
.iter_mut()
.zip(ScriptTags::from_unicode(style.script.tag).iter())
{
*a = Some(*b);
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L153>
const DEFAULT_SCRIPT: Tag = Tag::new(b"Dflt");
if coverage_kind == ShaperCoverageKind::Default {
if script_tags[0].is_none() {
script_tags[0] = Some(DEFAULT_SCRIPT);
} else if script_tags[1].is_none() {
script_tags[1] = Some(DEFAULT_SCRIPT);
} else if script_tags[1] != Some(DEFAULT_SCRIPT) {
script_tags[2] = Some(DEFAULT_SCRIPT);
}
} else {
// Script classes contain some non-standard tags used for special
// purposes. We ignore these
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L167>
const NON_STANDARD_TAGS: &[Option<Tag>] = &[
// Khmer symbols
Some(Tag::new(b"Khms")),
// Latin subscript fallbacks
Some(Tag::new(b"Latb")),
// Latin superscript fallbacks
Some(Tag::new(b"Latp")),
];
if NON_STANDARD_TAGS.contains(&script_tags[0]) {
return false;
}
}
// Check each requested script that is available in GSUB
let mut gsub_handler = GsubHandler::new(
&self.charmap,
&lookup_list,
style,
glyph_styles,
visited_set,
);
for script in script_tags.iter().filter_map(|tag| {
tag.and_then(|tag| script_list.index_for_tag(tag))
.and_then(|ix| script_list.script_records().get(ix as usize))
.and_then(|rec| rec.script(script_list.offset_data()).ok())
}) {
// And all language systems for each script
for langsys in script
.lang_sys_records()
.iter()
.filter_map(|rec| rec.lang_sys(script.offset_data()).ok())
.chain(script.default_lang_sys().transpose().ok().flatten())
{
for feature_ix in langsys.feature_indices() {
let Some(feature) = feature_list
.feature_records()
.get(feature_ix.get() as usize)
.and_then(|rec| {
// If our style has a feature tag, we only look at that specific
// feature; otherwise, handle all of them
if style.feature == Some(rec.feature_tag()) || style.feature.is_none() {
rec.feature(feature_list.offset_data()).ok()
} else {
None
}
})
else {
continue;
};
// And now process associated lookups
for index in feature.lookup_list_indices().iter() {
// We only care about errors here for testing
let _ = gsub_handler.process_lookup(index.get());
}
}
}
}
if let Some(range) = gsub_handler.finish() {
// If we get a range then we captured at least some glyphs so
// let's try to assign our current style
let mut result = false;
for glyph_style in &mut glyph_styles[range] {
// We only want to return true here if we actually assign the
// style to avoid computing unnecessary metrics
result |= glyph_style.maybe_assign_gsub_output_style(style);
}
result
} else {
false
}
}
}
pub(crate) struct ClusterShaper<'a> {
shaper: &'a Shaper<'a>,
lookup_list: Option<SubstitutionLookupList<'a>>,
kind: ClusterShaperKind<'a>,
}
impl ClusterShaper<'_> {
pub(crate) fn shape(&mut self, input: &str, output: &mut ShapedCluster) {
// First fill the output cluster with the nominal character
// to glyph id mapping
output.clear();
for ch in input.chars() {
output.push(ShapedGlyph {
id: self.shaper.charmap.map(ch).unwrap_or_default(),
y_offset: 0,
});
}
match self.kind.clone() {
ClusterShaperKind::Nominal => {
// In nominal mode, reject clusters with multiple glyphs
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L639>
if self.shaper.mode == ShaperMode::Nominal && output.len() > 1 {
output.clear();
}
}
ClusterShaperKind::SingleFeature(feature) => {
let mut did_subst = false;
for lookup_ix in feature.lookup_list_indices() {
let mut glyph_ix = 0;
while glyph_ix < output.len() {
did_subst |= self.apply_lookup(lookup_ix.get(), output, glyph_ix, 0);
glyph_ix += 1;
}
}
// Reject clusters that weren't modified by the feature.
// FreeType detects this by shaping twice and comparing gids
// but we just track substitutions
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L528>
if !did_subst {
output.clear();
}
}
}
}
fn apply_lookup(
&self,
lookup_index: u16,
cluster: &mut ShapedCluster,
glyph_ix: usize,
nesting_depth: usize,
) -> bool {
if nesting_depth > MAX_NESTING_DEPTH {
return false;
}
let Some(glyph) = cluster.get_mut(glyph_ix) else {
return false;
};
let Some(subtables) = self
.lookup_list
.as_ref()
.and_then(|list| list.lookups().get(lookup_index as usize).ok())
.and_then(|lookup| lookup.subtables().ok())
else {
return false;
};
match subtables {
// For now, just applying single substitutions because we're
// currently only handling shaping for "feature" styles like
// c2sc (caps to small caps) which are (almost?) always
// single substs
SubstitutionSubtables::Single(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
SingleSubst::Format1(table) => {
let Some(_) = table.coverage().ok().and_then(|cov| cov.get(glyph.id))
else {
continue;
};
let delta = table.delta_glyph_id() as i32;
glyph.id = GlyphId::from((glyph.id.to_u32() as i32 + delta) as u16);
return true;
}
SingleSubst::Format2(table) => {
let Some(cov_ix) =
table.coverage().ok().and_then(|cov| cov.get(glyph.id))
else {
continue;
};
let Some(subst) = table.substitute_glyph_ids().get(cov_ix as usize)
else {
continue;
};
glyph.id = subst.get().into();
return true;
}
}
}
}
SubstitutionSubtables::Multiple(_tables) => {}
SubstitutionSubtables::Ligature(_tables) => {}
SubstitutionSubtables::Alternate(_tables) => {}
SubstitutionSubtables::Contextual(_tables) => {}
SubstitutionSubtables::ChainContextual(_tables) => {}
SubstitutionSubtables::Reverse(_tables) => {}
}
false
}
}
#[derive(Clone)]
enum ClusterShaperKind<'a> {
Nominal,
SingleFeature(Feature<'a>),
}
/// Captures glyphs from the GSUB table that aren't present in cmap.
///
/// FreeType does this in a few phases:
/// 1. Collect all lookups for a given set of scripts and features.
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L174>
/// 2. For each lookup, collect all _output_ glyphs.
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L201>
/// 3. If the style represents a specific feature, make sure at least one of
/// the characters in the associated blue string would be substituted by
/// those lookups. If none would be substituted, then we don't assign the
/// style to any glyphs because we don't have any modified alignment
/// zones.
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L264>
///
/// We roll these into one pass over the lookups below so that we don't have
/// to allocate a lookup set or iterate them twice. Note that since
/// substitutions are checked for individual characters, we ignore ligatures
/// and contextual lookups (and alternates since they aren't applicable).
struct GsubHandler<'a, 'b> {
charmap: &'a Charmap<'a>,
lookup_list: &'a SubstitutionLookupList<'a>,
style: &'a StyleClass,
glyph_styles: &'a mut [GlyphStyle],
// Set to true when we need to check if any substitutions are available
// for our blue strings. This is the case when style.feature != None
need_blue_substs: bool,
// Keep track of our range of touched gids in the style list
min_gid: usize,
max_gid: usize,
lookup_depth: usize,
visited_set: &'a mut VisitedLookupSet<'b>,
}
impl<'a, 'b> GsubHandler<'a, 'b> {
fn new(
charmap: &'a Charmap<'a>,
lookup_list: &'a SubstitutionLookupList,
style: &'a StyleClass,
glyph_styles: &'a mut [GlyphStyle],
visited_set: &'a mut VisitedLookupSet<'b>,
) -> Self {
let min_gid = glyph_styles.len();
// If we have a feature, then we need to check the blue string to see
// if any substitutions are available. If not, we don't enable this
// style because it won't have any affect on alignment zones
let need_blue_substs = style.feature.is_some();
Self {
charmap,
lookup_list,
style,
glyph_styles,
need_blue_substs,
min_gid,
max_gid: 0,
lookup_depth: 0,
visited_set,
}
}
fn process_lookup(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
// General protection against stack overflows
if self.lookup_depth == MAX_NESTING_DEPTH {
return Err(ProcessLookupError::ExceededMaxDepth);
}
// Skip lookups that have already been processed
if !self.visited_set.insert(lookup_index) {
return Ok(());
}
self.lookup_depth += 1;
// Actually process the lookup
let result = self.process_lookup_inner(lookup_index);
// Out we go again
self.lookup_depth -= 1;
result
}
#[inline(always)]
fn process_lookup_inner(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
let Ok(subtables) = self
.lookup_list
.lookups()
.get(lookup_index as usize)
.and_then(|lookup| lookup.subtables())
else {
return Ok(());
};
match subtables {
SubstitutionSubtables::Single(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
SingleSubst::Format1(table) => {
let Ok(coverage) = table.coverage() else {
continue;
};
let delta = table.delta_glyph_id() as i32;
for gid in coverage.iter() {
self.capture_glyph((gid.to_u32() as i32 + delta) as u16 as u32);
}
// Check input coverage for blue strings if
// required and if we're not under a contextual
// lookup
if self.need_blue_substs && self.lookup_depth == 1 {
self.check_blue_coverage(Ok(coverage));
}
}
SingleSubst::Format2(table) => {
for gid in table.substitute_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
// See above
if self.need_blue_substs && self.lookup_depth == 1 {
self.check_blue_coverage(table.coverage());
}
}
}
}
}
SubstitutionSubtables::Multiple(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for seq in table.sequences().iter().filter_map(|seq| seq.ok()) {
for gid in seq.substitute_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
}
// See above
if self.need_blue_substs && self.lookup_depth == 1 {
self.check_blue_coverage(table.coverage());
}
}
}
SubstitutionSubtables::Ligature(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for set in table.ligature_sets().iter().filter_map(|set| set.ok()) {
for lig in set.ligatures().iter().filter_map(|lig| lig.ok()) {
self.capture_glyph(lig.ligature_glyph().to_u32());
}
}
}
}
SubstitutionSubtables::Alternate(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for set in table.alternate_sets().iter().filter_map(|set| set.ok()) {
for gid in set.alternate_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
}
}
}
SubstitutionSubtables::Contextual(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
SequenceContext::Format1(table) => {
for set in table
.seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in set.seq_rules().iter().filter_map(|rule| rule.ok()) {
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
SequenceContext::Format2(table) => {
for set in table
.class_seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in
set.class_seq_rules().iter().filter_map(|rule| rule.ok())
{
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
SequenceContext::Format3(table) => {
for rec in table.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
}
SubstitutionSubtables::ChainContextual(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
ChainedSequenceContext::Format1(table) => {
for set in table
.chained_seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in
set.chained_seq_rules().iter().filter_map(|rule| rule.ok())
{
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
ChainedSequenceContext::Format2(table) => {
for set in table
.chained_class_seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in set
.chained_class_seq_rules()
.iter()
.filter_map(|rule| rule.ok())
{
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
ChainedSequenceContext::Format3(table) => {
for rec in table.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
}
SubstitutionSubtables::Reverse(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for gid in table.substitute_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
}
}
}
Ok(())
}
/// Finishes processing for this set of GSUB lookups and
/// returns the range of touched glyphs.
fn finish(self) -> Option<Range<usize>> {
self.visited_set.clear();
if self.min_gid > self.max_gid {
// We didn't touch any glyphs
return None;
}
let range = self.min_gid..self.max_gid + 1;
if self.need_blue_substs {
// We didn't find any substitutions for our blue strings so
// we ignore the style. Clear the GSUB marker for any touched
// glyphs
for glyph in &mut self.glyph_styles[range] {
glyph.clear_from_gsub();
}
None
} else {
Some(range)
}
}
/// Checks the given coverage table for any characters in the blue
/// strings associated with our current style.
fn check_blue_coverage(&mut self, coverage: Result<CoverageTable<'a>, ReadError>) {
let Ok(coverage) = coverage else {
return;
};
for (blue_str, _) in self.style.script.blues {
if blue_str
.chars()
.filter_map(|ch| self.charmap.map(ch))
.filter_map(|gid| coverage.get(gid))
.next()
.is_some()
{
// Condition satisfied, so don't check any further subtables
self.need_blue_substs = false;
return;
}
}
}
fn capture_glyph(&mut self, gid: u32) {
let gid = gid as usize;
if let Some(style) = self.glyph_styles.get_mut(gid) {
style.set_from_gsub_output();
self.min_gid = gid.min(self.min_gid);
self.max_gid = gid.max(self.max_gid);
}
}
}
pub(crate) struct VisitedLookupSet<'a>(&'a mut [u8]);
impl<'a> VisitedLookupSet<'a> {
pub fn new(storage: &'a mut [u8]) -> Self {
Self(storage)
}
/// If the given lookup index is not already in the set, adds it and
/// returns `true`. Returns `false` otherwise.
///
/// This follows the behavior of `HashSet::insert`.
fn insert(&mut self, lookup_index: u16) -> bool {
let byte_ix = lookup_index as usize / 8;
let bit_mask = 1 << (lookup_index % 8) as u8;
if let Some(byte) = self.0.get_mut(byte_ix) {
if *byte & bit_mask == 0 {
*byte |= bit_mask;
true
} else {
false
}
} else {
false
}
}
fn clear(&mut self) {
self.0.fill(0);
}
}
#[derive(PartialEq, Debug)]
enum ProcessLookupError {
ExceededMaxDepth,
}
#[cfg(test)]
mod tests {
use super::{super::style, *};
use font_test_data::bebuffer::BeBuffer;
use raw::{FontData, FontRead};
#[test]
fn small_caps_subst() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut cluster = ShapedCluster::new();
cluster_shaper.shape("H", &mut cluster);
assert_eq!(cluster.len(), 1);
// from ttx, gid 8 is small caps "H"
assert_eq!(cluster[0].id, GlyphId::new(8));
}
#[test]
fn small_caps_nominal() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut cluster = ShapedCluster::new();
cluster_shaper.shape("H", &mut cluster);
assert_eq!(cluster.len(), 1);
// from ttx, gid 1 is "H"
assert_eq!(cluster[0].id, GlyphId::new(1));
}
#[test]
fn exceed_max_depth() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
// Build a lookup chain exceeding our max depth
let mut bad_lookup_builder = BadLookupBuilder::default();
for i in 0..MAX_NESTING_DEPTH {
// each lookup calls the next
bad_lookup_builder.lookups.push(i as u16 + 1);
}
let lookup_list_buf = bad_lookup_builder.build();
let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
let mut set_buf = [0u8; 8192];
let mut visited_set = VisitedLookupSet(&mut set_buf);
let mut gsub_handler = GsubHandler::new(
&shaper.charmap,
&lookup_list,
style,
&mut [],
&mut visited_set,
);
assert_eq!(
gsub_handler.process_lookup(0),
Err(ProcessLookupError::ExceededMaxDepth)
);
}
#[test]
fn dont_cycle_forever() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
// Build a lookup chain that cycles; 0 calls 1 which calls 0
let mut bad_lookup_builder = BadLookupBuilder::default();
bad_lookup_builder.lookups.push(1);
bad_lookup_builder.lookups.push(0);
let lookup_list_buf = bad_lookup_builder.build();
let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
let mut set_buf = [0u8; 8192];
let mut visited_set = VisitedLookupSet(&mut set_buf);
let mut gsub_handler = GsubHandler::new(
&shaper.charmap,
&lookup_list,
style,
&mut [],
&mut visited_set,
);
gsub_handler.process_lookup(0).unwrap();
}
#[test]
fn visited_set() {
let count = 2341u16;
let n_bytes = (count as usize).div_ceil(8);
let mut set_buf = vec![0u8; n_bytes];
let mut set = VisitedLookupSet::new(&mut set_buf);
for i in 0..count {
assert!(set.insert(i));
assert!(!set.insert(i));
}
for byte in &set_buf[0..set_buf.len() - 1] {
assert_eq!(*byte, 0xFF);
}
assert_eq!(*set_buf.last().unwrap(), 0b00011111);
}
#[derive(Default)]
struct BadLookupBuilder {
/// Just a list of nested lookup indices for each generated lookup
lookups: Vec<u16>,
}
impl BadLookupBuilder {
fn build(&self) -> Vec<u8> {
// Full byte size of a contextual format 3 lookup with one
// subtable and one nested lookup
const CONTEXT3_FULL_SIZE: usize = 18;
let mut buf = BeBuffer::default();
// LookupList table
// count
buf = buf.push(self.lookups.len() as u16);
// offsets for each lookup
let base_offset = 2 + 2 * self.lookups.len();
for i in 0..self.lookups.len() {
buf = buf.push((base_offset + i * CONTEXT3_FULL_SIZE) as u16);
}
// now the actual lookups
for nested_ix in &self.lookups {
// lookup type: GSUB contextual substitution
buf = buf.push(5u16);
// lookup flag
buf = buf.push(0u16);
// subtable count
buf = buf.push(1u16);
// offset to single subtable (always 8 bytes from start of lookup)
buf = buf.push(8u16);
// start of subtable, format == 3
buf = buf.push(3u16);
// number of glyphs in sequence
buf = buf.push(0u16);
// sequence lookup count
buf = buf.push(1u16);
// (no coverage offsets)
// sequence lookup (sequence index, lookup index)
buf = buf.push(0u16).push(*nested_ix);
}
buf.to_vec()
}
}
}