ZhengyangZhang commited on
Commit
fb7b977
·
verified ·
1 Parent(s): 6e50806

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/fontTools/__init__.py +8 -0
  2. lib/python3.12/site-packages/fontTools/__main__.py +35 -0
  3. lib/python3.12/site-packages/fontTools/afmLib.py +439 -0
  4. lib/python3.12/site-packages/fontTools/agl.py +0 -0
  5. lib/python3.12/site-packages/fontTools/annotations.py +30 -0
  6. lib/python3.12/site-packages/fontTools/colorLib/__init__.py +0 -0
  7. lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/fontTools/colorLib/builder.py +664 -0
  14. lib/python3.12/site-packages/fontTools/colorLib/errors.py +2 -0
  15. lib/python3.12/site-packages/fontTools/colorLib/geometry.py +143 -0
  16. lib/python3.12/site-packages/fontTools/colorLib/table_builder.py +223 -0
  17. lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py +81 -0
  18. lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py +0 -0
  19. lib/python3.12/site-packages/fontTools/designspaceLib/__main__.py +6 -0
  20. lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/__main__.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/split.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/statNames.cpython-312.pyc +0 -0
  23. lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/types.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/fontTools/designspaceLib/split.py +475 -0
  25. lib/python3.12/site-packages/fontTools/designspaceLib/statNames.py +260 -0
  26. lib/python3.12/site-packages/fontTools/designspaceLib/types.py +147 -0
  27. lib/python3.12/site-packages/fontTools/encodings/MacRoman.py +258 -0
  28. lib/python3.12/site-packages/fontTools/encodings/StandardEncoding.py +258 -0
  29. lib/python3.12/site-packages/fontTools/encodings/__init__.py +1 -0
  30. lib/python3.12/site-packages/fontTools/encodings/__pycache__/MacRoman.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/fontTools/encodings/__pycache__/StandardEncoding.cpython-312.pyc +0 -0
  32. lib/python3.12/site-packages/fontTools/encodings/__pycache__/__init__.cpython-312.pyc +0 -0
  33. lib/python3.12/site-packages/fontTools/encodings/__pycache__/codecs.cpython-312.pyc +0 -0
  34. lib/python3.12/site-packages/fontTools/encodings/codecs.py +135 -0
  35. lib/python3.12/site-packages/fontTools/fontBuilder.py +1014 -0
  36. lib/python3.12/site-packages/fontTools/help.py +36 -0
  37. lib/python3.12/site-packages/fontTools/mtiLib/__init__.py +1400 -0
  38. lib/python3.12/site-packages/fontTools/mtiLib/__main__.py +5 -0
  39. lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__init__.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__main__.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/fontTools/otlLib/__init__.py +1 -0
  42. lib/python3.12/site-packages/fontTools/otlLib/__pycache__/__init__.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/fontTools/otlLib/__pycache__/error.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/fontTools/otlLib/__pycache__/maxContextCalc.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/fontTools/otlLib/builder.py +0 -0
  46. lib/python3.12/site-packages/fontTools/otlLib/error.py +11 -0
  47. lib/python3.12/site-packages/fontTools/otlLib/maxContextCalc.py +96 -0
  48. lib/python3.12/site-packages/fontTools/otlLib/optimize/__init__.py +53 -0
  49. lib/python3.12/site-packages/fontTools/otlLib/optimize/__main__.py +6 -0
  50. lib/python3.12/site-packages/fontTools/otlLib/optimize/__pycache__/__init__.cpython-312.pyc +0 -0
