[subset] optimize glyph closure method: step 5

add testcase and some fixes
This commit is contained in:
Qunxin Liu 2021-01-12 10:17:14 -08:00 committed by Garret Rieger
parent b8a58a0c0b
commit 0e1c0fa404
41 changed files with 124 additions and 52 deletions

View File

@ -1396,6 +1396,7 @@ struct CoverageFormat2
for (unsigned i = 0; i < count; i++) for (unsigned i = 0; i < count; i++)
{ {
const RangeRecord &range = rangeRecord[i]; const RangeRecord &range = rangeRecord[i];
if (!range.intersects (glyphs)) continue;
for (hb_codepoint_t g = range.first; g <= range.last; g++) for (hb_codepoint_t g = range.first; g <= range.last; g++)
if (glyphs->has (g)) intersect_glyphs->add (g); if (glyphs->has (g)) intersect_glyphs->add (g);
} }
@ -2094,24 +2095,24 @@ struct ClassDefFormat2
hb_codepoint_t g = HB_SET_VALUE_INVALID; hb_codepoint_t g = HB_SET_VALUE_INVALID;
for (unsigned int i = 0; i < count; i++) for (unsigned int i = 0; i < count; i++)
{ {
if (rangeRecord[i].value == klass) if (rangeRecord[i].value != klass) continue;
if (g != HB_SET_VALUE_INVALID)
{ {
if (g != HB_SET_VALUE_INVALID) if (g >= rangeRecord[i].first &&
{ g <= rangeRecord[i].last)
if (g >= rangeRecord[i].first && intersect_glyphs->add (g);
g <= rangeRecord[i].last) if (g > rangeRecord[i].last)
intersect_glyphs->add (g); continue;
if (g > rangeRecord[i].last) }
continue;
} g = rangeRecord[i].first - 1;
while (hb_set_next (glyphs, &g))
while (hb_set_next (glyphs, &g)) {
{ if (g >= rangeRecord[i].first && g <= rangeRecord[i].last)
if (g >= rangeRecord[i].first && g <= rangeRecord[i].last) intersect_glyphs->add (g);
intersect_glyphs->add (g); else if (g > rangeRecord[i].last)
else if (g > rangeRecord[i].last) break;
break;
}
} }
} }
} }

View File

