File: Substitution.py

package info (click to toggle)
python-fontfeatures 1.9.0%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,096 kB
  • sloc: python: 9,112; makefile: 22
file content (212 lines) | stat: -rw-r--r-- 7,375 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# Code for converting a Substitution object into feaLib statements
import fontTools.feaLib.ast as feaast
from fontFeatures.ttLib.Substitution import lookup_type
from itertools import cycle
from typing import Union


def glyphref(g) -> Union[feaast.GlyphName, feaast.GlyphClass]:
    if len(g) == 1:
        return feaast.GlyphName(g[0])
    return feaast.GlyphClass([feaast.GlyphName(x) for x in g])


# Whether or not the input or the replacement has classes (so, entire statement).
def has_classes(self) -> bool:
    input_has_class = next(
        (i for i, v in enumerate([len(x) for x in self.input]) if v > 1), None
    )
    replacement_has_class = next(
        (i for i, v in enumerate([len(x) for x in self.replacement]) if v > 1), None
    )

    return input_has_class is not None or replacement_has_class is not None


# Excepting single glyphs, if classes are defined, the input and replacement
# all use classes of equal length.
def all_classes_equal(self) -> bool:
    input_lengths = [len(x) for x in self.input if len(x) != 1]
    replacement_lengths = [len(x) for x in self.replacement if len(x) != 1]

    if len(input_lengths) == 0 and len(replacement_lengths) == 0:
        return True

    return len(set(input_lengths + replacement_lengths)) == 1


# One of the substitution/replacements has all-but-one arity one,
# and both arities are the same
def is_paired(self) -> bool:
    input_lengths = [len(x) for x in self.input if len(x) != 1]
    replacement_lengths = [len(x) for x in self.replacement if len(x) != 1]
    if not (len(input_lengths) == 1 and len(replacement_lengths) == 1):
        return False
    if input_lengths[0] != replacement_lengths[0]:
        import warnings

        warnings.warn("Unbalanced paired substitution")
        return False
    return True


# Expand ligature substitutions ending in a class, such that:
#   * Substitute [f f.ss01] i -> [f_i f_i.ss01];
# Expands to:
#   * sub f i -> f_i;
#   * sub f.ss01 i -> f_i.ss01;
# Likewise:
#   * Substitute [f f.ss01] [i i.ss01] -> [f_i f_i.ss01];
# Expands to:
#   * sub f i -> f_i;
#   * sub f.ss01 i.ss01 -> f_i.ss01;
def paired_ligature(self) -> feaast.LigatureSubstStatement:
    b = feaast.Block()
    inputs = []
    for i in self.input:
        if len(i) == 1:
            inputs.append(cycle(i))
        else:
            inputs.append(i)
    lhs = zip(*inputs)
    replacements = []
    for j in self.replacement:
        if len(j) == 1:
            replacements.append(cycle(j))
        else:
            replacements.append(j)
    rhs = zip(*replacements)

    for l, r in zip(lhs, rhs):
        stmt = feaast.LigatureSubstStatement(
            [glyphref(x) for x in self.precontext],
            [glyphref([x]) for x in l],
            [glyphref(x) for x in self.postcontext],
            glyphref([r[0]]),
            False,
        )
        b.statements.append(stmt)
    return b


# Expand multiple substitutions, such that:
#   * Substitute [a b] -> before_tail [a.2 b.2] tail;
# Becomes in FEA:
#   * sub a by before_tail a.2 tail;
#   * sub b by before_tail b.2 tail;
def paired_mult(self) -> feaast.MultipleSubstStatement:
    b = feaast.Block()

    input_lengths = [len(x) for x in self.input]
    replacement_lengths = [len(x) for x in self.replacement]

    if len(input_lengths) != 1:
        raise ValueError(
            "Multiple substitution only valid on input of length one, use a Chain instead"
        )

    input_length = input_lengths[0]

    if sum([l for l in replacement_lengths if l == 1]) not in [
        len(replacement_lengths),
        len(replacement_lengths) - 1,
    ]:
        raise ValueError(
            "Cannot expand multiple glyph classes in a multiple substitution — creates ambiguity"
        )

    # Look for the glyph class in the replacement, or default to first glyph in replacement
    glyphcls = next((i for i, v in enumerate(self.replacement) if len(v) > 1), 0)

    if input_length != len(self.replacement[glyphcls]):
        raise ValueError(
            "Glyph class in input must be same length as that in replacement. {} != {}".format(
                input_length, len(self.replacement[glyphcls])
            )
        )

    zipped = zip(self.input[0], self.replacement[glyphcls])

    prior_reps = self.replacement[:glyphcls]
    after_reps = self.replacement[glyphcls + 1 :]

    for f, t in zipped:
        stmt = feaast.MultipleSubstStatement(
            [glyphref(x) for x in self.precontext],
            glyphref([f]),
            [glyphref(x) for x in self.postcontext],
            [glyphref(g) for g in prior_reps + [[t]] + after_reps],
        )
        b.statements.append(stmt)

    return b


def asFeaAST(self):
    lut = lookup_type(self, forFea=True)
    if not lut:
        return feaast.Comment("")

    if not self.replacement:  # Delete
        return feaast.MultipleSubstStatement(
            [glyphref(x) for x in self.precontext],
            glyphref(self.input[0]),
            [glyphref(x) for x in self.postcontext],
            [],
        )

    if lut == 1:  # GSUB 1 Single Substitution
        return feaast.SingleSubstStatement(
            [glyphref(x) for x in self.input],
            [glyphref(x) for x in self.replacement],
            [glyphref(x) for x in self.precontext],
            [glyphref(x) for x in self.postcontext],
            False,
        )
    elif lut == 2:  # GSUB 2 Multiple Substitution
        # Paired rules need to become a set of statements
        if is_paired(self):
            return paired_mult(self)

        return feaast.MultipleSubstStatement(
            [glyphref(x) for x in self.precontext],
            glyphref(self.input[0]),
            [glyphref(x) for x in self.postcontext],
            [glyphref(x) for x in self.replacement],
        )
    elif lut == 3:  # GSUB 3 Alternate Substitution
        return feaast.AlternateSubstStatement(
            [glyphref(x) for x in self.precontext],
            glyphref(self.input[0]),
            [glyphref(x) for x in self.postcontext],
            feaast.GlyphClass([feaast.GlyphName(x) for x in self.replacement[0]]),
        )
    elif lut == 4:  # GSUB 4 Ligature Substitution
        # Some rules with classes need to become a set of statements.
        if (
            has_classes(self)
            and all_classes_equal(self)
            and not len(self.replacement[0]) == 1
        ):
            return paired_ligature(self)

        return feaast.LigatureSubstStatement(
            [glyphref(x) for x in self.precontext],
            [glyphref(x) for x in self.input],
            [glyphref(x) for x in self.postcontext],
            glyphref(self.replacement[0]),
            False,
        )
    elif lut in [5, 6, 7]:  # GSUB 5, 6, 7 Different types of contextual substitutions
        raise NotImplementedError("Use the Chain verb for this")
    elif lut == 8:  # GSUB 8 Reverse Chaining Single Substitution
        return feaast.ReverseChainSingleSubstStatement(
            [glyphref(x) for x in self.precontext],
            [glyphref(x) for x in self.postcontext],
            [glyphref(x) for x in self.input],
            [glyphref(self.replacement[0])],
        )
    elif lut >= 9:
        raise NotImplementedError("Invalid GSUB lookup type requested: {}".format(lut))

    raise ValueError("LookupType must be a single positive integer")