Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
27 / 30
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextShaper
90.00% covered (success)
90.00%
27 / 30
0.00% covered (danger)
0.00%
0 / 1
11.12
0.00% covered (danger)
0.00%
0 / 1
 applyLigatures
90.00% covered (success)
90.00%
27 / 30
0.00% covered (danger)
0.00%
0 / 1
11.12
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\FontParser;
6
7/**
8 * Simple text shaper for Latin ligature substitution.
9 *
10 * Given a GID sequence, applies GSUB ligature rules to produce
11 * the shaped GID sequence. This is NOT a full shaping engine —
12 * it handles standard Latin ligatures (fi, fl, ffi, ffl, etc.)
13 * but does not support Arabic joining, Indic reordering, mark
14 * positioning, or other complex script features.
15 */
16final class TextShaper
17{
18    /**
19     * Apply ligature substitutions to a GID sequence.
20     *
21     * @param int[] $gids Input glyph IDs
22     * @param array<int, list<array{components: int[], ligature: int}>> $ligatures
23     *     Ligature rules from GsubParser, keyed by first GID
24     * @return int[] Shaped glyph IDs (may be shorter if ligatures were applied)
25     */
26    public static function applyLigatures(array $gids, array $ligatures): array
27    {
28        if ($gids === [] || $ligatures === []) {
29            return $gids;
30        }
31
32        $result = [];
33        $i = 0;
34        $len = count($gids);
35
36        while ($i < $len) {
37            $gid = $gids[$i];
38
39            if (isset($ligatures[$gid])) {
40                $matched = false;
41
42                // Try each ligature rule (sorted longest-first by GsubParser)
43                foreach ($ligatures[$gid] as $rule) {
44                    $components = $rule['components'];
45                    $compLen = count($components);
46
47                    // Check if enough glyphs remain
48                    if ($i + $compLen >= $len) {
49                        continue;
50                    }
51
52                    // Check if component GIDs match
53                    $match = true;
54                    for ($j = 0; $j < $compLen; $j++) {
55                        if ($gids[$i + 1 + $j] !== $components[$j]) {
56                            $match = false;
57                            break;
58                        }
59                    }
60
61                    if ($match) {
62                        $result[] = $rule['ligature'];
63                        $i += 1 + $compLen; // skip first glyph + all components
64                        $matched = true;
65                        break;
66                    }
67                }
68
69                if (!$matched) {
70                    $result[] = $gid;
71                    $i++;
72                }
73            } else {
74                $result[] = $gid;
75                $i++;
76            }
77        }
78
79        return $result;
80    }
81}