@ -1552,14 +1552,14 @@ struct SubstLookup : Lookup
template <typename context_t> template <typename context_t>
static inline typename context_t::return_t dispatch_recurse_func (context_t *c, unsigned int lookup_index); static inline typename context_t::return_t dispatch_recurse_func (context_t *c, unsigned int lookup_index);
static inline typename hb_closure_context_t::return_t closure_glyphs_recurse_func (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *chaos, unsigned seq_index, unsigned end_index); static inline typename hb_closure_context_t::return_t closure_glyphs_recurse_func (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *covered_seq_indices, unsigned seq_index, unsigned end_index);
static inline hb_closure_context_t::return_t dispatch_closure_recurse_func (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *chaos, unsigned seq_index, unsigned end_index) static inline hb_closure_context_t::return_t dispatch_closure_recurse_func (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *covered_seq_indices, unsigned seq_index, unsigned end_index)
{ {
if (!c->should_visit_lookup (lookup_index)) if (!c->should_visit_lookup (lookup_index))
return hb_empty_t (); return hb_empty_t ();
hb_closure_context_t::return_t ret = closure_glyphs_recurse_func (c, lookup_index, chaos, seq_index, end_index); hb_closure_context_t::return_t ret = closure_glyphs_recurse_func (c, lookup_index, covered_seq_indices, seq_index, end_index);
/* While in theory we should flush here, it will cause timeouts because a recursive /* While in theory we should flush here, it will cause timeouts because a recursive
* lookup can keep growing the glyph set. Skip, and outer loop will retry up to * lookup can keep growing the glyph set. Skip, and outer loop will retry up to
@ -1632,11 +1632,11 @@ template <typename context_t>
return l.dispatch (c); return l.dispatch (c);
} }
/*static*/ typename hb_closure_context_t::return_t SubstLookup::closure_glyphs_recurse_func (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *chaos, unsigned seq_index, unsigned end_index) /*static*/ typename hb_closure_context_t::return_t SubstLookup::closure_glyphs_recurse_func (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *covered_seq_indices, unsigned seq_index, unsigned end_index)
{ {
const SubstLookup &l = c->face->table.GSUB.get_relaxed ()->table->get_lookup (lookup_index); const SubstLookup &l = c->face->table.GSUB.get_relaxed ()->table->get_lookup (lookup_index);
if (l.may_have_non_1to1 ()) if (l.may_have_non_1to1 ())
hb_set_add_range (chaos, seq_index, end_index); hb_set_add_range (covered_seq_indices, seq_index, end_index);
return l.dispatch (c); return l.dispatch (c);
} }

View File

@ -67,17 +67,17 @@ struct hb_have_non_1to1_context_t :
struct hb_closure_context_t : struct hb_closure_context_t :
hb_dispatch_context_t<hb_closure_context_t> hb_dispatch_context_t<hb_closure_context_t>
{ {
typedef return_t (*recurse_func_t) (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *chaos, unsigned seq_index, unsigned end_index); typedef return_t (*recurse_func_t) (hb_closure_context_t *c, unsigned lookup_index, hb_set_t *covered_seq_indicies, unsigned seq_index, unsigned end_index);
template <typename T> template <typename T>
return_t dispatch (const T &obj) { obj.closure (this); return hb_empty_t (); } return_t dispatch (const T &obj) { obj.closure (this); return hb_empty_t (); }
static return_t default_return_value () { return hb_empty_t (); } static return_t default_return_value () { return hb_empty_t (); }
void recurse (unsigned lookup_index, hb_set_t *chaos, unsigned seq_index, unsigned end_index) void recurse (unsigned lookup_index, hb_set_t *covered_seq_indicies, unsigned seq_index, unsigned end_index)
{ {
if (unlikely (nesting_level_left == 0 || !recurse_func)) if (unlikely (nesting_level_left == 0 || !recurse_func))
return; return;
nesting_level_left--; nesting_level_left--;
recurse_func (this, lookup_index, chaos, seq_index, end_index); recurse_func (this, lookup_index, covered_seq_indicies, seq_index, end_index);
nesting_level_left++; nesting_level_left++;
} }
@ -92,17 +92,32 @@ struct hb_closure_context_t :
if (is_lookup_done (lookup_index)) if (is_lookup_done (lookup_index))
return false; return false;
done_lookups->set (lookup_index, glyphs->get_population ());
return true; return true;
} }
bool is_lookup_done (unsigned int lookup_index) bool is_lookup_done (unsigned int lookup_index)
{ {
if (unlikely (done_lookups->in_error ())) if (done_lookups_glyph_count->in_error () ||
done_lookups_glyph_set->in_error ())
return true; return true;
/* Have we visited this lookup with the current set of glyphs? */ /* Have we visited this lookup with the current set of glyphs? */
return done_lookups->get (lookup_index) == glyphs->get_population (); if (done_lookups_glyph_count->get (lookup_index) != glyphs->get_population ())
{
done_lookups_glyph_count->set (lookup_index, glyphs->get_population ());
if (!done_lookups_glyph_set->get (lookup_index))
done_lookups_glyph_set->set (lookup_index, hb_set_create ());
done_lookups_glyph_set->get (lookup_index)->clear ();
}
hb_set_t *covered_glyph_set = done_lookups_glyph_set->get (lookup_index);
if (parent_active_glyphs ()->is_subset (covered_glyph_set))
return true;
hb_set_union (covered_glyph_set, parent_active_glyphs ());
return false;
} }
hb_set_t* parent_active_glyphs () hb_set_t* parent_active_glyphs ()
@ -129,6 +144,7 @@ struct hb_closure_context_t :
hb_face_t *face; hb_face_t *face;
hb_set_t *glyphs; hb_set_t *glyphs;
hb_set_t *cur_intersected_glyphs;
hb_set_t output[1]; hb_set_t output[1];
hb_vector_t<hb_set_t *> active_glyphs_stack; hb_vector_t<hb_set_t *> active_glyphs_stack;
recurse_func_t recurse_func; recurse_func_t recurse_func;
@ -136,13 +152,17 @@ struct hb_closure_context_t :
hb_closure_context_t (hb_face_t *face_, hb_closure_context_t (hb_face_t *face_,
hb_set_t *glyphs_, hb_set_t *glyphs_,
hb_map_t *done_lookups_, hb_set_t *cur_intersected_glyphs_,
hb_map_t *done_lookups_glyph_count_,
hb_hashmap_t<unsigned, hb_set_t *, (unsigned)-1, nullptr> *done_lookups_glyph_set_,
unsigned int nesting_level_left_ = HB_MAX_NESTING_LEVEL) : unsigned int nesting_level_left_ = HB_MAX_NESTING_LEVEL) :
face (face_), face (face_),
glyphs (glyphs_), glyphs (glyphs_),
cur_intersected_glyphs (cur_intersected_glyphs_),
recurse_func (nullptr), recurse_func (nullptr),
nesting_level_left (nesting_level_left_), nesting_level_left (nesting_level_left_),
done_lookups (done_lookups_), done_lookups_glyph_count (done_lookups_glyph_count_),
done_lookups_glyph_set (done_lookups_glyph_set_),
lookup_count (0) lookup_count (0)
{ {
push_cur_active_glyphs (glyphs_); push_cur_active_glyphs (glyphs_);
@ -162,10 +182,13 @@ struct hb_closure_context_t :
} }
private: private:
hb_map_t *done_lookups; hb_map_t *done_lookups_glyph_count;
hb_hashmap_t<unsigned, hb_set_t *, (unsigned)-1, nullptr> *done_lookups_glyph_set;
unsigned int lookup_count; unsigned int lookup_count;
}; };
struct hb_closure_lookups_context_t : struct hb_closure_lookups_context_t :
hb_dispatch_context_t<hb_closure_lookups_context_t> hb_dispatch_context_t<hb_closure_lookups_context_t>
{ {
@ -1214,9 +1237,7 @@ static void context_closure_recurse_lookups (hb_closure_context_t *c,
const void *data, const void *data,
intersected_glyphs_func_t intersected_glyphs_func) intersected_glyphs_func_t intersected_glyphs_func)
{ {
printf ("qxliu_1\n"); hb_set_t *covered_seq_indicies = hb_set_create ();
hb_set_t *chaos = hb_set_create ();
for (unsigned int i = 0; i < lookupCount; i++) for (unsigned int i = 0; i < lookupCount; i++)
{ {
unsigned seqIndex = lookupRecord[i].sequenceIndex; unsigned seqIndex = lookupRecord[i].sequenceIndex;
@ -1224,7 +1245,7 @@ static void context_closure_recurse_lookups (hb_closure_context_t *c,
hb_set_t *pos_glyphs = hb_set_create (); hb_set_t *pos_glyphs = hb_set_create ();
if (hb_set_is_empty (chaos) || !hb_set_has (chaos, seqIndex)) if (hb_set_is_empty (covered_seq_indicies) || !hb_set_has (covered_seq_indicies, seqIndex))
{ {
if (seqIndex == 0) if (seqIndex == 0)
{ {
@ -1233,10 +1254,10 @@ static void context_closure_recurse_lookups (hb_closure_context_t *c,
pos_glyphs->add (value); pos_glyphs->add (value);
break; break;
case ContextFormat::ClassBasedContext: case ContextFormat::ClassBasedContext:
intersected_glyphs_func (c->parent_active_glyphs (), data, value, pos_glyphs); intersected_glyphs_func (c->cur_intersected_glyphs, data, value, pos_glyphs);
break; break;
case ContextFormat::CoverageBasedContext: case ContextFormat::CoverageBasedContext:
hb_set_set (pos_glyphs, c->parent_active_glyphs ()); hb_set_set (pos_glyphs, c->cur_intersected_glyphs);
break; break;
} }
} }
@ -1244,30 +1265,30 @@ static void context_closure_recurse_lookups (hb_closure_context_t *c,
{ {
const void *input_data = input; const void *input_data = input;
unsigned input_value = seqIndex - 1; unsigned input_value = seqIndex - 1;
if (context_format == ContextFormat::ClassBasedContext) if (context_format != ContextFormat::SimpleContext)
{ {
input_data = data; input_data = data;
input_value = input[seqIndex - 1]; input_value = input[seqIndex - 1];
} }
intersected_glyphs_func (c->parent_active_glyphs (), input_data, input_value, pos_glyphs); intersected_glyphs_func (c->glyphs, input_data, input_value, pos_glyphs);
} }
} }
hb_set_add (chaos, seqIndex); hb_set_add (covered_seq_indicies, seqIndex);
c->push_cur_active_glyphs (pos_glyphs); c->push_cur_active_glyphs (pos_glyphs);
unsigned endIndex = inputCount; unsigned endIndex = inputCount;
if (context_format == ContextFormat::CoverageBasedContext) if (context_format == ContextFormat::CoverageBasedContext)
endIndex += 1; endIndex += 1;
c->recurse (lookupRecord[i].lookupListIndex, chaos, seqIndex, endIndex); c->recurse (lookupRecord[i].lookupListIndex, covered_seq_indicies, seqIndex, endIndex);
c->pop_cur_done_glyphs (); c->pop_cur_done_glyphs ();
hb_set_destroy (pos_glyphs); hb_set_destroy (pos_glyphs);
} }
hb_set_destroy (chaos); hb_set_destroy (covered_seq_indicies);
} }
template <typename context_t> template <typename context_t>
@ -1768,6 +1789,9 @@ struct ContextFormat1
void closure (hb_closure_context_t *c) const void closure (hb_closure_context_t *c) const
{ {
c->cur_intersected_glyphs->clear ();
get_coverage ().intersected_coverage_glyphs (c->parent_active_glyphs (), c->cur_intersected_glyphs);
struct ContextClosureLookupContext lookup_context = { struct ContextClosureLookupContext lookup_context = {
{intersects_glyph, intersected_glyph}, {intersects_glyph, intersected_glyph},
ContextFormat::SimpleContext, ContextFormat::SimpleContext,
@ -1784,7 +1808,8 @@ struct ContextFormat1
void closure_lookups (hb_closure_lookups_context_t *c) const void closure_lookups (hb_closure_lookups_context_t *c) const
{ {
struct ContextClosureLookupContext lookup_context = { struct ContextClosureLookupContext lookup_context = {
{intersects_glyph}, {intersects_glyph, intersected_glyph},
ContextFormat::SimpleContext,
nullptr nullptr
}; };
@ -1918,6 +1943,9 @@ struct ContextFormat2
if (!(this+coverage).intersects (c->glyphs)) if (!(this+coverage).intersects (c->glyphs))
return; return;
c->cur_intersected_glyphs->clear ();
get_coverage ().intersected_coverage_glyphs (c->parent_active_glyphs (), c->cur_intersected_glyphs);
const ClassDef &class_def = this+classDef; const ClassDef &class_def = this+classDef;
struct ContextClosureLookupContext lookup_context = { struct ContextClosureLookupContext lookup_context = {
@ -1929,7 +1957,7 @@ struct ContextFormat2
return return
+ hb_enumerate (ruleSet) + hb_enumerate (ruleSet)
| hb_filter ([&] (unsigned _) | hb_filter ([&] (unsigned _)
{ return class_def.intersects_class (c->parent_active_glyphs (), _); }, { return class_def.intersects_class (c->cur_intersected_glyphs, _); },
hb_first) hb_first)
| hb_apply ([&] (const hb_pair_t<unsigned, const OffsetTo<RuleSet>&> _) | hb_apply ([&] (const hb_pair_t<unsigned, const OffsetTo<RuleSet>&> _)
{ {
@ -1947,7 +1975,8 @@ struct ContextFormat2
const ClassDef &class_def = this+classDef; const ClassDef &class_def = this+classDef;
struct ContextClosureLookupContext lookup_context = { struct ContextClosureLookupContext lookup_context = {
{intersects_class}, {intersects_class, intersected_class_glyphs},
ContextFormat::ClassBasedContext,
&class_def &class_def
}; };
@ -2100,6 +2129,9 @@ struct ContextFormat3
if (!(this+coverageZ[0]).intersects (c->glyphs)) if (!(this+coverageZ[0]).intersects (c->glyphs))
return; return;
c->cur_intersected_glyphs->clear ();
get_coverage ().intersected_coverage_glyphs (c->parent_active_glyphs (), c->cur_intersected_glyphs);
const LookupRecord *lookupRecord = &StructAfter<LookupRecord> (coverageZ.as_array (glyphCount)); const LookupRecord *lookupRecord = &StructAfter<LookupRecord> (coverageZ.as_array (glyphCount));
struct ContextClosureLookupContext lookup_context = { struct ContextClosureLookupContext lookup_context = {
{intersects_coverage, intersected_coverage_glyphs}, {intersects_coverage, intersected_coverage_glyphs},
@ -2726,6 +2758,9 @@ struct ChainContextFormat1
void closure (hb_closure_context_t *c) const void closure (hb_closure_context_t *c) const
{ {
c->cur_intersected_glyphs->clear ();
get_coverage ().intersected_coverage_glyphs (c->parent_active_glyphs (), c->cur_intersected_glyphs);
struct ChainContextClosureLookupContext lookup_context = { struct ChainContextClosureLookupContext lookup_context = {
{intersects_glyph, intersected_glyph}, {intersects_glyph, intersected_glyph},
ContextFormat::SimpleContext, ContextFormat::SimpleContext,
@ -2742,7 +2777,8 @@ struct ChainContextFormat1
void closure_lookups (hb_closure_lookups_context_t *c) const void closure_lookups (hb_closure_lookups_context_t *c) const
{ {
struct ChainContextClosureLookupContext lookup_context = { struct ChainContextClosureLookupContext lookup_context = {
{intersects_glyph}, {intersects_glyph, intersected_glyph},
ContextFormat::SimpleContext,
{nullptr, nullptr, nullptr} {nullptr, nullptr, nullptr}
}; };
@ -2878,6 +2914,9 @@ struct ChainContextFormat2
if (!(this+coverage).intersects (c->glyphs)) if (!(this+coverage).intersects (c->glyphs))
return; return;
c->cur_intersected_glyphs->clear ();
get_coverage ().intersected_coverage_glyphs (c->parent_active_glyphs (), c->cur_intersected_glyphs);
const ClassDef &backtrack_class_def = this+backtrackClassDef; const ClassDef &backtrack_class_def = this+backtrackClassDef;
const ClassDef &input_class_def = this+inputClassDef; const ClassDef &input_class_def = this+inputClassDef;
const ClassDef &lookahead_class_def = this+lookaheadClassDef; const ClassDef &lookahead_class_def = this+lookaheadClassDef;
@ -2893,7 +2932,7 @@ struct ChainContextFormat2
return return
+ hb_enumerate (ruleSet) + hb_enumerate (ruleSet)
| hb_filter ([&] (unsigned _) | hb_filter ([&] (unsigned _)
{ return input_class_def.intersects_class (c->parent_active_glyphs (), _); }, { return input_class_def.intersects_class (c->cur_intersected_glyphs, _); },
hb_first) hb_first)
| hb_apply ([&] (const hb_pair_t<unsigned, const OffsetTo<ChainRuleSet>&> _) | hb_apply ([&] (const hb_pair_t<unsigned, const OffsetTo<ChainRuleSet>&> _)
{ {
@ -2913,7 +2952,8 @@ struct ChainContextFormat2
const ClassDef &lookahead_class_def = this+lookaheadClassDef; const ClassDef &lookahead_class_def = this+lookaheadClassDef;
struct ChainContextClosureLookupContext lookup_context = { struct ChainContextClosureLookupContext lookup_context = {
{intersects_class}, {intersects_class, intersected_class_glyphs},
ContextFormat::ClassBasedContext,
{&backtrack_class_def, {&backtrack_class_def,
&input_class_def, &input_class_def,
&lookahead_class_def} &lookahead_class_def}
@ -3119,6 +3159,9 @@ struct ChainContextFormat3
if (!(this+input[0]).intersects (c->glyphs)) if (!(this+input[0]).intersects (c->glyphs))
return; return;
c->cur_intersected_glyphs->clear ();
get_coverage ().intersected_coverage_glyphs (c->parent_active_glyphs (), c->cur_intersected_glyphs);
const OffsetArrayOf<Coverage> &lookahead = StructAfter<OffsetArrayOf<Coverage>> (input); const OffsetArrayOf<Coverage> &lookahead = StructAfter<OffsetArrayOf<Coverage>> (input);
const ArrayOf<LookupRecord> &lookup = StructAfter<ArrayOf<LookupRecord>> (lookahead); const ArrayOf<LookupRecord> &lookup = StructAfter<ArrayOf<LookupRecord>> (lookahead);
struct ChainContextClosureLookupContext lookup_context = { struct ChainContextClosureLookupContext lookup_context = {

View File

@ -1443,12 +1443,17 @@ hb_ot_layout_lookup_substitute_closure (hb_face_t *face,
unsigned int lookup_index, unsigned int lookup_index,
hb_set_t *glyphs /* OUT */) hb_set_t *glyphs /* OUT */)
{ {
hb_map_t done_lookups; hb_set_t cur_intersected_glyphs;
OT::hb_closure_context_t c (face, glyphs, &done_lookups); hb_map_t done_lookups_glyph_count;
hb_hashmap_t<unsigned, hb_set_t *, (unsigned)-1, nullptr> done_lookups_glyph_set;
OT::hb_closure_context_t c (face, glyphs, &cur_intersected_glyphs, &done_lookups_glyph_count, &done_lookups_glyph_set);
const OT::SubstLookup& l = face->table.GSUB->table->get_lookup (lookup_index); const OT::SubstLookup& l = face->table.GSUB->table->get_lookup (lookup_index);
l.closure (&c, lookup_index); l.closure (&c, lookup_index);
for (auto _ : done_lookups_glyph_set.iter ())
hb_set_destroy (_.second);
} }
/** /**
@ -1467,8 +1472,10 @@ hb_ot_layout_lookups_substitute_closure (hb_face_t *face,
const hb_set_t *lookups, const hb_set_t *lookups,
hb_set_t *glyphs /* OUT */) hb_set_t *glyphs /* OUT */)
{ {
hb_map_t done_lookups; hb_set_t cur_intersected_glyphs;
OT::hb_closure_context_t c (face, glyphs, &done_lookups); hb_map_t done_lookups_glyph_count;
hb_hashmap_t<unsigned, hb_set_t *, (unsigned)-1, nullptr> done_lookups_glyph_set;
OT::hb_closure_context_t c (face, glyphs, &cur_intersected_glyphs, &done_lookups_glyph_count, &done_lookups_glyph_set);
const OT::GSUB& gsub = *face->table.GSUB->table; const OT::GSUB& gsub = *face->table.GSUB->table;
unsigned int iteration_count = 0; unsigned int iteration_count = 0;
@ -1488,6 +1495,9 @@ hb_ot_layout_lookups_substitute_closure (hb_face_t *face,
} }
} while (iteration_count++ <= HB_CLOSURE_MAX_STAGES && } while (iteration_count++ <= HB_CLOSURE_MAX_STAGES &&
glyphs_length != glyphs->get_population ()); glyphs_length != glyphs->get_population ());
for (auto _ : done_lookups_glyph_set.iter ())
hb_set_destroy (_.second);
} }
/* /*

View File

@ -23,6 +23,7 @@ EXTRA_DIST += \
expected/layout.gpos8.amiri \ expected/layout.gpos8.amiri \
expected/layout.gpos9 \ expected/layout.gpos9 \
expected/layout.gsub3 \ expected/layout.gsub3 \
expected/layout.gsub5 \
expected/layout.gsub6 \ expected/layout.gsub6 \
expected/layout.gdef \ expected/layout.gdef \
expected/layout.context \ expected/layout.context \

View File

@ -22,6 +22,7 @@ TESTS = \
tests/layout.gpos8.amiri.tests \ tests/layout.gpos8.amiri.tests \
tests/layout.gpos9.tests \ tests/layout.gpos9.tests \
tests/layout.gsub3.tests \ tests/layout.gsub3.tests \
tests/layout.gsub5.tests \
tests/layout.gsub6.tests \ tests/layout.gsub6.tests \
tests/layout.tests \ tests/layout.tests \
tests/sbix.tests \ tests/sbix.tests \

Binary file not shown.

View File

@ -0,0 +1,15 @@
FONTS:
gsub_context1_multiple_subrules_f2.otf
gsub_context2_multiple_subrules_f2.otf
gsub_context3_successive_f1.otf
PROFILES:
keep-layout.txt
keep-layout-retain-gids.txt
SUBSETS:
A
AB
AC
ABC
*

View File

@ -15,6 +15,7 @@ tests = [
'layout.gpos8.amiri', 'layout.gpos8.amiri',
'layout.gpos9', 'layout.gpos9',
'layout.gsub3', 'layout.gsub3',
'layout.gsub5',
'layout.gsub6', 'layout.gsub6',
'layout.gdef', 'layout.gdef',
'layout.context', 'layout.context',