| /* | |
| * 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); | |
| } | |
| static void | |
| test_depend_varc (void) | |
| { | |
| hb_face_t *face = hb_test_open_font_file ("fonts/varc-ac00-ac01.ttf"); | |
| hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face); | |
| g_assert_nonnull (depend); | |
| /* uniAC00 (1) has two direct VARC components (3 and 5). */ | |
| g_assert_true (find_dependency (depend, 1, 3, HB_TAG ('V','A','R','C'), | |
| NULL, NULL)); | |
| g_assert_true (find_dependency (depend, 1, 5, HB_TAG ('V','A','R','C'), | |
| NULL, NULL)); | |
| hb_subset_depend_destroy (depend); | |
| hb_face_destroy (face); | |
| } | |
| 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_varc); | |
| 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 |