lib/python3.12/site-packages/fontTools/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fontTools.misc.loggingTools import configLogger
3
+
4
+ log = logging.getLogger(__name__)
5
+
6
+ version = __version__ = "4.61.1"
7
+
8
+ __all__ = ["version", "log", "configLogger"]
lib/python3.12/site-packages/fontTools/__main__.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+
4
+ def main(args=None):
5
+ if args is None:
6
+ args = sys.argv[1:]
7
+
8
+ # TODO Handle library-wide options. Eg.:
9
+ # --unicodedata
10
+ # --verbose / other logging stuff
11
+
12
+ # TODO Allow a way to run arbitrary modules? Useful for setting
13
+ # library-wide options and calling another library. Eg.:
14
+ #
15
+ # $ fonttools --unicodedata=... fontmake ...
16
+ #
17
+ # This allows for a git-like command where thirdparty commands
18
+ # can be added. Should we just try importing the fonttools
19
+ # module first and try without if it fails?
20
+
21
+ if len(sys.argv) < 2:
22
+ sys.argv.append("help")
23
+ if sys.argv[1] == "-h" or sys.argv[1] == "--help":
24
+ sys.argv[1] = "help"
25
+ mod = "fontTools." + sys.argv[1]
26
+ sys.argv[1] = sys.argv[0] + " " + sys.argv[1]
27
+ del sys.argv[0]
28
+
29
+ import runpy
30
+
31
+ runpy.run_module(mod, run_name="__main__")
32
+
33
+
34
+ if __name__ == "__main__":
35
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/afmLib.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Module for reading and writing AFM (Adobe Font Metrics) files.
2
+
3
+ Note that this has been designed to read in AFM files generated by Fontographer
4
+ and has not been tested on many other files. In particular, it does not
5
+ implement the whole Adobe AFM specification [#f1]_ but, it should read most
6
+ "common" AFM files.
7
+
8
+ Here is an example of using `afmLib` to read, modify and write an AFM file:
9
+
10
+ >>> from fontTools.afmLib import AFM
11
+ >>> f = AFM("Tests/afmLib/data/TestAFM.afm")
12
+ >>>
13
+ >>> # Accessing a pair gets you the kern value
14
+ >>> f[("V","A")]
15
+ -60
16
+ >>>
17
+ >>> # Accessing a glyph name gets you metrics
18
+ >>> f["A"]
19
+ (65, 668, (8, -25, 660, 666))
20
+ >>> # (charnum, width, bounding box)
21
+ >>>
22
+ >>> # Accessing an attribute gets you metadata
23
+ >>> f.FontName
24
+ 'TestFont-Regular'
25
+ >>> f.FamilyName
26
+ 'TestFont'
27
+ >>> f.Weight
28
+ 'Regular'
29
+ >>> f.XHeight
30
+ 500
31
+ >>> f.Ascender
32
+ 750
33
+ >>>
34
+ >>> # Attributes and items can also be set
35
+ >>> f[("A","V")] = -150 # Tighten kerning
36
+ >>> f.FontName = "TestFont Squished"
37
+ >>>
38
+ >>> # And the font written out again (remove the # in front)
39
+ >>> #f.write("testfont-squished.afm")
40
+
41
+ .. rubric:: Footnotes
42
+
43
+ .. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
44
+ Adobe Font Metrics File Format Specification.
45
+
46
+ """
47
+
48
+ import re
49
+
50
+ # every single line starts with a "word"
51
+ identifierRE = re.compile(r"^([A-Za-z]+).*")
52
+
53
+ # regular expression to parse char lines
54
+ charRE = re.compile(
55
+ r"(-?\d+)" # charnum
56
+ r"\s*;\s*WX\s+" # ; WX
57
+ r"(-?\d+)" # width
58
+ r"\s*;\s*N\s+" # ; N
59
+ r"([.A-Za-z0-9_]+)" # charname
60
+ r"\s*;\s*B\s+" # ; B
61
+ r"(-?\d+)" # left
62
+ r"\s+"
63
+ r"(-?\d+)" # bottom
64
+ r"\s+"
65
+ r"(-?\d+)" # right
66
+ r"\s+"
67
+ r"(-?\d+)" # top
68
+ r"\s*;\s*" # ;
69
+ )
70
+
71
+ # regular expression to parse kerning lines
72
+ kernRE = re.compile(
73
+ r"([.A-Za-z0-9_]+)" # leftchar
74
+ r"\s+"
75
+ r"([.A-Za-z0-9_]+)" # rightchar
76
+ r"\s+"
77
+ r"(-?\d+)" # value
78
+ r"\s*"
79
+ )
80
+
81
+ # regular expressions to parse composite info lines of the form:
82
+ # Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
83
+ compositeRE = re.compile(
84
+ r"([.A-Za-z0-9_]+)" # char name
85
+ r"\s+"
86
+ r"(\d+)" # number of parts
87
+ r"\s*;\s*"
88
+ )
89
+ componentRE = re.compile(
90
+ r"PCC\s+" # PPC
91
+ r"([.A-Za-z0-9_]+)" # base char name
92
+ r"\s+"
93
+ r"(-?\d+)" # x offset
94
+ r"\s+"
95
+ r"(-?\d+)" # y offset
96
+ r"\s*;\s*"
97
+ )
98
+
99
+ preferredAttributeOrder = [
100
+ "FontName",
101
+ "FullName",
102
+ "FamilyName",
103
+ "Weight",
104
+ "ItalicAngle",
105
+ "IsFixedPitch",
106
+ "FontBBox",
107
+ "UnderlinePosition",
108
+ "UnderlineThickness",
109
+ "Version",
110
+ "Notice",
111
+ "EncodingScheme",
112
+ "CapHeight",
113
+ "XHeight",
114
+ "Ascender",
115
+ "Descender",
116
+ ]
117
+
118
+
119
+ class error(Exception):
120
+ pass
121
+
122
+
123
+ class AFM(object):
124
+ _attrs = None
125
+
126
+ _keywords = [
127
+ "StartFontMetrics",
128
+ "EndFontMetrics",
129
+ "StartCharMetrics",
130
+ "EndCharMetrics",
131
+ "StartKernData",
132
+ "StartKernPairs",
133
+ "EndKernPairs",
134
+ "EndKernData",
135
+ "StartComposites",
136
+ "EndComposites",
137
+ ]
138
+
139
+ def __init__(self, path=None):
140
+ """AFM file reader.
141
+
142
+ Instantiating an object with a path name will cause the file to be opened,
143
+ read, and parsed. Alternatively the path can be left unspecified, and a
144
+ file can be parsed later with the :meth:`read` method."""
145
+ self._attrs = {}
146
+ self._chars = {}
147
+ self._kerning = {}
148
+ self._index = {}
149
+ self._comments = []
150
+ self._composites = {}
151
+ if path is not None:
152
+ self.read(path)
153
+
154
+ def read(self, path):
155
+ """Opens, reads and parses a file."""
156
+ lines = readlines(path)
157
+ for line in lines:
158
+ if not line.strip():
159
+ continue
160
+ m = identifierRE.match(line)
161
+ if m is None:
162
+ raise error("syntax error in AFM file: " + repr(line))
163
+
164
+ pos = m.regs[1][1]
165
+ word = line[:pos]
166
+ rest = line[pos:].strip()
167
+ if word in self._keywords:
168
+ continue
169
+ if word == "C":
170
+ self.parsechar(rest)
171
+ elif word == "KPX":
172
+ self.parsekernpair(rest)
173
+ elif word == "CC":
174
+ self.parsecomposite(rest)
175
+ else:
176
+ self.parseattr(word, rest)
177
+
178
+ def parsechar(self, rest):
179
+ m = charRE.match(rest)
180
+ if m is None:
181
+ raise error("syntax error in AFM file: " + repr(rest))
182
+ things = []
183
+ for fr, to in m.regs[1:]:
184
+ things.append(rest[fr:to])
185
+ charname = things[2]
186
+ del things[2]
187
+ charnum, width, l, b, r, t = (int(thing) for thing in things)
188
+ self._chars[charname] = charnum, width, (l, b, r, t)
189
+
190
+ def parsekernpair(self, rest):
191
+ m = kernRE.match(rest)
192
+ if m is None:
193
+ raise error("syntax error in AFM file: " + repr(rest))
194
+ things = []
195
+ for fr, to in m.regs[1:]:
196
+ things.append(rest[fr:to])
197
+ leftchar, rightchar, value = things
198
+ value = int(value)
199
+ self._kerning[(leftchar, rightchar)] = value
200
+
201
+ def parseattr(self, word, rest):
202
+ if word == "FontBBox":
203
+ l, b, r, t = [int(thing) for thing in rest.split()]
204
+ self._attrs[word] = l, b, r, t
205
+ elif word == "Comment":
206
+ self._comments.append(rest)
207
+ else:
208
+ try:
209
+ value = int(rest)
210
+ except (ValueError, OverflowError):
211
+ self._attrs[word] = rest
212
+ else:
213
+ self._attrs[word] = value
214
+
215
+ def parsecomposite(self, rest):
216
+ m = compositeRE.match(rest)
217
+ if m is None:
218
+ raise error("syntax error in AFM file: " + repr(rest))
219
+ charname = m.group(1)
220
+ ncomponents = int(m.group(2))
221
+ rest = rest[m.regs[0][1] :]
222
+ components = []
223
+ while True:
224
+ m = componentRE.match(rest)
225
+ if m is None:
226
+ raise error("syntax error in AFM file: " + repr(rest))
227
+ basechar = m.group(1)
228
+ xoffset = int(m.group(2))
229
+ yoffset = int(m.group(3))
230
+ components.append((basechar, xoffset, yoffset))
231
+ rest = rest[m.regs[0][1] :]
232
+ if not rest:
233
+ break
234
+ assert len(components) == ncomponents
235
+ self._composites[charname] = components
236
+
237
+ def write(self, path, sep="\r"):
238
+ """Writes out an AFM font to the given path."""
239
+ import time
240
+
241
+ lines = [
242
+ "StartFontMetrics 2.0",
243
+ "Comment Generated by afmLib; at %s"
244
+ % (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))),
245
+ ]
246
+
247
+ # write comments, assuming (possibly wrongly!) they should
248
+ # all appear at the top
249
+ for comment in self._comments:
250
+ lines.append("Comment " + comment)
251
+
252
+ # write attributes, first the ones we know about, in
253
+ # a preferred order
254
+ attrs = self._attrs
255
+ for attr in preferredAttributeOrder:
256
+ if attr in attrs:
257
+ value = attrs[attr]
258
+ if attr == "FontBBox":
259
+ value = "%s %s %s %s" % value
260
+ lines.append(attr + " " + str(value))
261
+ # then write the attributes we don't know about,
262
+ # in alphabetical order
263
+ items = sorted(attrs.items())
264
+ for attr, value in items:
265
+ if attr in preferredAttributeOrder:
266
+ continue
267
+ lines.append(attr + " " + str(value))
268
+
269
+ # write char metrics
270
+ lines.append("StartCharMetrics " + repr(len(self._chars)))
271
+ items = [
272
+ (charnum, (charname, width, box))
273
+ for charname, (charnum, width, box) in self._chars.items()
274
+ ]
275
+
276
+ def myKey(a):
277
+ """Custom key function to make sure unencoded chars (-1)
278
+ end up at the end of the list after sorting."""
279
+ if a[0] == -1:
280
+ a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number
281
+ return a
282
+
283
+ items.sort(key=myKey)
284
+
285
+ for charnum, (charname, width, (l, b, r, t)) in items:
286
+ lines.append(
287
+ "C %d ; WX %d ; N %s ; B %d %d %d %d ;"
288
+ % (charnum, width, charname, l, b, r, t)
289
+ )
290
+ lines.append("EndCharMetrics")
291
+
292
+ # write kerning info
293
+ lines.append("StartKernData")
294
+ lines.append("StartKernPairs " + repr(len(self._kerning)))
295
+ items = sorted(self._kerning.items())
296
+ for (leftchar, rightchar), value in items:
297
+ lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
298
+ lines.append("EndKernPairs")
299
+ lines.append("EndKernData")
300
+
301
+ if self._composites:
302
+ composites = sorted(self._composites.items())
303
+ lines.append("StartComposites %s" % len(self._composites))
304
+ for charname, components in composites:
305
+ line = "CC %s %s ;" % (charname, len(components))
306
+ for basechar, xoffset, yoffset in components:
307
+ line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
308
+ lines.append(line)
309
+ lines.append("EndComposites")
310
+
311
+ lines.append("EndFontMetrics")
312
+
313
+ writelines(path, lines, sep)
314
+
315
+ def has_kernpair(self, pair):
316
+ """Returns `True` if the given glyph pair (specified as a tuple) exists
317
+ in the kerning dictionary."""
318
+ return pair in self._kerning
319
+
320
+ def kernpairs(self):
321
+ """Returns a list of all kern pairs in the kerning dictionary."""
322
+ return list(self._kerning.keys())
323
+
324
+ def has_char(self, char):
325
+ """Returns `True` if the given glyph exists in the font."""
326
+ return char in self._chars
327
+
328
+ def chars(self):
329
+ """Returns a list of all glyph names in the font."""
330
+ return list(self._chars.keys())
331
+
332
+ def comments(self):
333
+ """Returns all comments from the file."""
334
+ return self._comments
335
+
336
+ def addComment(self, comment):
337
+ """Adds a new comment to the file."""
338
+ self._comments.append(comment)
339
+
340
+ def addComposite(self, glyphName, components):
341
+ """Specifies that the glyph `glyphName` is made up of the given components.
342
+ The components list should be of the following form::
343
+
344
+ [
345
+ (glyphname, xOffset, yOffset),
346
+ ...
347
+ ]
348
+
349
+ """
350
+ self._composites[glyphName] = components
351
+
352
+ def __getattr__(self, attr):
353
+ if attr in self._attrs:
354
+ return self._attrs[attr]
355
+ else:
356
+ raise AttributeError(attr)
357
+
358
+ def __setattr__(self, attr, value):
359
+ # all attrs *not* starting with "_" are consider to be AFM keywords
360
+ if attr[:1] == "_":
361
+ self.__dict__[attr] = value
362
+ else:
363
+ self._attrs[attr] = value
364
+
365
+ def __delattr__(self, attr):
366
+ # all attrs *not* starting with "_" are consider to be AFM keywords
367
+ if attr[:1] == "_":
368
+ try:
369
+ del self.__dict__[attr]
370
+ except KeyError:
371
+ raise AttributeError(attr)
372
+ else:
373
+ try:
374
+ del self._attrs[attr]
375
+ except KeyError:
376
+ raise AttributeError(attr)
377
+
378
+ def __getitem__(self, key):
379
+ if isinstance(key, tuple):
380
+ # key is a tuple, return the kernpair
381
+ return self._kerning[key]
382
+ else:
383
+ # return the metrics instead
384
+ return self._chars[key]
385
+
386
+ def __setitem__(self, key, value):
387
+ if isinstance(key, tuple):
388
+ # key is a tuple, set kernpair
389
+ self._kerning[key] = value
390
+ else:
391
+ # set char metrics
392
+ self._chars[key] = value
393
+
394
+ def __delitem__(self, key):
395
+ if isinstance(key, tuple):
396
+ # key is a tuple, del kernpair
397
+ del self._kerning[key]
398
+ else:
399
+ # del char metrics
400
+ del self._chars[key]
401
+
402
+ def __repr__(self):
403
+ if hasattr(self, "FullName"):
404
+ return "<AFM object for %s>" % self.FullName
405
+ else:
406
+ return "<AFM object at %x>" % id(self)
407
+
408
+
409
+ def readlines(path):
410
+ with open(path, "r", encoding="ascii") as f:
411
+ data = f.read()
412
+ return data.splitlines()
413
+
414
+
415
+ def writelines(path, lines, sep="\r"):
416
+ with open(path, "w", encoding="ascii", newline=sep) as f:
417
+ f.write("\n".join(lines) + "\n")
418
+
419
+
420
+ if __name__ == "__main__":
421
+ import EasyDialogs
422
+
423
+ path = EasyDialogs.AskFileForOpen()
424
+ if path:
425
+ afm = AFM(path)
426
+ char = "A"
427
+ if afm.has_char(char):
428
+ print(afm[char]) # print charnum, width and boundingbox
429
+ pair = ("A", "V")
430
+ if afm.has_kernpair(pair):
431
+ print(afm[pair]) # print kerning value for pair
432
+ print(afm.Version) # various other afm entries have become attributes
433
+ print(afm.Weight)
434
+ # afm.comments() returns a list of all Comment lines found in the AFM
435
+ print(afm.comments())
436
+ # print afm.chars()
437
+ # print afm.kernpairs()
438
+ print(afm)
439
+ afm.write(path + ".muck")
lib/python3.12/site-packages/fontTools/agl.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/fontTools/annotations.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Iterable, Optional, TypeVar, Union
3
+ from collections.abc import Callable, Sequence
4
+ from fontTools.misc.filesystem._base import FS
5
+ from os import PathLike
6
+ from xml.etree.ElementTree import Element as ElementTreeElement
7
+
8
+ if TYPE_CHECKING:
9
+ from fontTools.ufoLib import UFOFormatVersion
10
+ from fontTools.ufoLib.glifLib import GLIFFormatVersion
11
+ from lxml.etree import _Element as LxmlElement
12
+
13
+
14
+ T = TypeVar("T") # Generic type
15
+ K = TypeVar("K") # Generic dict key type
16
+ V = TypeVar("V") # Generic dict value type
17
+
18
+ GlyphNameToFileNameFunc = Optional[Callable[[str, set[str]], str]]
19
+ ElementType = Union[ElementTreeElement, "LxmlElement"]
20
+ FormatVersion = Union[int, tuple[int, int]]
21
+ FormatVersions = Optional[Iterable[FormatVersion]]
22
+ GLIFFormatVersionInput = Optional[Union[int, tuple[int, int], "GLIFFormatVersion"]]
23
+ UFOFormatVersionInput = Optional[Union[int, tuple[int, int], "UFOFormatVersion"]]
24
+ IntFloat = Union[int, float]
25
+ KerningPair = tuple[str, str]
26
+ KerningDict = dict[KerningPair, IntFloat]
27
+ KerningGroups = dict[str, Sequence[str]]
28
+ KerningNested = dict[str, dict[str, IntFloat]]
29
+ PathStr = Union[str, PathLike[str]]
30
+ PathOrFS = Union[PathStr, FS]
lib/python3.12/site-packages/fontTools/colorLib/__init__.py ADDED
File without changes
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (197 Bytes). View file
 
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc ADDED
Binary file (29.9 kB). View file
 
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc ADDED
Binary file (385 Bytes). View file
 
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc ADDED
Binary file (5.37 kB). View file
 
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc ADDED
Binary file (9.98 kB). View file
 
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc ADDED
Binary file (3.67 kB). View file
 
lib/python3.12/site-packages/fontTools/colorLib/builder.py ADDED
@@ -0,0 +1,664 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ colorLib.builder: Build COLR/CPAL tables from scratch
3
+
4
+ """
5
+
6
+ import collections
7
+ import copy
8
+ import enum
9
+ from functools import partial
10
+ from math import ceil, log
11
+ from typing import (
12
+ Any,
13
+ Dict,
14
+ Generator,
15
+ Iterable,
16
+ List,
17
+ Mapping,
18
+ Optional,
19
+ Sequence,
20
+ Tuple,
21
+ Type,
22
+ TypeVar,
23
+ Union,
24
+ )
25
+ from fontTools.misc.arrayTools import intRect
26
+ from fontTools.misc.fixedTools import fixedToFloat
27
+ from fontTools.misc.treeTools import build_n_ary_tree
28
+ from fontTools.ttLib.tables import C_O_L_R_
29
+ from fontTools.ttLib.tables import C_P_A_L_
30
+ from fontTools.ttLib.tables import _n_a_m_e
31
+ from fontTools.ttLib.tables import otTables as ot
32
+ from fontTools.ttLib.tables.otTables import ExtendMode, CompositeMode
33
+ from .errors import ColorLibError
34
+ from .geometry import round_start_circle_stable_containment
35
+ from .table_builder import BuildCallback, TableBuilder
36
+
37
+
38
+ # TODO move type aliases to colorLib.types?
39
+ T = TypeVar("T")
40
+ _Kwargs = Mapping[str, Any]
41
+ _PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
42
+ _PaintInputList = Sequence[_PaintInput]
43
+ _ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
44
+ _ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
45
+ _ClipBoxInput = Union[
46
+ Tuple[int, int, int, int, int], # format 1, variable
47
+ Tuple[int, int, int, int], # format 0, non-variable
48
+ ot.ClipBox,
49
+ ]
50
+
51
+
52
+ MAX_PAINT_COLR_LAYER_COUNT = 255
53
+ _DEFAULT_ALPHA = 1.0
54
+ _MAX_REUSE_LEN = 32
55
+
56
+
57
+ def _beforeBuildPaintRadialGradient(paint, source):
58
+ x0 = source["x0"]
59
+ y0 = source["y0"]
60
+ r0 = source["r0"]
61
+ x1 = source["x1"]
62
+ y1 = source["y1"]
63
+ r1 = source["r1"]
64
+
65
+ # TODO apparently no builder_test confirms this works (?)
66
+
67
+ # avoid abrupt change after rounding when c0 is near c1's perimeter
68
+ c = round_start_circle_stable_containment((x0, y0), r0, (x1, y1), r1)
69
+ x0, y0 = c.centre
70
+ r0 = c.radius
71
+
72
+ # update source to ensure paint is built with corrected values
73
+ source["x0"] = x0
74
+ source["y0"] = y0
75
+ source["r0"] = r0
76
+ source["x1"] = x1
77
+ source["y1"] = y1
78
+ source["r1"] = r1
79
+
80
+ return paint, source
81
+
82
+
83
+ def _defaultColorStop():
84
+ colorStop = ot.ColorStop()
85
+ colorStop.Alpha = _DEFAULT_ALPHA
86
+ return colorStop
87
+
88
+
89
+ def _defaultVarColorStop():
90
+ colorStop = ot.VarColorStop()
91
+ colorStop.Alpha = _DEFAULT_ALPHA
92
+ return colorStop
93
+
94
+
95
+ def _defaultColorLine():
96
+ colorLine = ot.ColorLine()
97
+ colorLine.Extend = ExtendMode.PAD
98
+ return colorLine
99
+
100
+
101
+ def _defaultVarColorLine():
102
+ colorLine = ot.VarColorLine()
103
+ colorLine.Extend = ExtendMode.PAD
104
+ return colorLine
105
+
106
+
107
+ def _defaultPaintSolid():
108
+ paint = ot.Paint()
109
+ paint.Alpha = _DEFAULT_ALPHA
110
+ return paint
111
+
112
+
113
+ def _buildPaintCallbacks():
114
+ return {
115
+ (
116
+ BuildCallback.BEFORE_BUILD,
117
+ ot.Paint,
118
+ ot.PaintFormat.PaintRadialGradient,
119
+ ): _beforeBuildPaintRadialGradient,
120
+ (
121
+ BuildCallback.BEFORE_BUILD,
122
+ ot.Paint,
123
+ ot.PaintFormat.PaintVarRadialGradient,
124
+ ): _beforeBuildPaintRadialGradient,
125
+ (BuildCallback.CREATE_DEFAULT, ot.ColorStop): _defaultColorStop,
126
+ (BuildCallback.CREATE_DEFAULT, ot.VarColorStop): _defaultVarColorStop,
127
+ (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
128
+ (BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
129
+ (
130
+ BuildCallback.CREATE_DEFAULT,
131
+ ot.Paint,
132
+ ot.PaintFormat.PaintSolid,
133
+ ): _defaultPaintSolid,
134
+ (
135
+ BuildCallback.CREATE_DEFAULT,
136
+ ot.Paint,
137
+ ot.PaintFormat.PaintVarSolid,
138
+ ): _defaultPaintSolid,
139
+ }
140
+
141
+
142
+ def populateCOLRv0(
143
+ table: ot.COLR,
144
+ colorGlyphsV0: _ColorGlyphsV0Dict,
145
+ glyphMap: Optional[Mapping[str, int]] = None,
146
+ ):
147
+ """Build v0 color layers and add to existing COLR table.
148
+
149
+ Args:
150
+ table: a raw ``otTables.COLR()`` object (not ttLib's ``table_C_O_L_R_``).
151
+ colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
152
+ color palette index) tuples. Can be empty.
153
+ glyphMap: a map from glyph names to glyph indices, as returned from
154
+ ``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID.
155
+ """
156
+ if glyphMap is not None:
157
+ colorGlyphItems = sorted(
158
+ colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
159
+ )
160
+ else:
161
+ colorGlyphItems = colorGlyphsV0.items()
162
+ baseGlyphRecords = []
163
+ layerRecords = []
164
+ for baseGlyph, layers in colorGlyphItems:
165
+ baseRec = ot.BaseGlyphRecord()
166
+ baseRec.BaseGlyph = baseGlyph
167
+ baseRec.FirstLayerIndex = len(layerRecords)
168
+ baseRec.NumLayers = len(layers)
169
+ baseGlyphRecords.append(baseRec)
170
+
171
+ for layerGlyph, paletteIndex in layers:
172
+ layerRec = ot.LayerRecord()
173
+ layerRec.LayerGlyph = layerGlyph
174
+ layerRec.PaletteIndex = paletteIndex
175
+ layerRecords.append(layerRec)
176
+
177
+ table.BaseGlyphRecordArray = table.LayerRecordArray = None
178
+ if baseGlyphRecords:
179
+ table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
180
+ table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
181
+ if layerRecords:
182
+ table.LayerRecordArray = ot.LayerRecordArray()
183
+ table.LayerRecordArray.LayerRecord = layerRecords
184
+ table.BaseGlyphRecordCount = len(baseGlyphRecords)
185
+ table.LayerRecordCount = len(layerRecords)
186
+
187
+
188
+ def buildCOLR(
189
+ colorGlyphs: _ColorGlyphsDict,
190
+ version: Optional[int] = None,
191
+ *,
192
+ glyphMap: Optional[Mapping[str, int]] = None,
193
+ varStore: Optional[ot.VarStore] = None,
194
+ varIndexMap: Optional[ot.DeltaSetIndexMap] = None,
195
+ clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None,
196
+ allowLayerReuse: bool = True,
197
+ ) -> C_O_L_R_.table_C_O_L_R_:
198
+ """Build COLR table from color layers mapping.
199
+
200
+ Args:
201
+
202
+ colorGlyphs: map of base glyph name to, either list of (layer glyph name,
203
+ color palette index) tuples for COLRv0; or a single ``Paint`` (dict) or
204
+ list of ``Paint`` for COLRv1.
205
+ version: the version of COLR table. If None, the version is determined
206
+ by the presence of COLRv1 paints or variation data (varStore), which
207
+ require version 1; otherwise, if all base glyphs use only simple color
208
+ layers, version 0 is used.
209
+ glyphMap: a map from glyph names to glyph indices, as returned from
210
+ TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
211
+ varStore: Optional ItemVarationStore for deltas associated with v1 layer.
212
+ varIndexMap: Optional DeltaSetIndexMap for deltas associated with v1 layer.
213
+ clipBoxes: Optional map of base glyph name to clip box 4- or 5-tuples:
214
+ (xMin, yMin, xMax, yMax) or (xMin, yMin, xMax, yMax, varIndexBase).
215
+
216
+ Returns:
217
+ A new COLR table.
218
+ """
219
+ self = C_O_L_R_.table_C_O_L_R_()
220
+
221
+ if varStore is not None and version == 0:
222
+ raise ValueError("Can't add VarStore to COLRv0")
223
+
224
+ if version in (None, 0) and not varStore:
225
+ # split color glyphs into v0 and v1 and encode separately
226
+ colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
227
+ if version == 0 and colorGlyphsV1:
228
+ raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
229
+ else:
230
+ # unless explicitly requested for v1 or have variations, in which case
231
+ # we encode all color glyph as v1
232
+ colorGlyphsV0, colorGlyphsV1 = {}, colorGlyphs
233
+
234
+ colr = ot.COLR()
235
+
236
+ populateCOLRv0(colr, colorGlyphsV0, glyphMap)
237
+
238
+ colr.LayerList, colr.BaseGlyphList = buildColrV1(
239
+ colorGlyphsV1,
240
+ glyphMap,
241
+ allowLayerReuse=allowLayerReuse,
242
+ )
243
+
244
+ if version is None:
245
+ version = 1 if (varStore or colorGlyphsV1) else 0
246
+ elif version not in (0, 1):
247
+ raise NotImplementedError(version)
248
+ self.version = colr.Version = version
249
+
250
+ if version == 0:
251
+ self.ColorLayers = self._decompileColorLayersV0(colr)
252
+ else:
253
+ colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None
254
+ colr.VarIndexMap = varIndexMap
255
+ colr.VarStore = varStore
256
+ self.table = colr
257
+
258
+ return self
259
+
260
+
261
+ def buildClipList(clipBoxes: Dict[str, _ClipBoxInput]) -> ot.ClipList:
262
+ clipList = ot.ClipList()
263
+ clipList.Format = 1
264
+ clipList.clips = {name: buildClipBox(box) for name, box in clipBoxes.items()}
265
+ return clipList
266
+
267
+
268
+ def buildClipBox(clipBox: _ClipBoxInput) -> ot.ClipBox:
269
+ if isinstance(clipBox, ot.ClipBox):
270
+ return clipBox
271
+ n = len(clipBox)
272
+ clip = ot.ClipBox()
273
+ if n not in (4, 5):
274
+ raise ValueError(f"Invalid ClipBox: expected 4 or 5 values, found {n}")
275
+ clip.xMin, clip.yMin, clip.xMax, clip.yMax = intRect(clipBox[:4])
276
+ clip.Format = int(n == 5) + 1
277
+ if n == 5:
278
+ clip.VarIndexBase = int(clipBox[4])
279
+ return clip
280
+
281
+
282
+ class ColorPaletteType(enum.IntFlag):
283
+ USABLE_WITH_LIGHT_BACKGROUND = 0x0001
284
+ USABLE_WITH_DARK_BACKGROUND = 0x0002
285
+
286
+ @classmethod
287
+ def _missing_(cls, value):
288
+ # enforce reserved bits
289
+ if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0):
290
+ raise ValueError(f"{value} is not a valid {cls.__name__}")
291
+ return super()._missing_(value)
292
+
293
+
294
+ # None, 'abc' or {'en': 'abc', 'de': 'xyz'}
295
+ _OptionalLocalizedString = Union[None, str, Dict[str, str]]
296
+
297
+
298
+ def buildPaletteLabels(
299
+ labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
300
+ ) -> List[Optional[int]]:
301
+ return [
302
+ (
303
+ nameTable.addMultilingualName(l, mac=False)
304
+ if isinstance(l, dict)
305
+ else (
306
+ C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
307
+ if l is None
308
+ else nameTable.addMultilingualName({"en": l}, mac=False)
309
+ )
310
+ )
311
+ for l in labels
312
+ ]
313
+
314
+
315
+ def buildCPAL(
316
+ palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
317
+ paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
318
+ paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
319
+ paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
320
+ nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
321
+ ) -> C_P_A_L_.table_C_P_A_L_:
322
+ """Build CPAL table from list of color palettes.
323
+
324
+ Args:
325
+ palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats
326
+ in the range [0..1].
327
+ paletteTypes: optional list of ColorPaletteType, one for each palette.
328
+ paletteLabels: optional list of palette labels. Each lable can be either:
329
+ None (no label), a string (for for default English labels), or a
330
+ localized string (as a dict keyed with BCP47 language codes).
331
+ paletteEntryLabels: optional list of palette entry labels, one for each
332
+ palette entry (see paletteLabels).
333
+ nameTable: optional name table where to store palette and palette entry
334
+ labels. Required if either paletteLabels or paletteEntryLabels is set.
335
+
336
+ Return:
337
+ A new CPAL v0 or v1 table, if custom palette types or labels are specified.
338
+ """
339
+ if len({len(p) for p in palettes}) != 1:
340
+ raise ColorLibError("color palettes have different lengths")
341
+
342
+ if (paletteLabels or paletteEntryLabels) and not nameTable:
343
+ raise TypeError(
344
+ "nameTable is required if palette or palette entries have labels"
345
+ )
346
+
347
+ cpal = C_P_A_L_.table_C_P_A_L_()
348
+ cpal.numPaletteEntries = len(palettes[0])
349
+
350
+ cpal.palettes = []
351
+ for i, palette in enumerate(palettes):
352
+ colors = []
353
+ for j, color in enumerate(palette):
354
+ if not isinstance(color, tuple) or len(color) != 4:
355
+ raise ColorLibError(
356
+ f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}"
357
+ )
358
+ if any(v > 1 or v < 0 for v in color):
359
+ raise ColorLibError(
360
+ f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}"
361
+ )
362
+ # input colors are RGBA, CPAL encodes them as BGRA
363
+ red, green, blue, alpha = color
364
+ colors.append(
365
+ C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
366
+ )
367
+ cpal.palettes.append(colors)
368
+
369
+ if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
370
+ cpal.version = 1
371
+
372
+ if paletteTypes is not None:
373
+ if len(paletteTypes) != len(palettes):
374
+ raise ColorLibError(
375
+ f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}"
376
+ )
377
+ cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
378
+ else:
379
+ cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
380
+ palettes
381
+ )
382
+
383
+ if paletteLabels is not None:
384
+ if len(paletteLabels) != len(palettes):
385
+ raise ColorLibError(
386
+ f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}"
387
+ )
388
+ cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
389
+ else:
390
+ cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
391
+
392
+ if paletteEntryLabels is not None:
393
+ if len(paletteEntryLabels) != cpal.numPaletteEntries:
394
+ raise ColorLibError(
395
+ f"Expected {cpal.numPaletteEntries} paletteEntryLabels, "
396
+ f"got {len(paletteEntryLabels)}"
397
+ )
398
+ cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
399
+ else:
400
+ cpal.paletteEntryLabels = [
401
+ C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
402
+ ] * cpal.numPaletteEntries
403
+ else:
404
+ cpal.version = 0
405
+
406
+ return cpal
407
+
408
+
409
+ # COLR v1 tables
410
+ # See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
411
+
412
+
413
+ def _is_colrv0_layer(layer: Any) -> bool:
414
+ # Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
415
+ # the first element is a str (the layerGlyph) and the second element is an int
416
+ # (CPAL paletteIndex).
417
+ # https://github.com/googlefonts/ufo2ft/issues/426
418
+ try:
419
+ layerGlyph, paletteIndex = layer
420
+ except (TypeError, ValueError):
421
+ return False
422
+ else:
423
+ return isinstance(layerGlyph, str) and isinstance(paletteIndex, int)
424
+
425
+
426
+ def _split_color_glyphs_by_version(
427
+ colorGlyphs: _ColorGlyphsDict,
428
+ ) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
429
+ colorGlyphsV0 = {}
430
+ colorGlyphsV1 = {}
431
+ for baseGlyph, layers in colorGlyphs.items():
432
+ if all(_is_colrv0_layer(l) for l in layers):
433
+ colorGlyphsV0[baseGlyph] = layers
434
+ else:
435
+ colorGlyphsV1[baseGlyph] = layers
436
+
437
+ # sanity check
438
+ assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
439
+
440
+ return colorGlyphsV0, colorGlyphsV1
441
+
442
+
443
+ def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
444
+ # TODO feels like something itertools might have already
445
+ for lbound in range(num_layers):
446
+ # Reuse of very large #s of layers is relatively unlikely
447
+ # +2: we want sequences of at least 2
448
+ # otData handles single-record duplication
449
+ for ubound in range(
450
+ lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
451
+ ):
452
+ yield (lbound, ubound)
453
+
454
+
455
+ class LayerReuseCache:
456
+ reusePool: Mapping[Tuple[Any, ...], int]
457
+ tuples: Mapping[int, Tuple[Any, ...]]
458
+ keepAlive: List[ot.Paint] # we need id to remain valid
459
+
460
+ def __init__(self):
461
+ self.reusePool = {}
462
+ self.tuples = {}
463
+ self.keepAlive = []
464
+
465
+ def _paint_tuple(self, paint: ot.Paint):
466
+ # start simple, who even cares about cyclic graphs or interesting field types
467
+ def _tuple_safe(value):
468
+ if isinstance(value, enum.Enum):
469
+ return value
470
+ elif hasattr(value, "__dict__"):
471
+ return tuple(
472
+ (k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items())
473
+ )
474
+ elif isinstance(value, collections.abc.MutableSequence):
475
+ return tuple(_tuple_safe(e) for e in value)
476
+ return value
477
+
478
+ # Cache the tuples for individual Paint instead of the whole sequence
479
+ # because the seq could be a transient slice
480
+ result = self.tuples.get(id(paint), None)
481
+ if result is None:
482
+ result = _tuple_safe(paint)
483
+ self.tuples[id(paint)] = result
484
+ self.keepAlive.append(paint)
485
+ return result
486
+
487
+ def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
488
+ return tuple(self._paint_tuple(p) for p in paints)
489
+
490
+ def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
491
+ found_reuse = True
492
+ while found_reuse:
493
+ found_reuse = False
494
+
495
+ ranges = sorted(
496
+ _reuse_ranges(len(layers)),
497
+ key=lambda t: (t[1] - t[0], t[1], t[0]),
498
+ reverse=True,
499
+ )
500
+ for lbound, ubound in ranges:
501
+ reuse_lbound = self.reusePool.get(
502
+ self._as_tuple(layers[lbound:ubound]), -1
503
+ )
504
+ if reuse_lbound == -1:
505
+ continue
506
+ new_slice = ot.Paint()
507
+ new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
508
+ new_slice.NumLayers = ubound - lbound
509
+ new_slice.FirstLayerIndex = reuse_lbound
510
+ layers = layers[:lbound] + [new_slice] + layers[ubound:]
511
+ found_reuse = True
512
+ break
513
+ return layers
514
+
515
+ def add(self, layers: List[ot.Paint], first_layer_index: int):
516
+ for lbound, ubound in _reuse_ranges(len(layers)):
517
+ self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
518
+ lbound + first_layer_index
519
+ )
520
+
521
+
522
+ class LayerListBuilder:
523
+ layers: List[ot.Paint]
524
+ cache: LayerReuseCache
525
+ allowLayerReuse: bool
526
+
527
+ def __init__(self, *, allowLayerReuse=True):
528
+ self.layers = []
529
+ if allowLayerReuse:
530
+ self.cache = LayerReuseCache()
531
+ else:
532
+ self.cache = None
533
+
534
+ # We need to intercept construction of PaintColrLayers
535
+ callbacks = _buildPaintCallbacks()
536
+ callbacks[
537
+ (
538
+ BuildCallback.BEFORE_BUILD,
539
+ ot.Paint,
540
+ ot.PaintFormat.PaintColrLayers,
541
+ )
542
+ ] = self._beforeBuildPaintColrLayers
543
+ self.tableBuilder = TableBuilder(callbacks)
544
+
545
+ # COLR layers is unusual in that it modifies shared state
546
+ # so we need a callback into an object
547
+ def _beforeBuildPaintColrLayers(self, dest, source):
548
+ # Sketchy gymnastics: a sequence input will have dropped it's layers
549
+ # into NumLayers; get it back
550
+ if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
551
+ layers = source["NumLayers"]
552
+ else:
553
+ layers = source["Layers"]
554
+
555
+ # Convert maps seqs or whatever into typed objects
556
+ layers = [self.buildPaint(l) for l in layers]
557
+
558
+ # No reason to have a colr layers with just one entry
559
+ if len(layers) == 1:
560
+ return layers[0], {}
561
+
562
+ if self.cache is not None:
563
+ # Look for reuse, with preference to longer sequences
564
+ # This may make the layer list smaller
565
+ layers = self.cache.try_reuse(layers)
566
+
567
+ # The layer list is now final; if it's too big we need to tree it
568
+ is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
569
+ layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
570
+
571
+ # We now have a tree of sequences with Paint leaves.
572
+ # Convert the sequences into PaintColrLayers.
573
+ def listToColrLayers(layer):
574
+ if isinstance(layer, collections.abc.Sequence):
575
+ return self.buildPaint(
576
+ {
577
+ "Format": ot.PaintFormat.PaintColrLayers,
578
+ "Layers": [listToColrLayers(l) for l in layer],
579
+ }
580
+ )
581
+ return layer
582
+
583
+ layers = [listToColrLayers(l) for l in layers]
584
+
585
+ # No reason to have a colr layers with just one entry
586
+ if len(layers) == 1:
587
+ return layers[0], {}
588
+
589
+ paint = ot.Paint()
590
+ paint.Format = int(ot.PaintFormat.PaintColrLayers)
591
+ paint.NumLayers = len(layers)
592
+ paint.FirstLayerIndex = len(self.layers)
593
+ self.layers.extend(layers)
594
+
595
+ # Register our parts for reuse provided we aren't a tree
596
+ # If we are a tree the leaves registered for reuse and that will suffice
597
+ if self.cache is not None and not is_tree:
598
+ self.cache.add(layers, paint.FirstLayerIndex)
599
+
600
+ # we've fully built dest; empty source prevents generalized build from kicking in
601
+ return paint, {}
602
+
603
+ def buildPaint(self, paint: _PaintInput) -> ot.Paint:
604
+ return self.tableBuilder.build(ot.Paint, paint)
605
+
606
+ def build(self) -> Optional[ot.LayerList]:
607
+ if not self.layers:
608
+ return None
609
+ layers = ot.LayerList()
610
+ layers.LayerCount = len(self.layers)
611
+ layers.Paint = self.layers
612
+ return layers
613
+
614
+
615
+ def buildBaseGlyphPaintRecord(
616
+ baseGlyph: str, layerBuilder: LayerListBuilder, paint: _PaintInput
617
+ ) -> ot.BaseGlyphList:
618
+ self = ot.BaseGlyphPaintRecord()
619
+ self.BaseGlyph = baseGlyph
620
+ self.Paint = layerBuilder.buildPaint(paint)
621
+ return self
622
+
623
+
624
+ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
625
+ lines = []
626
+ for baseGlyph, error in sorted(errors.items()):
627
+ lines.append(f" {baseGlyph} => {type(error).__name__}: {error}")
628
+ return "\n".join(lines)
629
+
630
+
631
+ def buildColrV1(
632
+ colorGlyphs: _ColorGlyphsDict,
633
+ glyphMap: Optional[Mapping[str, int]] = None,
634
+ *,
635
+ allowLayerReuse: bool = True,
636
+ ) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
637
+ if glyphMap is not None:
638
+ colorGlyphItems = sorted(
639
+ colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
640
+ )
641
+ else:
642
+ colorGlyphItems = colorGlyphs.items()
643
+
644
+ errors = {}
645
+ baseGlyphs = []
646
+ layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
647
+ for baseGlyph, paint in colorGlyphItems:
648
+ try:
649
+ baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
650
+
651
+ except (ColorLibError, OverflowError, ValueError, TypeError) as e:
652
+ errors[baseGlyph] = e
653
+
654
+ if errors:
655
+ failed_glyphs = _format_glyph_errors(errors)
656
+ exc = ColorLibError(f"Failed to build BaseGlyphList:\n{failed_glyphs}")
657
+ exc.errors = errors
658
+ raise exc from next(iter(errors.values()))
659
+
660
+ layers = layerBuilder.build()
661
+ glyphs = ot.BaseGlyphList()
662
+ glyphs.BaseGlyphCount = len(baseGlyphs)
663
+ glyphs.BaseGlyphPaintRecord = baseGlyphs
664
+ return (layers, glyphs)
lib/python3.12/site-packages/fontTools/colorLib/errors.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ class ColorLibError(Exception):
2
+ pass
lib/python3.12/site-packages/fontTools/colorLib/geometry.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers for manipulating 2D points and vectors in COLR table."""
2
+
3
+ from math import copysign, cos, hypot, isclose, pi
4
+ from fontTools.misc.roundTools import otRound
5
+
6
+
7
+ def _vector_between(origin, target):
8
+ return (target[0] - origin[0], target[1] - origin[1])
9
+
10
+
11
+ def _round_point(pt):
12
+ return (otRound(pt[0]), otRound(pt[1]))
13
+
14
+
15
+ def _unit_vector(vec):
16
+ length = hypot(*vec)
17
+ if length == 0:
18
+ return None
19
+ return (vec[0] / length, vec[1] / length)
20
+
21
+
22
+ _CIRCLE_INSIDE_TOLERANCE = 1e-4
23
+
24
+
25
+ # The unit vector's X and Y components are respectively
26
+ # U = (cos(α), sin(α))
27
+ # where α is the angle between the unit vector and the positive x axis.
28
+ _UNIT_VECTOR_THRESHOLD = cos(3 / 8 * pi) # == sin(1/8 * pi) == 0.38268343236508984
29
+
30
+
31
+ def _rounding_offset(direction):
32
+ # Return 2-tuple of -/+ 1.0 or 0.0 approximately based on the direction vector.
33
+ # We divide the unit circle in 8 equal slices oriented towards the cardinal
34
+ # (N, E, S, W) and intermediate (NE, SE, SW, NW) directions. To each slice we
35
+ # map one of the possible cases: -1, 0, +1 for either X and Y coordinate.
36
+ # E.g. Return (+1.0, -1.0) if unit vector is oriented towards SE, or
37
+ # (-1.0, 0.0) if it's pointing West, etc.
38
+ uv = _unit_vector(direction)
39
+ if not uv:
40
+ return (0, 0)
41
+
42
+ result = []
43
+ for uv_component in uv:
44
+ if -_UNIT_VECTOR_THRESHOLD <= uv_component < _UNIT_VECTOR_THRESHOLD:
45
+ # unit vector component near 0: direction almost orthogonal to the
46
+ # direction of the current axis, thus keep coordinate unchanged
47
+ result.append(0)
48
+ else:
49
+ # nudge coord by +/- 1.0 in direction of unit vector
50
+ result.append(copysign(1.0, uv_component))
51
+ return tuple(result)
52
+
53
+
54
+ class Circle:
55
+ def __init__(self, centre, radius):
56
+ self.centre = centre
57
+ self.radius = radius
58
+
59
+ def __repr__(self):
60
+ return f"Circle(centre={self.centre}, radius={self.radius})"
61
+
62
+ def round(self):
63
+ return Circle(_round_point(self.centre), otRound(self.radius))
64
+
65
+ def inside(self, outer_circle, tolerance=_CIRCLE_INSIDE_TOLERANCE):
66
+ dist = self.radius + hypot(*_vector_between(self.centre, outer_circle.centre))
67
+ return (
68
+ isclose(outer_circle.radius, dist, rel_tol=_CIRCLE_INSIDE_TOLERANCE)
69
+ or outer_circle.radius > dist
70
+ )
71
+
72
+ def concentric(self, other):
73
+ return self.centre == other.centre
74
+
75
+ def move(self, dx, dy):
76
+ self.centre = (self.centre[0] + dx, self.centre[1] + dy)
77
+
78
+
79
+ def round_start_circle_stable_containment(c0, r0, c1, r1):
80
+ """Round start circle so that it stays inside/outside end circle after rounding.
81
+
82
+ The rounding of circle coordinates to integers may cause an abrupt change
83
+ if the start circle c0 is so close to the end circle c1's perimiter that
84
+ it ends up falling outside (or inside) as a result of the rounding.
85
+ To keep the gradient unchanged, we nudge it in the right direction.
86
+
87
+ See:
88
+ https://github.com/googlefonts/colr-gradients-spec/issues/204
89
+ https://github.com/googlefonts/picosvg/issues/158
90
+ """
91
+ start, end = Circle(c0, r0), Circle(c1, r1)
92
+
93
+ inside_before_round = start.inside(end)
94
+
95
+ round_start = start.round()
96
+ round_end = end.round()
97
+ inside_after_round = round_start.inside(round_end)
98
+
99
+ if inside_before_round == inside_after_round:
100
+ return round_start
101
+ elif inside_after_round:
102
+ # start was outside before rounding: we need to push start away from end
103
+ direction = _vector_between(round_end.centre, round_start.centre)
104
+ radius_delta = +1.0
105
+ else:
106
+ # start was inside before rounding: we need to push start towards end
107
+ direction = _vector_between(round_start.centre, round_end.centre)
108
+ radius_delta = -1.0
109
+ dx, dy = _rounding_offset(direction)
110
+
111
+ # At most 2 iterations ought to be enough to converge. Before the loop, we
112
+ # know the start circle didn't keep containment after normal rounding; thus
113
+ # we continue adjusting by -/+ 1.0 until containment is restored.
114
+ # Normal rounding can at most move each coordinates -/+0.5; in the worst case
115
+ # both the start and end circle's centres and radii will be rounded in opposite
116
+ # directions, e.g. when they move along a 45 degree diagonal:
117
+ # c0 = (1.5, 1.5) ===> (2.0, 2.0)
118
+ # r0 = 0.5 ===> 1.0
119
+ # c1 = (0.499, 0.499) ===> (0.0, 0.0)
120
+ # r1 = 2.499 ===> 2.0
121
+ # In this example, the relative distance between the circles, calculated
122
+ # as r1 - (r0 + distance(c0, c1)) is initially 0.57437 (c0 is inside c1), and
123
+ # -1.82842 after rounding (c0 is now outside c1). Nudging c0 by -1.0 on both
124
+ # x and y axes moves it towards c1 by hypot(-1.0, -1.0) = 1.41421. Two of these
125
+ # moves cover twice that distance, which is enough to restore containment.
126
+ max_attempts = 2
127
+ for _ in range(max_attempts):
128
+ if round_start.concentric(round_end):
129
+ # can't move c0 towards c1 (they are the same), so we change the radius
130
+ round_start.radius += radius_delta
131
+ assert round_start.radius >= 0
132
+ else:
133
+ round_start.move(dx, dy)
134
+ if inside_before_round == round_start.inside(round_end):
135
+ break
136
+ else: # likely a bug
137
+ raise AssertionError(
138
+ f"Rounding circle {start} "
139
+ f"{'inside' if inside_before_round else 'outside'} "
140
+ f"{end} failed after {max_attempts} attempts!"
141
+ )
142
+
143
+ return round_start
lib/python3.12/site-packages/fontTools/colorLib/table_builder.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such.
3
+
4
+ """
5
+
6
+ import collections
7
+ import enum
8
+ from fontTools.ttLib.tables.otBase import (
9
+ BaseTable,
10
+ FormatSwitchingBaseTable,
11
+ UInt8FormatSwitchingBaseTable,
12
+ )
13
+ from fontTools.ttLib.tables.otConverters import (
14
+ ComputedInt,
15
+ SimpleValue,
16
+ Struct,
17
+ Short,
18
+ UInt8,
19
+ UShort,
20
+ IntValue,
21
+ FloatValue,
22
+ OptionalValue,
23
+ )
24
+ from fontTools.misc.roundTools import otRound
25
+
26
+
27
+ class BuildCallback(enum.Enum):
28
+ """Keyed on (BEFORE_BUILD, class[, Format if available]).
29
+ Receives (dest, source).
30
+ Should return (dest, source), which can be new objects.
31
+ """
32
+
33
+ BEFORE_BUILD = enum.auto()
34
+
35
+ """Keyed on (AFTER_BUILD, class[, Format if available]).
36
+ Receives (dest).
37
+ Should return dest, which can be a new object.
38
+ """
39
+ AFTER_BUILD = enum.auto()
40
+
41
+ """Keyed on (CREATE_DEFAULT, class[, Format if available]).
42
+ Receives no arguments.
43
+ Should return a new instance of class.
44
+ """
45
+ CREATE_DEFAULT = enum.auto()
46
+
47
+
48
+ def _assignable(convertersByName):
49
+ return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)}
50
+
51
+
52
+ def _isNonStrSequence(value):
53
+ return isinstance(value, collections.abc.Sequence) and not isinstance(value, str)
54
+
55
+
56
+ def _split_format(cls, source):
57
+ if _isNonStrSequence(source):
58
+ assert len(source) > 0, f"{cls} needs at least format from {source}"
59
+ fmt, remainder = source[0], source[1:]
60
+ elif isinstance(source, collections.abc.Mapping):
61
+ assert "Format" in source, f"{cls} needs at least Format from {source}"
62
+ remainder = source.copy()
63
+ fmt = remainder.pop("Format")
64
+ else:
65
+ raise ValueError(f"Not sure how to populate {cls} from {source}")
66
+
67
+ assert isinstance(
68
+ fmt, collections.abc.Hashable
69
+ ), f"{cls} Format is not hashable: {fmt!r}"
70
+ assert fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}"
71
+
72
+ return fmt, remainder
73
+
74
+
75
+ class TableBuilder:
76
+ """
77
+ Helps to populate things derived from BaseTable from maps, tuples, etc.
78
+
79
+ A table of lifecycle callbacks may be provided to add logic beyond what is possible
80
+ based on otData info for the target class. See BuildCallbacks.
81
+ """
82
+
83
+ def __init__(self, callbackTable=None):
84
+ if callbackTable is None:
85
+ callbackTable = {}
86
+ self._callbackTable = callbackTable
87
+
88
+ def _convert(self, dest, field, converter, value):
89
+ enumClass = getattr(converter, "enumClass", None)
90
+
91
+ if enumClass:
92
+ if isinstance(value, enumClass):
93
+ pass
94
+ elif isinstance(value, str):
95
+ try:
96
+ value = getattr(enumClass, value.upper())
97
+ except AttributeError:
98
+ raise ValueError(f"{value} is not a valid {enumClass}")
99
+ else:
100
+ value = enumClass(value)
101
+
102
+ elif isinstance(converter, IntValue):
103
+ value = otRound(value)
104
+ elif isinstance(converter, FloatValue):
105
+ value = float(value)
106
+
107
+ elif isinstance(converter, Struct):
108
+ if converter.repeat:
109
+ if _isNonStrSequence(value):
110
+ value = [self.build(converter.tableClass, v) for v in value]
111
+ else:
112
+ value = [self.build(converter.tableClass, value)]
113
+ setattr(dest, converter.repeat, len(value))
114
+ else:
115
+ value = self.build(converter.tableClass, value)
116
+ elif callable(converter):
117
+ value = converter(value)
118
+
119
+ setattr(dest, field, value)
120
+
121
+ def build(self, cls, source):
122
+ assert issubclass(cls, BaseTable)
123
+
124
+ if isinstance(source, cls):
125
+ return source
126
+
127
+ callbackKey = (cls,)
128
+ fmt = None
129
+ if issubclass(cls, FormatSwitchingBaseTable):
130
+ fmt, source = _split_format(cls, source)
131
+ callbackKey = (cls, fmt)
132
+
133
+ dest = self._callbackTable.get(
134
+ (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
135
+ )()
136
+ assert isinstance(dest, cls)
137
+
138
+ convByName = _assignable(cls.convertersByName)
139
+ skippedFields = set()
140
+
141
+ # For format switchers we need to resolve converters based on format
142
+ if issubclass(cls, FormatSwitchingBaseTable):
143
+ dest.Format = fmt
144
+ convByName = _assignable(convByName[dest.Format])
145
+ skippedFields.add("Format")
146
+
147
+ # Convert sequence => mapping so before thunk only has to handle one format
148
+ if _isNonStrSequence(source):
149
+ # Sequence (typically list or tuple) assumed to match fields in declaration order
150
+ assert len(source) <= len(
151
+ convByName
152
+ ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values"
153
+ source = dict(zip(convByName.keys(), source))
154
+
155
+ dest, source = self._callbackTable.get(
156
+ (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
157
+ )(dest, source)
158
+
159
+ if isinstance(source, collections.abc.Mapping):
160
+ for field, value in source.items():
161
+ if field in skippedFields:
162
+ continue
163
+ converter = convByName.get(field, None)
164
+ if not converter:
165
+ raise ValueError(
166
+ f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}"
167
+ )
168
+ self._convert(dest, field, converter, value)
169
+ else:
170
+ # let's try as a 1-tuple
171
+ dest = self.build(cls, (source,))
172
+
173
+ for field, conv in convByName.items():
174
+ if not hasattr(dest, field) and isinstance(conv, OptionalValue):
175
+ setattr(dest, field, conv.DEFAULT)
176
+
177
+ dest = self._callbackTable.get(
178
+ (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
179
+ )(dest)
180
+
181
+ return dest
182
+
183
+
184
+ class TableUnbuilder:
185
+ def __init__(self, callbackTable=None):
186
+ if callbackTable is None:
187
+ callbackTable = {}
188
+ self._callbackTable = callbackTable
189
+
190
+ def unbuild(self, table):
191
+ assert isinstance(table, BaseTable)
192
+
193
+ source = {}
194
+
195
+ callbackKey = (type(table),)
196
+ if isinstance(table, FormatSwitchingBaseTable):
197
+ source["Format"] = int(table.Format)
198
+ callbackKey += (table.Format,)
199
+
200
+ for converter in table.getConverters():
201
+ if isinstance(converter, ComputedInt):
202
+ continue
203
+ value = getattr(table, converter.name)
204
+
205
+ enumClass = getattr(converter, "enumClass", None)
206
+ if enumClass:
207
+ source[converter.name] = value.name.lower()
208
+ elif isinstance(converter, Struct):
209
+ if converter.repeat:
210
+ source[converter.name] = [self.unbuild(v) for v in value]
211
+ else:
212
+ source[converter.name] = self.unbuild(value)
213
+ elif isinstance(converter, SimpleValue):
214
+ # "simple" values (e.g. int, float, str) need no further un-building
215
+ source[converter.name] = value
216
+ else:
217
+ raise NotImplementedError(
218
+ "Don't know how unbuild {value!r} with {converter!r}"
219
+ )
220
+
221
+ source = self._callbackTable.get(callbackKey, lambda s: s)(source)
222
+
223
+ return source
lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.ttLib.tables import otTables as ot
2
+ from .table_builder import TableUnbuilder
3
+
4
+
5
+ def unbuildColrV1(layerList, baseGlyphList):
6
+ layers = []
7
+ if layerList:
8
+ layers = layerList.Paint
9
+ unbuilder = LayerListUnbuilder(layers)
10
+ return {
11
+ rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint)
12
+ for rec in baseGlyphList.BaseGlyphPaintRecord
13
+ }
14
+
15
+
16
+ def _flatten_layers(lst):
17
+ for paint in lst:
18
+ if paint["Format"] == ot.PaintFormat.PaintColrLayers:
19
+ yield from _flatten_layers(paint["Layers"])
20
+ else:
21
+ yield paint
22
+
23
+
24
+ class LayerListUnbuilder:
25
+ def __init__(self, layers):
26
+ self.layers = layers
27
+
28
+ callbacks = {
29
+ (
30
+ ot.Paint,
31
+ ot.PaintFormat.PaintColrLayers,
32
+ ): self._unbuildPaintColrLayers,
33
+ }
34
+ self.tableUnbuilder = TableUnbuilder(callbacks)
35
+
36
+ def unbuildPaint(self, paint):
37
+ assert isinstance(paint, ot.Paint)
38
+ return self.tableUnbuilder.unbuild(paint)
39
+
40
+ def _unbuildPaintColrLayers(self, source):
41
+ assert source["Format"] == ot.PaintFormat.PaintColrLayers
42
+
43
+ layers = list(
44
+ _flatten_layers(
45
+ [
46
+ self.unbuildPaint(childPaint)
47
+ for childPaint in self.layers[
48
+ source["FirstLayerIndex"] : source["FirstLayerIndex"]
49
+ + source["NumLayers"]
50
+ ]
51
+ ]
52
+ )
53
+ )
54
+
55
+ if len(layers) == 1:
56
+ return layers[0]
57
+
58
+ return {"Format": source["Format"], "Layers": layers}
59
+
60
+
61
+ if __name__ == "__main__":
62
+ from pprint import pprint
63
+ import sys
64
+ from fontTools.ttLib import TTFont
65
+
66
+ try:
67
+ fontfile = sys.argv[1]
68
+ except IndexError:
69
+ sys.exit("usage: fonttools colorLib.unbuilder FONTFILE")
70
+
71
+ font = TTFont(fontfile)
72
+ colr = font["COLR"]
73
+ if colr.version < 1:
74
+ sys.exit(f"error: No COLR table version=1 found in {fontfile}")
75
+
76
+ colorGlyphs = unbuildColrV1(
77
+ colr.table.LayerList,
78
+ colr.table.BaseGlyphList,
79
+ )
80
+
81
+ pprint(colorGlyphs)
lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/fontTools/designspaceLib/__main__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import sys
2
+ from fontTools.designspaceLib import main
3
+
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/__main__.cpython-312.pyc ADDED
Binary file (390 Bytes). View file
 
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/split.cpython-312.pyc ADDED
Binary file (17.9 kB). View file
 
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/statNames.cpython-312.pyc ADDED
Binary file (9.43 kB). View file
 
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/types.cpython-312.pyc ADDED
Binary file (6.33 kB). View file
 
lib/python3.12/site-packages/fontTools/designspaceLib/split.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Allows building all the variable fonts of a DesignSpace version 5 by
2
+ splitting the document into interpolable sub-space, then into each VF.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import itertools
8
+ import logging
9
+ import math
10
+ from typing import Any, Callable, Dict, Iterator, List, Tuple, cast
11
+
12
+ from fontTools.designspaceLib import (
13
+ AxisDescriptor,
14
+ AxisMappingDescriptor,
15
+ DesignSpaceDocument,
16
+ DiscreteAxisDescriptor,
17
+ InstanceDescriptor,
18
+ RuleDescriptor,
19
+ SimpleLocationDict,
20
+ SourceDescriptor,
21
+ VariableFontDescriptor,
22
+ )
23
+ from fontTools.designspaceLib.statNames import StatNames, getStatNames
24
+ from fontTools.designspaceLib.types import (
25
+ ConditionSet,
26
+ Range,
27
+ Region,
28
+ getVFUserRegion,
29
+ locationInRegion,
30
+ regionInRegion,
31
+ userRegionToDesignRegion,
32
+ )
33
+
34
+ LOGGER = logging.getLogger(__name__)
35
+
36
+ MakeInstanceFilenameCallable = Callable[
37
+ [DesignSpaceDocument, InstanceDescriptor, StatNames], str
38
+ ]
39
+
40
+
41
+ def defaultMakeInstanceFilename(
42
+ doc: DesignSpaceDocument, instance: InstanceDescriptor, statNames: StatNames
43
+ ) -> str:
44
+ """Default callable to synthesize an instance filename
45
+ when makeNames=True, for instances that don't specify an instance name
46
+ in the designspace. This part of the name generation can be overriden
47
+ because it's not specified by the STAT table.
48
+ """
49
+ familyName = instance.familyName or statNames.familyNames.get("en")
50
+ styleName = instance.styleName or statNames.styleNames.get("en")
51
+ return f"{familyName}-{styleName}.ttf"
52
+
53
+
54
+ def splitInterpolable(
55
+ doc: DesignSpaceDocument,
56
+ makeNames: bool = True,
57
+ expandLocations: bool = True,
58
+ makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
59
+ ) -> Iterator[Tuple[SimpleLocationDict, DesignSpaceDocument]]:
60
+ """Split the given DS5 into several interpolable sub-designspaces.
61
+ There are as many interpolable sub-spaces as there are combinations of
62
+ discrete axis values.
63
+
64
+ E.g. with axes:
65
+ - italic (discrete) Upright or Italic
66
+ - style (discrete) Sans or Serif
67
+ - weight (continuous) 100 to 900
68
+
69
+ There are 4 sub-spaces in which the Weight axis should interpolate:
70
+ (Upright, Sans), (Upright, Serif), (Italic, Sans) and (Italic, Serif).
71
+
72
+ The sub-designspaces still include the full axis definitions and STAT data,
73
+ but the rules, sources, variable fonts, instances are trimmed down to only
74
+ keep what falls within the interpolable sub-space.
75
+
76
+ Args:
77
+ - ``makeNames``: Whether to compute the instance family and style
78
+ names using the STAT data.
79
+ - ``expandLocations``: Whether to turn all locations into "full"
80
+ locations, including implicit default axis values where missing.
81
+ - ``makeInstanceFilename``: Callable to synthesize an instance filename
82
+ when makeNames=True, for instances that don't specify an instance name
83
+ in the designspace. This part of the name generation can be overridden
84
+ because it's not specified by the STAT table.
85
+
86
+ .. versionadded:: 5.0
87
+ """
88
+ discreteAxes = []
89
+ interpolableUserRegion: Region = {}
90
+ for axis in doc.axes:
91
+ if hasattr(axis, "values"):
92
+ # Mypy doesn't support narrowing union types via hasattr()
93
+ # TODO(Python 3.10): use TypeGuard
94
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
95
+ axis = cast(DiscreteAxisDescriptor, axis)
96
+ discreteAxes.append(axis)
97
+ else:
98
+ axis = cast(AxisDescriptor, axis)
99
+ interpolableUserRegion[axis.name] = Range(
100
+ axis.minimum,
101
+ axis.maximum,
102
+ axis.default,
103
+ )
104
+ valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
105
+ for values in valueCombinations:
106
+ discreteUserLocation = {
107
+ discreteAxis.name: value
108
+ for discreteAxis, value in zip(discreteAxes, values)
109
+ }
110
+ subDoc = _extractSubSpace(
111
+ doc,
112
+ {**interpolableUserRegion, **discreteUserLocation},
113
+ keepVFs=True,
114
+ makeNames=makeNames,
115
+ expandLocations=expandLocations,
116
+ makeInstanceFilename=makeInstanceFilename,
117
+ )
118
+ yield discreteUserLocation, subDoc
119
+
120
+
121
+ def splitVariableFonts(
122
+ doc: DesignSpaceDocument,
123
+ makeNames: bool = False,
124
+ expandLocations: bool = False,
125
+ makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
126
+ ) -> Iterator[Tuple[str, DesignSpaceDocument]]:
127
+ """Convert each variable font listed in this document into a standalone
128
+ designspace. This can be used to compile all the variable fonts from a
129
+ format 5 designspace using tools that can only deal with 1 VF at a time.
130
+
131
+ Args:
132
+ - ``makeNames``: Whether to compute the instance family and style
133
+ names using the STAT data.
134
+ - ``expandLocations``: Whether to turn all locations into "full"
135
+ locations, including implicit default axis values where missing.
136
+ - ``makeInstanceFilename``: Callable to synthesize an instance filename
137
+ when makeNames=True, for instances that don't specify an instance name
138
+ in the designspace. This part of the name generation can be overridden
139
+ because it's not specified by the STAT table.
140
+
141
+ .. versionadded:: 5.0
142
+ """
143
+ # Make one DesignspaceDoc v5 for each variable font
144
+ for vf in doc.getVariableFonts():
145
+ vfUserRegion = getVFUserRegion(doc, vf)
146
+ vfDoc = _extractSubSpace(
147
+ doc,
148
+ vfUserRegion,
149
+ keepVFs=False,
150
+ makeNames=makeNames,
151
+ expandLocations=expandLocations,
152
+ makeInstanceFilename=makeInstanceFilename,
153
+ )
154
+ vfDoc.lib = {**vfDoc.lib, **vf.lib}
155
+ yield vf.name, vfDoc
156
+
157
+
158
+ def convert5to4(
159
+ doc: DesignSpaceDocument,
160
+ ) -> Dict[str, DesignSpaceDocument]:
161
+ """Convert each variable font listed in this document into a standalone
162
+ format 4 designspace. This can be used to compile all the variable fonts
163
+ from a format 5 designspace using tools that only know about format 4.
164
+
165
+ .. versionadded:: 5.0
166
+ """
167
+ vfs = {}
168
+ for _location, subDoc in splitInterpolable(doc):
169
+ for vfName, vfDoc in splitVariableFonts(subDoc):
170
+ vfDoc.formatVersion = "4.1"
171
+ vfs[vfName] = vfDoc
172
+ return vfs
173
+
174
+
175
+ def _extractSubSpace(
176
+ doc: DesignSpaceDocument,
177
+ userRegion: Region,
178
+ *,
179
+ keepVFs: bool,
180
+ makeNames: bool,
181
+ expandLocations: bool,
182
+ makeInstanceFilename: MakeInstanceFilenameCallable,
183
+ ) -> DesignSpaceDocument:
184
+ subDoc = DesignSpaceDocument()
185
+ # Don't include STAT info
186
+ # FIXME: (Jany) let's think about it. Not include = OK because the point of
187
+ # the splitting is to build VFs and we'll use the STAT data of the full
188
+ # document to generate the STAT of the VFs, so "no need" to have STAT data
189
+ # in sub-docs. Counterpoint: what if someone wants to split this DS for
190
+ # other purposes? Maybe for that it would be useful to also subset the STAT
191
+ # data?
192
+ # subDoc.elidedFallbackName = doc.elidedFallbackName
193
+
194
+ def maybeExpandDesignLocation(object):
195
+ if expandLocations:
196
+ return object.getFullDesignLocation(doc)
197
+ else:
198
+ return object.designLocation
199
+
200
+ for axis in doc.axes:
201
+ range = userRegion[axis.name]
202
+ if isinstance(range, Range) and hasattr(axis, "minimum"):
203
+ # Mypy doesn't support narrowing union types via hasattr()
204
+ # TODO(Python 3.10): use TypeGuard
205
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
206
+ axis = cast(AxisDescriptor, axis)
207
+ subDoc.addAxis(
208
+ AxisDescriptor(
209
+ # Same info
210
+ tag=axis.tag,
211
+ name=axis.name,
212
+ labelNames=axis.labelNames,
213
+ hidden=axis.hidden,
214
+ # Subset range
215
+ minimum=max(range.minimum, axis.minimum),
216
+ default=range.default or axis.default,
217
+ maximum=min(range.maximum, axis.maximum),
218
+ map=[
219
+ (user, design)
220
+ for user, design in axis.map
221
+ if range.minimum <= user <= range.maximum
222
+ ],
223
+ # Don't include STAT info
224
+ axisOrdering=None,
225
+ axisLabels=None,
226
+ )
227
+ )
228
+
229
+ subDoc.axisMappings = mappings = []
230
+ subDocAxes = {axis.name for axis in subDoc.axes}
231
+ for mapping in doc.axisMappings:
232
+ if not all(axis in subDocAxes for axis in mapping.inputLocation.keys()):
233
+ continue
234
+ if not all(axis in subDocAxes for axis in mapping.outputLocation.keys()):
235
+ LOGGER.error(
236
+ "In axis mapping from input %s, some output axes are not in the variable-font: %s",
237
+ mapping.inputLocation,
238
+ mapping.outputLocation,
239
+ )
240
+ continue
241
+
242
+ mappingAxes = set()
243
+ mappingAxes.update(mapping.inputLocation.keys())
244
+ mappingAxes.update(mapping.outputLocation.keys())
245
+ for axis in doc.axes:
246
+ if axis.name not in mappingAxes:
247
+ continue
248
+ range = userRegion[axis.name]
249
+ if (
250
+ range.minimum != axis.minimum
251
+ or (range.default is not None and range.default != axis.default)
252
+ or range.maximum != axis.maximum
253
+ ):
254
+ LOGGER.error(
255
+ "Limiting axis ranges used in <mapping> elements not supported: %s",
256
+ axis.name,
257
+ )
258
+ continue
259
+
260
+ mappings.append(
261
+ AxisMappingDescriptor(
262
+ inputLocation=mapping.inputLocation,
263
+ outputLocation=mapping.outputLocation,
264
+ )
265
+ )
266
+
267
+ # Don't include STAT info
268
+ # subDoc.locationLabels = doc.locationLabels
269
+
270
+ # Rules: subset them based on conditions
271
+ designRegion = userRegionToDesignRegion(doc, userRegion)
272
+ subDoc.rules = _subsetRulesBasedOnConditions(doc.rules, designRegion)
273
+ subDoc.rulesProcessingLast = doc.rulesProcessingLast
274
+
275
+ # Sources: keep only the ones that fall within the kept axis ranges
276
+ for source in doc.sources:
277
+ if not locationInRegion(doc.map_backward(source.designLocation), userRegion):
278
+ continue
279
+
280
+ subDoc.addSource(
281
+ SourceDescriptor(
282
+ filename=source.filename,
283
+ path=source.path,
284
+ font=source.font,
285
+ name=source.name,
286
+ designLocation=_filterLocation(
287
+ userRegion, maybeExpandDesignLocation(source)
288
+ ),
289
+ layerName=source.layerName,
290
+ familyName=source.familyName,
291
+ styleName=source.styleName,
292
+ muteKerning=source.muteKerning,
293
+ muteInfo=source.muteInfo,
294
+ mutedGlyphNames=source.mutedGlyphNames,
295
+ )
296
+ )
297
+
298
+ # Copy family name translations from the old default source to the new default
299
+ vfDefault = subDoc.findDefault()
300
+ oldDefault = doc.findDefault()
301
+ if vfDefault is not None and oldDefault is not None:
302
+ vfDefault.localisedFamilyName = oldDefault.localisedFamilyName
303
+
304
+ # Variable fonts: keep only the ones that fall within the kept axis ranges
305
+ if keepVFs:
306
+ # Note: call getVariableFont() to make the implicit VFs explicit
307
+ for vf in doc.getVariableFonts():
308
+ vfUserRegion = getVFUserRegion(doc, vf)
309
+ if regionInRegion(vfUserRegion, userRegion):
310
+ subDoc.addVariableFont(
311
+ VariableFontDescriptor(
312
+ name=vf.name,
313
+ filename=vf.filename,
314
+ axisSubsets=[
315
+ axisSubset
316
+ for axisSubset in vf.axisSubsets
317
+ if isinstance(userRegion[axisSubset.name], Range)
318
+ ],
319
+ lib=vf.lib,
320
+ )
321
+ )
322
+
323
+ # Instances: same as Sources + compute missing names
324
+ for instance in doc.instances:
325
+ if not locationInRegion(instance.getFullUserLocation(doc), userRegion):
326
+ continue
327
+
328
+ if makeNames:
329
+ statNames = getStatNames(doc, instance.getFullUserLocation(doc))
330
+ familyName = instance.familyName or statNames.familyNames.get("en")
331
+ styleName = instance.styleName or statNames.styleNames.get("en")
332
+ subDoc.addInstance(
333
+ InstanceDescriptor(
334
+ filename=instance.filename
335
+ or makeInstanceFilename(doc, instance, statNames),
336
+ path=instance.path,
337
+ font=instance.font,
338
+ name=instance.name or f"{familyName} {styleName}",
339
+ userLocation={} if expandLocations else instance.userLocation,
340
+ designLocation=_filterLocation(
341
+ userRegion, maybeExpandDesignLocation(instance)
342
+ ),
343
+ familyName=familyName,
344
+ styleName=styleName,
345
+ postScriptFontName=instance.postScriptFontName
346
+ or statNames.postScriptFontName,
347
+ styleMapFamilyName=instance.styleMapFamilyName
348
+ or statNames.styleMapFamilyNames.get("en"),
349
+ styleMapStyleName=instance.styleMapStyleName
350
+ or statNames.styleMapStyleName,
351
+ localisedFamilyName=instance.localisedFamilyName
352
+ or statNames.familyNames,
353
+ localisedStyleName=instance.localisedStyleName
354
+ or statNames.styleNames,
355
+ localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName
356
+ or statNames.styleMapFamilyNames,
357
+ localisedStyleMapStyleName=instance.localisedStyleMapStyleName
358
+ or {},
359
+ lib=instance.lib,
360
+ )
361
+ )
362
+ else:
363
+ subDoc.addInstance(
364
+ InstanceDescriptor(
365
+ filename=instance.filename,
366
+ path=instance.path,
367
+ font=instance.font,
368
+ name=instance.name,
369
+ userLocation={} if expandLocations else instance.userLocation,
370
+ designLocation=_filterLocation(
371
+ userRegion, maybeExpandDesignLocation(instance)
372
+ ),
373
+ familyName=instance.familyName,
374
+ styleName=instance.styleName,
375
+ postScriptFontName=instance.postScriptFontName,
376
+ styleMapFamilyName=instance.styleMapFamilyName,
377
+ styleMapStyleName=instance.styleMapStyleName,
378
+ localisedFamilyName=instance.localisedFamilyName,
379
+ localisedStyleName=instance.localisedStyleName,
380
+ localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName,
381
+ localisedStyleMapStyleName=instance.localisedStyleMapStyleName,
382
+ lib=instance.lib,
383
+ )
384
+ )
385
+
386
+ subDoc.lib = doc.lib
387
+
388
+ return subDoc
389
+
390
+
391
+ def _conditionSetFrom(conditionSet: List[Dict[str, Any]]) -> ConditionSet:
392
+ c: Dict[str, Range] = {}
393
+ for condition in conditionSet:
394
+ minimum, maximum = condition.get("minimum"), condition.get("maximum")
395
+ c[condition["name"]] = Range(
396
+ minimum if minimum is not None else -math.inf,
397
+ maximum if maximum is not None else math.inf,
398
+ )
399
+ return c
400
+
401
+
402
+ def _subsetRulesBasedOnConditions(
403
+ rules: List[RuleDescriptor], designRegion: Region
404
+ ) -> List[RuleDescriptor]:
405
+ # What rules to keep:
406
+ # - Keep the rule if any conditionset is relevant.
407
+ # - A conditionset is relevant if all conditions are relevant or it is empty.
408
+ # - A condition is relevant if
409
+ # - axis is point (C-AP),
410
+ # - and point in condition's range (C-AP-in)
411
+ # (in this case remove the condition because it's always true)
412
+ # - else (C-AP-out) whole conditionset can be discarded (condition false
413
+ # => conditionset false)
414
+ # - axis is range (C-AR),
415
+ # - (C-AR-all) and axis range fully contained in condition range: we can
416
+ # scrap the condition because it's always true
417
+ # - (C-AR-inter) and intersection(axis range, condition range) not empty:
418
+ # keep the condition with the smaller range (= intersection)
419
+ # - (C-AR-none) else, whole conditionset can be discarded
420
+ newRules: List[RuleDescriptor] = []
421
+ for rule in rules:
422
+ newRule: RuleDescriptor = RuleDescriptor(
423
+ name=rule.name, conditionSets=[], subs=rule.subs
424
+ )
425
+ for conditionset in rule.conditionSets:
426
+ cs = _conditionSetFrom(conditionset)
427
+ newConditionset: List[Dict[str, Any]] = []
428
+ discardConditionset = False
429
+ for selectionName, selectionValue in designRegion.items():
430
+ # TODO: Ensure that all(key in conditionset for key in region.keys())?
431
+ if selectionName not in cs:
432
+ # raise Exception("Selection has different axes than the rules")
433
+ continue
434
+ if isinstance(selectionValue, (float, int)): # is point
435
+ # Case C-AP-in
436
+ if selectionValue in cs[selectionName]:
437
+ pass # always matches, conditionset can stay empty for this one.
438
+ # Case C-AP-out
439
+ else:
440
+ discardConditionset = True
441
+ else: # is range
442
+ # Case C-AR-all
443
+ if selectionValue in cs[selectionName]:
444
+ pass # always matches, conditionset can stay empty for this one.
445
+ else:
446
+ intersection = cs[selectionName].intersection(selectionValue)
447
+ # Case C-AR-inter
448
+ if intersection is not None:
449
+ newConditionset.append(
450
+ {
451
+ "name": selectionName,
452
+ "minimum": intersection.minimum,
453
+ "maximum": intersection.maximum,
454
+ }
455
+ )
456
+ # Case C-AR-none
457
+ else:
458
+ discardConditionset = True
459
+ if not discardConditionset:
460
+ newRule.conditionSets.append(newConditionset)
461
+ if newRule.conditionSets:
462
+ newRules.append(newRule)
463
+
464
+ return newRules
465
+
466
+
467
+ def _filterLocation(
468
+ userRegion: Region,
469
+ location: Dict[str, float],
470
+ ) -> Dict[str, float]:
471
+ return {
472
+ name: value
473
+ for name, value in location.items()
474
+ if name in userRegion and isinstance(userRegion[name], Range)
475
+ }
lib/python3.12/site-packages/fontTools/designspaceLib/statNames.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute name information for a given location in user-space coordinates
2
+ using STAT data. This can be used to fill-in automatically the names of an
3
+ instance:
4
+
5
+ .. code:: python
6
+
7
+ instance = doc.instances[0]
8
+ names = getStatNames(doc, instance.getFullUserLocation(doc))
9
+ print(names.styleNames)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Dict, Literal, Optional, Tuple, Union
16
+ import logging
17
+
18
+ from fontTools.designspaceLib import (
19
+ AxisDescriptor,
20
+ AxisLabelDescriptor,
21
+ DesignSpaceDocument,
22
+ DiscreteAxisDescriptor,
23
+ SimpleLocationDict,
24
+ SourceDescriptor,
25
+ )
26
+
27
+ LOGGER = logging.getLogger(__name__)
28
+
29
+ RibbiStyleName = Union[
30
+ Literal["regular"],
31
+ Literal["bold"],
32
+ Literal["italic"],
33
+ Literal["bold italic"],
34
+ ]
35
+
36
+ BOLD_ITALIC_TO_RIBBI_STYLE = {
37
+ (False, False): "regular",
38
+ (False, True): "italic",
39
+ (True, False): "bold",
40
+ (True, True): "bold italic",
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class StatNames:
46
+ """Name data generated from the STAT table information."""
47
+
48
+ familyNames: Dict[str, str]
49
+ styleNames: Dict[str, str]
50
+ postScriptFontName: Optional[str]
51
+ styleMapFamilyNames: Dict[str, str]
52
+ styleMapStyleName: Optional[RibbiStyleName]
53
+
54
+
55
+ def getStatNames(
56
+ doc: DesignSpaceDocument, userLocation: SimpleLocationDict
57
+ ) -> StatNames:
58
+ """Compute the family, style, PostScript names of the given ``userLocation``
59
+ using the document's STAT information.
60
+
61
+ Also computes localizations.
62
+
63
+ If not enough STAT data is available for a given name, either its dict of
64
+ localized names will be empty (family and style names), or the name will be
65
+ None (PostScript name).
66
+
67
+ Note: this method does not consider info attached to the instance, like
68
+ family name. The user needs to override all names on an instance that STAT
69
+ information would compute differently than desired.
70
+
71
+ .. versionadded:: 5.0
72
+ """
73
+ familyNames: Dict[str, str] = {}
74
+ defaultSource: Optional[SourceDescriptor] = doc.findDefault()
75
+ if defaultSource is None:
76
+ LOGGER.warning("Cannot determine default source to look up family name.")
77
+ elif defaultSource.familyName is None:
78
+ LOGGER.warning(
79
+ "Cannot look up family name, assign the 'familyname' attribute to the default source."
80
+ )
81
+ else:
82
+ familyNames = {
83
+ "en": defaultSource.familyName,
84
+ **defaultSource.localisedFamilyName,
85
+ }
86
+
87
+ styleNames: Dict[str, str] = {}
88
+ # If a free-standing label matches the location, use it for name generation.
89
+ label = doc.labelForUserLocation(userLocation)
90
+ if label is not None:
91
+ styleNames = {"en": label.name, **label.labelNames}
92
+ # Otherwise, scour the axis labels for matches.
93
+ else:
94
+ # Gather all languages in which at least one translation is provided
95
+ # Then build names for all these languages, but fallback to English
96
+ # whenever a translation is missing.
97
+ labels = _getAxisLabelsForUserLocation(doc.axes, userLocation)
98
+ if labels:
99
+ languages = set(
100
+ language for label in labels for language in label.labelNames
101
+ )
102
+ languages.add("en")
103
+ for language in languages:
104
+ styleName = " ".join(
105
+ label.labelNames.get(language, label.defaultName)
106
+ for label in labels
107
+ if not label.elidable
108
+ )
109
+ if not styleName and doc.elidedFallbackName is not None:
110
+ styleName = doc.elidedFallbackName
111
+ styleNames[language] = styleName
112
+
113
+ if "en" not in familyNames or "en" not in styleNames:
114
+ # Not enough information to compute PS names of styleMap names
115
+ return StatNames(
116
+ familyNames=familyNames,
117
+ styleNames=styleNames,
118
+ postScriptFontName=None,
119
+ styleMapFamilyNames={},
120
+ styleMapStyleName=None,
121
+ )
122
+
123
+ postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")
124
+
125
+ styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation)
126
+
127
+ styleNamesForStyleMap = styleNames
128
+ if regularUserLocation != userLocation:
129
+ regularStatNames = getStatNames(doc, regularUserLocation)
130
+ styleNamesForStyleMap = regularStatNames.styleNames
131
+
132
+ styleMapFamilyNames = {}
133
+ for language in set(familyNames).union(styleNames.keys()):
134
+ familyName = familyNames.get(language, familyNames["en"])
135
+ styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"])
136
+ styleMapFamilyNames[language] = (familyName + " " + styleName).strip()
137
+
138
+ return StatNames(
139
+ familyNames=familyNames,
140
+ styleNames=styleNames,
141
+ postScriptFontName=postScriptFontName,
142
+ styleMapFamilyNames=styleMapFamilyNames,
143
+ styleMapStyleName=styleMapStyleName,
144
+ )
145
+
146
+
147
+ def _getSortedAxisLabels(
148
+ axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
149
+ ) -> Dict[str, list[AxisLabelDescriptor]]:
150
+ """Returns axis labels sorted by their ordering, with unordered ones appended as
151
+ they are listed."""
152
+
153
+ # First, get the axis labels with explicit ordering...
154
+ sortedAxes = sorted(
155
+ (axis for axis in axes if axis.axisOrdering is not None),
156
+ key=lambda a: a.axisOrdering,
157
+ )
158
+ sortedLabels: Dict[str, list[AxisLabelDescriptor]] = {
159
+ axis.name: axis.axisLabels for axis in sortedAxes
160
+ }
161
+
162
+ # ... then append the others in the order they appear.
163
+ # NOTE: This relies on Python 3.7+ dict's preserved insertion order.
164
+ for axis in axes:
165
+ if axis.axisOrdering is None:
166
+ sortedLabels[axis.name] = axis.axisLabels
167
+
168
+ return sortedLabels
169
+
170
+
171
+ def _getAxisLabelsForUserLocation(
172
+ axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
173
+ userLocation: SimpleLocationDict,
174
+ ) -> list[AxisLabelDescriptor]:
175
+ labels: list[AxisLabelDescriptor] = []
176
+
177
+ allAxisLabels = _getSortedAxisLabels(axes)
178
+ if allAxisLabels.keys() != userLocation.keys():
179
+ LOGGER.warning(
180
+ f"Mismatch between user location '{userLocation.keys()}' and available "
181
+ f"labels for '{allAxisLabels.keys()}'."
182
+ )
183
+
184
+ for axisName, axisLabels in allAxisLabels.items():
185
+ userValue = userLocation[axisName]
186
+ label: Optional[AxisLabelDescriptor] = next(
187
+ (
188
+ l
189
+ for l in axisLabels
190
+ if l.userValue == userValue
191
+ or (
192
+ l.userMinimum is not None
193
+ and l.userMaximum is not None
194
+ and l.userMinimum <= userValue <= l.userMaximum
195
+ )
196
+ ),
197
+ None,
198
+ )
199
+ if label is None:
200
+ LOGGER.debug(
201
+ f"Document needs a label for axis '{axisName}', user value '{userValue}'."
202
+ )
203
+ else:
204
+ labels.append(label)
205
+
206
+ return labels
207
+
208
+
209
+ def _getRibbiStyle(
210
+ self: DesignSpaceDocument, userLocation: SimpleLocationDict
211
+ ) -> Tuple[RibbiStyleName, SimpleLocationDict]:
212
+ """Compute the RIBBI style name of the given user location,
213
+ return the location of the matching Regular in the RIBBI group.
214
+
215
+ .. versionadded:: 5.0
216
+ """
217
+ regularUserLocation = {}
218
+ axes_by_tag = {axis.tag: axis for axis in self.axes}
219
+
220
+ bold: bool = False
221
+ italic: bool = False
222
+
223
+ axis = axes_by_tag.get("wght")
224
+ if axis is not None:
225
+ for regular_label in axis.axisLabels:
226
+ if (
227
+ regular_label.linkedUserValue == userLocation[axis.name]
228
+ # In the "recursive" case where both the Regular has
229
+ # linkedUserValue pointing the Bold, and the Bold has
230
+ # linkedUserValue pointing to the Regular, only consider the
231
+ # first case: Regular (e.g. 400) has linkedUserValue pointing to
232
+ # Bold (e.g. 700, higher than Regular)
233
+ and regular_label.userValue < regular_label.linkedUserValue
234
+ ):
235
+ regularUserLocation[axis.name] = regular_label.userValue
236
+ bold = True
237
+ break
238
+
239
+ axis = axes_by_tag.get("ital") or axes_by_tag.get("slnt")
240
+ if axis is not None:
241
+ for upright_label in axis.axisLabels:
242
+ if (
243
+ upright_label.linkedUserValue == userLocation[axis.name]
244
+ # In the "recursive" case where both the Upright has
245
+ # linkedUserValue pointing the Italic, and the Italic has
246
+ # linkedUserValue pointing to the Upright, only consider the
247
+ # first case: Upright (e.g. ital=0, slant=0) has
248
+ # linkedUserValue pointing to Italic (e.g ital=1, slant=-12 or
249
+ # slant=12 for backwards italics, in any case higher than
250
+ # Upright in absolute value, hence the abs() below.
251
+ and abs(upright_label.userValue) < abs(upright_label.linkedUserValue)
252
+ ):
253
+ regularUserLocation[axis.name] = upright_label.userValue
254
+ italic = True
255
+ break
256
+
257
+ return BOLD_ITALIC_TO_RIBBI_STYLE[bold, italic], {
258
+ **userLocation,
259
+ **regularUserLocation,
260
+ }
lib/python3.12/site-packages/fontTools/designspaceLib/types.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List, Optional, Union, cast
5
+
6
+ from fontTools.designspaceLib import (
7
+ AxisDescriptor,
8
+ DesignSpaceDocument,
9
+ DesignSpaceDocumentError,
10
+ RangeAxisSubsetDescriptor,
11
+ SimpleLocationDict,
12
+ ValueAxisSubsetDescriptor,
13
+ VariableFontDescriptor,
14
+ )
15
+
16
+
17
+ def clamp(value, minimum, maximum):
18
+ return min(max(value, minimum), maximum)
19
+
20
+
21
+ @dataclass
22
+ class Range:
23
+ minimum: float
24
+ """Inclusive minimum of the range."""
25
+ maximum: float
26
+ """Inclusive maximum of the range."""
27
+ default: float = 0
28
+ """Default value"""
29
+
30
+ def __post_init__(self):
31
+ self.minimum, self.maximum = sorted((self.minimum, self.maximum))
32
+ self.default = clamp(self.default, self.minimum, self.maximum)
33
+
34
+ def __contains__(self, value: Union[float, Range]) -> bool:
35
+ if isinstance(value, Range):
36
+ return self.minimum <= value.minimum and value.maximum <= self.maximum
37
+ return self.minimum <= value <= self.maximum
38
+
39
+ def intersection(self, other: Range) -> Optional[Range]:
40
+ if self.maximum < other.minimum or self.minimum > other.maximum:
41
+ return None
42
+ else:
43
+ return Range(
44
+ max(self.minimum, other.minimum),
45
+ min(self.maximum, other.maximum),
46
+ self.default, # We don't care about the default in this use-case
47
+ )
48
+
49
+
50
+ # A region selection is either a range or a single value, as a Designspace v5
51
+ # axis-subset element only allows a single discrete value or a range for a
52
+ # variable-font element.
53
+ Region = Dict[str, Union[Range, float]]
54
+
55
+ # A conditionset is a set of named ranges.
56
+ ConditionSet = Dict[str, Range]
57
+
58
+ # A rule is a list of conditionsets where any has to be relevant for the whole rule to be relevant.
59
+ Rule = List[ConditionSet]
60
+ Rules = Dict[str, Rule]
61
+
62
+
63
+ def locationInRegion(location: SimpleLocationDict, region: Region) -> bool:
64
+ for name, value in location.items():
65
+ if name not in region:
66
+ return False
67
+ regionValue = region[name]
68
+ if isinstance(regionValue, (float, int)):
69
+ if value != regionValue:
70
+ return False
71
+ else:
72
+ if value not in regionValue:
73
+ return False
74
+ return True
75
+
76
+
77
+ def regionInRegion(region: Region, superRegion: Region) -> bool:
78
+ for name, value in region.items():
79
+ if not name in superRegion:
80
+ return False
81
+ superValue = superRegion[name]
82
+ if isinstance(superValue, (float, int)):
83
+ if value != superValue:
84
+ return False
85
+ else:
86
+ if value not in superValue:
87
+ return False
88
+ return True
89
+
90
+
91
+ def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Region:
92
+ designRegion = {}
93
+ for name, value in userRegion.items():
94
+ axis = doc.getAxis(name)
95
+ if axis is None:
96
+ raise DesignSpaceDocumentError(
97
+ f"Cannot find axis named '{name}' for region."
98
+ )
99
+ if isinstance(value, (float, int)):
100
+ designRegion[name] = axis.map_forward(value)
101
+ else:
102
+ designRegion[name] = Range(
103
+ axis.map_forward(value.minimum),
104
+ axis.map_forward(value.maximum),
105
+ axis.map_forward(value.default),
106
+ )
107
+ return designRegion
108
+
109
+
110
+ def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region:
111
+ vfUserRegion: Region = {}
112
+ # For each axis, 2 cases:
113
+ # - it has a range = it's an axis in the VF DS
114
+ # - it's a single location = use it to know which rules should apply in the VF
115
+ for axisSubset in vf.axisSubsets:
116
+ axis = doc.getAxis(axisSubset.name)
117
+ if axis is None:
118
+ raise DesignSpaceDocumentError(
119
+ f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'."
120
+ )
121
+ if hasattr(axisSubset, "userMinimum"):
122
+ # Mypy doesn't support narrowing union types via hasattr()
123
+ # TODO(Python 3.10): use TypeGuard
124
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
125
+ axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset)
126
+ if not hasattr(axis, "minimum"):
127
+ raise DesignSpaceDocumentError(
128
+ f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' "
129
+ "because it's a discrete axis, use only 'userValue' instead."
130
+ )
131
+ axis = cast(AxisDescriptor, axis)
132
+ vfUserRegion[axis.name] = Range(
133
+ max(axisSubset.userMinimum, axis.minimum),
134
+ min(axisSubset.userMaximum, axis.maximum),
135
+ axisSubset.userDefault or axis.default,
136
+ )
137
+ else:
138
+ axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset)
139
+ vfUserRegion[axis.name] = axisSubset.userValue
140
+ # Any axis not mentioned explicitly has a single location = default value
141
+ for axis in doc.axes:
142
+ if axis.name not in vfUserRegion:
143
+ assert isinstance(
144
+ axis.default, (int, float)
145
+ ), f"Axis '{axis.name}' has no valid default value."
146
+ vfUserRegion[axis.name] = axis.default
147
+ return vfUserRegion
lib/python3.12/site-packages/fontTools/encodings/MacRoman.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MacRoman = [
2
+ "NUL",
3
+ "Eth",
4
+ "eth",
5
+ "Lslash",
6
+ "lslash",
7
+ "Scaron",
8
+ "scaron",
9
+ "Yacute",
10
+ "yacute",
11
+ "HT",
12
+ "LF",
13
+ "Thorn",
14
+ "thorn",
15
+ "CR",
16
+ "Zcaron",
17
+ "zcaron",
18
+ "DLE",
19
+ "DC1",
20
+ "DC2",
21
+ "DC3",
22
+ "DC4",
23
+ "onehalf",
24
+ "onequarter",
25
+ "onesuperior",
26
+ "threequarters",
27
+ "threesuperior",
28
+ "twosuperior",
29
+ "brokenbar",
30
+ "minus",
31
+ "multiply",
32
+ "RS",
33
+ "US",
34
+ "space",
35
+ "exclam",
36
+ "quotedbl",
37
+ "numbersign",
38
+ "dollar",
39
+ "percent",
40
+ "ampersand",
41
+ "quotesingle",
42
+ "parenleft",
43
+ "parenright",
44
+ "asterisk",
45
+ "plus",
46
+ "comma",
47
+ "hyphen",
48
+ "period",
49
+ "slash",
50
+ "zero",
51
+ "one",
52
+ "two",
53
+ "three",
54
+ "four",
55
+ "five",
56
+ "six",
57
+ "seven",
58
+ "eight",
59
+ "nine",
60
+ "colon",
61
+ "semicolon",
62
+ "less",
63
+ "equal",
64
+ "greater",
65
+ "question",
66
+ "at",
67
+ "A",
68
+ "B",
69
+ "C",
70
+ "D",
71
+ "E",
72
+ "F",
73
+ "G",
74
+ "H",
75
+ "I",
76
+ "J",
77
+ "K",
78
+ "L",
79
+ "M",
80
+ "N",
81
+ "O",
82
+ "P",
83
+ "Q",
84
+ "R",
85
+ "S",
86
+ "T",
87
+ "U",
88
+ "V",
89
+ "W",
90
+ "X",
91
+ "Y",
92
+ "Z",
93
+ "bracketleft",
94
+ "backslash",
95
+ "bracketright",
96
+ "asciicircum",
97
+ "underscore",
98
+ "grave",
99
+ "a",
100
+ "b",
101
+ "c",
102
+ "d",
103
+ "e",
104
+ "f",
105
+ "g",
106
+ "h",
107
+ "i",
108
+ "j",
109
+ "k",
110
+ "l",
111
+ "m",
112
+ "n",
113
+ "o",
114
+ "p",
115
+ "q",
116
+ "r",
117
+ "s",
118
+ "t",
119
+ "u",
120
+ "v",
121
+ "w",
122
+ "x",
123
+ "y",
124
+ "z",
125
+ "braceleft",
126
+ "bar",
127
+ "braceright",
128
+ "asciitilde",
129
+ "DEL",
130
+ "Adieresis",
131
+ "Aring",
132
+ "Ccedilla",
133
+ "Eacute",
134
+ "Ntilde",
135
+ "Odieresis",
136
+ "Udieresis",
137
+ "aacute",
138
+ "agrave",
139
+ "acircumflex",
140
+ "adieresis",
141
+ "atilde",
142
+ "aring",
143
+ "ccedilla",
144
+ "eacute",
145
+ "egrave",
146
+ "ecircumflex",
147
+ "edieresis",
148
+ "iacute",
149
+ "igrave",
150
+ "icircumflex",
151
+ "idieresis",
152
+ "ntilde",
153
+ "oacute",
154
+ "ograve",
155
+ "ocircumflex",
156
+ "odieresis",
157
+ "otilde",
158
+ "uacute",
159
+ "ugrave",
160
+ "ucircumflex",
161
+ "udieresis",
162
+ "dagger",
163
+ "degree",
164
+ "cent",
165
+ "sterling",
166
+ "section",
167
+ "bullet",
168
+ "paragraph",
169
+ "germandbls",
170
+ "registered",
171
+ "copyright",
172
+ "trademark",
173
+ "acute",
174
+ "dieresis",
175
+ "notequal",
176
+ "AE",
177
+ "Oslash",
178
+ "infinity",
179
+ "plusminus",
180
+ "lessequal",
181
+ "greaterequal",
182
+ "yen",
183
+ "mu",
184
+ "partialdiff",
185
+ "summation",
186
+ "product",
187
+ "pi",
188
+ "integral",
189
+ "ordfeminine",
190
+ "ordmasculine",
191
+ "Omega",
192
+ "ae",
193
+ "oslash",
194
+ "questiondown",
195
+ "exclamdown",
196
+ "logicalnot",
197
+ "radical",
198
+ "florin",
199
+ "approxequal",
200
+ "Delta",
201
+ "guillemotleft",
202
+ "guillemotright",
203
+ "ellipsis",
204
+ "nbspace",
205
+ "Agrave",
206
+ "Atilde",
207
+ "Otilde",
208
+ "OE",
209
+ "oe",
210
+ "endash",
211
+ "emdash",
212
+ "quotedblleft",
213
+ "quotedblright",
214
+ "quoteleft",
215
+ "quoteright",
216
+ "divide",
217
+ "lozenge",
218
+ "ydieresis",
219
+ "Ydieresis",
220
+ "fraction",
221
+ "currency",
222
+ "guilsinglleft",
223
+ "guilsinglright",
224
+ "fi",
225
+ "fl",
226
+ "daggerdbl",
227
+ "periodcentered",
228
+ "quotesinglbase",
229
+ "quotedblbase",
230
+ "perthousand",
231
+ "Acircumflex",
232
+ "Ecircumflex",
233
+ "Aacute",
234
+ "Edieresis",
235
+ "Egrave",
236
+ "Iacute",
237
+ "Icircumflex",
238
+ "Idieresis",
239
+ "Igrave",
240
+ "Oacute",
241
+ "Ocircumflex",
242
+ "apple",
243
+ "Ograve",
244
+ "Uacute",
245
+ "Ucircumflex",
246
+ "Ugrave",
247
+ "dotlessi",
248
+ "circumflex",
249
+ "tilde",
250
+ "macron",
251
+ "breve",
252
+ "dotaccent",
253
+ "ring",
254
+ "cedilla",
255
+ "hungarumlaut",
256
+ "ogonek",
257
+ "caron",
258
+ ]
lib/python3.12/site-packages/fontTools/encodings/StandardEncoding.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ StandardEncoding = [
2
+ ".notdef",
3
+ ".notdef",
4
+ ".notdef",
5
+ ".notdef",
6
+ ".notdef",
7
+ ".notdef",
8
+ ".notdef",
9
+ ".notdef",
10
+ ".notdef",
11
+ ".notdef",
12
+ ".notdef",
13
+ ".notdef",
14
+ ".notdef",
15
+ ".notdef",
16
+ ".notdef",
17
+ ".notdef",
18
+ ".notdef",
19
+ ".notdef",
20
+ ".notdef",
21
+ ".notdef",
22
+ ".notdef",
23
+ ".notdef",
24
+ ".notdef",
25
+ ".notdef",
26
+ ".notdef",
27
+ ".notdef",
28
+ ".notdef",
29
+ ".notdef",
30
+ ".notdef",
31
+ ".notdef",
32
+ ".notdef",
33
+ ".notdef",
34
+ "space",
35
+ "exclam",
36
+ "quotedbl",
37
+ "numbersign",
38
+ "dollar",
39
+ "percent",
40
+ "ampersand",
41
+ "quoteright",
42
+ "parenleft",
43
+ "parenright",
44
+ "asterisk",
45
+ "plus",
46
+ "comma",
47
+ "hyphen",
48
+ "period",
49
+ "slash",
50
+ "zero",
51
+ "one",
52
+ "two",
53
+ "three",
54
+ "four",
55
+ "five",
56
+ "six",
57
+ "seven",
58
+ "eight",
59
+ "nine",
60
+ "colon",
61
+ "semicolon",
62
+ "less",
63
+ "equal",
64
+ "greater",
65
+ "question",
66
+ "at",
67
+ "A",
68
+ "B",
69
+ "C",
70
+ "D",
71
+ "E",
72
+ "F",
73
+ "G",
74
+ "H",
75
+ "I",
76
+ "J",
77
+ "K",
78
+ "L",
79
+ "M",
80
+ "N",
81
+ "O",
82
+ "P",
83
+ "Q",
84
+ "R",
85
+ "S",
86
+ "T",
87
+ "U",
88
+ "V",
89
+ "W",
90
+ "X",
91
+ "Y",
92
+ "Z",
93
+ "bracketleft",
94
+ "backslash",
95
+ "bracketright",
96
+ "asciicircum",
97
+ "underscore",
98
+ "quoteleft",
99
+ "a",
100
+ "b",
101
+ "c",
102
+ "d",
103
+ "e",
104
+ "f",
105
+ "g",
106
+ "h",
107
+ "i",
108
+ "j",
109
+ "k",
110
+ "l",
111
+ "m",
112
+ "n",
113
+ "o",
114
+ "p",
115
+ "q",
116
+ "r",
117
+ "s",
118
+ "t",
119
+ "u",
120
+ "v",
121
+ "w",
122
+ "x",
123
+ "y",
124
+ "z",
125
+ "braceleft",
126
+ "bar",
127
+ "braceright",
128
+ "asciitilde",
129
+ ".notdef",
130
+ ".notdef",
131
+ ".notdef",
132
+ ".notdef",
133
+ ".notdef",
134
+ ".notdef",
135
+ ".notdef",
136
+ ".notdef",
137
+ ".notdef",
138
+ ".notdef",
139
+ ".notdef",
140
+ ".notdef",
141
+ ".notdef",
142
+ ".notdef",
143
+ ".notdef",
144
+ ".notdef",
145
+ ".notdef",
146
+ ".notdef",
147
+ ".notdef",
148
+ ".notdef",
149
+ ".notdef",
150
+ ".notdef",
151
+ ".notdef",
152
+ ".notdef",
153
+ ".notdef",
154
+ ".notdef",
155
+ ".notdef",
156
+ ".notdef",
157
+ ".notdef",
158
+ ".notdef",
159
+ ".notdef",
160
+ ".notdef",
161
+ ".notdef",
162
+ ".notdef",
163
+ "exclamdown",
164
+ "cent",
165
+ "sterling",
166
+ "fraction",
167
+ "yen",
168
+ "florin",
169
+ "section",
170
+ "currency",
171
+ "quotesingle",
172
+ "quotedblleft",
173
+ "guillemotleft",
174
+ "guilsinglleft",
175
+ "guilsinglright",
176
+ "fi",
177
+ "fl",
178
+ ".notdef",
179
+ "endash",
180
+ "dagger",
181
+ "daggerdbl",
182
+ "periodcentered",
183
+ ".notdef",
184
+ "paragraph",
185
+ "bullet",
186
+ "quotesinglbase",
187
+ "quotedblbase",
188
+ "quotedblright",
189
+ "guillemotright",
190
+ "ellipsis",
191
+ "perthousand",
192
+ ".notdef",
193
+ "questiondown",
194
+ ".notdef",
195
+ "grave",
196
+ "acute",
197
+ "circumflex",
198
+ "tilde",
199
+ "macron",
200
+ "breve",
201
+ "dotaccent",
202
+ "dieresis",
203
+ ".notdef",
204
+ "ring",
205
+ "cedilla",
206
+ ".notdef",
207
+ "hungarumlaut",
208
+ "ogonek",
209
+ "caron",
210
+ "emdash",
211
+ ".notdef",
212
+ ".notdef",
213
+ ".notdef",
214
+ ".notdef",
215
+ ".notdef",
216
+ ".notdef",
217
+ ".notdef",
218
+ ".notdef",
219
+ ".notdef",
220
+ ".notdef",
221
+ ".notdef",
222
+ ".notdef",
223
+ ".notdef",
224
+ ".notdef",
225
+ ".notdef",
226
+ ".notdef",
227
+ "AE",
228
+ ".notdef",
229
+ "ordfeminine",
230
+ ".notdef",
231
+ ".notdef",
232
+ ".notdef",
233
+ ".notdef",
234
+ "Lslash",
235
+ "Oslash",
236
+ "OE",
237
+ "ordmasculine",
238
+ ".notdef",
239
+ ".notdef",
240
+ ".notdef",
241
+ ".notdef",
242
+ ".notdef",
243
+ "ae",
244
+ ".notdef",
245
+ ".notdef",
246
+ ".notdef",
247
+ "dotlessi",
248
+ ".notdef",
249
+ ".notdef",
250
+ "lslash",
251
+ "oslash",
252
+ "oe",
253
+ "germandbls",
254
+ ".notdef",
255
+ ".notdef",
256
+ ".notdef",
257
+ ".notdef",
258
+ ]
lib/python3.12/site-packages/fontTools/encodings/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Empty __init__.py file to signal Python this directory is a package."""
lib/python3.12/site-packages/fontTools/encodings/__pycache__/MacRoman.cpython-312.pyc ADDED
Binary file (2.25 kB). View file
 
lib/python3.12/site-packages/fontTools/encodings/__pycache__/StandardEncoding.cpython-312.pyc ADDED
Binary file (1.84 kB). View file
 
lib/python3.12/site-packages/fontTools/encodings/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (281 Bytes). View file
 
lib/python3.12/site-packages/fontTools/encodings/__pycache__/codecs.cpython-312.pyc ADDED
Binary file (5.74 kB). View file
 
lib/python3.12/site-packages/fontTools/encodings/codecs.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Extend the Python codecs module with a few encodings that are used in OpenType (name table)
2
+ but missing from Python. See https://github.com/fonttools/fonttools/issues/236 for details."""
3
+
4
+ import codecs
5
+ import encodings
6
+
7
+
8
+ class ExtendCodec(codecs.Codec):
9
+ def __init__(self, name, base_encoding, mapping):
10
+ self.name = name
11
+ self.base_encoding = base_encoding
12
+ self.mapping = mapping
13
+ self.reverse = {v: k for k, v in mapping.items()}
14
+ self.max_len = max(len(v) for v in mapping.values())
15
+ self.info = codecs.CodecInfo(
16
+ name=self.name, encode=self.encode, decode=self.decode
17
+ )
18
+ codecs.register_error(name, self.error)
19
+
20
+ def _map(self, mapper, output_type, exc_type, input, errors):
21
+ base_error_handler = codecs.lookup_error(errors)
22
+ length = len(input)
23
+ out = output_type()
24
+ while input:
25
+ # first try to use self.error as the error handler
26
+ try:
27
+ part = mapper(input, self.base_encoding, errors=self.name)
28
+ out += part
29
+ break # All converted
30
+ except exc_type as e:
31
+ # else convert the correct part, handle error as requested and continue
32
+ out += mapper(input[: e.start], self.base_encoding, self.name)
33
+ replacement, pos = base_error_handler(e)
34
+ out += replacement
35
+ input = input[pos:]
36
+ return out, length
37
+
38
+ def encode(self, input, errors="strict"):
39
+ return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
40
+
41
+ def decode(self, input, errors="strict"):
42
+ return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
43
+
44
+ def error(self, e):
45
+ if isinstance(e, UnicodeDecodeError):
46
+ for end in range(e.start + 1, e.end + 1):
47
+ s = e.object[e.start : end]
48
+ if s in self.mapping:
49
+ return self.mapping[s], end
50
+ elif isinstance(e, UnicodeEncodeError):
51
+ for end in range(e.start + 1, e.start + self.max_len + 1):
52
+ s = e.object[e.start : end]
53
+ if s in self.reverse:
54
+ return self.reverse[s], end
55
+ e.encoding = self.name
56
+ raise e
57
+
58
+
59
+ _extended_encodings = {
60
+ "x_mac_japanese_ttx": (
61
+ "shift_jis",
62
+ {
63
+ b"\xFC": chr(0x007C),
64
+ b"\x7E": chr(0x007E),
65
+ b"\x80": chr(0x005C),
66
+ b"\xA0": chr(0x00A0),
67
+ b"\xFD": chr(0x00A9),
68
+ b"\xFE": chr(0x2122),
69
+ b"\xFF": chr(0x2026),
70
+ },
71
+ ),
72
+ "x_mac_trad_chinese_ttx": (
73
+ "big5",
74
+ {
75
+ b"\x80": chr(0x005C),
76
+ b"\xA0": chr(0x00A0),
77
+ b"\xFD": chr(0x00A9),
78
+ b"\xFE": chr(0x2122),
79
+ b"\xFF": chr(0x2026),
80
+ },
81
+ ),
82
+ "x_mac_korean_ttx": (
83
+ "euc_kr",
84
+ {
85
+ b"\x80": chr(0x00A0),
86
+ b"\x81": chr(0x20A9),
87
+ b"\x82": chr(0x2014),
88
+ b"\x83": chr(0x00A9),
89
+ b"\xFE": chr(0x2122),
90
+ b"\xFF": chr(0x2026),
91
+ },
92
+ ),
93
+ "x_mac_simp_chinese_ttx": (
94
+ "gb2312",
95
+ {
96
+ b"\x80": chr(0x00FC),
97
+ b"\xA0": chr(0x00A0),
98
+ b"\xFD": chr(0x00A9),
99
+ b"\xFE": chr(0x2122),
100
+ b"\xFF": chr(0x2026),
101
+ },
102
+ ),
103
+ }
104
+
105
+ _cache = {}
106
+
107
+
108
+ def search_function(name):
109
+ name = encodings.normalize_encoding(name) # Rather undocumented...
110
+ if name in _extended_encodings:
111
+ if name not in _cache:
112
+ base_encoding, mapping = _extended_encodings[name]
113
+ assert name[-4:] == "_ttx"
114
+ # Python 2 didn't have any of the encodings that we are implementing
115
+ # in this file. Python 3 added aliases for the East Asian ones, mapping
116
+ # them "temporarily" to the same base encoding as us, with a comment
117
+ # suggesting that full implementation will appear some time later.
118
+ # As such, try the Python version of the x_mac_... first, if that is found,
119
+ # use *that* as our base encoding. This would make our encoding upgrade
120
+ # to the full encoding when and if Python finally implements that.
121
+ # http://bugs.python.org/issue24041
122
+ base_encodings = [name[:-4], base_encoding]
123
+ for base_encoding in base_encodings:
124
+ try:
125
+ codecs.lookup(base_encoding)
126
+ except LookupError:
127
+ continue
128
+ _cache[name] = ExtendCodec(name, base_encoding, mapping)
129
+ break
130
+ return _cache[name].info
131
+
132
+ return None
133
+
134
+
135
+ codecs.register(search_function)
lib/python3.12/site-packages/fontTools/fontBuilder.py ADDED
@@ -0,0 +1,1014 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __all__ = ["FontBuilder"]
2
+
3
+ """
4
+ This module is *experimental*, meaning it still may evolve and change.
5
+
6
+ The `FontBuilder` class is a convenient helper to construct working TTF or
7
+ OTF fonts from scratch.
8
+
9
+ Note that the various setup methods cannot be called in arbitrary order,
10
+ due to various interdependencies between OpenType tables. Here is an order
11
+ that works:
12
+
13
+ fb = FontBuilder(...)
14
+ fb.setupGlyphOrder(...)
15
+ fb.setupCharacterMap(...)
16
+ fb.setupGlyf(...) --or-- fb.setupCFF(...)
17
+ fb.setupHorizontalMetrics(...)
18
+ fb.setupHorizontalHeader()
19
+ fb.setupNameTable(...)
20
+ fb.setupOS2()
21
+ fb.addOpenTypeFeatures(...)
22
+ fb.setupPost()
23
+ fb.save(...)
24
+
25
+ Here is how to build a minimal TTF:
26
+
27
+ ```python
28
+ from fontTools.fontBuilder import FontBuilder
29
+ from fontTools.pens.ttGlyphPen import TTGlyphPen
30
+
31
+
32
+ def drawTestGlyph(pen):
33
+ pen.moveTo((100, 100))
34
+ pen.lineTo((100, 1000))
35
+ pen.qCurveTo((200, 900), (400, 900), (500, 1000))
36
+ pen.lineTo((500, 100))
37
+ pen.closePath()
38
+
39
+
40
+ fb = FontBuilder(1024, isTTF=True)
41
+ fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
42
+ fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
43
+ advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
44
+
45
+ familyName = "HelloTestFont"
46
+ styleName = "TotallyNormal"
47
+ version = "0.1"
48
+
49
+ nameStrings = dict(
50
+ familyName=dict(en=familyName, nl="HalloTestFont"),
51
+ styleName=dict(en=styleName, nl="TotaalNormaal"),
52
+ uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
53
+ fullName=familyName + "-" + styleName,
54
+ psName=familyName + "-" + styleName,
55
+ version="Version " + version,
56
+ )
57
+
58
+ pen = TTGlyphPen(None)
59
+ drawTestGlyph(pen)
60
+ glyph = pen.glyph()
61
+ glyphs = {".notdef": glyph, "space": glyph, "A": glyph, "a": glyph, ".null": glyph}
62
+ fb.setupGlyf(glyphs)
63
+ metrics = {}
64
+ glyphTable = fb.font["glyf"]
65
+ for gn, advanceWidth in advanceWidths.items():
66
+ metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
67
+ fb.setupHorizontalMetrics(metrics)
68
+ fb.setupHorizontalHeader(ascent=824, descent=-200)
69
+ fb.setupNameTable(nameStrings)
70
+ fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
71
+ fb.setupPost()
72
+ fb.save("test.ttf")
73
+ ```
74
+
75
+ And here's how to build a minimal OTF:
76
+
77
+ ```python
78
+ from fontTools.fontBuilder import FontBuilder
79
+ from fontTools.pens.t2CharStringPen import T2CharStringPen
80
+
81
+
82
+ def drawTestGlyph(pen):
83
+ pen.moveTo((100, 100))
84
+ pen.lineTo((100, 1000))
85
+ pen.curveTo((200, 900), (400, 900), (500, 1000))
86
+ pen.lineTo((500, 100))
87
+ pen.closePath()
88
+
89
+
90
+ fb = FontBuilder(1024, isTTF=False)
91
+ fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
92
+ fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
93
+ advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
94
+
95
+ familyName = "HelloTestFont"
96
+ styleName = "TotallyNormal"
97
+ version = "0.1"
98
+
99
+ nameStrings = dict(
100
+ familyName=dict(en=familyName, nl="HalloTestFont"),
101
+ styleName=dict(en=styleName, nl="TotaalNormaal"),
102
+ uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
103
+ fullName=familyName + "-" + styleName,
104
+ psName=familyName + "-" + styleName,
105
+ version="Version " + version,
106
+ )
107
+
108
+ pen = T2CharStringPen(600, None)
109
+ drawTestGlyph(pen)
110
+ charString = pen.getCharString()
111
+ charStrings = {
112
+ ".notdef": charString,
113
+ "space": charString,
114
+ "A": charString,
115
+ "a": charString,
116
+ ".null": charString,
117
+ }
118
+ fb.setupCFF(nameStrings["psName"], {"FullName": nameStrings["psName"]}, charStrings, {})
119
+ lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()}
120
+ metrics = {}
121
+ for gn, advanceWidth in advanceWidths.items():
122
+ metrics[gn] = (advanceWidth, lsb[gn])
123
+ fb.setupHorizontalMetrics(metrics)
124
+ fb.setupHorizontalHeader(ascent=824, descent=200)
125
+ fb.setupNameTable(nameStrings)
126
+ fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
127
+ fb.setupPost()
128
+ fb.save("test.otf")
129
+ ```
130
+ """
131
+
132
+ from .ttLib import TTFont, newTable
133
+ from .ttLib.tables._c_m_a_p import cmap_classes
134
+ from .ttLib.tables._g_l_y_f import flagCubic
135
+ from .ttLib.tables.O_S_2f_2 import Panose
136
+ from .misc.timeTools import timestampNow
137
+ import struct
138
+ from collections import OrderedDict
139
+
140
+
141
+ _headDefaults = dict(
142
+ tableVersion=1.0,
143
+ fontRevision=1.0,
144
+ checkSumAdjustment=0,
145
+ magicNumber=0x5F0F3CF5,
146
+ flags=0x0003,
147
+ unitsPerEm=1000,
148
+ created=0,
149
+ modified=0,
150
+ xMin=0,
151
+ yMin=0,
152
+ xMax=0,
153
+ yMax=0,
154
+ macStyle=0,
155
+ lowestRecPPEM=3,
156
+ fontDirectionHint=2,
157
+ indexToLocFormat=0,
158
+ glyphDataFormat=0,
159
+ )
160
+
161
+ _maxpDefaultsTTF = dict(
162
+ tableVersion=0x00010000,
163
+ numGlyphs=0,
164
+ maxPoints=0,
165
+ maxContours=0,
166
+ maxCompositePoints=0,
167
+ maxCompositeContours=0,
168
+ maxZones=2,
169
+ maxTwilightPoints=0,
170
+ maxStorage=0,
171
+ maxFunctionDefs=0,
172
+ maxInstructionDefs=0,
173
+ maxStackElements=0,
174
+ maxSizeOfInstructions=0,
175
+ maxComponentElements=0,
176
+ maxComponentDepth=0,
177
+ )
178
+ _maxpDefaultsOTF = dict(
179
+ tableVersion=0x00005000,
180
+ numGlyphs=0,
181
+ )
182
+
183
+ _postDefaults = dict(
184
+ formatType=3.0,
185
+ italicAngle=0,
186
+ underlinePosition=0,
187
+ underlineThickness=0,
188
+ isFixedPitch=0,
189
+ minMemType42=0,
190
+ maxMemType42=0,
191
+ minMemType1=0,
192
+ maxMemType1=0,
193
+ )
194
+
195
+ _hheaDefaults = dict(
196
+ tableVersion=0x00010000,
197
+ ascent=0,
198
+ descent=0,
199
+ lineGap=0,
200
+ advanceWidthMax=0,
201
+ minLeftSideBearing=0,
202
+ minRightSideBearing=0,
203
+ xMaxExtent=0,
204
+ caretSlopeRise=1,
205
+ caretSlopeRun=0,
206
+ caretOffset=0,
207
+ reserved0=0,
208
+ reserved1=0,
209
+ reserved2=0,
210
+ reserved3=0,
211
+ metricDataFormat=0,
212
+ numberOfHMetrics=0,
213
+ )
214
+
215
+ _vheaDefaults = dict(
216
+ tableVersion=0x00010000,
217
+ ascent=0,
218
+ descent=0,
219
+ lineGap=0,
220
+ advanceHeightMax=0,
221
+ minTopSideBearing=0,
222
+ minBottomSideBearing=0,
223
+ yMaxExtent=0,
224
+ caretSlopeRise=0,
225
+ caretSlopeRun=0,
226
+ reserved0=0,
227
+ reserved1=0,
228
+ reserved2=0,
229
+ reserved3=0,
230
+ reserved4=0,
231
+ metricDataFormat=0,
232
+ numberOfVMetrics=0,
233
+ )
234
+
235
+ _nameIDs = dict(
236
+ copyright=0,
237
+ familyName=1,
238
+ styleName=2,
239
+ uniqueFontIdentifier=3,
240
+ fullName=4,
241
+ version=5,
242
+ psName=6,
243
+ trademark=7,
244
+ manufacturer=8,
245
+ designer=9,
246
+ description=10,
247
+ vendorURL=11,
248
+ designerURL=12,
249
+ licenseDescription=13,
250
+ licenseInfoURL=14,
251
+ # reserved = 15,
252
+ typographicFamily=16,
253
+ typographicSubfamily=17,
254
+ compatibleFullName=18,
255
+ sampleText=19,
256
+ postScriptCIDFindfontName=20,
257
+ wwsFamilyName=21,
258
+ wwsSubfamilyName=22,
259
+ lightBackgroundPalette=23,
260
+ darkBackgroundPalette=24,
261
+ variationsPostScriptNamePrefix=25,
262
+ )
263
+
264
+ # to insert in setupNameTable doc string:
265
+ # print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
266
+
267
+
268
+ def _getOS2Defaults():
269
+ return dict(
270
+ version=3,
271
+ xAvgCharWidth=0,
272
+ usWeightClass=400,
273
+ usWidthClass=5,
274
+ fsType=0x0004, # default: Preview & Print embedding
275
+ ySubscriptXSize=0,
276
+ ySubscriptYSize=0,
277
+ ySubscriptXOffset=0,
278
+ ySubscriptYOffset=0,
279
+ ySuperscriptXSize=0,
280
+ ySuperscriptYSize=0,
281
+ ySuperscriptXOffset=0,
282
+ ySuperscriptYOffset=0,
283
+ yStrikeoutSize=0,
284
+ yStrikeoutPosition=0,
285
+ sFamilyClass=0,
286
+ panose=Panose(),
287
+ ulUnicodeRange1=0,
288
+ ulUnicodeRange2=0,
289
+ ulUnicodeRange3=0,
290
+ ulUnicodeRange4=0,
291
+ achVendID="????",
292
+ fsSelection=0,
293
+ usFirstCharIndex=0,
294
+ usLastCharIndex=0,
295
+ sTypoAscender=0,
296
+ sTypoDescender=0,
297
+ sTypoLineGap=0,
298
+ usWinAscent=0,
299
+ usWinDescent=0,
300
+ ulCodePageRange1=0,
301
+ ulCodePageRange2=0,
302
+ sxHeight=0,
303
+ sCapHeight=0,
304
+ usDefaultChar=0, # .notdef
305
+ usBreakChar=32, # space
306
+ usMaxContext=0,
307
+ usLowerOpticalPointSize=0,
308
+ usUpperOpticalPointSize=0,
309
+ )
310
+
311
+
312
+ class FontBuilder(object):
313
+ def __init__(self, unitsPerEm=None, font=None, isTTF=True, glyphDataFormat=0):
314
+ """Initialize a FontBuilder instance.
315
+
316
+ If the `font` argument is not given, a new `TTFont` will be
317
+ constructed, and `unitsPerEm` must be given. If `isTTF` is True,
318
+ the font will be a glyf-based TTF; if `isTTF` is False it will be
319
+ a CFF-based OTF.
320
+
321
+ The `glyphDataFormat` argument corresponds to the `head` table field
322
+ that defines the format of the TrueType `glyf` table (default=0).
323
+ TrueType glyphs historically can only contain quadratic splines and static
324
+ components, but there's a proposal to add support for cubic Bezier curves as well
325
+ as variable composites/components at
326
+ https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
327
+ You can experiment with the new features by setting `glyphDataFormat` to 1.
328
+ A ValueError is raised if `glyphDataFormat` is left at 0 but glyphs are added
329
+ that contain cubic splines or varcomposites. This is to prevent accidentally
330
+ creating fonts that are incompatible with existing TrueType implementations.
331
+
332
+ If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
333
+ must _not_ be given. The `isTTF` and `glyphDataFormat` arguments will be ignored.
334
+ """
335
+ if font is None:
336
+ self.font = TTFont(recalcTimestamp=False)
337
+ self.isTTF = isTTF
338
+ now = timestampNow()
339
+ assert unitsPerEm is not None
340
+ self.setupHead(
341
+ unitsPerEm=unitsPerEm,
342
+ created=now,
343
+ modified=now,
344
+ glyphDataFormat=glyphDataFormat,
345
+ )
346
+ self.setupMaxp()
347
+ else:
348
+ assert unitsPerEm is None
349
+ self.font = font
350
+ self.isTTF = "glyf" in font
351
+
352
+ def save(self, file):
353
+ """Save the font. The 'file' argument can be either a pathname or a
354
+ writable file object.
355
+ """
356
+ self.font.save(file)
357
+
358
+ def _initTableWithValues(self, tableTag, defaults, values):
359
+ table = self.font[tableTag] = newTable(tableTag)
360
+ for k, v in defaults.items():
361
+ setattr(table, k, v)
362
+ for k, v in values.items():
363
+ setattr(table, k, v)
364
+ return table
365
+
366
+ def _updateTableWithValues(self, tableTag, values):
367
+ table = self.font[tableTag]
368
+ for k, v in values.items():
369
+ setattr(table, k, v)
370
+
371
+ def setupHead(self, **values):
372
+ """Create a new `head` table and initialize it with default values,
373
+ which can be overridden by keyword arguments.
374
+ """
375
+ self._initTableWithValues("head", _headDefaults, values)
376
+
377
+ def updateHead(self, **values):
378
+ """Update the head table with the fields and values passed as
379
+ keyword arguments.
380
+ """
381
+ self._updateTableWithValues("head", values)
382
+
383
+ def setupGlyphOrder(self, glyphOrder):
384
+ """Set the glyph order for the font."""
385
+ self.font.setGlyphOrder(glyphOrder)
386
+
387
+ def setupCharacterMap(self, cmapping, uvs=None, allowFallback=False):
388
+ """Build the `cmap` table for the font. The `cmapping` argument should
389
+ be a dict mapping unicode code points as integers to glyph names.
390
+
391
+ The `uvs` argument, when passed, must be a list of tuples, describing
392
+ Unicode Variation Sequences. These tuples have three elements:
393
+ (unicodeValue, variationSelector, glyphName)
394
+ `unicodeValue` and `variationSelector` are integer code points.
395
+ `glyphName` may be None, to indicate this is the default variation.
396
+ Text processors will then use the cmap to find the glyph name.
397
+ Each Unicode Variation Sequence should be an officially supported
398
+ sequence, but this is not policed.
399
+ """
400
+ subTables = []
401
+ highestUnicode = max(cmapping) if cmapping else 0
402
+ if highestUnicode > 0xFFFF:
403
+ cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
404
+ subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
405
+ subTables.append(subTable_3_10)
406
+ else:
407
+ cmapping_3_1 = cmapping
408
+ format = 4
409
+ subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
410
+ try:
411
+ subTable_3_1.compile(self.font)
412
+ except struct.error:
413
+ # format 4 overflowed, fall back to format 12
414
+ if not allowFallback:
415
+ raise ValueError(
416
+ "cmap format 4 subtable overflowed; sort glyph order by unicode to fix."
417
+ )
418
+ format = 12
419
+ subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
420
+ subTables.append(subTable_3_1)
421
+ subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3)
422
+ subTables.append(subTable_0_3)
423
+
424
+ if uvs is not None:
425
+ uvsDict = {}
426
+ for unicodeValue, variationSelector, glyphName in uvs:
427
+ if cmapping.get(unicodeValue) == glyphName:
428
+ # this is a default variation
429
+ glyphName = None
430
+ if variationSelector not in uvsDict:
431
+ uvsDict[variationSelector] = []
432
+ uvsDict[variationSelector].append((unicodeValue, glyphName))
433
+ uvsSubTable = buildCmapSubTable({}, 14, 0, 5)
434
+ uvsSubTable.uvsDict = uvsDict
435
+ subTables.append(uvsSubTable)
436
+
437
+ self.font["cmap"] = newTable("cmap")
438
+ self.font["cmap"].tableVersion = 0
439
+ self.font["cmap"].tables = subTables
440
+
441
+ def setupNameTable(self, nameStrings, windows=True, mac=True):
442
+ """Create the `name` table for the font. The `nameStrings` argument must
443
+ be a dict, mapping nameIDs or descriptive names for the nameIDs to name
444
+ record values. A value is either a string, or a dict, mapping language codes
445
+ to strings, to allow localized name table entries.
446
+
447
+ By default, both Windows (platformID=3) and Macintosh (platformID=1) name
448
+ records are added, unless any of `windows` or `mac` arguments is False.
449
+
450
+ The following descriptive names are available for nameIDs:
451
+
452
+ copyright (nameID 0)
453
+ familyName (nameID 1)
454
+ styleName (nameID 2)
455
+ uniqueFontIdentifier (nameID 3)
456
+ fullName (nameID 4)
457
+ version (nameID 5)
458
+ psName (nameID 6)
459
+ trademark (nameID 7)
460
+ manufacturer (nameID 8)
461
+ designer (nameID 9)
462
+ description (nameID 10)
463
+ vendorURL (nameID 11)
464
+ designerURL (nameID 12)
465
+ licenseDescription (nameID 13)
466
+ licenseInfoURL (nameID 14)
467
+ typographicFamily (nameID 16)
468
+ typographicSubfamily (nameID 17)
469
+ compatibleFullName (nameID 18)
470
+ sampleText (nameID 19)
471
+ postScriptCIDFindfontName (nameID 20)
472
+ wwsFamilyName (nameID 21)
473
+ wwsSubfamilyName (nameID 22)
474
+ lightBackgroundPalette (nameID 23)
475
+ darkBackgroundPalette (nameID 24)
476
+ variationsPostScriptNamePrefix (nameID 25)
477
+ """
478
+ nameTable = self.font["name"] = newTable("name")
479
+ nameTable.names = []
480
+
481
+ for nameName, nameValue in nameStrings.items():
482
+ if isinstance(nameName, int):
483
+ nameID = nameName
484
+ else:
485
+ nameID = _nameIDs[nameName]
486
+ if isinstance(nameValue, str):
487
+ nameValue = dict(en=nameValue)
488
+ nameTable.addMultilingualName(
489
+ nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac
490
+ )
491
+
492
+ def setupOS2(self, **values):
493
+ """Create a new `OS/2` table and initialize it with default values,
494
+ which can be overridden by keyword arguments.
495
+ """
496
+ self._initTableWithValues("OS/2", _getOS2Defaults(), values)
497
+ if "xAvgCharWidth" not in values:
498
+ assert (
499
+ "hmtx" in self.font
500
+ ), "the 'hmtx' table must be setup before the 'OS/2' table"
501
+ self.font["OS/2"].recalcAvgCharWidth(self.font)
502
+ if not (
503
+ "ulUnicodeRange1" in values
504
+ or "ulUnicodeRange2" in values
505
+ or "ulUnicodeRange3" in values
506
+ or "ulUnicodeRange3" in values
507
+ ):
508
+ assert (
509
+ "cmap" in self.font
510
+ ), "the 'cmap' table must be setup before the 'OS/2' table"
511
+ self.font["OS/2"].recalcUnicodeRanges(self.font)
512
+
513
+ def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
514
+ from .cffLib import (
515
+ CFFFontSet,
516
+ TopDictIndex,
517
+ TopDict,
518
+ CharStrings,
519
+ GlobalSubrsIndex,
520
+ PrivateDict,
521
+ )
522
+
523
+ assert not self.isTTF
524
+ self.font.sfntVersion = "OTTO"
525
+ fontSet = CFFFontSet()
526
+ fontSet.major = 1
527
+ fontSet.minor = 0
528
+ fontSet.otFont = self.font
529
+ fontSet.fontNames = [psName]
530
+ fontSet.topDictIndex = TopDictIndex()
531
+
532
+ globalSubrs = GlobalSubrsIndex()
533
+ fontSet.GlobalSubrs = globalSubrs
534
+ private = PrivateDict()
535
+ for key, value in privateDict.items():
536
+ setattr(private, key, value)
537
+ fdSelect = None
538
+ fdArray = None
539
+
540
+ topDict = TopDict()
541
+ topDict.charset = self.font.getGlyphOrder()
542
+ topDict.Private = private
543
+ topDict.GlobalSubrs = fontSet.GlobalSubrs
544
+ for key, value in fontInfo.items():
545
+ setattr(topDict, key, value)
546
+ if "FontMatrix" not in fontInfo:
547
+ scale = 1 / self.font["head"].unitsPerEm
548
+ topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
549
+
550
+ charStrings = CharStrings(
551
+ None, topDict.charset, globalSubrs, private, fdSelect, fdArray
552
+ )
553
+ for glyphName, charString in charStringsDict.items():
554
+ charString.private = private
555
+ charString.globalSubrs = globalSubrs
556
+ charStrings[glyphName] = charString
557
+ topDict.CharStrings = charStrings
558
+
559
+ fontSet.topDictIndex.append(topDict)
560
+
561
+ self.font["CFF "] = newTable("CFF ")
562
+ self.font["CFF "].cff = fontSet
563
+
564
+ def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
565
+ from .cffLib import (
566
+ CFFFontSet,
567
+ TopDictIndex,
568
+ TopDict,
569
+ CharStrings,
570
+ GlobalSubrsIndex,
571
+ PrivateDict,
572
+ FDArrayIndex,
573
+ FontDict,
574
+ )
575
+
576
+ assert not self.isTTF
577
+ self.font.sfntVersion = "OTTO"
578
+ fontSet = CFFFontSet()
579
+ fontSet.major = 2
580
+ fontSet.minor = 0
581
+
582
+ cff2GetGlyphOrder = self.font.getGlyphOrder
583
+ fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
584
+
585
+ globalSubrs = GlobalSubrsIndex()
586
+ fontSet.GlobalSubrs = globalSubrs
587
+
588
+ if fdArrayList is None:
589
+ fdArrayList = [{}]
590
+ fdSelect = None
591
+ fdArray = FDArrayIndex()
592
+ fdArray.strings = None
593
+ fdArray.GlobalSubrs = globalSubrs
594
+ for privateDict in fdArrayList:
595
+ fontDict = FontDict()
596
+ fontDict.setCFF2(True)
597
+ private = PrivateDict()
598
+ for key, value in privateDict.items():
599
+ setattr(private, key, value)
600
+ fontDict.Private = private
601
+ fdArray.append(fontDict)
602
+
603
+ topDict = TopDict()
604
+ topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
605
+ topDict.FDArray = fdArray
606
+ scale = 1 / self.font["head"].unitsPerEm
607
+ topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
608
+
609
+ private = fdArray[0].Private
610
+ charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray)
611
+ for glyphName, charString in charStringsDict.items():
612
+ charString.private = private
613
+ charString.globalSubrs = globalSubrs
614
+ charStrings[glyphName] = charString
615
+ topDict.CharStrings = charStrings
616
+
617
+ fontSet.topDictIndex.append(topDict)
618
+
619
+ self.font["CFF2"] = newTable("CFF2")
620
+ self.font["CFF2"].cff = fontSet
621
+
622
+ if regions:
623
+ self.setupCFF2Regions(regions)
624
+
625
+ def setupCFF2Regions(self, regions):
626
+ from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore
627
+ from .cffLib import VarStoreData
628
+
629
+ assert "fvar" in self.font, "fvar must to be set up first"
630
+ assert "CFF2" in self.font, "CFF2 must to be set up first"
631
+ axisTags = [a.axisTag for a in self.font["fvar"].axes]
632
+ varRegionList = buildVarRegionList(regions, axisTags)
633
+ varData = buildVarData(list(range(len(regions))), None, optimize=False)
634
+ varStore = buildVarStore(varRegionList, [varData])
635
+ vstore = VarStoreData(otVarStore=varStore)
636
+ topDict = self.font["CFF2"].cff.topDictIndex[0]
637
+ topDict.VarStore = vstore
638
+ for fontDict in topDict.FDArray:
639
+ fontDict.Private.vstore = vstore
640
+
641
+ def setupGlyf(self, glyphs, calcGlyphBounds=True, validateGlyphFormat=True):
642
+ """Create the `glyf` table from a dict, that maps glyph names
643
+ to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
644
+ as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
645
+
646
+ If `calcGlyphBounds` is True, the bounds of all glyphs will be
647
+ calculated. Only pass False if your glyph objects already have
648
+ their bounding box values set.
649
+
650
+ If `validateGlyphFormat` is True, raise ValueError if any of the glyphs contains
651
+ cubic curves or is a variable composite but head.glyphDataFormat=0.
652
+ Set it to False to skip the check if you know in advance all the glyphs are
653
+ compatible with the specified glyphDataFormat.
654
+ """
655
+ assert self.isTTF
656
+
657
+ if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
658
+ for name, g in glyphs.items():
659
+ if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
660
+ raise ValueError(
661
+ f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
662
+ "either convert to quadratics with cu2qu or set glyphDataFormat=1."
663
+ )
664
+
665
+ self.font["loca"] = newTable("loca")
666
+ self.font["glyf"] = newTable("glyf")
667
+ self.font["glyf"].glyphs = glyphs
668
+ if hasattr(self.font, "glyphOrder"):
669
+ self.font["glyf"].glyphOrder = self.font.glyphOrder
670
+ if calcGlyphBounds:
671
+ self.calcGlyphBounds()
672
+
673
+ def setupFvar(self, axes, instances):
674
+ """Adds an font variations table to the font.
675
+
676
+ Args:
677
+ axes (list): See below.
678
+ instances (list): See below.
679
+
680
+ ``axes`` should be a list of axes, with each axis either supplied as
681
+ a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
682
+ format ```tupletag, minValue, defaultValue, maxValue, name``.
683
+ The ``name`` is either a string, or a dict, mapping language codes
684
+ to strings, to allow localized name table entries.
685
+
686
+ ```instances`` should be a list of instances, with each instance either
687
+ supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
688
+ dict with keys ``location`` (mapping of axis tags to float values),
689
+ ``stylename`` and (optionally) ``postscriptfontname``.
690
+ The ``stylename`` is either a string, or a dict, mapping language codes
691
+ to strings, to allow localized name table entries.
692
+ """
693
+
694
+ addFvar(self.font, axes, instances)
695
+
696
+ def setupAvar(self, axes, mappings=None):
697
+ """Adds an axis variations table to the font.
698
+
699
+ Args:
700
+ axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
701
+ """
702
+ from .varLib import _add_avar
703
+
704
+ if "fvar" not in self.font:
705
+ raise KeyError("'fvar' table is missing; can't add 'avar'.")
706
+
707
+ axisTags = [axis.axisTag for axis in self.font["fvar"].axes]
708
+ axes = OrderedDict(enumerate(axes)) # Only values are used
709
+ _add_avar(self.font, axes, mappings, axisTags)
710
+
711
+ def setupGvar(self, variations):
712
+ gvar = self.font["gvar"] = newTable("gvar")
713
+ gvar.version = 1
714
+ gvar.reserved = 0
715
+ gvar.variations = variations
716
+
717
+ def setupGVAR(self, variations):
718
+ gvar = self.font["GVAR"] = newTable("GVAR")
719
+ gvar.version = 1
720
+ gvar.reserved = 0
721
+ gvar.variations = variations
722
+
723
+ def calcGlyphBounds(self):
724
+ """Calculate the bounding boxes of all glyphs in the `glyf` table.
725
+ This is usually not called explicitly by client code.
726
+ """
727
+ glyphTable = self.font["glyf"]
728
+ for glyph in glyphTable.glyphs.values():
729
+ glyph.recalcBounds(glyphTable)
730
+
731
+ def setupHorizontalMetrics(self, metrics):
732
+ """Create a new `hmtx` table, for horizontal metrics.
733
+
734
+ The `metrics` argument must be a dict, mapping glyph names to
735
+ `(width, leftSidebearing)` tuples.
736
+ """
737
+ self.setupMetrics("hmtx", metrics)
738
+
739
+ def setupVerticalMetrics(self, metrics):
740
+ """Create a new `vmtx` table, for horizontal metrics.
741
+
742
+ The `metrics` argument must be a dict, mapping glyph names to
743
+ `(height, topSidebearing)` tuples.
744
+ """
745
+ self.setupMetrics("vmtx", metrics)
746
+
747
+ def setupMetrics(self, tableTag, metrics):
748
+ """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
749
+ assert tableTag in ("hmtx", "vmtx")
750
+ mtxTable = self.font[tableTag] = newTable(tableTag)
751
+ roundedMetrics = {}
752
+ for gn in metrics:
753
+ w, lsb = metrics[gn]
754
+ roundedMetrics[gn] = int(round(w)), int(round(lsb))
755
+ mtxTable.metrics = roundedMetrics
756
+
757
+ def setupHorizontalHeader(self, **values):
758
+ """Create a new `hhea` table initialize it with default values,
759
+ which can be overridden by keyword arguments.
760
+ """
761
+ self._initTableWithValues("hhea", _hheaDefaults, values)
762
+
763
+ def setupVerticalHeader(self, **values):
764
+ """Create a new `vhea` table initialize it with default values,
765
+ which can be overridden by keyword arguments.
766
+ """
767
+ self._initTableWithValues("vhea", _vheaDefaults, values)
768
+
769
+ def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
770
+ """Create a new `VORG` table. The `verticalOrigins` argument must be
771
+ a dict, mapping glyph names to vertical origin values.
772
+
773
+ The `defaultVerticalOrigin` argument should be the most common vertical
774
+ origin value. If omitted, this value will be derived from the actual
775
+ values in the `verticalOrigins` argument.
776
+ """
777
+ if defaultVerticalOrigin is None:
778
+ # find the most frequent vorg value
779
+ bag = {}
780
+ for gn in verticalOrigins:
781
+ vorg = verticalOrigins[gn]
782
+ if vorg not in bag:
783
+ bag[vorg] = 1
784
+ else:
785
+ bag[vorg] += 1
786
+ defaultVerticalOrigin = sorted(
787
+ bag, key=lambda vorg: bag[vorg], reverse=True
788
+ )[0]
789
+ self._initTableWithValues(
790
+ "VORG",
791
+ {},
792
+ dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
793
+ )
794
+ vorgTable = self.font["VORG"]
795
+ vorgTable.majorVersion = 1
796
+ vorgTable.minorVersion = 0
797
+ for gn in verticalOrigins:
798
+ vorgTable[gn] = verticalOrigins[gn]
799
+
800
+ def setupPost(self, keepGlyphNames=True, **values):
801
+ """Create a new `post` table and initialize it with default values,
802
+ which can be overridden by keyword arguments.
803
+ """
804
+ isCFF2 = "CFF2" in self.font
805
+ postTable = self._initTableWithValues("post", _postDefaults, values)
806
+ if (self.isTTF or isCFF2) and keepGlyphNames:
807
+ postTable.formatType = 2.0
808
+ postTable.extraNames = []
809
+ postTable.mapping = {}
810
+ else:
811
+ postTable.formatType = 3.0
812
+
813
+ def setupMaxp(self):
814
+ """Create a new `maxp` table. This is called implicitly by FontBuilder
815
+ itself and is usually not called by client code.
816
+ """
817
+ if self.isTTF:
818
+ defaults = _maxpDefaultsTTF
819
+ else:
820
+ defaults = _maxpDefaultsOTF
821
+ self._initTableWithValues("maxp", defaults, {})
822
+
823
+ def setupDummyDSIG(self):
824
+ """This adds an empty DSIG table to the font to make some MS applications
825
+ happy. This does not properly sign the font.
826
+ """
827
+ values = dict(
828
+ ulVersion=1,
829
+ usFlag=0,
830
+ usNumSigs=0,
831
+ signatureRecords=[],
832
+ )
833
+ self._initTableWithValues("DSIG", {}, values)
834
+
835
+ def addOpenTypeFeatures(self, features, filename=None, tables=None, debug=False):
836
+ """Add OpenType features to the font from a string containing
837
+ Feature File syntax.
838
+
839
+ The `filename` argument is used in error messages and to determine
840
+ where to look for "include" files.
841
+
842
+ The optional `tables` argument can be a list of OTL tables tags to
843
+ build, allowing the caller to only build selected OTL tables. See
844
+ `fontTools.feaLib` for details.
845
+
846
+ The optional `debug` argument controls whether to add source debugging
847
+ information to the font in the `Debg` table.
848
+ """
849
+ from .feaLib.builder import addOpenTypeFeaturesFromString
850
+
851
+ addOpenTypeFeaturesFromString(
852
+ self.font, features, filename=filename, tables=tables, debug=debug
853
+ )
854
+
855
+ def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
856
+ """Add conditional substitutions to a Variable Font.
857
+
858
+ See `fontTools.varLib.featureVars.addFeatureVariations`.
859
+ """
860
+ from .varLib import featureVars
861
+
862
+ if "fvar" not in self.font:
863
+ raise KeyError("'fvar' table is missing; can't add FeatureVariations.")
864
+
865
+ featureVars.addFeatureVariations(
866
+ self.font, conditionalSubstitutions, featureTag=featureTag
867
+ )
868
+
869
+ def setupCOLR(
870
+ self,
871
+ colorLayers,
872
+ version=None,
873
+ varStore=None,
874
+ varIndexMap=None,
875
+ clipBoxes=None,
876
+ allowLayerReuse=True,
877
+ ):
878
+ """Build new COLR table using color layers dictionary.
879
+
880
+ Cf. `fontTools.colorLib.builder.buildCOLR`.
881
+ """
882
+ from fontTools.colorLib.builder import buildCOLR
883
+
884
+ glyphMap = self.font.getReverseGlyphMap()
885
+ self.font["COLR"] = buildCOLR(
886
+ colorLayers,
887
+ version=version,
888
+ glyphMap=glyphMap,
889
+ varStore=varStore,
890
+ varIndexMap=varIndexMap,
891
+ clipBoxes=clipBoxes,
892
+ allowLayerReuse=allowLayerReuse,
893
+ )
894
+
895
+ def setupCPAL(
896
+ self,
897
+ palettes,
898
+ paletteTypes=None,
899
+ paletteLabels=None,
900
+ paletteEntryLabels=None,
901
+ ):
902
+ """Build new CPAL table using list of palettes.
903
+
904
+ Optionally build CPAL v1 table using paletteTypes, paletteLabels and
905
+ paletteEntryLabels.
906
+
907
+ Cf. `fontTools.colorLib.builder.buildCPAL`.
908
+ """
909
+ from fontTools.colorLib.builder import buildCPAL
910
+
911
+ self.font["CPAL"] = buildCPAL(
912
+ palettes,
913
+ paletteTypes=paletteTypes,
914
+ paletteLabels=paletteLabels,
915
+ paletteEntryLabels=paletteEntryLabels,
916
+ nameTable=self.font.get("name"),
917
+ )
918
+
919
+ def setupStat(self, axes, locations=None, elidedFallbackName=2):
920
+ """Build a new 'STAT' table.
921
+
922
+ See `fontTools.otlLib.builder.buildStatTable` for details about
923
+ the arguments.
924
+ """
925
+ from .otlLib.builder import buildStatTable
926
+
927
+ assert "name" in self.font, "name must to be set up first"
928
+
929
+ buildStatTable(
930
+ self.font,
931
+ axes,
932
+ locations,
933
+ elidedFallbackName,
934
+ macNames=any(nr.platformID == 1 for nr in self.font["name"].names),
935
+ )
936
+
937
+
938
+ def buildCmapSubTable(cmapping, format, platformID, platEncID):
939
+ subTable = cmap_classes[format](format)
940
+ subTable.cmap = cmapping
941
+ subTable.platformID = platformID
942
+ subTable.platEncID = platEncID
943
+ subTable.language = 0
944
+ return subTable
945
+
946
+
947
+ def addFvar(font, axes, instances):
948
+ from .ttLib.tables._f_v_a_r import Axis, NamedInstance
949
+
950
+ assert axes
951
+
952
+ fvar = newTable("fvar")
953
+ nameTable = font["name"]
954
+
955
+ # if there are not currently any mac names don't add them here, that's inconsistent
956
+ # https://github.com/fonttools/fonttools/issues/683
957
+ macNames = any(nr.platformID == 1 for nr in getattr(nameTable, "names", ()))
958
+
959
+ # we have all the best ways to express mac names
960
+ platforms = ((3, 1, 0x409),)
961
+ if macNames:
962
+ platforms = ((1, 0, 0),) + platforms
963
+
964
+ for axis_def in axes:
965
+ axis = Axis()
966
+
967
+ if isinstance(axis_def, tuple):
968
+ (
969
+ axis.axisTag,
970
+ axis.minValue,
971
+ axis.defaultValue,
972
+ axis.maxValue,
973
+ name,
974
+ ) = axis_def
975
+ else:
976
+ (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
977
+ axis_def.tag,
978
+ axis_def.minimum,
979
+ axis_def.default,
980
+ axis_def.maximum,
981
+ axis_def.name,
982
+ )
983
+ if axis_def.hidden:
984
+ axis.flags = 0x0001 # HIDDEN_AXIS
985
+
986
+ if isinstance(name, str):
987
+ name = dict(en=name)
988
+
989
+ axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font, mac=macNames)
990
+ fvar.axes.append(axis)
991
+
992
+ for instance in instances:
993
+ if isinstance(instance, dict):
994
+ coordinates = instance["location"]
995
+ name = instance["stylename"]
996
+ psname = instance.get("postscriptfontname")
997
+ else:
998
+ coordinates = instance.location
999
+ name = instance.localisedStyleName or instance.styleName
1000
+ psname = instance.postScriptFontName
1001
+
1002
+ if isinstance(name, str):
1003
+ name = dict(en=name)
1004
+
1005
+ inst = NamedInstance()
1006
+ inst.subfamilyNameID = nameTable.addMultilingualName(
1007
+ name, ttFont=font, mac=macNames
1008
+ )
1009
+ if psname is not None:
1010
+ inst.postscriptNameID = nameTable.addName(psname, platforms=platforms)
1011
+ inst.coordinates = coordinates
1012
+ fvar.instances.append(inst)
1013
+
1014
+ font["fvar"] = fvar
lib/python3.12/site-packages/fontTools/help.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pkgutil
2
+ import sys
3
+ import fontTools
4
+ import importlib
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def main():
10
+ """Show this help"""
11
+ path = fontTools.__path__
12
+ descriptions = {}
13
+ for pkg in sorted(
14
+ mod.name
15
+ for mod in pkgutil.walk_packages([fontTools.__path__[0]], prefix="fontTools.")
16
+ ):
17
+ try:
18
+ imports = __import__(pkg, globals(), locals(), ["main"])
19
+ except ImportError as e:
20
+ continue
21
+ try:
22
+ description = imports.main.__doc__
23
+ # Cython modules seem to return "main()" as the docstring
24
+ if description and description != "main()":
25
+ pkg = pkg.replace("fontTools.", "").replace(".__main__", "")
26
+ # show the docstring's first line only
27
+ descriptions[pkg] = description.splitlines()[0]
28
+ except AttributeError as e:
29
+ pass
30
+ for pkg, description in descriptions.items():
31
+ print("fonttools %-25s %s" % (pkg, description), file=sys.stderr)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ print("fonttools v%s\n" % fontTools.__version__, file=sys.stderr)
36
+ main()
lib/python3.12/site-packages/fontTools/mtiLib/__init__.py ADDED
@@ -0,0 +1,1400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FontDame-to-FontTools for OpenType Layout tables
2
+ #
3
+ # Source language spec is available at:
4
+ # http://monotype.github.io/OpenType_Table_Source/otl_source.html
5
+ # https://github.com/Monotype/OpenType_Table_Source/
6
+
7
+ from fontTools import ttLib
8
+ from fontTools.ttLib.tables._c_m_a_p import cmap_classes
9
+ from fontTools.ttLib.tables import otTables as ot
10
+ from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
11
+ from fontTools.otlLib import builder as otl
12
+ from contextlib import contextmanager
13
+ from fontTools.ttLib import newTable
14
+ from fontTools.feaLib.lookupDebugInfo import LOOKUP_DEBUG_ENV_VAR, LOOKUP_DEBUG_INFO_KEY
15
+ from operator import setitem
16
+ import os
17
+ import logging
18
+
19
+
20
+ class MtiLibError(Exception):
21
+ pass
22
+
23
+
24
+ class ReferenceNotFoundError(MtiLibError):
25
+ pass
26
+
27
+
28
+ class FeatureNotFoundError(ReferenceNotFoundError):
29
+ pass
30
+
31
+
32
+ class LookupNotFoundError(ReferenceNotFoundError):
33
+ pass
34
+
35
+
36
+ log = logging.getLogger("fontTools.mtiLib")
37
+
38
+
39
+ def makeGlyph(s):
40
+ if s[:2] in ["U ", "u "]:
41
+ return ttLib.TTFont._makeGlyphName(int(s[2:], 16))
42
+ elif s[:2] == "# ":
43
+ return "glyph%.5d" % int(s[2:])
44
+ assert s.find(" ") < 0, "Space found in glyph name: %s" % s
45
+ assert s, "Glyph name is empty"
46
+ return s
47
+
48
+
49
+ def makeGlyphs(l):
50
+ return [makeGlyph(g) for g in l]
51
+
52
+
53
+ def mapLookup(sym, mapping):
54
+ # Lookups are addressed by name. So resolved them using a map if available.
55
+ # Fallback to parsing as lookup index if a map isn't provided.
56
+ if mapping is not None:
57
+ try:
58
+ idx = mapping[sym]
59
+ except KeyError:
60
+ raise LookupNotFoundError(sym)
61
+ else:
62
+ idx = int(sym)
63
+ return idx
64
+
65
+
66
+ def mapFeature(sym, mapping):
67
+ # Features are referenced by index according the spec. So, if symbol is an
68
+ # integer, use it directly. Otherwise look up in the map if provided.
69
+ try:
70
+ idx = int(sym)
71
+ except ValueError:
72
+ try:
73
+ idx = mapping[sym]
74
+ except KeyError:
75
+ raise FeatureNotFoundError(sym)
76
+ return idx
77
+
78
+
79
+ def setReference(mapper, mapping, sym, setter, collection, key):
80
+ try:
81
+ mapped = mapper(sym, mapping)
82
+ except ReferenceNotFoundError as e:
83
+ try:
84
+ if mapping is not None:
85
+ mapping.addDeferredMapping(
86
+ lambda ref: setter(collection, key, ref), sym, e
87
+ )
88
+ return
89
+ except AttributeError:
90
+ pass
91
+ raise
92
+ setter(collection, key, mapped)
93
+
94
+
95
+ class DeferredMapping(dict):
96
+ def __init__(self):
97
+ self._deferredMappings = []
98
+
99
+ def addDeferredMapping(self, setter, sym, e):
100
+ log.debug("Adding deferred mapping for symbol '%s' %s", sym, type(e).__name__)
101
+ self._deferredMappings.append((setter, sym, e))
102
+
103
+ def applyDeferredMappings(self):
104
+ for setter, sym, e in self._deferredMappings:
105
+ log.debug(
106
+ "Applying deferred mapping for symbol '%s' %s", sym, type(e).__name__
107
+ )
108
+ try:
109
+ mapped = self[sym]
110
+ except KeyError:
111
+ raise e
112
+ setter(mapped)
113
+ log.debug("Set to %s", mapped)
114
+ self._deferredMappings = []
115
+
116
+
117
+ def parseScriptList(lines, featureMap=None):
118
+ self = ot.ScriptList()
119
+ records = []
120
+ with lines.between("script table"):
121
+ for line in lines:
122
+ while len(line) < 4:
123
+ line.append("")
124
+ scriptTag, langSysTag, defaultFeature, features = line
125
+ log.debug("Adding script %s language-system %s", scriptTag, langSysTag)
126
+
127
+ langSys = ot.LangSys()
128
+ langSys.LookupOrder = None
129
+ if defaultFeature:
130
+ setReference(
131
+ mapFeature,
132
+ featureMap,
133
+ defaultFeature,
134
+ setattr,
135
+ langSys,
136
+ "ReqFeatureIndex",
137
+ )
138
+ else:
139
+ langSys.ReqFeatureIndex = 0xFFFF
140
+ syms = stripSplitComma(features)
141
+ langSys.FeatureIndex = theList = [3] * len(syms)
142
+ for i, sym in enumerate(syms):
143
+ setReference(mapFeature, featureMap, sym, setitem, theList, i)
144
+ langSys.FeatureCount = len(langSys.FeatureIndex)
145
+
146
+ script = [s for s in records if s.ScriptTag == scriptTag]
147
+ if script:
148
+ script = script[0].Script
149
+ else:
150
+ scriptRec = ot.ScriptRecord()
151
+ scriptRec.ScriptTag = scriptTag + " " * (4 - len(scriptTag))
152
+ scriptRec.Script = ot.Script()
153
+ records.append(scriptRec)
154
+ script = scriptRec.Script
155
+ script.DefaultLangSys = None
156
+ script.LangSysRecord = []
157
+ script.LangSysCount = 0
158
+
159
+ if langSysTag == "default":
160
+ script.DefaultLangSys = langSys
161
+ else:
162
+ langSysRec = ot.LangSysRecord()
163
+ langSysRec.LangSysTag = langSysTag + " " * (4 - len(langSysTag))
164
+ langSysRec.LangSys = langSys
165
+ script.LangSysRecord.append(langSysRec)
166
+ script.LangSysCount = len(script.LangSysRecord)
167
+
168
+ for script in records:
169
+ script.Script.LangSysRecord = sorted(
170
+ script.Script.LangSysRecord, key=lambda rec: rec.LangSysTag
171
+ )
172
+ self.ScriptRecord = sorted(records, key=lambda rec: rec.ScriptTag)
173
+ self.ScriptCount = len(self.ScriptRecord)
174
+ return self
175
+
176
+
177
+ def parseFeatureList(lines, lookupMap=None, featureMap=None):
178
+ self = ot.FeatureList()
179
+ self.FeatureRecord = []
180
+ with lines.between("feature table"):
181
+ for line in lines:
182
+ name, featureTag, lookups = line
183
+ if featureMap is not None:
184
+ assert name not in featureMap, "Duplicate feature name: %s" % name
185
+ featureMap[name] = len(self.FeatureRecord)
186
+ # If feature name is integer, make sure it matches its index.
187
+ try:
188
+ assert int(name) == len(self.FeatureRecord), "%d %d" % (
189
+ name,
190
+ len(self.FeatureRecord),
191
+ )
192
+ except ValueError:
193
+ pass
194
+ featureRec = ot.FeatureRecord()
195
+ featureRec.FeatureTag = featureTag
196
+ featureRec.Feature = ot.Feature()
197
+ self.FeatureRecord.append(featureRec)
198
+ feature = featureRec.Feature
199
+ feature.FeatureParams = None
200
+ syms = stripSplitComma(lookups)
201
+ feature.LookupListIndex = theList = [None] * len(syms)
202
+ for i, sym in enumerate(syms):
203
+ setReference(mapLookup, lookupMap, sym, setitem, theList, i)
204
+ feature.LookupCount = len(feature.LookupListIndex)
205
+
206
+ self.FeatureCount = len(self.FeatureRecord)
207
+ return self
208
+
209
+
210
+ def parseLookupFlags(lines):
211
+ flags = 0
212
+ filterset = None
213
+ allFlags = [
214
+ "righttoleft",
215
+ "ignorebaseglyphs",
216
+ "ignoreligatures",
217
+ "ignoremarks",
218
+ "markattachmenttype",
219
+ "markfiltertype",
220
+ ]
221
+ while lines.peeks()[0].lower() in allFlags:
222
+ line = next(lines)
223
+ flag = {
224
+ "righttoleft": 0x0001,
225
+ "ignorebaseglyphs": 0x0002,
226
+ "ignoreligatures": 0x0004,
227
+ "ignoremarks": 0x0008,
228
+ }.get(line[0].lower())
229
+ if flag:
230
+ assert line[1].lower() in ["yes", "no"], line[1]
231
+ if line[1].lower() == "yes":
232
+ flags |= flag
233
+ continue
234
+ if line[0].lower() == "markattachmenttype":
235
+ flags |= int(line[1]) << 8
236
+ continue
237
+ if line[0].lower() == "markfiltertype":
238
+ flags |= 0x10
239
+ filterset = int(line[1])
240
+ return flags, filterset
241
+
242
+
243
+ def parseSingleSubst(lines, font, _lookupMap=None):
244
+ mapping = {}
245
+ for line in lines:
246
+ assert len(line) == 2, line
247
+ line = makeGlyphs(line)
248
+ mapping[line[0]] = line[1]
249
+ return otl.buildSingleSubstSubtable(mapping)
250
+
251
+
252
+ def parseMultiple(lines, font, _lookupMap=None):
253
+ mapping = {}
254
+ for line in lines:
255
+ line = makeGlyphs(line)
256
+ mapping[line[0]] = line[1:]
257
+ return otl.buildMultipleSubstSubtable(mapping)
258
+
259
+
260
+ def parseAlternate(lines, font, _lookupMap=None):
261
+ mapping = {}
262
+ for line in lines:
263
+ line = makeGlyphs(line)
264
+ mapping[line[0]] = line[1:]
265
+ return otl.buildAlternateSubstSubtable(mapping)
266
+
267
+
268
+ def parseLigature(lines, font, _lookupMap=None):
269
+ mapping = {}
270
+ for line in lines:
271
+ assert len(line) >= 2, line
272
+ line = makeGlyphs(line)
273
+ mapping[tuple(line[1:])] = line[0]
274
+ return otl.buildLigatureSubstSubtable(mapping)
275
+
276
+
277
+ def parseSinglePos(lines, font, _lookupMap=None):
278
+ values = {}
279
+ for line in lines:
280
+ assert len(line) == 3, line
281
+ w = line[0].title().replace(" ", "")
282
+ assert w in valueRecordFormatDict
283
+ g = makeGlyph(line[1])
284
+ v = int(line[2])
285
+ if g not in values:
286
+ values[g] = ValueRecord()
287
+ assert not hasattr(values[g], w), (g, w)
288
+ setattr(values[g], w, v)
289
+ return otl.buildSinglePosSubtable(values, font.getReverseGlyphMap())
290
+
291
+
292
+ def parsePair(lines, font, _lookupMap=None):
293
+ self = ot.PairPos()
294
+ self.ValueFormat1 = self.ValueFormat2 = 0
295
+ typ = lines.peeks()[0].split()[0].lower()
296
+ if typ in ("left", "right"):
297
+ self.Format = 1
298
+ values = {}
299
+ for line in lines:
300
+ assert len(line) == 4, line
301
+ side = line[0].split()[0].lower()
302
+ assert side in ("left", "right"), side
303
+ what = line[0][len(side) :].title().replace(" ", "")
304
+ mask = valueRecordFormatDict[what][0]
305
+ glyph1, glyph2 = makeGlyphs(line[1:3])
306
+ value = int(line[3])
307
+ if not glyph1 in values:
308
+ values[glyph1] = {}
309
+ if not glyph2 in values[glyph1]:
310
+ values[glyph1][glyph2] = (ValueRecord(), ValueRecord())
311
+ rec2 = values[glyph1][glyph2]
312
+ if side == "left":
313
+ self.ValueFormat1 |= mask
314
+ vr = rec2[0]
315
+ else:
316
+ self.ValueFormat2 |= mask
317
+ vr = rec2[1]
318
+ assert not hasattr(vr, what), (vr, what)
319
+ setattr(vr, what, value)
320
+ self.Coverage = makeCoverage(set(values.keys()), font)
321
+ self.PairSet = []
322
+ for glyph1 in self.Coverage.glyphs:
323
+ values1 = values[glyph1]
324
+ pairset = ot.PairSet()
325
+ records = pairset.PairValueRecord = []
326
+ for glyph2 in sorted(values1.keys(), key=font.getGlyphID):
327
+ values2 = values1[glyph2]
328
+ pair = ot.PairValueRecord()
329
+ pair.SecondGlyph = glyph2
330
+ pair.Value1 = values2[0]
331
+ pair.Value2 = values2[1] if self.ValueFormat2 else None
332
+ records.append(pair)
333
+ pairset.PairValueCount = len(pairset.PairValueRecord)
334
+ self.PairSet.append(pairset)
335
+ self.PairSetCount = len(self.PairSet)
336
+ elif typ.endswith("class"):
337
+ self.Format = 2
338
+ classDefs = [None, None]
339
+ while lines.peeks()[0].endswith("class definition begin"):
340
+ typ = lines.peek()[0][: -len("class definition begin")].lower()
341
+ idx, klass = {
342
+ "first": (0, ot.ClassDef1),
343
+ "second": (1, ot.ClassDef2),
344
+ }[typ]
345
+ assert classDefs[idx] is None
346
+ classDefs[idx] = parseClassDef(lines, font, klass=klass)
347
+ self.ClassDef1, self.ClassDef2 = classDefs
348
+ self.Class1Count, self.Class2Count = (
349
+ 1 + max(c.classDefs.values()) for c in classDefs
350
+ )
351
+ self.Class1Record = [ot.Class1Record() for i in range(self.Class1Count)]
352
+ for rec1 in self.Class1Record:
353
+ rec1.Class2Record = [ot.Class2Record() for j in range(self.Class2Count)]
354
+ for rec2 in rec1.Class2Record:
355
+ rec2.Value1 = ValueRecord()
356
+ rec2.Value2 = ValueRecord()
357
+ for line in lines:
358
+ assert len(line) == 4, line
359
+ side = line[0].split()[0].lower()
360
+ assert side in ("left", "right"), side
361
+ what = line[0][len(side) :].title().replace(" ", "")
362
+ mask = valueRecordFormatDict[what][0]
363
+ class1, class2, value = (int(x) for x in line[1:4])
364
+ rec2 = self.Class1Record[class1].Class2Record[class2]
365
+ if side == "left":
366
+ self.ValueFormat1 |= mask
367
+ vr = rec2.Value1
368
+ else:
369
+ self.ValueFormat2 |= mask
370
+ vr = rec2.Value2
371
+ assert not hasattr(vr, what), (vr, what)
372
+ setattr(vr, what, value)
373
+ for rec1 in self.Class1Record:
374
+ for rec2 in rec1.Class2Record:
375
+ rec2.Value1 = ValueRecord(self.ValueFormat1, rec2.Value1)
376
+ rec2.Value2 = (
377
+ ValueRecord(self.ValueFormat2, rec2.Value2)
378
+ if self.ValueFormat2
379
+ else None
380
+ )
381
+
382
+ self.Coverage = makeCoverage(set(self.ClassDef1.classDefs.keys()), font)
383
+ else:
384
+ assert 0, typ
385
+ return self
386
+
387
+
388
+ def parseKernset(lines, font, _lookupMap=None):
389
+ typ = lines.peeks()[0].split()[0].lower()
390
+ if typ in ("left", "right"):
391
+ with lines.until(
392
+ ("firstclass definition begin", "secondclass definition begin")
393
+ ):
394
+ return parsePair(lines, font)
395
+ return parsePair(lines, font)
396
+
397
+
398
+ def makeAnchor(data, klass=ot.Anchor):
399
+ assert len(data) <= 2
400
+ anchor = klass()
401
+ anchor.Format = 1
402
+ anchor.XCoordinate, anchor.YCoordinate = intSplitComma(data[0])
403
+ if len(data) > 1 and data[1] != "":
404
+ anchor.Format = 2
405
+ anchor.AnchorPoint = int(data[1])
406
+ return anchor
407
+
408
+
409
+ def parseCursive(lines, font, _lookupMap=None):
410
+ records = {}
411
+ for line in lines:
412
+ assert len(line) in [3, 4], line
413
+ idx, klass = {
414
+ "entry": (0, ot.EntryAnchor),
415
+ "exit": (1, ot.ExitAnchor),
416
+ }[line[0]]
417
+ glyph = makeGlyph(line[1])
418
+ if glyph not in records:
419
+ records[glyph] = [None, None]
420
+ assert records[glyph][idx] is None, (glyph, idx)
421
+ records[glyph][idx] = makeAnchor(line[2:], klass)
422
+ return otl.buildCursivePosSubtable(records, font.getReverseGlyphMap())
423
+
424
+
425
+ def makeMarkRecords(data, coverage, c):
426
+ records = []
427
+ for glyph in coverage.glyphs:
428
+ klass, anchor = data[glyph]
429
+ record = c.MarkRecordClass()
430
+ record.Class = klass
431
+ setattr(record, c.MarkAnchor, anchor)
432
+ records.append(record)
433
+ return records
434
+
435
+
436
+ def makeBaseRecords(data, coverage, c, classCount):
437
+ records = []
438
+ idx = {}
439
+ for glyph in coverage.glyphs:
440
+ idx[glyph] = len(records)
441
+ record = c.BaseRecordClass()
442
+ anchors = [None] * classCount
443
+ setattr(record, c.BaseAnchor, anchors)
444
+ records.append(record)
445
+ for (glyph, klass), anchor in data.items():
446
+ record = records[idx[glyph]]
447
+ anchors = getattr(record, c.BaseAnchor)
448
+ assert anchors[klass] is None, (glyph, klass)
449
+ anchors[klass] = anchor
450
+ return records
451
+
452
+
453
+ def makeLigatureRecords(data, coverage, c, classCount):
454
+ records = [None] * len(coverage.glyphs)
455
+ idx = {g: i for i, g in enumerate(coverage.glyphs)}
456
+
457
+ for (glyph, klass, compIdx, compCount), anchor in data.items():
458
+ record = records[idx[glyph]]
459
+ if record is None:
460
+ record = records[idx[glyph]] = ot.LigatureAttach()
461
+ record.ComponentCount = compCount
462
+ record.ComponentRecord = [ot.ComponentRecord() for i in range(compCount)]
463
+ for compRec in record.ComponentRecord:
464
+ compRec.LigatureAnchor = [None] * classCount
465
+ assert record.ComponentCount == compCount, (
466
+ glyph,
467
+ record.ComponentCount,
468
+ compCount,
469
+ )
470
+
471
+ anchors = record.ComponentRecord[compIdx - 1].LigatureAnchor
472
+ assert anchors[klass] is None, (glyph, compIdx, klass)
473
+ anchors[klass] = anchor
474
+ return records
475
+
476
+
477
+ def parseMarkToSomething(lines, font, c):
478
+ self = c.Type()
479
+ self.Format = 1
480
+ markData = {}
481
+ baseData = {}
482
+ Data = {
483
+ "mark": (markData, c.MarkAnchorClass),
484
+ "base": (baseData, c.BaseAnchorClass),
485
+ "ligature": (baseData, c.BaseAnchorClass),
486
+ }
487
+ maxKlass = 0
488
+ for line in lines:
489
+ typ = line[0]
490
+ assert typ in ("mark", "base", "ligature")
491
+ glyph = makeGlyph(line[1])
492
+ data, anchorClass = Data[typ]
493
+ extraItems = 2 if typ == "ligature" else 0
494
+ extras = tuple(int(i) for i in line[2 : 2 + extraItems])
495
+ klass = int(line[2 + extraItems])
496
+ anchor = makeAnchor(line[3 + extraItems :], anchorClass)
497
+ if typ == "mark":
498
+ key, value = glyph, (klass, anchor)
499
+ else:
500
+ key, value = ((glyph, klass) + extras), anchor
501
+ assert key not in data, key
502
+ data[key] = value
503
+ maxKlass = max(maxKlass, klass)
504
+
505
+ # Mark
506
+ markCoverage = makeCoverage(set(markData.keys()), font, c.MarkCoverageClass)
507
+ markArray = c.MarkArrayClass()
508
+ markRecords = makeMarkRecords(markData, markCoverage, c)
509
+ setattr(markArray, c.MarkRecord, markRecords)
510
+ setattr(markArray, c.MarkCount, len(markRecords))
511
+ setattr(self, c.MarkCoverage, markCoverage)
512
+ setattr(self, c.MarkArray, markArray)
513
+ self.ClassCount = maxKlass + 1
514
+
515
+ # Base
516
+ self.classCount = 0 if not baseData else 1 + max(k[1] for k, v in baseData.items())
517
+ baseCoverage = makeCoverage(
518
+ set([k[0] for k in baseData.keys()]), font, c.BaseCoverageClass
519
+ )
520
+ baseArray = c.BaseArrayClass()
521
+ if c.Base == "Ligature":
522
+ baseRecords = makeLigatureRecords(baseData, baseCoverage, c, self.classCount)
523
+ else:
524
+ baseRecords = makeBaseRecords(baseData, baseCoverage, c, self.classCount)
525
+ setattr(baseArray, c.BaseRecord, baseRecords)
526
+ setattr(baseArray, c.BaseCount, len(baseRecords))
527
+ setattr(self, c.BaseCoverage, baseCoverage)
528
+ setattr(self, c.BaseArray, baseArray)
529
+
530
+ return self
531
+
532
+
533
+ class MarkHelper(object):
534
+ def __init__(self):
535
+ for Which in ("Mark", "Base"):
536
+ for What in ("Coverage", "Array", "Count", "Record", "Anchor"):
537
+ key = Which + What
538
+ if Which == "Mark" and What in ("Count", "Record", "Anchor"):
539
+ value = key
540
+ else:
541
+ value = getattr(self, Which) + What
542
+ if value == "LigatureRecord":
543
+ value = "LigatureAttach"
544
+ setattr(self, key, value)
545
+ if What != "Count":
546
+ klass = getattr(ot, value)
547
+ setattr(self, key + "Class", klass)
548
+
549
+
550
+ class MarkToBaseHelper(MarkHelper):
551
+ Mark = "Mark"
552
+ Base = "Base"
553
+ Type = ot.MarkBasePos
554
+
555
+
556
+ class MarkToMarkHelper(MarkHelper):
557
+ Mark = "Mark1"
558
+ Base = "Mark2"
559
+ Type = ot.MarkMarkPos
560
+
561
+
562
+ class MarkToLigatureHelper(MarkHelper):
563
+ Mark = "Mark"
564
+ Base = "Ligature"
565
+ Type = ot.MarkLigPos
566
+
567
+
568
+ def parseMarkToBase(lines, font, _lookupMap=None):
569
+ return parseMarkToSomething(lines, font, MarkToBaseHelper())
570
+
571
+
572
+ def parseMarkToMark(lines, font, _lookupMap=None):
573
+ return parseMarkToSomething(lines, font, MarkToMarkHelper())
574
+
575
+
576
+ def parseMarkToLigature(lines, font, _lookupMap=None):
577
+ return parseMarkToSomething(lines, font, MarkToLigatureHelper())
578
+
579
+
580
+ def stripSplitComma(line):
581
+ return [s.strip() for s in line.split(",")] if line else []
582
+
583
+
584
+ def intSplitComma(line):
585
+ return [int(i) for i in line.split(",")] if line else []
586
+
587
+
588
+ # Copied from fontTools.subset
589
+ class ContextHelper(object):
590
+ def __init__(self, klassName, Format):
591
+ if klassName.endswith("Subst"):
592
+ Typ = "Sub"
593
+ Type = "Subst"
594
+ else:
595
+ Typ = "Pos"
596
+ Type = "Pos"
597
+ if klassName.startswith("Chain"):
598
+ Chain = "Chain"
599
+ InputIdx = 1
600
+ DataLen = 3
601
+ else:
602
+ Chain = ""
603
+ InputIdx = 0
604
+ DataLen = 1
605
+ ChainTyp = Chain + Typ
606
+
607
+ self.Typ = Typ
608
+ self.Type = Type
609
+ self.Chain = Chain
610
+ self.ChainTyp = ChainTyp
611
+ self.InputIdx = InputIdx
612
+ self.DataLen = DataLen
613
+
614
+ self.LookupRecord = Type + "LookupRecord"
615
+
616
+ if Format == 1:
617
+ Coverage = lambda r: r.Coverage
618
+ ChainCoverage = lambda r: r.Coverage
619
+ ContextData = lambda r: (None,)
620
+ ChainContextData = lambda r: (None, None, None)
621
+ SetContextData = None
622
+ SetChainContextData = None
623
+ RuleData = lambda r: (r.Input,)
624
+ ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
625
+
626
+ def SetRuleData(r, d):
627
+ (r.Input,) = d
628
+ (r.GlyphCount,) = (len(x) + 1 for x in d)
629
+
630
+ def ChainSetRuleData(r, d):
631
+ (r.Backtrack, r.Input, r.LookAhead) = d
632
+ (
633
+ r.BacktrackGlyphCount,
634
+ r.InputGlyphCount,
635
+ r.LookAheadGlyphCount,
636
+ ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
637
+
638
+ elif Format == 2:
639
+ Coverage = lambda r: r.Coverage
640
+ ChainCoverage = lambda r: r.Coverage
641
+ ContextData = lambda r: (r.ClassDef,)
642
+ ChainContextData = lambda r: (
643
+ r.BacktrackClassDef,
644
+ r.InputClassDef,
645
+ r.LookAheadClassDef,
646
+ )
647
+
648
+ def SetContextData(r, d):
649
+ (r.ClassDef,) = d
650
+
651
+ def SetChainContextData(r, d):
652
+ (r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d
653
+
654
+ RuleData = lambda r: (r.Class,)
655
+ ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
656
+
657
+ def SetRuleData(r, d):
658
+ (r.Class,) = d
659
+ (r.GlyphCount,) = (len(x) + 1 for x in d)
660
+
661
+ def ChainSetRuleData(r, d):
662
+ (r.Backtrack, r.Input, r.LookAhead) = d
663
+ (
664
+ r.BacktrackGlyphCount,
665
+ r.InputGlyphCount,
666
+ r.LookAheadGlyphCount,
667
+ ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
668
+
669
+ elif Format == 3:
670
+ Coverage = lambda r: r.Coverage[0]
671
+ ChainCoverage = lambda r: r.InputCoverage[0]
672
+ ContextData = None
673
+ ChainContextData = None
674
+ SetContextData = None
675
+ SetChainContextData = None
676
+ RuleData = lambda r: r.Coverage
677
+ ChainRuleData = lambda r: (
678
+ r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage
679
+ )
680
+
681
+ def SetRuleData(r, d):
682
+ (r.Coverage,) = d
683
+ (r.GlyphCount,) = (len(x) for x in d)
684
+
685
+ def ChainSetRuleData(r, d):
686
+ (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
687
+ (
688
+ r.BacktrackGlyphCount,
689
+ r.InputGlyphCount,
690
+ r.LookAheadGlyphCount,
691
+ ) = (len(x) for x in d)
692
+
693
+ else:
694
+ assert 0, "unknown format: %s" % Format
695
+
696
+ if Chain:
697
+ self.Coverage = ChainCoverage
698
+ self.ContextData = ChainContextData
699
+ self.SetContextData = SetChainContextData
700
+ self.RuleData = ChainRuleData
701
+ self.SetRuleData = ChainSetRuleData
702
+ else:
703
+ self.Coverage = Coverage
704
+ self.ContextData = ContextData
705
+ self.SetContextData = SetContextData
706
+ self.RuleData = RuleData
707
+ self.SetRuleData = SetRuleData
708
+
709
+ if Format == 1:
710
+ self.Rule = ChainTyp + "Rule"
711
+ self.RuleCount = ChainTyp + "RuleCount"
712
+ self.RuleSet = ChainTyp + "RuleSet"
713
+ self.RuleSetCount = ChainTyp + "RuleSetCount"
714
+ self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
715
+ elif Format == 2:
716
+ self.Rule = ChainTyp + "ClassRule"
717
+ self.RuleCount = ChainTyp + "ClassRuleCount"
718
+ self.RuleSet = ChainTyp + "ClassSet"
719
+ self.RuleSetCount = ChainTyp + "ClassSetCount"
720
+ self.Intersect = lambda glyphs, c, r: (
721
+ c.intersect_class(glyphs, r)
722
+ if c
723
+ else (set(glyphs) if r == 0 else set())
724
+ )
725
+
726
+ self.ClassDef = "InputClassDef" if Chain else "ClassDef"
727
+ self.ClassDefIndex = 1 if Chain else 0
728
+ self.Input = "Input" if Chain else "Class"
729
+
730
+
731
+ def parseLookupRecords(items, klassName, lookupMap=None):
732
+ klass = getattr(ot, klassName)
733
+ lst = []
734
+ for item in items:
735
+ rec = klass()
736
+ item = stripSplitComma(item)
737
+ assert len(item) == 2, item
738
+ idx = int(item[0])
739
+ assert idx > 0, idx
740
+ rec.SequenceIndex = idx - 1
741
+ setReference(mapLookup, lookupMap, item[1], setattr, rec, "LookupListIndex")
742
+ lst.append(rec)
743
+ return lst
744
+
745
+
746
+ def makeClassDef(classDefs, font, klass=ot.Coverage):
747
+ if not classDefs:
748
+ return None
749
+ self = klass()
750
+ self.classDefs = dict(classDefs)
751
+ return self
752
+
753
+
754
+ def parseClassDef(lines, font, klass=ot.ClassDef):
755
+ classDefs = {}
756
+ with lines.between("class definition"):
757
+ for line in lines:
758
+ glyph = makeGlyph(line[0])
759
+ assert glyph not in classDefs, glyph
760
+ classDefs[glyph] = int(line[1])
761
+ return makeClassDef(classDefs, font, klass)
762
+
763
+
764
+ def makeCoverage(glyphs, font, klass=ot.Coverage):
765
+ if not glyphs:
766
+ return None
767
+ if isinstance(glyphs, set):
768
+ glyphs = sorted(glyphs)
769
+ coverage = klass()
770
+ coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID)
771
+ return coverage
772
+
773
+
774
+ def parseCoverage(lines, font, klass=ot.Coverage):
775
+ glyphs = []
776
+ with lines.between("coverage definition"):
777
+ for line in lines:
778
+ glyphs.append(makeGlyph(line[0]))
779
+ return makeCoverage(glyphs, font, klass)
780
+
781
+
782
+ def bucketizeRules(self, c, rules, bucketKeys):
783
+ buckets = {}
784
+ for seq, recs in rules:
785
+ buckets.setdefault(seq[c.InputIdx][0], []).append(
786
+ (tuple(s[1 if i == c.InputIdx else 0 :] for i, s in enumerate(seq)), recs)
787
+ )
788
+
789
+ rulesets = []
790
+ for firstGlyph in bucketKeys:
791
+ if firstGlyph not in buckets:
792
+ rulesets.append(None)
793
+ continue
794
+ thisRules = []
795
+ for seq, recs in buckets[firstGlyph]:
796
+ rule = getattr(ot, c.Rule)()
797
+ c.SetRuleData(rule, seq)
798
+ setattr(rule, c.Type + "Count", len(recs))
799
+ setattr(rule, c.LookupRecord, recs)
800
+ thisRules.append(rule)
801
+
802
+ ruleset = getattr(ot, c.RuleSet)()
803
+ setattr(ruleset, c.Rule, thisRules)
804
+ setattr(ruleset, c.RuleCount, len(thisRules))
805
+ rulesets.append(ruleset)
806
+
807
+ setattr(self, c.RuleSet, rulesets)
808
+ setattr(self, c.RuleSetCount, len(rulesets))
809
+
810
+
811
+ def parseContext(lines, font, Type, lookupMap=None):
812
+ self = getattr(ot, Type)()
813
+ typ = lines.peeks()[0].split()[0].lower()
814
+ if typ == "glyph":
815
+ self.Format = 1
816
+ log.debug("Parsing %s format %s", Type, self.Format)
817
+ c = ContextHelper(Type, self.Format)
818
+ rules = []
819
+ for line in lines:
820
+ assert line[0].lower() == "glyph", line[0]
821
+ while len(line) < 1 + c.DataLen:
822
+ line.append("")
823
+ seq = tuple(makeGlyphs(stripSplitComma(i)) for i in line[1 : 1 + c.DataLen])
824
+ recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap)
825
+ rules.append((seq, recs))
826
+
827
+ firstGlyphs = set(seq[c.InputIdx][0] for seq, recs in rules)
828
+ self.Coverage = makeCoverage(firstGlyphs, font)
829
+ bucketizeRules(self, c, rules, self.Coverage.glyphs)
830
+ elif typ.endswith("class"):
831
+ self.Format = 2
832
+ log.debug("Parsing %s format %s", Type, self.Format)
833
+ c = ContextHelper(Type, self.Format)
834
+ classDefs = [None] * c.DataLen
835
+ while lines.peeks()[0].endswith("class definition begin"):
836
+ typ = lines.peek()[0][: -len("class definition begin")].lower()
837
+ idx, klass = {
838
+ 1: {
839
+ "": (0, ot.ClassDef),
840
+ },
841
+ 3: {
842
+ "backtrack": (0, ot.BacktrackClassDef),
843
+ "": (1, ot.InputClassDef),
844
+ "lookahead": (2, ot.LookAheadClassDef),
845
+ },
846
+ }[c.DataLen][typ]
847
+ assert classDefs[idx] is None, idx
848
+ classDefs[idx] = parseClassDef(lines, font, klass=klass)
849
+ c.SetContextData(self, classDefs)
850
+ rules = []
851
+ for line in lines:
852
+ assert line[0].lower().startswith("class"), line[0]
853
+ while len(line) < 1 + c.DataLen:
854
+ line.append("")
855
+ seq = tuple(intSplitComma(i) for i in line[1 : 1 + c.DataLen])
856
+ recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap)
857
+ rules.append((seq, recs))
858
+ firstClasses = set(seq[c.InputIdx][0] for seq, recs in rules)
859
+ firstGlyphs = set(
860
+ g for g, c in classDefs[c.InputIdx].classDefs.items() if c in firstClasses
861
+ )
862
+ self.Coverage = makeCoverage(firstGlyphs, font)
863
+ bucketizeRules(self, c, rules, range(max(firstClasses) + 1))
864
+ elif typ.endswith("coverage"):
865
+ self.Format = 3
866
+ log.debug("Parsing %s format %s", Type, self.Format)
867
+ c = ContextHelper(Type, self.Format)
868
+ coverages = tuple([] for i in range(c.DataLen))
869
+ while lines.peeks()[0].endswith("coverage definition begin"):
870
+ typ = lines.peek()[0][: -len("coverage definition begin")].lower()
871
+ idx, klass = {
872
+ 1: {
873
+ "": (0, ot.Coverage),
874
+ },
875
+ 3: {
876
+ "backtrack": (0, ot.BacktrackCoverage),
877
+ "input": (1, ot.InputCoverage),
878
+ "lookahead": (2, ot.LookAheadCoverage),
879
+ },
880
+ }[c.DataLen][typ]
881
+ coverages[idx].append(parseCoverage(lines, font, klass=klass))
882
+ c.SetRuleData(self, coverages)
883
+ lines = list(lines)
884
+ assert len(lines) == 1
885
+ line = lines[0]
886
+ assert line[0].lower() == "coverage", line[0]
887
+ recs = parseLookupRecords(line[1:], c.LookupRecord, lookupMap)
888
+ setattr(self, c.Type + "Count", len(recs))
889
+ setattr(self, c.LookupRecord, recs)
890
+ else:
891
+ assert 0, typ
892
+ return self
893
+
894
+
895
+ def parseContextSubst(lines, font, lookupMap=None):
896
+ return parseContext(lines, font, "ContextSubst", lookupMap=lookupMap)
897
+
898
+
899
+ def parseContextPos(lines, font, lookupMap=None):
900
+ return parseContext(lines, font, "ContextPos", lookupMap=lookupMap)
901
+
902
+
903
+ def parseChainedSubst(lines, font, lookupMap=None):
904
+ return parseContext(lines, font, "ChainContextSubst", lookupMap=lookupMap)
905
+
906
+
907
+ def parseChainedPos(lines, font, lookupMap=None):
908
+ return parseContext(lines, font, "ChainContextPos", lookupMap=lookupMap)
909
+
910
+
911
+ def parseReverseChainedSubst(lines, font, _lookupMap=None):
912
+ self = ot.ReverseChainSingleSubst()
913
+ self.Format = 1
914
+ coverages = ([], [])
915
+ while lines.peeks()[0].endswith("coverage definition begin"):
916
+ typ = lines.peek()[0][: -len("coverage definition begin")].lower()
917
+ idx, klass = {
918
+ "backtrack": (0, ot.BacktrackCoverage),
919
+ "lookahead": (1, ot.LookAheadCoverage),
920
+ }[typ]
921
+ coverages[idx].append(parseCoverage(lines, font, klass=klass))
922
+ self.BacktrackCoverage = coverages[0]
923
+ self.BacktrackGlyphCount = len(self.BacktrackCoverage)
924
+ self.LookAheadCoverage = coverages[1]
925
+ self.LookAheadGlyphCount = len(self.LookAheadCoverage)
926
+ mapping = {}
927
+ for line in lines:
928
+ assert len(line) == 2, line
929
+ line = makeGlyphs(line)
930
+ mapping[line[0]] = line[1]
931
+ self.Coverage = makeCoverage(set(mapping.keys()), font)
932
+ self.Substitute = [mapping[k] for k in self.Coverage.glyphs]
933
+ self.GlyphCount = len(self.Substitute)
934
+ return self
935
+
936
+
937
+ def parseLookup(lines, tableTag, font, lookupMap=None):
938
+ line = lines.expect("lookup")
939
+ _, name, typ = line
940
+ log.debug("Parsing lookup type %s %s", typ, name)
941
+ lookup = ot.Lookup()
942
+ lookup.LookupFlag, filterset = parseLookupFlags(lines)
943
+ if filterset is not None:
944
+ lookup.MarkFilteringSet = filterset
945
+ lookup.LookupType, parseLookupSubTable = {
946
+ "GSUB": {
947
+ "single": (1, parseSingleSubst),
948
+ "multiple": (2, parseMultiple),
949
+ "alternate": (3, parseAlternate),
950
+ "ligature": (4, parseLigature),
951
+ "context": (5, parseContextSubst),
952
+ "chained": (6, parseChainedSubst),
953
+ "reversechained": (8, parseReverseChainedSubst),
954
+ },
955
+ "GPOS": {
956
+ "single": (1, parseSinglePos),
957
+ "pair": (2, parsePair),
958
+ "kernset": (2, parseKernset),
959
+ "cursive": (3, parseCursive),
960
+ "mark to base": (4, parseMarkToBase),
961
+ "mark to ligature": (5, parseMarkToLigature),
962
+ "mark to mark": (6, parseMarkToMark),
963
+ "context": (7, parseContextPos),
964
+ "chained": (8, parseChainedPos),
965
+ },
966
+ }[tableTag][typ]
967
+
968
+ with lines.until("lookup end"):
969
+ subtables = []
970
+
971
+ while lines.peek():
972
+ with lines.until(("% subtable", "subtable end")):
973
+ while lines.peek():
974
+ subtable = parseLookupSubTable(lines, font, lookupMap)
975
+ assert lookup.LookupType == subtable.LookupType
976
+ subtables.append(subtable)
977
+ if lines.peeks()[0] in ("% subtable", "subtable end"):
978
+ next(lines)
979
+ lines.expect("lookup end")
980
+
981
+ lookup.SubTable = subtables
982
+ lookup.SubTableCount = len(lookup.SubTable)
983
+ if lookup.SubTableCount == 0:
984
+ # Remove this return when following is fixed:
985
+ # https://github.com/fonttools/fonttools/issues/789
986
+ return None
987
+ return lookup
988
+
989
+
990
+ def parseGSUBGPOS(lines, font, tableTag):
991
+ container = ttLib.getTableClass(tableTag)()
992
+ lookupMap = DeferredMapping()
993
+ featureMap = DeferredMapping()
994
+ assert tableTag in ("GSUB", "GPOS")
995
+ log.debug("Parsing %s", tableTag)
996
+ self = getattr(ot, tableTag)()
997
+ self.Version = 0x00010000
998
+ fields = {
999
+ "script table begin": (
1000
+ "ScriptList",
1001
+ lambda lines: parseScriptList(lines, featureMap),
1002
+ ),
1003
+ "feature table begin": (
1004
+ "FeatureList",
1005
+ lambda lines: parseFeatureList(lines, lookupMap, featureMap),
1006
+ ),
1007
+ "lookup": ("LookupList", None),
1008
+ }
1009
+ for attr, parser in fields.values():
1010
+ setattr(self, attr, None)
1011
+ while lines.peek() is not None:
1012
+ typ = lines.peek()[0].lower()
1013
+ if typ not in fields:
1014
+ log.debug("Skipping %s", lines.peek())
1015
+ next(lines)
1016
+ continue
1017
+ attr, parser = fields[typ]
1018
+ if typ == "lookup":
1019
+ if self.LookupList is None:
1020
+ self.LookupList = ot.LookupList()
1021
+ self.LookupList.Lookup = []
1022
+ _, name, _ = lines.peek()
1023
+ lookup = parseLookup(lines, tableTag, font, lookupMap)
1024
+ if lookupMap is not None:
1025
+ assert name not in lookupMap, "Duplicate lookup name: %s" % name
1026
+ lookupMap[name] = len(self.LookupList.Lookup)
1027
+ else:
1028
+ assert int(name) == len(self.LookupList.Lookup), "%d %d" % (
1029
+ name,
1030
+ len(self.Lookup),
1031
+ )
1032
+ self.LookupList.Lookup.append(lookup)
1033
+ else:
1034
+ assert getattr(self, attr) is None, attr
1035
+ setattr(self, attr, parser(lines))
1036
+ if self.LookupList:
1037
+ self.LookupList.LookupCount = len(self.LookupList.Lookup)
1038
+ if lookupMap is not None:
1039
+ lookupMap.applyDeferredMappings()
1040
+ if os.environ.get(LOOKUP_DEBUG_ENV_VAR):
1041
+ if "Debg" not in font:
1042
+ font["Debg"] = newTable("Debg")
1043
+ font["Debg"].data = {}
1044
+ debug = (
1045
+ font["Debg"]
1046
+ .data.setdefault(LOOKUP_DEBUG_INFO_KEY, {})
1047
+ .setdefault(tableTag, {})
1048
+ )
1049
+ for name, lookup in lookupMap.items():
1050
+ debug[str(lookup)] = ["", name, ""]
1051
+
1052
+ featureMap.applyDeferredMappings()
1053
+ container.table = self
1054
+ return container
1055
+
1056
+
1057
+ def parseGSUB(lines, font):
1058
+ return parseGSUBGPOS(lines, font, "GSUB")
1059
+
1060
+
1061
+ def parseGPOS(lines, font):
1062
+ return parseGSUBGPOS(lines, font, "GPOS")
1063
+
1064
+
1065
+ def parseAttachList(lines, font):
1066
+ points = {}
1067
+ with lines.between("attachment list"):
1068
+ for line in lines:
1069
+ glyph = makeGlyph(line[0])
1070
+ assert glyph not in points, glyph
1071
+ points[glyph] = [int(i) for i in line[1:]]
1072
+ return otl.buildAttachList(points, font.getReverseGlyphMap())
1073
+
1074
+
1075
+ def parseCaretList(lines, font):
1076
+ carets = {}
1077
+ with lines.between("carets"):
1078
+ for line in lines:
1079
+ glyph = makeGlyph(line[0])
1080
+ assert glyph not in carets, glyph
1081
+ num = int(line[1])
1082
+ thisCarets = [int(i) for i in line[2:]]
1083
+ assert num == len(thisCarets), line
1084
+ carets[glyph] = thisCarets
1085
+ return otl.buildLigCaretList(carets, {}, font.getReverseGlyphMap())
1086
+
1087
+
1088
+ def makeMarkFilteringSets(sets, font):
1089
+ self = ot.MarkGlyphSetsDef()
1090
+ self.MarkSetTableFormat = 1
1091
+ self.MarkSetCount = 1 + max(sets.keys())
1092
+ self.Coverage = [None] * self.MarkSetCount
1093
+ for k, v in sorted(sets.items()):
1094
+ self.Coverage[k] = makeCoverage(set(v), font)
1095
+ return self
1096
+
1097
+
1098
+ def parseMarkFilteringSets(lines, font):
1099
+ sets = {}
1100
+ with lines.between("set definition"):
1101
+ for line in lines:
1102
+ assert len(line) == 2, line
1103
+ glyph = makeGlyph(line[0])
1104
+ # TODO accept set names
1105
+ st = int(line[1])
1106
+ if st not in sets:
1107
+ sets[st] = []
1108
+ sets[st].append(glyph)
1109
+ return makeMarkFilteringSets(sets, font)
1110
+
1111
+
1112
+ def parseGDEF(lines, font):
1113
+ container = ttLib.getTableClass("GDEF")()
1114
+ log.debug("Parsing GDEF")
1115
+ self = ot.GDEF()
1116
+ fields = {
1117
+ "class definition begin": (
1118
+ "GlyphClassDef",
1119
+ lambda lines, font: parseClassDef(lines, font, klass=ot.GlyphClassDef),
1120
+ ),
1121
+ "attachment list begin": ("AttachList", parseAttachList),
1122
+ "carets begin": ("LigCaretList", parseCaretList),
1123
+ "mark attachment class definition begin": (
1124
+ "MarkAttachClassDef",
1125
+ lambda lines, font: parseClassDef(lines, font, klass=ot.MarkAttachClassDef),
1126
+ ),
1127
+ "markfilter set definition begin": ("MarkGlyphSetsDef", parseMarkFilteringSets),
1128
+ }
1129
+ for attr, parser in fields.values():
1130
+ setattr(self, attr, None)
1131
+ while lines.peek() is not None:
1132
+ typ = lines.peek()[0].lower()
1133
+ if typ not in fields:
1134
+ log.debug("Skipping %s", typ)
1135
+ next(lines)
1136
+ continue
1137
+ attr, parser = fields[typ]
1138
+ assert getattr(self, attr) is None, attr
1139
+ setattr(self, attr, parser(lines, font))
1140
+ self.Version = 0x00010000 if self.MarkGlyphSetsDef is None else 0x00010002
1141
+ container.table = self
1142
+ return container
1143
+
1144
+
1145
+ def parseCmap(lines, font):
1146
+ container = ttLib.getTableClass("cmap")()
1147
+ log.debug("Parsing cmap")
1148
+ tables = []
1149
+ while lines.peek() is not None:
1150
+ lines.expect("cmap subtable %d" % len(tables))
1151
+ platId, encId, fmt, lang = [
1152
+ parseCmapId(lines, field)
1153
+ for field in ("platformID", "encodingID", "format", "language")
1154
+ ]
1155
+ table = cmap_classes[fmt](fmt)
1156
+ table.platformID = platId
1157
+ table.platEncID = encId
1158
+ table.language = lang
1159
+ table.cmap = {}
1160
+ line = next(lines)
1161
+ while line[0] != "end subtable":
1162
+ table.cmap[int(line[0], 16)] = line[1]
1163
+ line = next(lines)
1164
+ tables.append(table)
1165
+ container.tableVersion = 0
1166
+ container.tables = tables
1167
+ return container
1168
+
1169
+
1170
+ def parseCmapId(lines, field):
1171
+ line = next(lines)
1172
+ assert field == line[0]
1173
+ return int(line[1])
1174
+
1175
+
1176
+ def parseTable(lines, font, tableTag=None):
1177
+ log.debug("Parsing table")
1178
+ line = lines.peeks()
1179
+ tag = None
1180
+ if line[0].split()[0] == "FontDame":
1181
+ tag = line[0].split()[1]
1182
+ elif " ".join(line[0].split()[:3]) == "Font Chef Table":
1183
+ tag = line[0].split()[3]
1184
+ if tag is not None:
1185
+ next(lines)
1186
+ tag = tag.ljust(4)
1187
+ if tableTag is None:
1188
+ tableTag = tag
1189
+ else:
1190
+ assert tableTag == tag, (tableTag, tag)
1191
+
1192
+ assert (
1193
+ tableTag is not None
1194
+ ), "Don't know what table to parse and data doesn't specify"
1195
+
1196
+ return {
1197
+ "GSUB": parseGSUB,
1198
+ "GPOS": parseGPOS,
1199
+ "GDEF": parseGDEF,
1200
+ "cmap": parseCmap,
1201
+ }[tableTag](lines, font)
1202
+
1203
+
1204
+ class Tokenizer(object):
1205
+ def __init__(self, f):
1206
+ # TODO BytesIO / StringIO as needed? also, figure out whether we work on bytes or unicode
1207
+ lines = iter(f)
1208
+ try:
1209
+ self.filename = f.name
1210
+ except:
1211
+ self.filename = None
1212
+ self.lines = iter(lines)
1213
+ self.line = ""
1214
+ self.lineno = 0
1215
+ self.stoppers = []
1216
+ self.buffer = None
1217
+
1218
+ def __iter__(self):
1219
+ return self
1220
+
1221
+ def _next_line(self):
1222
+ self.lineno += 1
1223
+ line = self.line = next(self.lines)
1224
+ line = [s.strip() for s in line.split("\t")]
1225
+ if len(line) == 1 and not line[0]:
1226
+ del line[0]
1227
+ if line and not line[-1]:
1228
+ log.warning("trailing tab found on line %d: %s" % (self.lineno, self.line))
1229
+ while line and not line[-1]:
1230
+ del line[-1]
1231
+ return line
1232
+
1233
+ def _next_nonempty(self):
1234
+ while True:
1235
+ line = self._next_line()
1236
+ # Skip comments and empty lines
1237
+ if line and line[0] and (line[0][0] != "%" or line[0] == "% subtable"):
1238
+ return line
1239
+
1240
+ def _next_buffered(self):
1241
+ if self.buffer:
1242
+ ret = self.buffer
1243
+ self.buffer = None
1244
+ return ret
1245
+ else:
1246
+ return self._next_nonempty()
1247
+
1248
+ def __next__(self):
1249
+ line = self._next_buffered()
1250
+ if line[0].lower() in self.stoppers:
1251
+ self.buffer = line
1252
+ raise StopIteration
1253
+ return line
1254
+
1255
+ def next(self):
1256
+ return self.__next__()
1257
+
1258
+ def peek(self):
1259
+ if not self.buffer:
1260
+ try:
1261
+ self.buffer = self._next_nonempty()
1262
+ except StopIteration:
1263
+ return None
1264
+ if self.buffer[0].lower() in self.stoppers:
1265
+ return None
1266
+ return self.buffer
1267
+
1268
+ def peeks(self):
1269
+ ret = self.peek()
1270
+ return ret if ret is not None else ("",)
1271
+
1272
+ @contextmanager
1273
+ def between(self, tag):
1274
+ start = tag + " begin"
1275
+ end = tag + " end"
1276
+ self.expectendswith(start)
1277
+ self.stoppers.append(end)
1278
+ yield
1279
+ del self.stoppers[-1]
1280
+ self.expect(tag + " end")
1281
+
1282
+ @contextmanager
1283
+ def until(self, tags):
1284
+ if type(tags) is not tuple:
1285
+ tags = (tags,)
1286
+ self.stoppers.extend(tags)
1287
+ yield
1288
+ del self.stoppers[-len(tags) :]
1289
+
1290
+ def expect(self, s):
1291
+ line = next(self)
1292
+ tag = line[0].lower()
1293
+ assert tag == s, "Expected '%s', got '%s'" % (s, tag)
1294
+ return line
1295
+
1296
+ def expectendswith(self, s):
1297
+ line = next(self)
1298
+ tag = line[0].lower()
1299
+ assert tag.endswith(s), "Expected '*%s', got '%s'" % (s, tag)
1300
+ return line
1301
+
1302
+
1303
+ def build(f, font, tableTag=None):
1304
+ """Convert a Monotype font layout file to an OpenType layout object
1305
+
1306
+ A font object must be passed, but this may be a "dummy" font; it is only
1307
+ used for sorting glyph sets when making coverage tables and to hold the
1308
+ OpenType layout table while it is being built.
1309
+
1310
+ Args:
1311
+ f: A file object.
1312
+ font (TTFont): A font object.
1313
+ tableTag (string): If provided, asserts that the file contains data for the
1314
+ given OpenType table.
1315
+
1316
+ Returns:
1317
+ An object representing the table. (e.g. ``table_G_S_U_B_``)
1318
+ """
1319
+ lines = Tokenizer(f)
1320
+ return parseTable(lines, font, tableTag=tableTag)
1321
+
1322
+
1323
+ def main(args=None, font=None):
1324
+ """Convert a FontDame OTL file to TTX XML
1325
+
1326
+ Writes XML output to stdout.
1327
+
1328
+ Args:
1329
+ args: Command line arguments (``--font``, ``--table``, input files).
1330
+ """
1331
+ import sys
1332
+ from fontTools import configLogger
1333
+ from fontTools.misc.testTools import MockFont
1334
+
1335
+ if args is None:
1336
+ args = sys.argv[1:]
1337
+
1338
+ # configure the library logger (for >= WARNING)
1339
+ configLogger()
1340
+ # comment this out to enable debug messages from mtiLib's logger
1341
+ # log.setLevel(logging.DEBUG)
1342
+
1343
+ import argparse
1344
+
1345
+ parser = argparse.ArgumentParser(
1346
+ "fonttools mtiLib",
1347
+ description=main.__doc__,
1348
+ )
1349
+
1350
+ parser.add_argument(
1351
+ "--font",
1352
+ "-f",
1353
+ metavar="FILE",
1354
+ dest="font",
1355
+ help="Input TTF files (used for glyph classes and sorting coverage tables)",
1356
+ )
1357
+ parser.add_argument(
1358
+ "--table",
1359
+ "-t",
1360
+ metavar="TABLE",
1361
+ dest="tableTag",
1362
+ help="Table to fill (sniffed from input file if not provided)",
1363
+ )
1364
+ parser.add_argument(
1365
+ "inputs", metavar="FILE", type=str, nargs="+", help="Input FontDame .txt files"
1366
+ )
1367
+
1368
+ args = parser.parse_args(args)
1369
+
1370
+ if font is None:
1371
+ if args.font:
1372
+ font = ttLib.TTFont(args.font)
1373
+ else:
1374
+ font = MockFont()
1375
+
1376
+ for f in args.inputs:
1377
+ log.debug("Processing %s", f)
1378
+ with open(f, "rt", encoding="utf-8-sig") as f:
1379
+ table = build(f, font, tableTag=args.tableTag)
1380
+ blob = table.compile(font) # Make sure it compiles
1381
+ decompiled = table.__class__()
1382
+ decompiled.decompile(blob, font) # Make sure it decompiles!
1383
+
1384
+ # continue
1385
+ from fontTools.misc import xmlWriter
1386
+
1387
+ tag = table.tableTag
1388
+ writer = xmlWriter.XMLWriter(sys.stdout)
1389
+ writer.begintag(tag)
1390
+ writer.newline()
1391
+ # table.toXML(writer, font)
1392
+ decompiled.toXML(writer, font)
1393
+ writer.endtag(tag)
1394
+ writer.newline()
1395
+
1396
+
1397
+ if __name__ == "__main__":
1398
+ import sys
1399
+
1400
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/mtiLib/__main__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import sys
2
+ from fontTools.mtiLib import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (72.3 kB). View file
 
lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__main__.cpython-312.pyc ADDED
Binary file (372 Bytes). View file
 
lib/python3.12/site-packages/fontTools/otlLib/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """OpenType Layout-related functionality."""
lib/python3.12/site-packages/fontTools/otlLib/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (248 Bytes). View file
 
lib/python3.12/site-packages/fontTools/otlLib/__pycache__/error.cpython-312.pyc ADDED
Binary file (918 Bytes). View file
 
lib/python3.12/site-packages/fontTools/otlLib/__pycache__/maxContextCalc.cpython-312.pyc ADDED
Binary file (3.64 kB). View file
 
lib/python3.12/site-packages/fontTools/otlLib/builder.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/fontTools/otlLib/error.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class OpenTypeLibError(Exception):
2
+ def __init__(self, message, location):
3
+ Exception.__init__(self, message)
4
+ self.location = location
5
+
6
+ def __str__(self):
7
+ message = Exception.__str__(self)
8
+ if self.location:
9
+ return f"{self.location}: {message}"
10
+ else:
11
+ return message
lib/python3.12/site-packages/fontTools/otlLib/maxContextCalc.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __all__ = ["maxCtxFont"]
2
+
3
+
4
+ def maxCtxFont(font):
5
+ """Calculate the usMaxContext value for an entire font."""
6
+
7
+ maxCtx = 0
8
+ for tag in ("GSUB", "GPOS"):
9
+ if tag not in font:
10
+ continue
11
+ table = font[tag].table
12
+ if not table.LookupList:
13
+ continue
14
+ for lookup in table.LookupList.Lookup:
15
+ for st in lookup.SubTable:
16
+ maxCtx = maxCtxSubtable(maxCtx, tag, lookup.LookupType, st)
17
+ return maxCtx
18
+
19
+
20
+ def maxCtxSubtable(maxCtx, tag, lookupType, st):
21
+ """Calculate usMaxContext based on a single lookup table (and an existing
22
+ max value).
23
+ """
24
+
25
+ # single positioning, single / multiple substitution
26
+ if (tag == "GPOS" and lookupType == 1) or (
27
+ tag == "GSUB" and lookupType in (1, 2, 3)
28
+ ):
29
+ maxCtx = max(maxCtx, 1)
30
+
31
+ # pair positioning
32
+ elif tag == "GPOS" and lookupType == 2:
33
+ maxCtx = max(maxCtx, 2)
34
+
35
+ # ligatures
36
+ elif tag == "GSUB" and lookupType == 4:
37
+ for ligatures in st.ligatures.values():
38
+ for ligature in ligatures:
39
+ maxCtx = max(maxCtx, ligature.CompCount)
40
+
41
+ # context
42
+ elif (tag == "GPOS" and lookupType == 7) or (tag == "GSUB" and lookupType == 5):
43
+ maxCtx = maxCtxContextualSubtable(maxCtx, st, "Pos" if tag == "GPOS" else "Sub")
44
+
45
+ # chained context
46
+ elif (tag == "GPOS" and lookupType == 8) or (tag == "GSUB" and lookupType == 6):
47
+ maxCtx = maxCtxContextualSubtable(
48
+ maxCtx, st, "Pos" if tag == "GPOS" else "Sub", "Chain"
49
+ )
50
+
51
+ # extensions
52
+ elif (tag == "GPOS" and lookupType == 9) or (tag == "GSUB" and lookupType == 7):
53
+ maxCtx = maxCtxSubtable(maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable)
54
+
55
+ # reverse-chained context
56
+ elif tag == "GSUB" and lookupType == 8:
57
+ maxCtx = maxCtxContextualRule(maxCtx, st, "Reverse")
58
+
59
+ return maxCtx
60
+
61
+
62
+ def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=""):
63
+ """Calculate usMaxContext based on a contextual feature subtable."""
64
+
65
+ if st.Format == 1:
66
+ for ruleset in getattr(st, "%s%sRuleSet" % (chain, ruleType)):
67
+ if ruleset is None:
68
+ continue
69
+ for rule in getattr(ruleset, "%s%sRule" % (chain, ruleType)):
70
+ if rule is None:
71
+ continue
72
+ maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
73
+
74
+ elif st.Format == 2:
75
+ for ruleset in getattr(st, "%s%sClassSet" % (chain, ruleType)):
76
+ if ruleset is None:
77
+ continue
78
+ for rule in getattr(ruleset, "%s%sClassRule" % (chain, ruleType)):
79
+ if rule is None:
80
+ continue
81
+ maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
82
+
83
+ elif st.Format == 3:
84
+ maxCtx = maxCtxContextualRule(maxCtx, st, chain)
85
+
86
+ return maxCtx
87
+
88
+
89
+ def maxCtxContextualRule(maxCtx, st, chain):
90
+ """Calculate usMaxContext based on a contextual feature rule."""
91
+
92
+ if not chain:
93
+ return max(maxCtx, st.GlyphCount)
94
+ elif chain == "Reverse":
95
+ return max(maxCtx, 1 + st.LookAheadGlyphCount)
96
+ return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount)
lib/python3.12/site-packages/fontTools/otlLib/optimize/__init__.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from argparse import RawTextHelpFormatter
2
+ from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL, compact
3
+ from fontTools.ttLib import TTFont
4
+
5
+
6
+ def main(args=None):
7
+ """Optimize the layout tables of an existing font"""
8
+ from argparse import ArgumentParser
9
+
10
+ from fontTools import configLogger
11
+
12
+ parser = ArgumentParser(
13
+ prog="otlLib.optimize",
14
+ description=main.__doc__,
15
+ formatter_class=RawTextHelpFormatter,
16
+ )
17
+ parser.add_argument("font")
18
+ parser.add_argument(
19
+ "-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
20
+ )
21
+ parser.add_argument(
22
+ "--gpos-compression-level",
23
+ help=COMPRESSION_LEVEL.help,
24
+ default=COMPRESSION_LEVEL.default,
25
+ choices=list(range(10)),
26
+ type=int,
27
+ )
28
+ logging_group = parser.add_mutually_exclusive_group(required=False)
29
+ logging_group.add_argument(
30
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
31
+ )
32
+ logging_group.add_argument(
33
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
34
+ )
35
+ options = parser.parse_args(args)
36
+
37
+ configLogger(
38
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
39
+ )
40
+
41
+ font = TTFont(options.font)
42
+ compact(font, options.gpos_compression_level)
43
+ font.save(options.outfile or options.font)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ import sys
48
+
49
+ if len(sys.argv) > 1:
50
+ sys.exit(main())
51
+ import doctest
52
+
53
+ sys.exit(doctest.testmod().failed)
lib/python3.12/site-packages/fontTools/otlLib/optimize/__main__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import sys
2
+ from fontTools.otlLib.optimize import main
3
+
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/otlLib/optimize/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (2.54 kB). View file