fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-musllinux_1_2_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fontTools/__init__.py +1 -1
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +65 -10
- fontTools/cffLib/__init__.py +61 -26
- fontTools/cffLib/specializer.py +4 -1
- fontTools/cffLib/transforms.py +11 -6
- fontTools/config/__init__.py +15 -0
- fontTools/cu2qu/cu2qu.c +6567 -5579
- fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/cu2qu/cu2qu.py +36 -4
- fontTools/cu2qu/ufo.py +14 -0
- fontTools/designspaceLib/__init__.py +8 -3
- fontTools/designspaceLib/statNames.py +14 -7
- fontTools/feaLib/ast.py +24 -15
- fontTools/feaLib/builder.py +139 -66
- fontTools/feaLib/error.py +1 -1
- fontTools/feaLib/lexer.c +7038 -7995
- fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/feaLib/parser.py +75 -40
- fontTools/feaLib/variableScalar.py +6 -1
- fontTools/fontBuilder.py +50 -44
- fontTools/merge/__init__.py +1 -1
- fontTools/merge/cmap.py +33 -1
- fontTools/merge/tables.py +12 -1
- fontTools/misc/bezierTools.c +14913 -17013
- fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/misc/bezierTools.py +4 -1
- fontTools/misc/configTools.py +3 -1
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +4 -27
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +1 -1
- fontTools/misc/loggingTools.py +1 -1
- fontTools/misc/psCharStrings.py +17 -2
- fontTools/misc/sstruct.py +2 -6
- fontTools/misc/symfont.py +6 -8
- fontTools/misc/testTools.py +5 -1
- fontTools/misc/textTools.py +4 -2
- fontTools/misc/visitor.py +32 -16
- fontTools/misc/xmlWriter.py +44 -8
- fontTools/mtiLib/__init__.py +1 -3
- fontTools/otlLib/builder.py +402 -155
- fontTools/otlLib/optimize/gpos.py +49 -63
- fontTools/pens/filterPen.py +218 -26
- fontTools/pens/momentsPen.c +5514 -5584
- fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/pens/pointPen.py +61 -18
- fontTools/pens/roundingPen.py +2 -2
- fontTools/pens/t2CharStringPen.py +31 -11
- fontTools/qu2cu/qu2cu.c +6581 -6168
- fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/subset/__init__.py +283 -25
- fontTools/subset/svg.py +2 -3
- fontTools/ttLib/__init__.py +4 -0
- fontTools/ttLib/__main__.py +47 -8
- fontTools/ttLib/removeOverlaps.py +7 -5
- fontTools/ttLib/reorderGlyphs.py +8 -7
- fontTools/ttLib/sfnt.py +11 -9
- fontTools/ttLib/tables/D__e_b_g.py +20 -2
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/S__i_l_f.py +2 -2
- fontTools/ttLib/tables/T_S_I__0.py +14 -3
- fontTools/ttLib/tables/T_S_I__1.py +2 -5
- fontTools/ttLib/tables/T_S_I__5.py +18 -7
- fontTools/ttLib/tables/__init__.py +1 -0
- fontTools/ttLib/tables/_a_v_a_r.py +12 -3
- fontTools/ttLib/tables/_c_m_a_p.py +20 -7
- fontTools/ttLib/tables/_c_v_t.py +3 -2
- fontTools/ttLib/tables/_f_p_g_m.py +3 -1
- fontTools/ttLib/tables/_g_l_y_f.py +45 -21
- fontTools/ttLib/tables/_g_v_a_r.py +67 -19
- fontTools/ttLib/tables/_h_d_m_x.py +4 -4
- fontTools/ttLib/tables/_h_m_t_x.py +7 -3
- fontTools/ttLib/tables/_l_o_c_a.py +2 -2
- fontTools/ttLib/tables/_n_a_m_e.py +11 -6
- fontTools/ttLib/tables/_p_o_s_t.py +9 -7
- fontTools/ttLib/tables/otBase.py +5 -12
- fontTools/ttLib/tables/otConverters.py +5 -2
- fontTools/ttLib/tables/otData.py +1 -1
- fontTools/ttLib/tables/otTables.py +33 -30
- fontTools/ttLib/tables/otTraverse.py +2 -1
- fontTools/ttLib/tables/sbixStrike.py +3 -3
- fontTools/ttLib/ttFont.py +666 -120
- fontTools/ttLib/ttGlyphSet.py +0 -10
- fontTools/ttLib/woff2.py +10 -13
- fontTools/ttx.py +13 -1
- fontTools/ufoLib/__init__.py +300 -202
- fontTools/ufoLib/converters.py +103 -30
- fontTools/ufoLib/errors.py +8 -0
- fontTools/ufoLib/etree.py +1 -1
- fontTools/ufoLib/filenames.py +171 -106
- fontTools/ufoLib/glifLib.py +303 -205
- fontTools/ufoLib/kerning.py +98 -48
- fontTools/ufoLib/utils.py +46 -15
- fontTools/ufoLib/validators.py +121 -99
- fontTools/unicodedata/Blocks.py +35 -20
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/ScriptExtensions.py +63 -37
- fontTools/unicodedata/Scripts.py +173 -152
- fontTools/unicodedata/__init__.py +10 -2
- fontTools/varLib/__init__.py +198 -109
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
- fontTools/varLib/avarPlanner.py +3 -999
- fontTools/varLib/featureVars.py +21 -7
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +180 -65
- fontTools/varLib/interpolatableHelpers.py +3 -0
- fontTools/varLib/iup.c +7564 -6903
- fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/varLib/models.py +17 -2
- fontTools/varLib/mutator.py +11 -0
- fontTools/varLib/varStore.py +10 -38
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +4 -0
- fontTools/voltLib/parser.py +16 -8
- fontTools/voltLib/voltToFea.py +347 -166
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
- fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
- {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
fontTools/voltLib/voltToFea.py
CHANGED
|
@@ -46,6 +46,7 @@ Limitations
|
|
|
46
46
|
import logging
|
|
47
47
|
import re
|
|
48
48
|
from io import StringIO
|
|
49
|
+
from graphlib import TopologicalSorter
|
|
49
50
|
|
|
50
51
|
from fontTools.feaLib import ast
|
|
51
52
|
from fontTools.ttLib import TTFont, TTLibError
|
|
@@ -57,32 +58,39 @@ log = logging.getLogger("fontTools.voltLib.voltToFea")
|
|
|
57
58
|
TABLES = ["GDEF", "GSUB", "GPOS"]
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
61
|
+
def _flatten_group(group):
|
|
62
|
+
ret = []
|
|
63
|
+
if isinstance(group, (tuple, list)):
|
|
64
|
+
for item in group:
|
|
65
|
+
ret.extend(_flatten_group(item))
|
|
66
|
+
elif hasattr(group, "enum"):
|
|
67
|
+
ret.extend(_flatten_group(group.enum))
|
|
68
|
+
else:
|
|
69
|
+
ret.append(group)
|
|
70
|
+
return ret
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Topologically sort of group definitions to ensure that all groups are defined
|
|
74
|
+
# before they are referenced. This is necessary because FEA requires it but
|
|
75
|
+
# VOLT does not, see below.
|
|
76
|
+
def sort_groups(groups):
|
|
77
|
+
group_map = {group.name.lower(): group for group in groups}
|
|
78
|
+
graph = {
|
|
79
|
+
group.name.lower(): [
|
|
80
|
+
x.group.lower()
|
|
81
|
+
for x in _flatten_group(group)
|
|
82
|
+
if isinstance(x, VAst.GroupName)
|
|
75
83
|
]
|
|
84
|
+
for group in groups
|
|
85
|
+
}
|
|
86
|
+
sorter = TopologicalSorter(graph)
|
|
87
|
+
return [group_map[name] for name in sorter.static_order()]
|
|
88
|
+
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return False
|
|
82
|
-
if self.groups and not other.groups:
|
|
83
|
-
return False
|
|
84
|
-
if not self.groups and other.groups:
|
|
85
|
-
return True
|
|
90
|
+
class Lookup(ast.LookupBlock):
|
|
91
|
+
def __init__(self, name, use_extension=False, location=None):
|
|
92
|
+
super().__init__(name, use_extension, location)
|
|
93
|
+
self.chained = []
|
|
86
94
|
|
|
87
95
|
|
|
88
96
|
class VoltToFea:
|
|
@@ -90,7 +98,10 @@ class VoltToFea:
|
|
|
90
98
|
_NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]")
|
|
91
99
|
|
|
92
100
|
def __init__(self, file_or_path, font=None):
|
|
93
|
-
|
|
101
|
+
if isinstance(file_or_path, VAst.VoltFile):
|
|
102
|
+
self._doc, self._file_or_path = file_or_path, None
|
|
103
|
+
else:
|
|
104
|
+
self._doc, self._file_or_path = None, file_or_path
|
|
94
105
|
self._font = font
|
|
95
106
|
|
|
96
107
|
self._glyph_map = {}
|
|
@@ -128,23 +139,26 @@ class VoltToFea:
|
|
|
128
139
|
self._class_names[name] = res
|
|
129
140
|
return self._class_names[name]
|
|
130
141
|
|
|
131
|
-
def _collectStatements(self, doc, tables):
|
|
142
|
+
def _collectStatements(self, doc, tables, ignore_unsupported_settings=False):
|
|
143
|
+
# Collect glyph difinitions first, as we need them to map VOLT glyph names to font glyph name.
|
|
144
|
+
for statement in doc.statements:
|
|
145
|
+
if isinstance(statement, VAst.GlyphDefinition):
|
|
146
|
+
self._glyphDefinition(statement)
|
|
147
|
+
|
|
132
148
|
# Collect and sort group definitions first, to make sure a group
|
|
133
149
|
# definition that references other groups comes after them since VOLT
|
|
134
150
|
# does not enforce such ordering, and feature file require it.
|
|
135
151
|
groups = [s for s in doc.statements if isinstance(s, VAst.GroupDefinition)]
|
|
136
|
-
for
|
|
137
|
-
self._groupDefinition(
|
|
152
|
+
for group in sort_groups(groups):
|
|
153
|
+
self._groupDefinition(group)
|
|
138
154
|
|
|
139
155
|
for statement in doc.statements:
|
|
140
|
-
if isinstance(statement, VAst.
|
|
141
|
-
self._glyphDefinition(statement)
|
|
142
|
-
elif isinstance(statement, VAst.AnchorDefinition):
|
|
156
|
+
if isinstance(statement, VAst.AnchorDefinition):
|
|
143
157
|
if "GPOS" in tables:
|
|
144
158
|
self._anchorDefinition(statement)
|
|
145
159
|
elif isinstance(statement, VAst.SettingDefinition):
|
|
146
|
-
self._settingDefinition(statement)
|
|
147
|
-
elif isinstance(statement, VAst.GroupDefinition):
|
|
160
|
+
self._settingDefinition(statement, ignore_unsupported_settings)
|
|
161
|
+
elif isinstance(statement, (VAst.GlyphDefinition, VAst.GroupDefinition)):
|
|
148
162
|
pass # Handled above
|
|
149
163
|
elif isinstance(statement, VAst.ScriptDefinition):
|
|
150
164
|
self._scriptDefinition(statement)
|
|
@@ -176,35 +190,57 @@ class VoltToFea:
|
|
|
176
190
|
if self._lookups:
|
|
177
191
|
statements.append(ast.Comment("\n# Lookups"))
|
|
178
192
|
for lookup in self._lookups.values():
|
|
179
|
-
statements.extend(
|
|
193
|
+
statements.extend(lookup.chained)
|
|
180
194
|
statements.append(lookup)
|
|
181
195
|
|
|
182
196
|
# Prune features
|
|
183
197
|
features = self._features.copy()
|
|
184
|
-
for
|
|
185
|
-
scripts = features[
|
|
186
|
-
for
|
|
187
|
-
langs = scripts[
|
|
188
|
-
for
|
|
189
|
-
langs[
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
for feature_tag in features:
|
|
199
|
+
scripts = features[feature_tag]
|
|
200
|
+
for script_tag in scripts:
|
|
201
|
+
langs = scripts[script_tag]
|
|
202
|
+
for language_tag in langs:
|
|
203
|
+
langs[language_tag] = [
|
|
204
|
+
l for l in langs[language_tag] if l.lower() in self._lookups
|
|
205
|
+
]
|
|
206
|
+
scripts[script_tag] = {t: l for t, l in langs.items() if l}
|
|
207
|
+
features[feature_tag] = {t: s for t, s in scripts.items() if s}
|
|
192
208
|
features = {t: f for t, f in features.items() if f}
|
|
193
209
|
|
|
194
210
|
if features:
|
|
195
211
|
statements.append(ast.Comment("# Features"))
|
|
196
|
-
for
|
|
197
|
-
feature = ast.FeatureBlock(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
for feature_tag, scripts in features.items():
|
|
213
|
+
feature = ast.FeatureBlock(feature_tag)
|
|
214
|
+
script_tags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1)
|
|
215
|
+
if feature_tag == "aalt" and len(script_tags) > 1:
|
|
216
|
+
log.warning(
|
|
217
|
+
"FEA syntax does not allow script statements in 'aalt' feature, "
|
|
218
|
+
"so only lookups from the first script will be included."
|
|
219
|
+
)
|
|
220
|
+
script_tags = script_tags[:1]
|
|
221
|
+
for script_tag in script_tags:
|
|
222
|
+
if feature_tag != "aalt":
|
|
223
|
+
feature.statements.append(ast.ScriptStatement(script_tag))
|
|
224
|
+
language_tags = sorted(
|
|
225
|
+
scripts[script_tag],
|
|
226
|
+
key=lambda k: 0 if k == "dflt" else 1,
|
|
227
|
+
)
|
|
228
|
+
if feature_tag == "aalt" and len(language_tags) > 1:
|
|
229
|
+
log.warning(
|
|
230
|
+
"FEA syntax does not allow language statements in 'aalt' feature, "
|
|
231
|
+
"so only lookups from the first language will be included."
|
|
206
232
|
)
|
|
207
|
-
|
|
233
|
+
language_tags = language_tags[:1]
|
|
234
|
+
for language_tag in language_tags:
|
|
235
|
+
if feature_tag != "aalt":
|
|
236
|
+
include_default = True if language_tag == "dflt" else False
|
|
237
|
+
feature.statements.append(
|
|
238
|
+
ast.LanguageStatement(
|
|
239
|
+
language_tag.ljust(4),
|
|
240
|
+
include_default=include_default,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
for name in scripts[script_tag][language_tag]:
|
|
208
244
|
lookup = self._lookups[name.lower()]
|
|
209
245
|
lookupref = ast.LookupReferenceStatement(lookup)
|
|
210
246
|
feature.statements.append(lookupref)
|
|
@@ -227,15 +263,17 @@ class VoltToFea:
|
|
|
227
263
|
|
|
228
264
|
return doc
|
|
229
265
|
|
|
230
|
-
def convert(self, tables=None):
|
|
231
|
-
|
|
266
|
+
def convert(self, tables=None, ignore_unsupported_settings=False):
|
|
267
|
+
if self._doc is None:
|
|
268
|
+
self._doc = VoltParser(self._file_or_path).parse()
|
|
269
|
+
doc = self._doc
|
|
232
270
|
|
|
233
271
|
if tables is None:
|
|
234
272
|
tables = TABLES
|
|
235
273
|
if self._font is not None:
|
|
236
274
|
self._glyph_order = self._font.getGlyphOrder()
|
|
237
275
|
|
|
238
|
-
self._collectStatements(doc, tables)
|
|
276
|
+
self._collectStatements(doc, tables, ignore_unsupported_settings)
|
|
239
277
|
fea = self._buildFeatureFile(tables)
|
|
240
278
|
return fea.asFea()
|
|
241
279
|
|
|
@@ -253,7 +291,13 @@ class VoltToFea:
|
|
|
253
291
|
name = group
|
|
254
292
|
return ast.GlyphClassName(self._glyphclasses[name.lower()])
|
|
255
293
|
|
|
256
|
-
def
|
|
294
|
+
def _glyphSet(self, item):
|
|
295
|
+
return [
|
|
296
|
+
(self._glyphName(x) if isinstance(x, (str, VAst.GlyphName)) else x)
|
|
297
|
+
for x in item.glyphSet()
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
def _coverage(self, coverage, flatten=False):
|
|
257
301
|
items = []
|
|
258
302
|
for item in coverage:
|
|
259
303
|
if isinstance(item, VAst.GlyphName):
|
|
@@ -261,31 +305,38 @@ class VoltToFea:
|
|
|
261
305
|
elif isinstance(item, VAst.GroupName):
|
|
262
306
|
items.append(self._groupName(item))
|
|
263
307
|
elif isinstance(item, VAst.Enum):
|
|
264
|
-
|
|
308
|
+
item = self._coverage(item.enum, flatten=True)
|
|
309
|
+
if flatten:
|
|
310
|
+
items.extend(item)
|
|
311
|
+
else:
|
|
312
|
+
items.append(ast.GlyphClass(item))
|
|
265
313
|
elif isinstance(item, VAst.Range):
|
|
266
|
-
|
|
314
|
+
item = self._glyphSet(item)
|
|
315
|
+
if flatten:
|
|
316
|
+
items.extend(item)
|
|
317
|
+
else:
|
|
318
|
+
items.append(ast.GlyphClass(item))
|
|
267
319
|
else:
|
|
268
320
|
raise NotImplementedError(item)
|
|
269
321
|
return items
|
|
270
322
|
|
|
271
|
-
def _enum(self, enum):
|
|
272
|
-
return ast.GlyphClass(self._coverage(enum.enum))
|
|
273
|
-
|
|
274
323
|
def _context(self, context):
|
|
275
324
|
out = []
|
|
276
325
|
for item in context:
|
|
277
|
-
coverage = self._coverage(item)
|
|
278
|
-
if
|
|
279
|
-
coverage =
|
|
280
|
-
|
|
326
|
+
coverage = self._coverage(item, flatten=True)
|
|
327
|
+
if len(coverage) > 1:
|
|
328
|
+
coverage = ast.GlyphClass(coverage)
|
|
329
|
+
else:
|
|
330
|
+
coverage = coverage[0]
|
|
331
|
+
out.append(coverage)
|
|
281
332
|
return out
|
|
282
333
|
|
|
283
334
|
def _groupDefinition(self, group):
|
|
284
335
|
name = self._className(group.name)
|
|
285
|
-
glyphs = self.
|
|
286
|
-
glyphclass = ast.
|
|
287
|
-
|
|
288
|
-
self._glyphclasses[group.name.lower()] =
|
|
336
|
+
glyphs = self._coverage(group.enum.enum, flatten=True)
|
|
337
|
+
glyphclass = ast.GlyphClass(glyphs)
|
|
338
|
+
classdef = ast.GlyphClassDefinition(name, glyphclass)
|
|
339
|
+
self._glyphclasses[group.name.lower()] = classdef
|
|
289
340
|
|
|
290
341
|
def _glyphDefinition(self, glyph):
|
|
291
342
|
try:
|
|
@@ -317,10 +368,10 @@ class VoltToFea:
|
|
|
317
368
|
assert ltag not in self._features[ftag][stag]
|
|
318
369
|
self._features[ftag][stag][ltag] = lookups.keys()
|
|
319
370
|
|
|
320
|
-
def _settingDefinition(self, setting):
|
|
371
|
+
def _settingDefinition(self, setting, ignore_unsupported=False):
|
|
321
372
|
if setting.name.startswith("COMPILER_"):
|
|
322
373
|
self._settings[setting.name] = setting.value
|
|
323
|
-
|
|
374
|
+
elif not ignore_unsupported:
|
|
324
375
|
log.warning(f"Unsupported setting ignored: {setting.name}")
|
|
325
376
|
|
|
326
377
|
def _adjustment(self, adjustment):
|
|
@@ -358,18 +409,15 @@ class VoltToFea:
|
|
|
358
409
|
glyphname = anchordef.glyph_name
|
|
359
410
|
anchor = self._anchor(anchordef.pos)
|
|
360
411
|
|
|
412
|
+
if glyphname not in self._anchors:
|
|
413
|
+
self._anchors[glyphname] = {}
|
|
361
414
|
if anchorname.startswith("MARK_"):
|
|
362
|
-
|
|
363
|
-
markclass = ast.MarkClass(self._className(name))
|
|
364
|
-
glyph = self._glyphName(glyphname)
|
|
365
|
-
markdef = MarkClassDefinition(markclass, anchor, glyph)
|
|
366
|
-
self._markclasses[(glyphname, anchorname)] = markdef
|
|
415
|
+
anchorname = anchorname[:5] + anchorname[5:].lower()
|
|
367
416
|
else:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
self._anchors[glyphname][anchorname][anchordef.component] = anchor
|
|
417
|
+
anchorname = anchorname.lower()
|
|
418
|
+
if anchorname not in self._anchors[glyphname]:
|
|
419
|
+
self._anchors[glyphname][anchorname] = {}
|
|
420
|
+
self._anchors[glyphname][anchorname][anchordef.component] = anchor
|
|
373
421
|
|
|
374
422
|
def _gposLookup(self, lookup, fealookup):
|
|
375
423
|
statements = fealookup.statements
|
|
@@ -408,43 +456,66 @@ class VoltToFea:
|
|
|
408
456
|
)
|
|
409
457
|
elif isinstance(pos, VAst.PositionAttachDefinition):
|
|
410
458
|
anchors = {}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
459
|
+
allmarks = set()
|
|
460
|
+
for coverage, anchorname in pos.coverage_to:
|
|
461
|
+
# In feature files mark classes are global, but in VOLT they
|
|
462
|
+
# are defined per-lookup. If we output mark class definitions
|
|
463
|
+
# for all marks that use a given anchor, we might end up with a
|
|
464
|
+
# mark used in two different classes in the same lookup, which
|
|
465
|
+
# is causes feature file compilation error.
|
|
466
|
+
# At the expense of uglier feature code, we make the mark class
|
|
467
|
+
# name by appending the current lookup name not the anchor
|
|
468
|
+
# name, and output mark class definitions only for marks used
|
|
469
|
+
# in this lookup.
|
|
470
|
+
classname = self._className(f"{anchorname}.{lookup.name}")
|
|
471
|
+
markclass = ast.MarkClass(classname)
|
|
472
|
+
|
|
473
|
+
# Anchor names are case-insensitive in VOLT
|
|
474
|
+
anchorname = anchorname.lower()
|
|
475
|
+
|
|
476
|
+
# We might still end in marks used in two different anchor
|
|
477
|
+
# classes, so we filter out already used marks.
|
|
478
|
+
marks = set()
|
|
479
|
+
for mark in coverage:
|
|
480
|
+
marks.update(mark.glyphSet())
|
|
481
|
+
if not marks.isdisjoint(allmarks):
|
|
482
|
+
marks.difference_update(allmarks)
|
|
483
|
+
if not marks:
|
|
484
|
+
continue
|
|
485
|
+
allmarks.update(marks)
|
|
486
|
+
|
|
487
|
+
for glyphname in marks:
|
|
488
|
+
glyph = self._glyphName(glyphname)
|
|
489
|
+
anchor = self._anchors[glyphname][f"MARK_{anchorname}"][1]
|
|
490
|
+
markdef = ast.MarkClassDefinition(markclass, anchor, glyph)
|
|
491
|
+
self._markclasses[(glyphname, classname)] = markdef
|
|
492
|
+
|
|
421
493
|
for base in pos.coverage:
|
|
422
494
|
for name in base.glyphSet():
|
|
423
495
|
if name not in anchors:
|
|
424
496
|
anchors[name] = []
|
|
425
|
-
if classname not in anchors[name]:
|
|
426
|
-
anchors[name].append(classname)
|
|
497
|
+
if (anchorname, classname) not in anchors[name]:
|
|
498
|
+
anchors[name].append((anchorname, classname))
|
|
427
499
|
|
|
500
|
+
is_ligature = all(n in self._ligatures for n in anchors)
|
|
501
|
+
is_mark = all(n in self._marks for n in anchors)
|
|
428
502
|
for name in anchors:
|
|
429
503
|
components = 1
|
|
430
|
-
if
|
|
504
|
+
if is_ligature:
|
|
431
505
|
components = self._ligatures[name]
|
|
432
506
|
|
|
433
|
-
marks = []
|
|
434
|
-
for mark in anchors[name]:
|
|
435
|
-
markclass = ast.MarkClass(
|
|
507
|
+
marks = [[] for _ in range(components)]
|
|
508
|
+
for mark, classname in anchors[name]:
|
|
509
|
+
markclass = ast.MarkClass(classname)
|
|
436
510
|
for component in range(1, components + 1):
|
|
437
|
-
if len(marks) < component:
|
|
438
|
-
marks.append([])
|
|
439
|
-
anchor = None
|
|
440
511
|
if component in self._anchors[name][mark]:
|
|
441
512
|
anchor = self._anchors[name][mark][component]
|
|
442
|
-
|
|
513
|
+
marks[component - 1].append((anchor, markclass))
|
|
443
514
|
|
|
444
515
|
base = self._glyphName(name)
|
|
445
|
-
if
|
|
516
|
+
if is_mark:
|
|
446
517
|
mark = ast.MarkMarkPosStatement(base, marks[0])
|
|
447
|
-
elif
|
|
518
|
+
elif is_ligature:
|
|
448
519
|
mark = ast.MarkLigPosStatement(base, marks)
|
|
449
520
|
else:
|
|
450
521
|
mark = ast.MarkBasePosStatement(base, marks[0])
|
|
@@ -481,13 +552,9 @@ class VoltToFea:
|
|
|
481
552
|
else:
|
|
482
553
|
raise NotImplementedError(pos)
|
|
483
554
|
|
|
484
|
-
def _gposContextLookup(
|
|
485
|
-
self, lookup, prefix, suffix, ignore, fealookup, targetlookup
|
|
486
|
-
):
|
|
555
|
+
def _gposContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained):
|
|
487
556
|
statements = fealookup.statements
|
|
488
557
|
|
|
489
|
-
assert not lookup.reversal
|
|
490
|
-
|
|
491
558
|
pos = lookup.pos
|
|
492
559
|
if isinstance(pos, VAst.PositionAdjustPairDefinition):
|
|
493
560
|
for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
|
|
@@ -500,79 +567,181 @@ class VoltToFea:
|
|
|
500
567
|
if ignore:
|
|
501
568
|
statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
|
|
502
569
|
else:
|
|
503
|
-
lookups = (targetlookup, targetlookup)
|
|
504
570
|
statement = ast.ChainContextPosStatement(
|
|
505
|
-
prefix, glyphs, suffix,
|
|
571
|
+
prefix, glyphs, suffix, [chained, chained]
|
|
506
572
|
)
|
|
507
573
|
statements.append(statement)
|
|
508
574
|
elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
|
|
509
575
|
glyphs = [ast.GlyphClass()]
|
|
510
|
-
for a,
|
|
511
|
-
|
|
512
|
-
glyphs[0].extend(glyph)
|
|
576
|
+
for a, _ in pos.adjust_single:
|
|
577
|
+
glyphs[0].extend(self._coverage(a, flatten=True))
|
|
513
578
|
|
|
514
579
|
if ignore:
|
|
515
580
|
statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
|
|
516
581
|
else:
|
|
517
582
|
statement = ast.ChainContextPosStatement(
|
|
518
|
-
prefix, glyphs, suffix, [
|
|
583
|
+
prefix, glyphs, suffix, [chained]
|
|
519
584
|
)
|
|
520
585
|
statements.append(statement)
|
|
521
586
|
elif isinstance(pos, VAst.PositionAttachDefinition):
|
|
522
587
|
glyphs = [ast.GlyphClass()]
|
|
523
588
|
for coverage, _ in pos.coverage_to:
|
|
524
|
-
glyphs[0].extend(self._coverage(coverage))
|
|
589
|
+
glyphs[0].extend(self._coverage(coverage, flatten=True))
|
|
525
590
|
|
|
526
591
|
if ignore:
|
|
527
592
|
statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
|
|
528
593
|
else:
|
|
529
594
|
statement = ast.ChainContextPosStatement(
|
|
530
|
-
prefix, glyphs, suffix, [
|
|
595
|
+
prefix, glyphs, suffix, [chained]
|
|
531
596
|
)
|
|
532
597
|
statements.append(statement)
|
|
533
598
|
else:
|
|
534
599
|
raise NotImplementedError(pos)
|
|
535
600
|
|
|
536
|
-
def _gsubLookup(self, lookup,
|
|
601
|
+
def _gsubLookup(self, lookup, fealookup):
|
|
537
602
|
statements = fealookup.statements
|
|
538
603
|
|
|
539
604
|
sub = lookup.sub
|
|
605
|
+
|
|
606
|
+
# Alternate substitutions are represented by adding multiple
|
|
607
|
+
# substitutions for the same glyph, so we need to collect them into one
|
|
608
|
+
# to many mapping.
|
|
609
|
+
if isinstance(sub, VAst.SubstitutionAlternateDefinition):
|
|
610
|
+
alternates = {}
|
|
611
|
+
for key, val in sub.mapping.items():
|
|
612
|
+
if not key or not val:
|
|
613
|
+
path, line, column = sub.location
|
|
614
|
+
log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
|
|
615
|
+
continue
|
|
616
|
+
glyphs = self._coverage(key)
|
|
617
|
+
replacements = self._coverage(val)
|
|
618
|
+
assert len(glyphs) == 1
|
|
619
|
+
for src_glyph, repl_glyph in zip(
|
|
620
|
+
glyphs[0].glyphSet(), replacements[0].glyphSet()
|
|
621
|
+
):
|
|
622
|
+
alternates.setdefault(str(self._glyphName(src_glyph)), []).append(
|
|
623
|
+
str(self._glyphName(repl_glyph))
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
for glyph, replacements in alternates.items():
|
|
627
|
+
statement = ast.AlternateSubstStatement(
|
|
628
|
+
[], glyph, [], ast.GlyphClass(replacements)
|
|
629
|
+
)
|
|
630
|
+
statements.append(statement)
|
|
631
|
+
return
|
|
632
|
+
|
|
540
633
|
for key, val in sub.mapping.items():
|
|
541
634
|
if not key or not val:
|
|
542
635
|
path, line, column = sub.location
|
|
543
636
|
log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
|
|
544
637
|
continue
|
|
545
|
-
statement = None
|
|
546
638
|
glyphs = self._coverage(key)
|
|
547
639
|
replacements = self._coverage(val)
|
|
548
|
-
if
|
|
549
|
-
chain_context = (prefix, glyphs, suffix)
|
|
550
|
-
statement = ast.IgnoreSubstStatement([chain_context])
|
|
551
|
-
elif isinstance(sub, VAst.SubstitutionSingleDefinition):
|
|
640
|
+
if isinstance(sub, VAst.SubstitutionSingleDefinition):
|
|
552
641
|
assert len(glyphs) == 1
|
|
553
642
|
assert len(replacements) == 1
|
|
554
|
-
|
|
555
|
-
glyphs, replacements,
|
|
643
|
+
statements.append(
|
|
644
|
+
ast.SingleSubstStatement(glyphs, replacements, [], [], False)
|
|
556
645
|
)
|
|
557
646
|
elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition):
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
statement = ast.ReverseChainSingleSubstStatement(
|
|
561
|
-
prefix, suffix, glyphs, replacements
|
|
562
|
-
)
|
|
647
|
+
# This is handled in gsubContextLookup()
|
|
648
|
+
pass
|
|
563
649
|
elif isinstance(sub, VAst.SubstitutionMultipleDefinition):
|
|
564
650
|
assert len(glyphs) == 1
|
|
565
|
-
|
|
566
|
-
|
|
651
|
+
statements.append(
|
|
652
|
+
ast.MultipleSubstStatement([], glyphs[0], [], replacements)
|
|
567
653
|
)
|
|
568
654
|
elif isinstance(sub, VAst.SubstitutionLigatureDefinition):
|
|
569
655
|
assert len(replacements) == 1
|
|
570
656
|
statement = ast.LigatureSubstStatement(
|
|
571
|
-
|
|
657
|
+
[], glyphs, [], replacements[0], False
|
|
572
658
|
)
|
|
659
|
+
|
|
660
|
+
# If any of the input glyphs is a group, we need to
|
|
661
|
+
# explode the substitution into multiple ligature substitutions
|
|
662
|
+
# since feature file syntax does not support classes in
|
|
663
|
+
# ligature substitutions.
|
|
664
|
+
n = max(len(x.glyphSet()) for x in glyphs)
|
|
665
|
+
if n > 1:
|
|
666
|
+
# All input should either be groups of the same length or single glyphs
|
|
667
|
+
assert all(len(x.glyphSet()) in (n, 1) for x in glyphs)
|
|
668
|
+
glyphs = [x.glyphSet() for x in glyphs]
|
|
669
|
+
glyphs = [([x[0]] * n if len(x) == 1 else x) for x in glyphs]
|
|
670
|
+
|
|
671
|
+
# In this case ligature replacements must be a group of the same length
|
|
672
|
+
# as the input groups, or a single glyph. VOLT
|
|
673
|
+
# allows the replacement glyphs to be longer and truncates them.
|
|
674
|
+
# So well allow that and zip() below will do the truncation
|
|
675
|
+
# for us.
|
|
676
|
+
replacement = replacements[0].glyphSet()
|
|
677
|
+
if len(replacement) == 1:
|
|
678
|
+
replacement = [replacement[0]] * n
|
|
679
|
+
assert len(replacement) >= n
|
|
680
|
+
|
|
681
|
+
# Add the unexploded statement commented out for reference.
|
|
682
|
+
statements.append(ast.Comment(f"# {statement}"))
|
|
683
|
+
|
|
684
|
+
for zipped in zip(*glyphs, replacement):
|
|
685
|
+
zipped = [self._glyphName(x) for x in zipped]
|
|
686
|
+
statements.append(
|
|
687
|
+
ast.LigatureSubstStatement(
|
|
688
|
+
[], zipped[:-1], [], zipped[-1], False
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
else:
|
|
692
|
+
statements.append(statement)
|
|
573
693
|
else:
|
|
574
694
|
raise NotImplementedError(sub)
|
|
575
|
-
|
|
695
|
+
|
|
696
|
+
def _gsubContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained):
|
|
697
|
+
statements = fealookup.statements
|
|
698
|
+
|
|
699
|
+
sub = lookup.sub
|
|
700
|
+
|
|
701
|
+
if isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition):
|
|
702
|
+
# Reverse substitutions is a special case, it can’t use chained lookups.
|
|
703
|
+
for key, val in sub.mapping.items():
|
|
704
|
+
if not key or not val:
|
|
705
|
+
path, line, column = sub.location
|
|
706
|
+
log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
|
|
707
|
+
continue
|
|
708
|
+
glyphs = self._coverage(key)
|
|
709
|
+
replacements = self._coverage(val)
|
|
710
|
+
statements.append(
|
|
711
|
+
ast.ReverseChainSingleSubstStatement(
|
|
712
|
+
prefix, suffix, glyphs, replacements
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
fealookup.chained = []
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
if not isinstance(
|
|
719
|
+
sub,
|
|
720
|
+
(
|
|
721
|
+
VAst.SubstitutionSingleDefinition,
|
|
722
|
+
VAst.SubstitutionMultipleDefinition,
|
|
723
|
+
VAst.SubstitutionLigatureDefinition,
|
|
724
|
+
VAst.SubstitutionAlternateDefinition,
|
|
725
|
+
),
|
|
726
|
+
):
|
|
727
|
+
raise NotImplementedError(type(sub))
|
|
728
|
+
|
|
729
|
+
glyphs = []
|
|
730
|
+
for key, val in sub.mapping.items():
|
|
731
|
+
if not key or not val:
|
|
732
|
+
path, line, column = sub.location
|
|
733
|
+
log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
|
|
734
|
+
continue
|
|
735
|
+
glyphs.extend(self._coverage(key, flatten=True))
|
|
736
|
+
|
|
737
|
+
if len(glyphs) > 1:
|
|
738
|
+
glyphs = [ast.GlyphClass(glyphs)]
|
|
739
|
+
if ignore:
|
|
740
|
+
statements.append(ast.IgnoreSubstStatement([(prefix, glyphs, suffix)]))
|
|
741
|
+
else:
|
|
742
|
+
statements.append(
|
|
743
|
+
ast.ChainContextSubstStatement(prefix, glyphs, suffix, [chained])
|
|
744
|
+
)
|
|
576
745
|
|
|
577
746
|
def _lookupDefinition(self, lookup):
|
|
578
747
|
mark_attachement = None
|
|
@@ -598,13 +767,21 @@ class VoltToFea:
|
|
|
598
767
|
lookupflags = ast.LookupFlagStatement(
|
|
599
768
|
flags, mark_attachement, mark_filtering
|
|
600
769
|
)
|
|
770
|
+
|
|
771
|
+
use_extension = False
|
|
772
|
+
if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"):
|
|
773
|
+
use_extension = True
|
|
774
|
+
|
|
601
775
|
if "\\" in lookup.name:
|
|
602
776
|
# Merge sub lookups as subtables (lookups named “base\sub”),
|
|
603
777
|
# makeotf/feaLib will issue a warning and ignore the subtable
|
|
604
778
|
# statement if it is not a pairpos lookup, though.
|
|
605
779
|
name = lookup.name.split("\\")[0]
|
|
606
780
|
if name.lower() not in self._lookups:
|
|
607
|
-
fealookup =
|
|
781
|
+
fealookup = Lookup(
|
|
782
|
+
self._lookupName(name),
|
|
783
|
+
use_extension=use_extension,
|
|
784
|
+
)
|
|
608
785
|
if lookupflags is not None:
|
|
609
786
|
fealookup.statements.append(lookupflags)
|
|
610
787
|
fealookup.statements.append(ast.Comment("# " + lookup.name))
|
|
@@ -614,7 +791,10 @@ class VoltToFea:
|
|
|
614
791
|
fealookup.statements.append(ast.Comment("# " + lookup.name))
|
|
615
792
|
self._lookups[name.lower()] = fealookup
|
|
616
793
|
else:
|
|
617
|
-
fealookup =
|
|
794
|
+
fealookup = Lookup(
|
|
795
|
+
self._lookupName(lookup.name),
|
|
796
|
+
use_extension=use_extension,
|
|
797
|
+
)
|
|
618
798
|
if lookupflags is not None:
|
|
619
799
|
fealookup.statements.append(lookupflags)
|
|
620
800
|
self._lookups[lookup.name.lower()] = fealookup
|
|
@@ -623,39 +803,40 @@ class VoltToFea:
|
|
|
623
803
|
fealookup.statements.append(ast.Comment("# " + lookup.comments))
|
|
624
804
|
|
|
625
805
|
contexts = []
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
806
|
+
for context in lookup.context:
|
|
807
|
+
prefix = self._context(context.left)
|
|
808
|
+
suffix = self._context(context.right)
|
|
809
|
+
ignore = context.ex_or_in == "EXCEPT_CONTEXT"
|
|
810
|
+
contexts.append([prefix, suffix, ignore])
|
|
811
|
+
# It seems that VOLT will create contextual substitution using
|
|
812
|
+
# only the input if there is no other contexts in this lookup.
|
|
813
|
+
if ignore and len(lookup.context) == 1:
|
|
814
|
+
contexts.append([[], [], False])
|
|
815
|
+
|
|
816
|
+
if contexts:
|
|
817
|
+
chained = ast.LookupBlock(
|
|
818
|
+
self._lookupName(lookup.name + " chained"),
|
|
819
|
+
use_extension=use_extension,
|
|
820
|
+
)
|
|
821
|
+
fealookup.chained.append(chained)
|
|
641
822
|
if lookup.sub is not None:
|
|
642
|
-
self._gsubLookup(lookup,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
fealookup.targets = getattr(fealookup, "targets", [])
|
|
652
|
-
fealookup.targets.append(targetlookup)
|
|
653
|
-
self._gposLookup(lookup, targetlookup)
|
|
823
|
+
self._gsubLookup(lookup, chained)
|
|
824
|
+
elif lookup.pos is not None:
|
|
825
|
+
self._gposLookup(lookup, chained)
|
|
826
|
+
for prefix, suffix, ignore in contexts:
|
|
827
|
+
if lookup.sub is not None:
|
|
828
|
+
self._gsubContextLookup(
|
|
829
|
+
lookup, prefix, suffix, ignore, fealookup, chained
|
|
830
|
+
)
|
|
831
|
+
elif lookup.pos is not None:
|
|
654
832
|
self._gposContextLookup(
|
|
655
|
-
lookup, prefix, suffix, ignore, fealookup,
|
|
833
|
+
lookup, prefix, suffix, ignore, fealookup, chained
|
|
656
834
|
)
|
|
657
|
-
|
|
658
|
-
|
|
835
|
+
else:
|
|
836
|
+
if lookup.sub is not None:
|
|
837
|
+
self._gsubLookup(lookup, fealookup)
|
|
838
|
+
elif lookup.pos is not None:
|
|
839
|
+
self._gposLookup(lookup, fealookup)
|
|
659
840
|
|
|
660
841
|
|
|
661
842
|
def main(args=None):
|