The depend API can be used to compute glyph closures - determining which glyphs are reachable from a given set of input codepoints and active features. This document explains how to compute closures using the depend graph and provides a reference implementation.
To compute a closure using the depend graph:
hb_font_get_nominal_glyph()hb_font_get_variation_glyph() for UVS-based glyph expansion (not in depend graph)Context and ChainContext GSUB rules have positional requirements encoded in the context_set field. Each context_set specifies which backtrack and/or lookahead glyphs must be present for a dependency edge to apply.
When context_set filtering is implemented, recent testing has not generated any over-approximation cases - the depend-based closure matches the subset-based closure in all tested scenarios.
With context_set filtering: Recent testing shows no over-approximation Without context_set filtering: Conservative over-approximation (safe but may include extra glyphs)
See docs/depend-api.md section “Working with Context Sets” for details on the context_set encoding and how to check context requirements.
The test suite includes a production-quality implementation of depend-based closure in test/fuzzing/hb-depend-closure-parity.cc. The compute_depend_closure() function demonstrates proper handling of:
hb_font_get_variation_glyph(), separate from depend graph)Key aspects of the implementation:
// entry is hb_subset_depend_entry_t // Feature filtering - only follow GSUB edges for active features if (entry.table_tag == HB_OT_TAG_GSUB) { if (active_features && !hb_set_has(active_features, entry.layout_tag)) continue; // Skip edge if feature not active } // Context filtering - only follow edges when requirements satisfied if (entry.context_set_index != HB_CODEPOINT_INVALID) { if (!check_context_satisfied(depend, entry.context_set_index, glyphs)) continue; // Skip edge if context not satisfied } // Ligature handling - only add ligature when all components present if (entry.ligature_set_index != HB_CODEPOINT_INVALID) { hb_set_t *ligature_glyphs = hb_set_create(); hb_subset_depend_lookup_set(depend, entry.ligature_set_index, ligature_glyphs); // Check if all component glyphs are in reachable set if (!hb_set_is_subset(ligature_glyphs, glyphs)) { hb_set_destroy(ligature_glyphs); continue; // Skip ligature if not all components present } hb_set_destroy(ligature_glyphs); } // Add dependent glyph to closure if (!hb_set_has(glyphs, entry.dependent)) { hb_set_add(glyphs, entry.dependent); hb_set_add(to_process, entry.dependent); }
MATH edges should be followed in a single pass: follow MATH edges for glyphs that are in the set before the MATH stage, but do NOT follow MATH edges for glyphs that the MATH stage itself added. This matches the subsetter's behavior, which retains MATH-closure glyphs (their outlines are needed for drawing assembly parts) but strips their own MATH constructions from the subset.
Math layout engines do not recursively stretch assembly parts, so the second-hop constructions are not functionally needed.
The depend graph captures all GSUB substitution edges. When computing transitive closure, a lookup‘s output glyph may itself be in the same lookup’s coverage, creating a chain: A → B → C → ... through the same lookup. No shaper actually re-applies a lookup to its own output within a single feature application, so these chains are over-approximation relative to real shaping behavior.
The subsetter's iterative closure (HB_CLOSURE_MAX_STAGES) truncates these chains. The depend closure, following edges to a fixed point, may go further.
To detect this condition (only relevant when unexpected over-approximation is found without flagged edges):
This detection only runs on the failure path and is O(edges), so real fonts pay no cost. In practice, well-formed fonts do not have self-feeding lookups.
Each dependency edge has an optional flags field that indicates potential over-approximation:
HB_SUBSET_DEPEND_EDGE_FLAG_FROM_CONTEXT_POSITION (0x01): Edge from a multi-position contextual ruleHB_SUBSET_DEPEND_EDGE_FLAG_FROM_NESTED_CONTEXT (0x02): Edge from a nested contextual lookupWhen computing closure, track whether any flagged edges contributed to the result:
bool hit_flagged_edge = false; // When following an edge (entry is hb_subset_depend_entry_t)... if (!hb_set_has(closure, entry.dependent)) { hb_set_add(closure, entry.dependent); if (entry.flags & (HB_SUBSET_DEPEND_EDGE_FLAG_FROM_CONTEXT_POSITION | HB_SUBSET_DEPEND_EDGE_FLAG_FROM_NESTED_CONTEXT)) hit_flagged_edge = true; } // After closure computation... if (depend_closure.is_superset_of(subset_closure)) { if (hit_flagged_edge) printf("Expected over-approximation (hit flagged edge)\n"); else printf("UNEXPECTED over-approximation (bug)\n"); }
You can choose to skip context_set checking for:
Skipping context_set produces a safe over-approximation - all needed glyphs are included, possibly with some additional glyphs that wouldn't be reached for the specific input.
Determine which glyphs must be retained to properly render a specific set of characters, accounting for all OpenType substitutions and compositions.
With context_set filtering, depend-based subsetting produces results that match subset-based subsetting in recent testing.
Analyze which features or scripts require which glyphs. For this use case, skipping context_set filtering may be desirable - it shows all glyphs that participate in a feature, not just those reachable for specific input.
Partition a font into smaller subsets where each segment contains glyphs reachable from a specific set of input characters, enabling more efficient font delivery for web applications.
Verify that font modifications haven't inadvertently broken glyph references or substitution chains. The depend graph provides a complete view of font structure that can be compared before and after modifications.
Dependencies from non-GSUB tables do not require context filtering:
These dependencies should always be followed during closure computation.
Note: Unicode Variation Sequences (UVS) are handled separately via hb_font_get_variation_glyph() and are not part of the depend graph.
GSUB edges should be filtered by active features. The layout_tag field contains the feature tag for GSUB dependencies. Only follow edges where the feature is active in your shaping configuration.
Ligature dependencies should only be followed when ALL component glyphs in the ligature_set are present in the current closure. This prevents adding ligature outputs prematurely.
For detailed API documentation:
docs/depend-api.md - API usage guide with examplesdocs/depend-implementation.md - Implementation details and architectureFor reference implementation:
test/fuzzing/hb-depend-closure-parity.cc - Production-quality closure computationtest/api/test-ot-depend.c - Unit tests for depend API