use write_fonts::{
NullableOffsetMarker, OffsetMarker,
tables::{
gpos::{
ExtensionSubtable as GposExt, Gpos, PositionChainContext, PositionLookup,
PositionSequenceContext,
},
gsub::{
ExtensionSubtable as GsubExt, Gsub, LigatureSubstFormat1,
ReverseChainSingleSubstFormat1, SubstitutionChainContext, SubstitutionLookup,
SubstitutionSequenceContext,
},
layout::{
ChainedClassSequenceRule, ChainedClassSequenceRuleSet, ChainedSequenceContext,
ChainedSequenceContextFormat1, ChainedSequenceContextFormat2, ChainedSequenceRule,
ChainedSequenceRuleSet, ClassSequenceRule, ClassSequenceRuleSet, Lookup,
SequenceContext, SequenceContextFormat1, SequenceContextFormat2, SequenceRule,
SequenceRuleSet,
},
},
};
pub(super) fn compute_max_context_value(gpos: Option<&Gpos>, gsub: Option<&Gsub>) -> u16 {
let max_gpos = gpos.map(|gpos| {
gpos.lookup_list
.lookups
.iter()
.fold(0, |max_ctx, lookup| max_ctx.max(lookup.max_context()))
});
let max_gsub = gsub.map(|gsub| {
gsub.lookup_list
.lookups
.iter()
.fold(0, |max_ctx, lookup| max_ctx.max(lookup.max_context()))
});
max_gpos
.map(|max_gpos| max_gpos.max(max_gsub.unwrap_or_default()))
.or(max_gsub)
.unwrap_or_default()
}
#[derive(Debug, Copy, Clone)]
enum ContextualRuleType {
Contextual,
Chained,
ReverseChained,
}
pub trait MaxContext {
fn max_context(&self) -> u16;
}
fn max_context_of_rule(
input_glyph_count: usize,
lookahead_glyph_count: usize,
rule_type: ContextualRuleType,
) -> u16 {
(match rule_type {
ContextualRuleType::Contextual => input_glyph_count,
ContextualRuleType::Chained => input_glyph_count + lookahead_glyph_count,
ContextualRuleType::ReverseChained => 1 + lookahead_glyph_count,
}) as u16
}
trait Rule {
fn sequence_len(&self) -> usize;
fn lookahead_len(&self) -> usize;
fn context(&self, rule_type: ContextualRuleType) -> u16 {
max_context_of_rule(self.sequence_len(), self.lookahead_len(), rule_type)
}
}
trait RuleSet {
type Rule;
fn rules(&self) -> &[OffsetMarker<Self::Rule>];
}
trait ContextualSubtable {
type Rule: Rule;
type RuleSet: RuleSet;
fn rule_sets(&self) -> &[NullableOffsetMarker<Self::RuleSet>];
}
impl ContextualSubtable for &SequenceContextFormat1 {
type Rule = SequenceRule;
type RuleSet = SequenceRuleSet;
fn rule_sets(&self) -> &[NullableOffsetMarker<Self::RuleSet>] {
&self.seq_rule_sets
}
}
impl RuleSet for SequenceRuleSet {
type Rule = SequenceRule;
fn rules(&self) -> &[OffsetMarker<Self::Rule>] {
&self.seq_rules
}
}
impl Rule for SequenceRule {
fn sequence_len(&self) -> usize {
self.input_sequence.len() + 1
}
fn lookahead_len(&self) -> usize {
0
}
}
impl ContextualSubtable for &SequenceContextFormat2 {
type Rule = ClassSequenceRule;
type RuleSet = ClassSequenceRuleSet;
fn rule_sets(&self) -> &[NullableOffsetMarker<Self::RuleSet>] {
&self.class_seq_rule_sets
}
}
impl RuleSet for ClassSequenceRuleSet {
type Rule = ClassSequenceRule;
fn rules(&self) -> &[OffsetMarker<Self::Rule>] {
&self.class_seq_rules
}
}
impl Rule for ClassSequenceRule {
fn sequence_len(&self) -> usize {
self.input_sequence.len() + 1
}
fn lookahead_len(&self) -> usize {
0
}
}
impl ContextualSubtable for &ChainedSequenceContextFormat1 {
type Rule = ChainedSequenceRule;
type RuleSet = ChainedSequenceRuleSet;
fn rule_sets(&self) -> &[NullableOffsetMarker<Self::RuleSet>] {
&self.chained_seq_rule_sets
}
}
impl RuleSet for ChainedSequenceRuleSet {
type Rule = ChainedSequenceRule;
fn rules(&self) -> &[OffsetMarker<Self::Rule>] {
&self.chained_seq_rules
}
}
impl Rule for ChainedSequenceRule {
fn sequence_len(&self) -> usize {
self.input_sequence.len() + 1
}
fn lookahead_len(&self) -> usize {
self.lookahead_sequence.len()
}
}
impl ContextualSubtable for &ChainedSequenceContextFormat2 {
type Rule = ChainedClassSequenceRule;
type RuleSet = ChainedClassSequenceRuleSet;
fn rule_sets(&self) -> &[NullableOffsetMarker<Self::RuleSet>] {
&self.chained_class_seq_rule_sets
}
}
impl RuleSet for ChainedClassSequenceRuleSet {
type Rule = ChainedClassSequenceRule;
fn rules(&self) -> &[OffsetMarker<Self::Rule>] {
&self.chained_class_seq_rules
}
}
impl Rule for ChainedClassSequenceRule {
fn sequence_len(&self) -> usize {
self.input_sequence.len() + 1
}
fn lookahead_len(&self) -> usize {
self.lookahead_sequence.len()
}
}
fn max_context_of_contextual_subtable<C, RS, R>(subtable: &C, rule_type: ContextualRuleType) -> u16
where
C: ContextualSubtable<Rule = R, RuleSet = RS>,
RS: RuleSet<Rule = R>,
R: Rule,
{
subtable
.rule_sets()
.iter()
.filter_map(|nullable| nullable.as_ref())
.flat_map(|ruleset| ruleset.rules().iter())
.map(|rule| rule.context(rule_type))
.max()
.unwrap_or_default()
}
impl MaxContext for LigatureSubstFormat1 {
fn max_context(&self) -> u16 {
self.ligature_sets
.iter()
.flat_map(|l| l.ligatures.iter())
.fold(0, |max_ctx, ligature| {
max_ctx.max((ligature.component_glyph_ids.len() + 1) as u16)
})
}
}
impl MaxContext for SequenceContext {
fn max_context(&self) -> u16 {
match self as &SequenceContext {
SequenceContext::Format1(format1) => {
max_context_of_contextual_subtable(&format1, ContextualRuleType::Contextual)
}
SequenceContext::Format2(format2) => {
max_context_of_contextual_subtable(&format2, ContextualRuleType::Contextual)
}
SequenceContext::Format3(format3) => {
max_context_of_rule(format3.coverages.len(), 0, ContextualRuleType::Contextual)
}
}
}
}
impl MaxContext for SubstitutionSequenceContext {
fn max_context(&self) -> u16 {
self.as_inner().max_context()
}
}
impl MaxContext for PositionSequenceContext {
fn max_context(&self) -> u16 {
self.as_inner().max_context()
}
}
impl MaxContext for ChainedSequenceContext {
fn max_context(&self) -> u16 {
match self {
ChainedSequenceContext::Format1(format1) => {
max_context_of_contextual_subtable(&format1, ContextualRuleType::Chained)
}
ChainedSequenceContext::Format2(format2) => {
max_context_of_contextual_subtable(&format2, ContextualRuleType::Chained)
}
ChainedSequenceContext::Format3(format3) => max_context_of_rule(
format3.input_coverages.len(),
format3.lookahead_coverages.len(),
ContextualRuleType::Chained,
),
}
}
}
impl MaxContext for SubstitutionChainContext {
fn max_context(&self) -> u16 {
self.as_inner().max_context()
}
}
impl MaxContext for PositionChainContext {
fn max_context(&self) -> u16 {
self.as_inner().max_context()
}
}
impl MaxContext for GsubExt {
fn max_context(&self) -> u16 {
match self {
GsubExt::Single(_) | GsubExt::Multiple(_) | GsubExt::Alternate(_) => 1,
GsubExt::Ligature(inner) => inner.extension.max_context(),
GsubExt::Contextual(inner) => inner.extension.max_context(),
GsubExt::ChainContextual(inner) => inner.extension.max_context(),
GsubExt::Reverse(inner) => inner.extension.max_context(),
}
}
}
impl MaxContext for ReverseChainSingleSubstFormat1 {
fn max_context(&self) -> u16 {
max_context_of_rule(
self.backtrack_coverages.len(),
self.lookahead_coverages.len(),
ContextualRuleType::ReverseChained,
)
}
}
impl MaxContext for SubstitutionLookup {
fn max_context(&self) -> u16 {
match self {
SubstitutionLookup::Single(_)
| SubstitutionLookup::Multiple(_)
| SubstitutionLookup::Alternate(_) => 1u16,
SubstitutionLookup::Ligature(sub) => sub.max_context(),
SubstitutionLookup::Contextual(sub) => sub.max_context(),
SubstitutionLookup::ChainContextual(sub) => sub.max_context(),
SubstitutionLookup::Extension(sub) => sub.max_context(),
SubstitutionLookup::Reverse(sub) => sub.max_context(),
}
}
}
impl MaxContext for PositionLookup {
fn max_context(&self) -> u16 {
match self {
PositionLookup::Single(_) => 1,
PositionLookup::Pair(_) => 2,
PositionLookup::Cursive(_)
| PositionLookup::MarkToBase(_)
| PositionLookup::MarkToLig(_)
| PositionLookup::MarkToMark(_) => 0,
PositionLookup::Contextual(sub) => sub.max_context(),
PositionLookup::ChainContextual(sub) => sub.max_context(),
PositionLookup::Extension(sub) => sub.max_context(),
}
}
}
impl MaxContext for GposExt {
fn max_context(&self) -> u16 {
match self {
GposExt::Single(_) => 1,
GposExt::Pair(_) => 2,
GposExt::Cursive(_)
| GposExt::MarkToBase(_)
| GposExt::MarkToLig(_)
| GposExt::MarkToMark(_) => 0,
GposExt::Contextual(inner) => inner.extension.max_context(),
GposExt::ChainContextual(inner) => inner.extension.max_context(),
}
}
}
impl<T: MaxContext> MaxContext for Lookup<T> {
fn max_context(&self) -> u16 {
self.subtables
.iter()
.map(|sub| sub.max_context())
.max()
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use write_fonts::{
tables::{
gpos::{
PairPosFormat1, PairSet, PairValueRecord, PositionLookupList, SinglePosFormat1,
ValueRecord,
},
gsub::{SingleSubstFormat1, SubstitutionLookupList},
layout::{ClassDef, ClassDefFormat1, LookupFlag},
},
types::GlyphId16,
};
fn make_gids<const N: usize, T: FromIterator<GlyphId16>>(gids: [u16; N]) -> T {
gids.into_iter().map(GlyphId16::new).collect()
}
fn make_lookup<T: Default>(subtable: T) -> Lookup<T> {
Lookup::new(LookupFlag::empty(), vec![subtable])
}
fn make_singlepos() -> PositionLookup {
PositionLookup::Single(make_lookup(
SinglePosFormat1::new(make_gids([5u16, 6]), ValueRecord::new().with_x_advance(12))
.into(),
))
}
fn make_pairpos() -> PositionLookup {
PositionLookup::Pair(make_lookup(
PairPosFormat1::new(
make_gids([1u16, 2]),
vec![
PairSet::new(vec![PairValueRecord::new(
GlyphId16::new(3),
ValueRecord::new().with_x_advance(1),
ValueRecord::default(),
)]),
PairSet::new(vec![PairValueRecord::new(
GlyphId16::new(4),
ValueRecord::new().with_x_advance(5),
ValueRecord::default(),
)]),
],
)
.into(),
))
}
fn make_singlesub() -> SubstitutionLookup {
SubstitutionLookup::Single(make_lookup(
SingleSubstFormat1::new(make_gids([12u16, 13]), 4).into(),
))
}
fn make_class_def(start_glyph_id: GlyphId16, class_value_array: Vec<u16>) -> ClassDef {
ClassDef::Format1(ClassDefFormat1 {
start_glyph_id,
class_value_array,
})
}
fn make_chain_context_format1<const N: usize, const M: usize>(
input: [u16; N],
lookahead: [u16; M],
) -> SubstitutionLookup {
SubstitutionLookup::ChainContextual(make_lookup(
ChainedSequenceContext::Format1(ChainedSequenceContextFormat1::new(
make_gids([31]),
vec![Some(ChainedSequenceRuleSet::new(vec![
ChainedSequenceRule::new(
make_gids([5, 111, 112, 114, 115, 117]),
make_gids(input),
make_gids(lookahead),
vec![],
),
]))],
))
.into(),
))
}
fn make_chain_context_format2<const N: usize, const M: usize>(
input: [u16; N],
lookahead: [u16; M],
) -> SubstitutionLookup {
SubstitutionLookup::ChainContextual(make_lookup(
ChainedSequenceContext::Format2(ChainedSequenceContextFormat2::new(
make_gids([31]),
make_class_def(1.into(), vec![2, 3]),
make_class_def(1.into(), vec![2, 3]),
make_class_def(1.into(), vec![2, 3]),
vec![Some(ChainedClassSequenceRuleSet::new(vec![
ChainedClassSequenceRule::new(
vec![5, 111, 112, 114, 115, 117],
input.to_vec(),
lookahead.to_vec(),
vec![],
),
]))],
))
.into(),
))
}
#[test]
fn max_context_nothin() {
assert_eq!(compute_max_context_value(None, None), 0);
assert_eq!(
compute_max_context_value(Some(&Default::default()), None),
0
);
assert_eq!(
compute_max_context_value(None, Some(&Default::default())),
0
);
}
#[test]
fn max_context_simple() {
let gpos = Gpos::new(
Default::default(),
Default::default(),
PositionLookupList::new(vec![make_singlepos()]),
);
assert_eq!(compute_max_context_value(Some(&gpos), None), 1);
}
#[test]
fn max_context_both_tables() {
let gpos = Gpos::new(
Default::default(),
Default::default(),
PositionLookupList::new(vec![make_pairpos()]),
);
let gsub = Gsub::new(
Default::default(),
Default::default(),
SubstitutionLookupList::new(vec![make_singlesub()]),
);
assert_eq!(compute_max_context_value(Some(&gpos), Some(&gsub)), 2);
}
#[test]
fn max_context_kitchen_sink() {
let gpos = Gpos::new(
Default::default(),
Default::default(),
PositionLookupList::new(vec![make_singlepos(), make_pairpos()]),
);
let gsub = Gsub::new(
Default::default(),
Default::default(),
SubstitutionLookupList::new(vec![
make_singlesub(),
make_chain_context_format1([1], [2, 3]),
]),
);
assert_eq!(compute_max_context_value(Some(&gpos), Some(&gsub)), 4);
}
#[test]
fn max_context_chaining_contextual_sub() {
let gpos = Gpos::new(
Default::default(),
Default::default(),
PositionLookupList::new(vec![make_pairpos()]),
);
let gsub = Gsub::new(
Default::default(),
Default::default(),
SubstitutionLookupList::new(vec![make_chain_context_format2([1], [2, 3])]),
);
assert_eq!(compute_max_context_value(Some(&gpos), Some(&gsub)), 4);
}
}