blob: 14e9b214bf87ee361d6bb76054c3a465e4e11674 [file] [edit]
/*
* Copyright © 2025 Skef Iterum
*
* This is part of HarfBuzz, a text shaping library.
*
* Permission is hereby granted, without written agreement and without
* license or royalty fees, to use, copy, modify, and distribute this
* software and its documentation for any purpose, provided that the
* above copyright notice and the following two paragraphs appear in
* all copies of this software.
*
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
*/
#include "hb-test.h"
#include <hb.h>
#include <hb-ot.h>
#include <hb-subset.h>
#ifndef HB_NO_SUBSET_DEPEND
/*
* ============================================================================
* DEPEND TEST DEVELOPMENT NOTES (TEMPORARY - REMOVE WHEN COMPLETE)
* ============================================================================
*
* TESTING PHILOSOPHY (Option B: "Double Duty")
* ----------------------------------------------
* We use a "double duty" approach where tests serve multiple purposes:
* 1. Basic substitution tests verify the fundamental substitution types work
* 2. Contextual format tests verify BOTH:
* - The contextual format parsing/traversal works
* - Recursion correctly invokes nested lookups
* - The invoked basic substitution type produces correct dependencies
*
* This reduces redundancy while maximizing coverage. Instead of separate tests
* for "SingleSubst works" and "ContextSubst Format 2 works", we test
* "ContextSubst Format 2 correctly invokes SingleSubst".
*
* COVERAGE REQUIREMENTS
* ---------------------
* Basic GSUB substitution types:
* ✓ SingleSubst - covered via contextual recursion tests
* ✓ MultipleSubst - covered via contextual recursion tests
* ✓ AlternateSubst - standalone test (has special AlternateSet behavior)
* ✓ LigatureSubst - standalone test (has special ligature_set tracking)
* ✓ ReverseChainSingleSubst - standalone test (special type 8)
*
* Contextual/Chaining substitution formats:
* ✓ ContextSubst Format 1 (glyph-based with SubRuleSets)
* ✓ ContextSubst Format 2 (class-based with SubClassSets)
* ✓ ContextSubst Format 3 (coverage-based inline)
* ✓ ChainContextSubst Format 1 (glyph-based with backtrack/lookahead)
* ✓ ChainContextSubst Format 2 (class-based with backtrack/lookahead)
* ✓ ChainContextSubst Format 3 (coverage-based with backtrack/lookahead)
*
* Special features:
* ✓ ExtensionSubst (Type 7) - implicitly tested (174 instances in NotoNastaliqUrdu)
* ✓ Feature tagging - verify layout_tag propagates through recursion
* ✓ Ligature set tracking - verify ligature_set is assigned for alternates
* ✓ Contextual filtering - verify glyphs unreachable from context are excluded
*
* IMPLICIT vs EXPLICIT TESTING
* -----------------------------
* Explicit: We have a test assertion that verifies a specific dependency
* from a known lookup format. If the format is broken, test fails.
*
* Implicit: The code path executes (font has those lookups), but we don't
* assert a specific dependency from that format. If format is broken,
* tests might still pass.
*
* We prefer EXPLICIT tests for all major formats. Implicit is acceptable only
* for ExtensionSubst (which is transparent) and edge cases.
*
* HOW TO FIND TEST CASES
* ----------------------
* 1. Extract GSUB table from font:
* ttx -q -t GSUB test/api/fonts/FontName.ttf -o /tmp/font-gsub.ttx
*
* 2. Find contextual lookups of desired format:
* grep -n "ContextSubst Format=\"2\"" /tmp/font-gsub.ttx
*
* 3. Look at the lookup structure (around the line number from step 2):
* sed -n 'START,END p' /tmp/font-gsub.ttx
* - Note the Coverage (which glyphs this applies to)
* - Note the ClassDef (for Format 2) or inline coverages (for Format 3)
* - Find SubstLookupRecord entries showing which lookups are invoked
*
* 4. Check what the invoked lookup does:
* grep -A15 '<Lookup index="N">' /tmp/font-gsub.ttx
*
* 5. Find glyph IDs:
* ttx -q -t GlyphOrder test/api/fonts/FontName.ttf -o - 2>&1 | grep "GlyphName"
*
* 6. Verify the dependency exists (test in isolation first if uncertain)
*
* CRITERIA FOR GOOD TEST CASES
* -----------------------------
* 1. Simple: Prefer lookups with small coverage, simple contexts
* 2. Clear: The glyph→glyph dependency should be unambiguous
* 3. Direct: Avoid deeply nested recursion if possible
* 4. Documented: Comments should explain:
* - Which lookup(s) are involved
* - What format is being tested
* - What the context is (if applicable)
* - What basic substitution type is invoked
*
* FIND_DEPENDENCY HELPER
* ----------------------
* The find_dependency() helper has special IN-OUT parameter behavior:
*
* layout_tag as filter (IN):
* If *out_layout_tag != HB_TAG_NONE, only matches that specific layout tag
* Example: layout_tag = HB_TAG('c','c','m','p');
* find_dependency(..., &layout_tag, NULL);
*
* layout_tag as output (OUT):
* If *out_layout_tag == HB_TAG_NONE, returns the layout_tag found
* Example: layout_tag = HB_TAG_NONE;
* find_dependency(..., &layout_tag, &ligature_set);
* // layout_tag now contains the tag of the dependency found
*
* This dual mode reduces code duplication for common patterns.
*
* REMAINING TESTS TO ADD
* ----------------------
* The depend API extracts dependencies from these tables:
* ✓ GSUB - glyph substitution (all formats and critical edge cases tested)
* - Multiple subtables per lookup (regression test for Bug #1)
* - Same lookup called multiple times in context (regression test for Bug #2)
* ✓ cmap - character mapping (COMPLETE)
* - Nominal glyph mappings: NOT stored as dependencies (Unicode → glyph
* is not a glyph→glyph relationship). Stored in separate nominal_glyphs map.
* - Unicode Variation Sequences (UVS): Only NonDefault UVS creates dependencies
* (nominal_glyph(U) → variant_glyph(U+S) with layout_tag=S)
* - Default UVS entries do NOT create dependencies
* - Tested with base.ttf: U+904D + 0xE01E5 → variant glyph
* ✓ glyf - composite glyphs (COMPLETE)
* - Component references in TrueType composite glyphs
* - Tested: Afghani (202) → three components in NotoNastaliqUrdu
* - Tagged with HB_TAG('g','l','y','f')
* ✓ COLR - color layering (COMPLETE)
* - COLRv0: Base glyph → layer glyphs (tested with COLRv0.extents.ttf)
* - COLRv1: Paint graph dependencies (tested with test_glyphs-glyf_colr_1.ttf)
* - Tagged with HB_TAG('C','O','L','R')
* - Fixed: Added guard to prevent v0-only fonts from accessing v1 data structures
* ✓ MATH - mathematical variants (COMPLETE)
* - Vertical/horizontal variants (tested with arrowup variants)
* - Size variants (uni2191_size2, etc.)
* - Glyph assembly parts (bottom, vertical)
* - Tested with MathTestFontFull.otf
* - Tagged with HB_TAG('M','A','T','H')
*
* SUMMARY OF TEST COVERAGE:
* ---------------------------
* ✓ GSUB: Complete - all 8 substitution types, 6 contextual formats, and edge cases:
* - Lookups with multiple subtables (first subtable may not contain target rule)
* - Contextual rules calling same lookup at multiple sequence positions
* ✓ cmap: Complete - NonDefault UVS dependencies tested (base.ttf)
* ✓ glyf: Complete - composite glyph dependencies tested
* ✓ MATH: Complete - size variants and glyph assembly tested
* ✓ COLR: Complete - both v0 layer dependencies and v1 Paint graph tested
*
* ADDING NEW TESTS
* ----------------
* When adding tests for new tables:
* 1. Follow the same "double duty" philosophy where applicable
* 2. Document format coverage in comments
* 3. Test both basic types AND contextual/recursive mechanisms (if applicable)
* 4. Include negative tests where filtering or exclusion is important
* 5. Verify feature tagging where relevant (table_tag should be correct)
* 6. Update this comment block with new insights
*
* TEST FONTS
* ----------
* - SourceSansPro-Regular.otf: Basic Latin font with common GSUB features
* Used for: AlternateSubst, LigatureSubst, ChainContextSubst Format 3
*
* - NotoSans-Bold.ttf: Fast font with comprehensive GSUB formats (0.01-0.02s init)
* Used for: glyf composite glyphs, ContextSubst Format 1
* Has: ContextSubst Format 1/2, ChainContextSubst Format 1/2/3
*
* - NotoNastaliqUrdu-Regular.ttf: Complex Arabic Nastaliq font (4+ seconds init)
* Used for: Contextual filtering negative test (issue #3397)
* TODO: Create minimal subset for remaining Format 2 tests
* Has: 174 ExtensionSubst lookups, 28 ChainContextSubst Format 2
*
* - Qahiri-Regular.ttf: Arabic font with various contextual formats
* Used for: ReverseChainSingleSubst, ContextSubst Format 3,
* ChainContextSubst Format 1
*
* KEY INSIGHTS FROM DEVELOPMENT
* ------------------------------
* 1. ExtensionSubst (Type 7) is transparent - it wraps other lookup types.
* No need for explicit tests; if wrapped types work, extension works.
*
* 2. ChainContextSubst Format 2 is the most complex:
* - Has BacktrackClassDef, InputClassDef, LookAheadClassDef
* - Rules reference classes, not glyphs directly
* - Need to map glyphs → classes → rules to find what happens
*
* 3. Contextual filtering (issue #3397) is crucial:
* - When context C invokes lookup L, only glyphs reachable from C's
* input pattern should get dependencies from L
* - Test both positive (dependency exists) and negative (doesn't exist)
*
* 4. Feature tagging propagation:
* - When feature F's lookup invokes another lookup via context,
* the resulting dependencies should be tagged with F
* - Important for subsetting - tells which glyphs are needed for which features
*
* ============================================================================
*/
/*
* Utility function to search for a specific dependency.
* Loops through all dependency entries for source_glyph and returns TRUE
* if a dependency on target_glyph with the expected table_tag is found.
*
* If out_layout_tag is non-NULL and *out_layout_tag is not HB_TAG_NONE,
* only matches entries with that specific layout_tag (in-out parameter).
* Otherwise returns the layout_tag of the match found (out parameter).
*
* If found, optionally returns the ligature_set via out parameter.
*/
static hb_bool_t
find_dependency (hb_subset_depend_t *depend,
hb_codepoint_t source_glyph,
hb_codepoint_t target_glyph,
hb_tag_t expected_table_tag,
hb_tag_t *out_layout_tag, /* IN-OUT (optional) */
hb_codepoint_t *out_ligature_set) /* OUT (optional) */
{
hb_tag_t filter_layout_tag = (out_layout_tag && *out_layout_tag != HB_TAG_NONE)
? *out_layout_tag : HB_TAG_NONE;
unsigned int total = hb_subset_depend_lookup_glyph (depend, source_glyph, 0, NULL, NULL);
for (unsigned int index = 0; index < total; index++)
{
hb_subset_depend_entry_t entry;
unsigned int count = 1;
hb_subset_depend_lookup_glyph (depend, source_glyph, index, &count, &entry);
if (entry.table_tag == expected_table_tag && entry.dependent == target_glyph)
{
/* If filtering by layout_tag, check if it matches */
if (filter_layout_tag != HB_TAG_NONE && entry.layout_tag != filter_layout_tag)
continue;
if (out_layout_tag)
*out_layout_tag = entry.layout_tag;
if (out_ligature_set)
*out_ligature_set = entry.ligature_set_index;
return TRUE;
}
}
return FALSE;
}
/* Test glyf (composite glyph) dependency extraction */
static void
test_depend_glyf (void)
{
/* NotoSans-Bold.ttf has composite glyphs */
hb_face_t *face = hb_test_open_font_file ("fonts/NotoSans-Bold.ttf");
hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face);
g_assert_nonnull (depend);
/* Composite glyph "Aacute" (131) is composed of two components:
* - A (36)
* - acute (118)
* The composite glyph depends on all its components. */
g_test_message ("Testing glyf composite: Aacute (131) → A (36)");
g_assert_true (find_dependency (depend, 131, 36, HB_TAG('g','l','y','f'),
NULL, NULL));
g_test_message ("Testing glyf composite: Aacute (131) → acute (118)");
g_assert_true (find_dependency (depend, 131, 118, HB_TAG('g','l','y','f'),
NULL, NULL));
hb_subset_depend_destroy (depend);
hb_face_destroy (face);
}
/* Test CFF SEAC (Standard Encoding Accented Character) dependency extraction */
static void
test_depend_cff (void)
{
/* cff1_seac.C0.otf has a SEAC composite glyph */
hb_face_t *face = hb_test_open_font_file ("fonts/cff1_seac.C0.otf");
hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face);
g_assert_nonnull (depend);
/* Glyph 2 is a SEAC composite defined as base (glyph 1) + accent (glyph 3).
* The composite glyph depends on both components. */
g_test_message ("Testing CFF SEAC composite: glyph 2 → base (1)");
g_assert_true (find_dependency (depend, 2, 1, HB_TAG('C','F','F',' '),
NULL, NULL));
g_test_message ("Testing CFF SEAC composite: glyph 2 → accent (3)");
g_assert_true (find_dependency (depend, 2, 3, HB_TAG('C','F','F',' '),
NULL, NULL));
hb_subset_depend_destroy (depend);
hb_face_destroy (face);
}
/* Test COLR (color layering) dependency extraction */
static void
test_depend_colr (void)
{
/* COLRv0: base glyph with layer glyphs */
hb_face_t *face = hb_test_open_font_file ("fonts/COLRv0.extents.ttf");
hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face);
g_assert_nonnull (depend);
g_test_message ("Testing COLRv0: glyph00013 (13) → glyph00010 (10)");
g_assert_true (find_dependency (depend, 13, 10, HB_TAG('C','O','L','R'), NULL, NULL));
hb_subset_depend_destroy (depend);
hb_face_destroy (face);
/* COLRv1: Paint graph dependencies */
face = hb_test_open_font_file ("fonts/test_glyphs-glyf_colr_1.ttf");
depend = hb_subset_depend_from_face_or_fail (face);
g_assert_nonnull (depend);
g_test_message ("Testing COLRv1 v0 layer: glyph 168 → glyph 176");
g_assert_true (find_dependency (depend, 168, 176, HB_TAG('C','O','L','R'), NULL, NULL));
g_test_message ("Testing COLRv1 PaintGlyph Format 10: glyph 12 → glyph 176");
g_assert_true (find_dependency (depend, 12, 176, HB_TAG('C','O','L','R'), NULL, NULL));
g_test_message ("Testing COLRv1 PaintGlyph self-reference filtered: glyph 8 should NOT depend on itself");
g_assert_false (find_dependency (depend, 8, 8, HB_TAG('C','O','L','R'), NULL, NULL));
hb_subset_depend_destroy (depend);
hb_face_destroy (face);
}
/* Test MATH (mathematical variants) dependency extraction */
static void
test_depend_math (void)
{
/* MathTestFontFull.otf has full MATH table with variants and assemblies */
hb_face_t *face = hb_test_open_font_file ("fonts/MathTestFontFull.otf");
hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face);
g_assert_nonnull (depend);
/* Base glyph "arrowup" (31) has:
* - Size variants: uni2191_size2 (68), uni2191_size3 (69), etc.
* - Glyph assembly parts: bottom (62), vertical (64)
*/
g_test_message ("Testing MATH size variant: arrowup (31) → uni2191_size2 (68)");
g_assert_true (find_dependency (depend, 31, 68, HB_TAG('M','A','T','H'),
NULL, NULL));
g_test_message ("Testing MATH assembly part: arrowup (31) → bottom (62)");
g_assert_true (find_dependency (depend, 31, 62, HB_TAG('M','A','T','H'),
NULL, NULL));
g_test_message ("Testing MATH assembly part: arrowup (31) → vertical (64)");
g_assert_true (find_dependency (depend, 31, 64, HB_TAG('M','A','T','H'),
NULL, NULL));
hb_subset_depend_destroy (depend);
hb_face_destroy (face);
}
/* Test GSUB dependency extraction for all substitution formats */
static void
test_depend_gsub_formats (void)
{
hb_tag_t layout_tag = HB_TAG_NONE;
hb_codepoint_t ligature_set;
/* ===== BASIC SUBSTITUTION TYPES ===== */
/* SourceSansPro-Regular.otf */
hb_face_t *face_source = hb_test_open_font_file ("fonts/SourceSansPro-Regular.otf");
hb_subset_depend_t *depend_source = hb_subset_depend_from_face_or_fail (face_source);
g_assert_nonnull (depend_source);
/* AlternateSubst: A → A.sc (tests AlternateSet with multiple alternates) */
g_test_message ("Testing AlternateSubst: A (2) → A.sc (1217)");
g_assert_true (find_dependency (depend_source, 2, 1217, HB_OT_TAG_GSUB,
NULL, NULL));
/* LigatureSubst: multiple glyphs to one (tests Ligature and LigatureSet) */
g_test_message ("Testing LigatureSubst: uni0302 (1829) → uni03020300 (1910)");
g_assert_true (find_dependency (depend_source, 1829, 1910, HB_OT_TAG_GSUB,
&layout_tag, &ligature_set));
g_test_message (" ligature_set = %u", ligature_set);
/* Verify ligature set contains both components (1823, 1829) */
g_assert_cmpuint (HB_CODEPOINT_INVALID, !=, ligature_set);
hb_set_t *lig_set_source = hb_set_create ();
g_assert_true (hb_subset_depend_lookup_set (depend_source, ligature_set, lig_set_source));
g_assert_true (hb_set_has (lig_set_source, 1823)); /* First ligature component */
g_assert_true (hb_set_has (lig_set_source, 1829)); /* Second ligature component (uni0302) */
hb_set_destroy (lig_set_source);
hb_subset_depend_destroy (depend_source);
hb_face_destroy (face_source);
/* Qahiri-Regular.ttf */
hb_face_t *face_qahiri = hb_test_open_font_file ("fonts/Qahiri-Regular.ttf");
hb_subset_depend_t *depend_qahiri = hb_subset_depend_from_face_or_fail (face_qahiri);
g_assert_nonnull (depend_qahiri);
/* ReverseChainSingleSubst */
g_test_message ("Testing ReverseChainSingleSubst: glyph00047 (47) → glyph00463 (463)");
g_assert_true (find_dependency (depend_qahiri, 47, 463, HB_OT_TAG_GSUB,
NULL, NULL));
/* ===== CONTEXTUAL SUBSTITUTION FORMATS (with recursion) ===== */
/* ContextSubst Format 3 → SingleSubst
* Lookup 31: Format 3 context invokes Lookup 1 (SingleSubst) */
g_test_message ("Testing ContextSubst Format 3 → SingleSubst: glyph00053 (53) → glyph00052 (52)");
g_assert_true (find_dependency (depend_qahiri, 53, 52, HB_OT_TAG_GSUB,
NULL, NULL));
/* ChainContextSubst Format 1 → SingleSubst
* Lookup 11: Format 1 chain context invokes Lookup 2 (SingleSubst) twice */
g_test_message ("Testing ChainContextSubst Format 1 → SingleSubst: glyph00159 (159) → glyph00162 (162)");
g_assert_true (find_dependency (depend_qahiri, 159, 162, HB_OT_TAG_GSUB,
NULL, NULL));
/* Verify second call to same lookup (Bug regression test)
* Same Lookup 11 calls Lookup 2 AGAIN at SequenceIndex=1 for glyph 154
* This catches Bug #2: lookups_seen blocking repeated lookup calls */
g_test_message ("Testing same lookup called twice (Bug regression): glyph00154 (154) → glyph00155 (155)");
g_assert_true (find_dependency (depend_qahiri, 154, 155, HB_OT_TAG_GSUB,
NULL, NULL));
/* Lookup 28: Multiple subtables with nested lookup calls (Bug regression tests)
* This lookup has 3 subtables:
* - Subtable 0: Has 4 inputs, SubstCount=0 (no nested lookups)
* - Subtable 1: Has 2 inputs, calls Lookup 3 twice
* - Subtable 2: Additional rules
* Tests both bugs:
* - Bug #1: Must process all subtables (not stop after first)
* - Bug #2: Must allow same lookup called multiple times */
/* Lookup 28 Subtable 1 calls Lookup 3 at SequenceIndex=0 for glyph 47 */
g_test_message ("Testing multiple subtables (Bug regression): glyph00047 (47) → glyph00051 (51) via Lookup 28→3");
g_assert_true (find_dependency (depend_qahiri, 47, 51, HB_OT_TAG_GSUB,
NULL, NULL));
/* Lookup 28 Subtable 1 calls Lookup 3 AGAIN at SequenceIndex=1 for glyph 212 */
g_test_message ("Testing same lookup twice in context (Bug regression): glyph00212 (212) → glyph00220 (220) via Lookup 28→3");
g_assert_true (find_dependency (depend_qahiri, 212, 220, HB_OT_TAG_GSUB,
NULL, NULL));
hb_subset_depend_destroy (depend_qahiri);
hb_face_destroy (face_qahiri);
/* NotoSans-Bold.ttf has ContextSubst Format 1, Format 2, and ChainContextSubst Format 2 */
hb_face_t *face_notosans = hb_test_open_font_file ("fonts/NotoSans-Bold.ttf");
hb_subset_depend_t *depend_notosans = hb_subset_depend_from_face_or_fail (face_notosans);
g_assert_nonnull (depend_notosans);
/* ContextSubst Format 1 → LigatureSubst
* Lookup 88: Format 1 context invokes Lookup 106 (LigatureSubst)
* When uni092C is followed by uni094D, creates ligature baprehalfdeva */
g_test_message ("Testing ContextSubst Format 1 → LigatureSubst: uni092C (3893) → baprehalfdeva (4156)");
layout_tag = HB_TAG('h','a','l','f');
g_assert_true (find_dependency (depend_notosans, 3893, 4156, HB_OT_TAG_GSUB,
&layout_tag, &ligature_set));
/* REGRESSION TEST: Sequential accumulation for intermediate glyphs
*
* Test case: NotoSans-Bold Devanagari contextual substitution
* - Glyph 3948: uni093C (nukta combining mark)
* - Glyph 3969: uni0944 (vowel sign)
* - Glyph 4017: rrvocalicvowelsignnuktaleftdeva (ligature output)
*
* Contextual rule flow:
* 1. Match pattern: [nukta-consonant] + [vowel-sign-uni0944]
* 2. Apply Lookup A at position 0: Decompose nukta-consonant → base + uni093C
* 3. Apply Lookup B at position 1: Ligate uni093C + uni0944 → glyph 4017
*
* The bug (before fix):
* - Lookup B only saw glyphs from input coverage
* - uni093C (3948) is an intermediate output, not in original coverage
* - Edge 3948 → 4017 was MISSING from dependency graph
*
* The fix (sequential accumulation):
* - Process lookups in order, accumulating outputs
* - Lookup B now sees accumulated glyphs including 3948
* - Edge 3948 → 4017 is correctly recorded
*
* This edge is critical: if a user subsets glyph 3948, they need 4017 too. */
g_test_message ("Testing sequential accumulation: uni093C (3948) → rrvocalicvowelsignnuktaleftdeva (4017)");
layout_tag = HB_TAG('b','l','w','s');
g_assert_true (find_dependency (depend_notosans, 3948, 4017, HB_OT_TAG_GSUB,
&layout_tag, &ligature_set));
g_test_message (" ligature_set = %u", ligature_set);
/* Verify ligature set contains the other component glyph (3969) */
g_assert_cmpuint (HB_CODEPOINT_INVALID, !=, ligature_set);
hb_set_t *lig_set = hb_set_create ();
g_assert_true (hb_subset_depend_lookup_set (depend_notosans, ligature_set, lig_set));
g_assert_true (hb_set_has (lig_set, 3969)); /* uni0944 is the other ligature component */
hb_set_destroy (lig_set);
hb_subset_depend_destroy (depend_notosans);
hb_face_destroy (face_notosans);
/* ContextFormat2-Depend-Test.ttf - Test font for contextual substitution formats
* Created from NotoNastaliqUrdu-Regular.ttf (486KB → 85KB, 83% reduction)
* - GSUB table minimized to 6 lookups needed for tests
* - All glyph outline data removed (only table structures kept)
* Test lookups:
* - Lookup 9, 10: ContextSubst for contextual filtering test
* - Lookup 17, 20: ContextSubst Format 2 → MultipleSubst test
* - Lookup 25, 119: ChainContextSubst Format 2 → SingleSubst test
* Test runtime: 4+ seconds → 0.02 seconds (200× faster) */
hb_face_t *face_context = hb_test_open_font_file ("fonts/ContextFormat2-Depend-Test.ttf");
hb_subset_depend_t *depend_context = hb_subset_depend_from_face_or_fail (face_context);
g_assert_nonnull (depend_context);
/* ContextSubst Format 2 → MultipleSubst
* Lookup 20: Format 2 class-based context invokes Lookup 17 (MultipleSubst) */
g_test_message ("Testing ContextSubst Format 2 → MultipleSubst: YehBarreeFin (252) → YehBarreeFin_3 (255)");
g_assert_true (find_dependency (depend_context, 252, 255, HB_OT_TAG_GSUB,
NULL, NULL));
/* ChainContextSubst Format 2 → SingleSubst
* Lookup 25: Format 2 class-based chain context with lookahead invokes Lookup 119 (SingleSubst)
* When BehxIni (class 2) is followed by AlefFin (class 1), BehxIni → BehxIni.A */
g_test_message ("Testing ChainContextSubst Format 2 → SingleSubst: BehxIni (281) → BehxIni.A (282)");
g_assert_true (find_dependency (depend_context, 281, 282, HB_OT_TAG_GSUB,
NULL, NULL));
/* ADDITIONAL REGRESSION TESTS
*
* The sequential accumulation and nested context bug fixes are also
* comprehensively tested by the depend fuzzer with these seeds:
* - 0x4388c9ba4973f4f9 (sequential accumulation bug)
* - 0x5a50e64a753f3298 (nested context bug)
*
* The fuzzer verifies depend closure matches subset closure across
* 1024 randomized test cases per font.
*/
/* Negative test: verify contextual filtering (issue #3397)
* Lookup 10 covers {AlefFin, LamIni, LamMed} but Lookup 9's SubRuleSet for LamIni
* only has LamIni in position 0. LamMed→LamFin.LA is unreachable from this context. */
g_test_message ("Testing contextual filtering: LamIni (416) should NOT depend on LamFin.LA (268)");
g_assert_false (find_dependency (depend_context, 416, 268, HB_OT_TAG_GSUB,
NULL, NULL));
hb_subset_depend_destroy (depend_context);
hb_face_destroy (face_context);
/* ChainContextSubst Format 3 → MultipleSubst + feature tagging */
face_source = hb_test_open_font_file ("fonts/SourceSansPro-Regular.otf");
depend_source = hb_subset_depend_from_face_or_fail (face_source);
g_assert_nonnull (depend_source);
/* Lookup 8: Format 3 chain context invokes Lookup 7 (MultipleSubst)
* Also verifies contextual recursion propagates feature tags */
g_test_message ("Testing ChainContextSubst Format 3 → MultipleSubst: Ecircumflex (92) → E (6) with ccmp");
layout_tag = HB_TAG('c','c','m','p');
g_assert_true (find_dependency (depend_source, 92, 6, HB_OT_TAG_GSUB,
&layout_tag, NULL));
hb_subset_depend_destroy (depend_source);
hb_face_destroy (face_source);
}
int
main (int argc, char **argv)
{
hb_test_init (&argc, &argv);
hb_test_add (test_depend_glyf);
hb_test_add (test_depend_cff);
hb_test_add (test_depend_colr);
hb_test_add (test_depend_math);
hb_test_add (test_depend_gsub_formats);
return hb_test_run ();
}
#else
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_skip ("HB_DEPEND_API not enabled");
return 0;
}
#endif