Using Depend for Closure Computation

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.

Computing Closure with Depend

To compute a closure using the depend graph:

  1. Map input codepoints to starting glyphs using hb_font_get_nominal_glyph()
  2. Handle Unicode Variation Sequences using hb_font_get_variation_glyph() for UVS-based glyph expansion (not in depend graph)
  3. Initialize the reachable set with these starting glyphs
  4. Expand through dependencies by following edges in the depend graph
  5. Filter by active features to only follow edges for enabled GSUB features
  6. Handle ligatures correctly - only add ligature outputs when all component glyphs are present
  7. Check context requirements - only follow edges when positional requirements (backtrack/lookahead) are satisfied

Context Set Filtering

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.

Reference Implementation

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:

  • Feature filtering (only following edges for active GSUB features)
  • Ligature sets (only adding ligature outputs when all components present)
  • Context sets (only following edges when positional requirements satisfied)
  • Non-GSUB dependencies (glyf, CFF, COLR, MATH)
  • UVS handling (via 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);
}

Table-Specific Closure Behavior

MATH Closure

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.

GSUB Self-Feeding Chains

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):

  1. For each active GSUB feature, collect source glyphs and dependent glyphs
  2. If those sets intersect for any feature, the font has self-feeding potential
  3. Trace extra glyphs through self-feeding feature edges from the subset closure
  4. If all extra glyphs are reachable through self-feeding chains, the over-approximation is a known artifact of self-feeding, not a bug

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.

Edge Flags and Over-Approximation Detection

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 rule
  • HB_SUBSET_DEPEND_EDGE_FLAG_FROM_NESTED_CONTEXT (0x02): Edge from a nested contextual lookup

When 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");
}

Optional Over-Approximation

You can choose to skip context_set checking for:

  • Simpler implementation - Fewer steps, less complex code
  • Coverage analysis - Finding all glyphs that participate in a feature, regardless of whether they're reachable for specific input
  • Conservative estimates - Ensuring no glyphs are missed

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.

Use Cases

Font Subsetting

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.

Coverage Analysis

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.

Font Segmentation

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.

Testing

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.

Implementation Notes

Non-GSUB Dependencies

Dependencies from non-GSUB tables do not require context filtering:

  • Composite glyphs (glyf, CFF): Structural component relationships
  • Color layers (COLR): Layer composition
  • Math variants (MATH): Size variant relationships

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.

Feature Filtering

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 Filtering

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.

Further Reading

For detailed API documentation:

  • docs/depend-api.md - API usage guide with examples
  • docs/depend-implementation.md - Implementation details and architecture

For reference implementation:

  • test/fuzzing/hb-depend-closure-parity.cc - Production-quality closure computation
  • test/api/test-ot-depend.c - Unit tests for depend API