ZhengyangZhang commited on
Commit
40c3acd
·
verified ·
1 Parent(s): fb7b977

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/__pycache__/__main__.cpython-312.pyc +0 -0
  2. lib/python3.12/site-packages/fontTools/__pycache__/afmLib.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/fontTools/__pycache__/fontBuilder.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/fontTools/__pycache__/help.cpython-312.pyc +0 -0
  5. lib/python3.12/site-packages/fontTools/__pycache__/tfmLib.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/fontTools/__pycache__/ttx.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/fontTools/__pycache__/unicode.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/fontTools/cffLib/CFF2ToCFF.py +258 -0
  9. lib/python3.12/site-packages/fontTools/cffLib/CFFToCFF2.py +305 -0
  10. lib/python3.12/site-packages/fontTools/cffLib/__init__.py +0 -0
  11. lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFFToCFF2.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/fontTools/cffLib/__pycache__/specializer.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/fontTools/cffLib/__pycache__/transforms.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/fontTools/cffLib/__pycache__/width.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/fontTools/cffLib/specializer.py +927 -0
  17. lib/python3.12/site-packages/fontTools/cffLib/transforms.py +495 -0
  18. lib/python3.12/site-packages/fontTools/cffLib/width.py +210 -0
  19. lib/python3.12/site-packages/fontTools/config/__init__.py +90 -0
  20. lib/python3.12/site-packages/fontTools/config/__pycache__/__init__.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/fontTools/merge/__init__.py +248 -0
  22. lib/python3.12/site-packages/fontTools/merge/__main__.py +6 -0
  23. lib/python3.12/site-packages/fontTools/merge/__pycache__/__init__.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/fontTools/merge/__pycache__/__main__.cpython-312.pyc +0 -0
  25. lib/python3.12/site-packages/fontTools/merge/__pycache__/base.cpython-312.pyc +0 -0
  26. lib/python3.12/site-packages/fontTools/merge/__pycache__/cmap.cpython-312.pyc +0 -0
  27. lib/python3.12/site-packages/fontTools/merge/__pycache__/layout.cpython-312.pyc +0 -0
  28. lib/python3.12/site-packages/fontTools/merge/__pycache__/options.cpython-312.pyc +0 -0
  29. lib/python3.12/site-packages/fontTools/merge/__pycache__/tables.cpython-312.pyc +0 -0
  30. lib/python3.12/site-packages/fontTools/merge/__pycache__/unicode.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/fontTools/merge/__pycache__/util.cpython-312.pyc +0 -0
  32. lib/python3.12/site-packages/fontTools/merge/base.py +81 -0
  33. lib/python3.12/site-packages/fontTools/merge/cmap.py +173 -0
  34. lib/python3.12/site-packages/fontTools/merge/layout.py +526 -0
  35. lib/python3.12/site-packages/fontTools/merge/options.py +85 -0
  36. lib/python3.12/site-packages/fontTools/merge/tables.py +352 -0
  37. lib/python3.12/site-packages/fontTools/merge/unicode.py +78 -0
  38. lib/python3.12/site-packages/fontTools/merge/util.py +143 -0
  39. lib/python3.12/site-packages/fontTools/misc/__init__.py +1 -0
  40. lib/python3.12/site-packages/fontTools/misc/__pycache__/__init__.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/fontTools/misc/__pycache__/arrayTools.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/fontTools/misc/__pycache__/bezierTools.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/fontTools/misc/__pycache__/classifyTools.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/fontTools/misc/__pycache__/cliTools.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/fontTools/misc/__pycache__/configTools.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/fontTools/misc/__pycache__/cython.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/fontTools/misc/__pycache__/dictTools.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/fontTools/misc/__pycache__/eexec.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/fontTools/misc/__pycache__/encodingTools.cpython-312.pyc +0 -0
  50. lib/python3.12/site-packages/fontTools/misc/__pycache__/enumTools.cpython-312.pyc +0 -0
lib/python3.12/site-packages/fontTools/__pycache__/__main__.cpython-312.pyc ADDED
Binary file (1.26 kB). View file
 
lib/python3.12/site-packages/fontTools/__pycache__/afmLib.cpython-312.pyc ADDED
Binary file (16.7 kB). View file
 
lib/python3.12/site-packages/fontTools/__pycache__/fontBuilder.cpython-312.pyc ADDED
Binary file (36.8 kB). View file
 
lib/python3.12/site-packages/fontTools/__pycache__/help.cpython-312.pyc ADDED
Binary file (2.03 kB). View file
 
lib/python3.12/site-packages/fontTools/__pycache__/tfmLib.cpython-312.pyc ADDED
Binary file (18.3 kB). View file
 
lib/python3.12/site-packages/fontTools/__pycache__/ttx.cpython-312.pyc ADDED
Binary file (19.7 kB). View file
 
lib/python3.12/site-packages/fontTools/__pycache__/unicode.cpython-312.pyc ADDED
Binary file (2.33 kB). View file
 
lib/python3.12/site-packages/fontTools/cffLib/CFF2ToCFF.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CFF2 to CFF converter."""
2
+
3
+ from fontTools.ttLib import TTFont, newTable
4
+ from fontTools.misc.cliTools import makeOutputFileName
5
+ from fontTools.misc.psCharStrings import T2StackUseExtractor
6
+ from fontTools.cffLib import (
7
+ TopDictIndex,
8
+ buildOrder,
9
+ buildDefaults,
10
+ topDictOperators,
11
+ privateDictOperators,
12
+ FDSelect,
13
+ )
14
+ from .transforms import desubroutinizeCharString
15
+ from .specializer import specializeProgram
16
+ from .width import optimizeWidths
17
+ from collections import defaultdict
18
+ import logging
19
+
20
+
21
+ __all__ = ["convertCFF2ToCFF", "main"]
22
+
23
+
24
+ log = logging.getLogger("fontTools.cffLib")
25
+
26
+
27
+ def _convertCFF2ToCFF(cff, otFont):
28
+ """Converts this object from CFF2 format to CFF format. This conversion
29
+ is done 'in-place'. The conversion cannot be reversed.
30
+
31
+ The CFF2 font cannot be variable. (TODO Accept those and convert to the
32
+ default instance?)
33
+
34
+ This assumes a decompiled CFF2 table. (i.e. that the object has been
35
+ filled via :meth:`decompile` and e.g. not loaded from XML.)"""
36
+
37
+ cff.major = 1
38
+
39
+ topDictData = TopDictIndex(None)
40
+ for item in cff.topDictIndex:
41
+ # Iterate over, such that all are decompiled
42
+ item.cff2GetGlyphOrder = None
43
+ topDictData.append(item)
44
+ cff.topDictIndex = topDictData
45
+ topDict = topDictData[0]
46
+
47
+ if hasattr(topDict, "VarStore"):
48
+ raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
49
+
50
+ opOrder = buildOrder(topDictOperators)
51
+ topDict.order = opOrder
52
+ for key in topDict.rawDict.keys():
53
+ if key not in opOrder:
54
+ del topDict.rawDict[key]
55
+ if hasattr(topDict, key):
56
+ delattr(topDict, key)
57
+
58
+ charStrings = topDict.CharStrings
59
+
60
+ fdArray = topDict.FDArray
61
+ if not hasattr(topDict, "FDSelect"):
62
+ # FDSelect is optional in CFF2, but required in CFF.
63
+ fdSelect = topDict.FDSelect = FDSelect()
64
+ fdSelect.gidArray = [0] * len(charStrings.charStrings)
65
+
66
+ defaults = buildDefaults(privateDictOperators)
67
+ order = buildOrder(privateDictOperators)
68
+ for fd in fdArray:
69
+ fd.setCFF2(False)
70
+ privateDict = fd.Private
71
+ privateDict.order = order
72
+ for key in order:
73
+ if key not in privateDict.rawDict and key in defaults:
74
+ privateDict.rawDict[key] = defaults[key]
75
+ for key in privateDict.rawDict.keys():
76
+ if key not in order:
77
+ del privateDict.rawDict[key]
78
+ if hasattr(privateDict, key):
79
+ delattr(privateDict, key)
80
+
81
+ # Add ending operators
82
+ for cs in charStrings.values():
83
+ cs.decompile()
84
+ cs.program.append("endchar")
85
+ for subrSets in [cff.GlobalSubrs] + [
86
+ getattr(fd.Private, "Subrs", []) for fd in fdArray
87
+ ]:
88
+ for cs in subrSets:
89
+ cs.program.append("return")
90
+
91
+ # Add (optimal) width to CharStrings that need it.
92
+ widths = defaultdict(list)
93
+ metrics = otFont["hmtx"].metrics
94
+ for glyphName in charStrings.keys():
95
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
96
+ if fdIndex == None:
97
+ fdIndex = 0
98
+ widths[fdIndex].append(metrics[glyphName][0])
99
+ for fdIndex, widthList in widths.items():
100
+ bestDefault, bestNominal = optimizeWidths(widthList)
101
+ private = fdArray[fdIndex].Private
102
+ private.defaultWidthX = bestDefault
103
+ private.nominalWidthX = bestNominal
104
+ for glyphName in charStrings.keys():
105
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
106
+ if fdIndex == None:
107
+ fdIndex = 0
108
+ private = fdArray[fdIndex].Private
109
+ width = metrics[glyphName][0]
110
+ if width != private.defaultWidthX:
111
+ cs.program.insert(0, width - private.nominalWidthX)
112
+
113
+ # Handle stack use since stack-depth is lower in CFF than in CFF2.
114
+ for glyphName in charStrings.keys():
115
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
116
+ if fdIndex is None:
117
+ fdIndex = 0
118
+ private = fdArray[fdIndex].Private
119
+ extractor = T2StackUseExtractor(
120
+ getattr(private, "Subrs", []), cff.GlobalSubrs, private=private
121
+ )
122
+ stackUse = extractor.execute(cs)
123
+ if stackUse > 48: # CFF stack depth is 48
124
+ desubroutinizeCharString(cs)
125
+ cs.program = specializeProgram(cs.program)
126
+
127
+ # Unused subroutines are still in CFF2 (ie. lacking 'return' operator)
128
+ # because they were not decompiled when we added the 'return'.
129
+ # Moreover, some used subroutines may have become unused after the
130
+ # stack-use fixup. So we remove all unused subroutines now.
131
+ cff.remove_unused_subroutines()
132
+
133
+ mapping = {
134
+ name: ("cid" + str(n).zfill(5) if n else ".notdef")
135
+ for n, name in enumerate(topDict.charset)
136
+ }
137
+ topDict.charset = [
138
+ "cid" + str(n).zfill(5) if n else ".notdef" for n in range(len(topDict.charset))
139
+ ]
140
+ charStrings.charStrings = {
141
+ mapping[name]: v for name, v in charStrings.charStrings.items()
142
+ }
143
+
144
+ topDict.ROS = ("Adobe", "Identity", 0)
145
+
146
+
147
+ def convertCFF2ToCFF(font, *, updatePostTable=True):
148
+ if "CFF2" not in font:
149
+ raise ValueError("Input font does not contain a CFF2 table.")
150
+ cff = font["CFF2"].cff
151
+ _convertCFF2ToCFF(cff, font)
152
+ del font["CFF2"]
153
+ table = font["CFF "] = newTable("CFF ")
154
+ table.cff = cff
155
+
156
+ if updatePostTable and "post" in font:
157
+ # Only version supported for fonts with CFF table is 0x00030000 not 0x20000
158
+ post = font["post"]
159
+ if post.formatType == 2.0:
160
+ post.formatType = 3.0
161
+
162
+
163
+ def main(args=None):
164
+ """Convert CFF2 OTF font to CFF OTF font"""
165
+ if args is None:
166
+ import sys
167
+
168
+ args = sys.argv[1:]
169
+
170
+ import argparse
171
+
172
+ parser = argparse.ArgumentParser(
173
+ "fonttools cffLib.CFF2ToCFF",
174
+ description="Convert a non-variable CFF2 font to CFF.",
175
+ )
176
+ parser.add_argument(
177
+ "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
178
+ )
179
+ parser.add_argument(
180
+ "-o",
181
+ "--output",
182
+ metavar="OUTPUT.ttf",
183
+ default=None,
184
+ help="Output instance OTF file (default: INPUT-CFF2.ttf).",
185
+ )
186
+ parser.add_argument(
187
+ "--no-recalc-timestamp",
188
+ dest="recalc_timestamp",
189
+ action="store_false",
190
+ help="Don't set the output font's timestamp to the current time.",
191
+ )
192
+ parser.add_argument(
193
+ "--remove-overlaps",
194
+ action="store_true",
195
+ help="Merge overlapping contours and components. Requires skia-pathops",
196
+ )
197
+ parser.add_argument(
198
+ "--ignore-overlap-errors",
199
+ action="store_true",
200
+ help="Don't crash if the remove-overlaps operation fails for some glyphs.",
201
+ )
202
+ loggingGroup = parser.add_mutually_exclusive_group(required=False)
203
+ loggingGroup.add_argument(
204
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
205
+ )
206
+ loggingGroup.add_argument(
207
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
208
+ )
209
+ options = parser.parse_args(args)
210
+
211
+ from fontTools import configLogger
212
+
213
+ configLogger(
214
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
215
+ )
216
+
217
+ import os
218
+
219
+ infile = options.input
220
+ if not os.path.isfile(infile):
221
+ parser.error("No such file '{}'".format(infile))
222
+
223
+ outfile = (
224
+ makeOutputFileName(infile, overWrite=True, suffix="-CFF")
225
+ if not options.output
226
+ else options.output
227
+ )
228
+
229
+ font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
230
+
231
+ convertCFF2ToCFF(font)
232
+
233
+ if options.remove_overlaps:
234
+ from fontTools.ttLib.removeOverlaps import removeOverlaps
235
+ from io import BytesIO
236
+
237
+ log.debug("Removing overlaps")
238
+
239
+ stream = BytesIO()
240
+ font.save(stream)
241
+ stream.seek(0)
242
+ font = TTFont(stream, recalcTimestamp=False, recalcBBoxes=False)
243
+ removeOverlaps(
244
+ font,
245
+ ignoreErrors=options.ignore_overlap_errors,
246
+ )
247
+
248
+ log.info(
249
+ "Saving %s",
250
+ outfile,
251
+ )
252
+ font.save(outfile)
253
+
254
+
255
+ if __name__ == "__main__":
256
+ import sys
257
+
258
+ sys.exit(main(sys.argv[1:]))
lib/python3.12/site-packages/fontTools/cffLib/CFFToCFF2.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CFF to CFF2 converter."""
2
+
3
+ from fontTools.ttLib import TTFont, newTable
4
+ from fontTools.misc.cliTools import makeOutputFileName
5
+ from fontTools.misc.psCharStrings import T2WidthExtractor
6
+ from fontTools.cffLib import (
7
+ TopDictIndex,
8
+ FDArrayIndex,
9
+ FontDict,
10
+ buildOrder,
11
+ topDictOperators,
12
+ privateDictOperators,
13
+ topDictOperators2,
14
+ privateDictOperators2,
15
+ )
16
+ from io import BytesIO
17
+ import logging
18
+
19
+ __all__ = ["convertCFFToCFF2", "main"]
20
+
21
+
22
+ log = logging.getLogger("fontTools.cffLib")
23
+
24
+
25
+ class _NominalWidthUsedError(Exception):
26
+ def __add__(self, other):
27
+ raise self
28
+
29
+ def __radd__(self, other):
30
+ raise self
31
+
32
+
33
+ def _convertCFFToCFF2(cff, otFont):
34
+ """Converts this object from CFF format to CFF2 format. This conversion
35
+ is done 'in-place'. The conversion cannot be reversed.
36
+
37
+ This assumes a decompiled CFF table. (i.e. that the object has been
38
+ filled via :meth:`decompile` and e.g. not loaded from XML.)"""
39
+
40
+ # Clean up T2CharStrings
41
+
42
+ topDict = cff.topDictIndex[0]
43
+ fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None
44
+ charStrings = topDict.CharStrings
45
+ globalSubrs = cff.GlobalSubrs
46
+ localSubrs = (
47
+ [getattr(fd.Private, "Subrs", []) for fd in fdArray]
48
+ if fdArray
49
+ else (
50
+ [topDict.Private.Subrs]
51
+ if hasattr(topDict, "Private") and hasattr(topDict.Private, "Subrs")
52
+ else []
53
+ )
54
+ )
55
+
56
+ for glyphName in charStrings.keys():
57
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
58
+ cs.decompile()
59
+
60
+ # Clean up subroutines first
61
+ for subrs in [globalSubrs] + localSubrs:
62
+ for subr in subrs:
63
+ program = subr.program
64
+ i = j = len(program)
65
+ try:
66
+ i = program.index("return")
67
+ except ValueError:
68
+ pass
69
+ try:
70
+ j = program.index("endchar")
71
+ except ValueError:
72
+ pass
73
+ program[min(i, j) :] = []
74
+
75
+ # Clean up glyph charstrings
76
+ removeUnusedSubrs = False
77
+ nominalWidthXError = _NominalWidthUsedError()
78
+ for glyphName in charStrings.keys():
79
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
80
+ program = cs.program
81
+
82
+ thisLocalSubrs = (
83
+ localSubrs[fdIndex]
84
+ if fdIndex is not None
85
+ else (
86
+ getattr(topDict.Private, "Subrs", [])
87
+ if hasattr(topDict, "Private")
88
+ else []
89
+ )
90
+ )
91
+
92
+ # Intentionally use custom type for nominalWidthX, such that any
93
+ # CharString that has an explicit width encoded will throw back to us.
94
+ extractor = T2WidthExtractor(
95
+ thisLocalSubrs,
96
+ globalSubrs,
97
+ nominalWidthXError,
98
+ 0,
99
+ )
100
+ try:
101
+ extractor.execute(cs)
102
+ except _NominalWidthUsedError:
103
+ # Program has explicit width. We want to drop it, but can't
104
+ # just pop the first number since it may be a subroutine call.
105
+ # Instead, when seeing that, we embed the subroutine and recurse.
106
+ # If this ever happened, we later prune unused subroutines.
107
+ while len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]:
108
+ removeUnusedSubrs = True
109
+ subrNumber = program.pop(0)
110
+ assert isinstance(subrNumber, int), subrNumber
111
+ op = program.pop(0)
112
+ bias = extractor.localBias if op == "callsubr" else extractor.globalBias
113
+ subrNumber += bias
114
+ subrSet = thisLocalSubrs if op == "callsubr" else globalSubrs
115
+ subrProgram = subrSet[subrNumber].program
116
+ program[:0] = subrProgram
117
+ # Now pop the actual width
118
+ assert len(program) >= 1, program
119
+ program.pop(0)
120
+
121
+ if program and program[-1] == "endchar":
122
+ program.pop()
123
+
124
+ if removeUnusedSubrs:
125
+ cff.remove_unused_subroutines()
126
+
127
+ # Upconvert TopDict
128
+
129
+ cff.major = 2
130
+ cff2GetGlyphOrder = cff.otFont.getGlyphOrder
131
+ topDictData = TopDictIndex(None, cff2GetGlyphOrder)
132
+ for item in cff.topDictIndex:
133
+ # Iterate over, such that all are decompiled
134
+ topDictData.append(item)
135
+ cff.topDictIndex = topDictData
136
+ topDict = topDictData[0]
137
+ if hasattr(topDict, "Private"):
138
+ privateDict = topDict.Private
139
+ else:
140
+ privateDict = None
141
+ opOrder = buildOrder(topDictOperators2)
142
+ topDict.order = opOrder
143
+ topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
144
+
145
+ if not hasattr(topDict, "FDArray"):
146
+ fdArray = topDict.FDArray = FDArrayIndex()
147
+ fdArray.strings = None
148
+ fdArray.GlobalSubrs = topDict.GlobalSubrs
149
+ topDict.GlobalSubrs.fdArray = fdArray
150
+ charStrings = topDict.CharStrings
151
+ if charStrings.charStringsAreIndexed:
152
+ charStrings.charStringsIndex.fdArray = fdArray
153
+ else:
154
+ charStrings.fdArray = fdArray
155
+ fontDict = FontDict()
156
+ fontDict.setCFF2(True)
157
+ fdArray.append(fontDict)
158
+ fontDict.Private = privateDict
159
+ privateOpOrder = buildOrder(privateDictOperators2)
160
+ if privateDict is not None:
161
+ for entry in privateDictOperators:
162
+ key = entry[1]
163
+ if key not in privateOpOrder:
164
+ if key in privateDict.rawDict:
165
+ # print "Removing private dict", key
166
+ del privateDict.rawDict[key]
167
+ if hasattr(privateDict, key):
168
+ delattr(privateDict, key)
169
+ # print "Removing privateDict attr", key
170
+ else:
171
+ # clean up the PrivateDicts in the fdArray
172
+ fdArray = topDict.FDArray
173
+ privateOpOrder = buildOrder(privateDictOperators2)
174
+ for fontDict in fdArray:
175
+ fontDict.setCFF2(True)
176
+ for key in list(fontDict.rawDict.keys()):
177
+ if key not in fontDict.order:
178
+ del fontDict.rawDict[key]
179
+ if hasattr(fontDict, key):
180
+ delattr(fontDict, key)
181
+
182
+ privateDict = fontDict.Private
183
+ for entry in privateDictOperators:
184
+ key = entry[1]
185
+ if key not in privateOpOrder:
186
+ if key in list(privateDict.rawDict.keys()):
187
+ # print "Removing private dict", key
188
+ del privateDict.rawDict[key]
189
+ if hasattr(privateDict, key):
190
+ delattr(privateDict, key)
191
+ # print "Removing privateDict attr", key
192
+
193
+ # Now delete up the deprecated topDict operators from CFF 1.0
194
+ for entry in topDictOperators:
195
+ key = entry[1]
196
+ # We seem to need to keep the charset operator for now,
197
+ # or we fail to compile with some fonts, like AdditionFont.otf.
198
+ # I don't know which kind of CFF font those are. But keeping
199
+ # charset seems to work. It will be removed when we save and
200
+ # read the font again.
201
+ #
202
+ # AdditionFont.otf has <Encoding name="StandardEncoding"/>.
203
+ if key == "charset":
204
+ continue
205
+ if key not in opOrder:
206
+ if key in topDict.rawDict:
207
+ del topDict.rawDict[key]
208
+ if hasattr(topDict, key):
209
+ delattr(topDict, key)
210
+
211
+ # TODO(behdad): What does the following comment even mean? Both CFF and CFF2
212
+ # use the same T2Charstring class. I *think* what it means is that the CharStrings
213
+ # were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc
214
+ # on them. At least that's what I understand. It's probably safe to remove this
215
+ # and just set vstore where needed.
216
+ #
217
+ # See comment above about charset as well.
218
+
219
+ # At this point, the Subrs and Charstrings are all still T2Charstring class
220
+ # easiest to fix this by compiling, then decompiling again
221
+ file = BytesIO()
222
+ cff.compile(file, otFont, isCFF2=True)
223
+ file.seek(0)
224
+ cff.decompile(file, otFont, isCFF2=True)
225
+
226
+
227
+ def convertCFFToCFF2(font):
228
+ cff = font["CFF "].cff
229
+ del font["CFF "]
230
+ _convertCFFToCFF2(cff, font)
231
+ table = font["CFF2"] = newTable("CFF2")
232
+ table.cff = cff
233
+
234
+
235
+ def main(args=None):
236
+ """Convert CFF OTF font to CFF2 OTF font"""
237
+ if args is None:
238
+ import sys
239
+
240
+ args = sys.argv[1:]
241
+
242
+ import argparse
243
+
244
+ parser = argparse.ArgumentParser(
245
+ "fonttools cffLib.CFFToCFF2",
246
+ description="Upgrade a CFF font to CFF2.",
247
+ )
248
+ parser.add_argument(
249
+ "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
250
+ )
251
+ parser.add_argument(
252
+ "-o",
253
+ "--output",
254
+ metavar="OUTPUT.ttf",
255
+ default=None,
256
+ help="Output instance OTF file (default: INPUT-CFF2.ttf).",
257
+ )
258
+ parser.add_argument(
259
+ "--no-recalc-timestamp",
260
+ dest="recalc_timestamp",
261
+ action="store_false",
262
+ help="Don't set the output font's timestamp to the current time.",
263
+ )
264
+ loggingGroup = parser.add_mutually_exclusive_group(required=False)
265
+ loggingGroup.add_argument(
266
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
267
+ )
268
+ loggingGroup.add_argument(
269
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
270
+ )
271
+ options = parser.parse_args(args)
272
+
273
+ from fontTools import configLogger
274
+
275
+ configLogger(
276
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
277
+ )
278
+
279
+ import os
280
+
281
+ infile = options.input
282
+ if not os.path.isfile(infile):
283
+ parser.error("No such file '{}'".format(infile))
284
+
285
+ outfile = (
286
+ makeOutputFileName(infile, overWrite=True, suffix="-CFF2")
287
+ if not options.output
288
+ else options.output
289
+ )
290
+
291
+ font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
292
+
293
+ convertCFFToCFF2(font)
294
+
295
+ log.info(
296
+ "Saving %s",
297
+ outfile,
298
+ )
299
+ font.save(outfile)
300
+
301
+
302
+ if __name__ == "__main__":
303
+ import sys
304
+
305
+ sys.exit(main(sys.argv[1:]))
lib/python3.12/site-packages/fontTools/cffLib/__init__.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-312.pyc ADDED
Binary file (10.2 kB). View file
 
lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFFToCFF2.cpython-312.pyc ADDED
Binary file (10.2 kB). View file
 
lib/python3.12/site-packages/fontTools/cffLib/__pycache__/specializer.cpython-312.pyc ADDED
Binary file (28.6 kB). View file
 
lib/python3.12/site-packages/fontTools/cffLib/__pycache__/transforms.cpython-312.pyc ADDED
Binary file (21.6 kB). View file
 
lib/python3.12/site-packages/fontTools/cffLib/__pycache__/width.cpython-312.pyc ADDED
Binary file (9.82 kB). View file
 
lib/python3.12/site-packages/fontTools/cffLib/specializer.py ADDED
@@ -0,0 +1,927 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """T2CharString operator specializer and generalizer.
4
+
5
+ PostScript glyph drawing operations can be expressed in multiple different
6
+ ways. For example, as well as the ``lineto`` operator, there is also a
7
+ ``hlineto`` operator which draws a horizontal line, removing the need to
8
+ specify a ``dx`` coordinate, and a ``vlineto`` operator which draws a
9
+ vertical line, removing the need to specify a ``dy`` coordinate. As well
10
+ as decompiling :class:`fontTools.misc.psCharStrings.T2CharString` objects
11
+ into lists of operations, this module allows for conversion between general
12
+ and specific forms of the operation.
13
+
14
+ """
15
+
16
+ from fontTools.cffLib import maxStackLimit
17
+
18
+
19
+ def stringToProgram(string):
20
+ if isinstance(string, str):
21
+ string = string.split()
22
+ program = []
23
+ for token in string:
24
+ try:
25
+ token = int(token)
26
+ except ValueError:
27
+ try:
28
+ token = float(token)
29
+ except ValueError:
30
+ pass
31
+ program.append(token)
32
+ return program
33
+
34
+
35
+ def programToString(program):
36
+ return " ".join(str(x) for x in program)
37
+
38
+
39
+ def programToCommands(program, getNumRegions=None):
40
+ """Takes a T2CharString program list and returns list of commands.
41
+ Each command is a two-tuple of commandname,arg-list. The commandname might
42
+ be empty string if no commandname shall be emitted (used for glyph width,
43
+ hintmask/cntrmask argument, as well as stray arguments at the end of the
44
+ program (🤷).
45
+ 'getNumRegions' may be None, or a callable object. It must return the
46
+ number of regions. 'getNumRegions' takes a single argument, vsindex. It
47
+ returns the numRegions for the vsindex.
48
+ The Charstring may or may not start with a width value. If the first
49
+ non-blend operator has an odd number of arguments, then the first argument is
50
+ a width, and is popped off. This is complicated with blend operators, as
51
+ there may be more than one before the first hint or moveto operator, and each
52
+ one reduces several arguments to just one list argument. We have to sum the
53
+ number of arguments that are not part of the blend arguments, and all the
54
+ 'numBlends' values. We could instead have said that by definition, if there
55
+ is a blend operator, there is no width value, since CFF2 Charstrings don't
56
+ have width values. I discussed this with Behdad, and we are allowing for an
57
+ initial width value in this case because developers may assemble a CFF2
58
+ charstring from CFF Charstrings, which could have width values.
59
+ """
60
+
61
+ seenWidthOp = False
62
+ vsIndex = 0
63
+ lenBlendStack = 0
64
+ lastBlendIndex = 0
65
+ commands = []
66
+ stack = []
67
+ it = iter(program)
68
+
69
+ for token in it:
70
+ if not isinstance(token, str):
71
+ stack.append(token)
72
+ continue
73
+
74
+ if token == "blend":
75
+ assert getNumRegions is not None
76
+ numSourceFonts = 1 + getNumRegions(vsIndex)
77
+ # replace the blend op args on the stack with a single list
78
+ # containing all the blend op args.
79
+ numBlends = stack[-1]
80
+ numBlendArgs = numBlends * numSourceFonts + 1
81
+ # replace first blend op by a list of the blend ops.
82
+ stack[-numBlendArgs:] = [stack[-numBlendArgs:]]
83
+ lenStack = len(stack)
84
+ lenBlendStack += numBlends + lenStack - 1
85
+ lastBlendIndex = lenStack
86
+ # if a blend op exists, this is or will be a CFF2 charstring.
87
+ continue
88
+
89
+ elif token == "vsindex":
90
+ vsIndex = stack[-1]
91
+ assert type(vsIndex) is int
92
+
93
+ elif (not seenWidthOp) and token in {
94
+ "hstem",
95
+ "hstemhm",
96
+ "vstem",
97
+ "vstemhm",
98
+ "cntrmask",
99
+ "hintmask",
100
+ "hmoveto",
101
+ "vmoveto",
102
+ "rmoveto",
103
+ "endchar",
104
+ }:
105
+ seenWidthOp = True
106
+ parity = token in {"hmoveto", "vmoveto"}
107
+ if lenBlendStack:
108
+ # lenBlendStack has the number of args represented by the last blend
109
+ # arg and all the preceding args. We need to now add the number of
110
+ # args following the last blend arg.
111
+ numArgs = lenBlendStack + len(stack[lastBlendIndex:])
112
+ else:
113
+ numArgs = len(stack)
114
+ if numArgs and (numArgs % 2) ^ parity:
115
+ width = stack.pop(0)
116
+ commands.append(("", [width]))
117
+
118
+ if token in {"hintmask", "cntrmask"}:
119
+ if stack:
120
+ commands.append(("", stack))
121
+ commands.append((token, []))
122
+ commands.append(("", [next(it)]))
123
+ else:
124
+ commands.append((token, stack))
125
+ stack = []
126
+ if stack:
127
+ commands.append(("", stack))
128
+ return commands
129
+
130
+
131
+ def _flattenBlendArgs(args):
132
+ token_list = []
133
+ for arg in args:
134
+ if isinstance(arg, list):
135
+ token_list.extend(arg)
136
+ token_list.append("blend")
137
+ else:
138
+ token_list.append(arg)
139
+ return token_list
140
+
141
+
142
+ def commandsToProgram(commands):
143
+ """Takes a commands list as returned by programToCommands() and converts
144
+ it back to a T2CharString program list."""
145
+ program = []
146
+ for op, args in commands:
147
+ if any(isinstance(arg, list) for arg in args):
148
+ args = _flattenBlendArgs(args)
149
+ program.extend(args)
150
+ if op:
151
+ program.append(op)
152
+ return program
153
+
154
+
155
+ def _everyN(el, n):
156
+ """Group the list el into groups of size n"""
157
+ l = len(el)
158
+ if l % n != 0:
159
+ raise ValueError(el)
160
+ for i in range(0, l, n):
161
+ yield el[i : i + n]
162
+
163
+
164
+ class _GeneralizerDecombinerCommandsMap(object):
165
+ @staticmethod
166
+ def rmoveto(args):
167
+ if len(args) != 2:
168
+ raise ValueError(args)
169
+ yield ("rmoveto", args)
170
+
171
+ @staticmethod
172
+ def hmoveto(args):
173
+ if len(args) != 1:
174
+ raise ValueError(args)
175
+ yield ("rmoveto", [args[0], 0])
176
+
177
+ @staticmethod
178
+ def vmoveto(args):
179
+ if len(args) != 1:
180
+ raise ValueError(args)
181
+ yield ("rmoveto", [0, args[0]])
182
+
183
+ @staticmethod
184
+ def rlineto(args):
185
+ if not args:
186
+ raise ValueError(args)
187
+ for args in _everyN(args, 2):
188
+ yield ("rlineto", args)
189
+
190
+ @staticmethod
191
+ def hlineto(args):
192
+ if not args:
193
+ raise ValueError(args)
194
+ it = iter(args)
195
+ try:
196
+ while True:
197
+ yield ("rlineto", [next(it), 0])
198
+ yield ("rlineto", [0, next(it)])
199
+ except StopIteration:
200
+ pass
201
+
202
+ @staticmethod
203
+ def vlineto(args):
204
+ if not args:
205
+ raise ValueError(args)
206
+ it = iter(args)
207
+ try:
208
+ while True:
209
+ yield ("rlineto", [0, next(it)])
210
+ yield ("rlineto", [next(it), 0])
211
+ except StopIteration:
212
+ pass
213
+
214
+ @staticmethod
215
+ def rrcurveto(args):
216
+ if not args:
217
+ raise ValueError(args)
218
+ for args in _everyN(args, 6):
219
+ yield ("rrcurveto", args)
220
+
221
+ @staticmethod
222
+ def hhcurveto(args):
223
+ l = len(args)
224
+ if l < 4 or l % 4 > 1:
225
+ raise ValueError(args)
226
+ if l % 2 == 1:
227
+ yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0])
228
+ args = args[5:]
229
+ for args in _everyN(args, 4):
230
+ yield ("rrcurveto", [args[0], 0, args[1], args[2], args[3], 0])
231
+
232
+ @staticmethod
233
+ def vvcurveto(args):
234
+ l = len(args)
235
+ if l < 4 or l % 4 > 1:
236
+ raise ValueError(args)
237
+ if l % 2 == 1:
238
+ yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]])
239
+ args = args[5:]
240
+ for args in _everyN(args, 4):
241
+ yield ("rrcurveto", [0, args[0], args[1], args[2], 0, args[3]])
242
+
243
+ @staticmethod
244
+ def hvcurveto(args):
245
+ l = len(args)
246
+ if l < 4 or l % 8 not in {0, 1, 4, 5}:
247
+ raise ValueError(args)
248
+ last_args = None
249
+ if l % 2 == 1:
250
+ lastStraight = l % 8 == 5
251
+ args, last_args = args[:-5], args[-5:]
252
+ it = _everyN(args, 4)
253
+ try:
254
+ while True:
255
+ args = next(it)
256
+ yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]])
257
+ args = next(it)
258
+ yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0])
259
+ except StopIteration:
260
+ pass
261
+ if last_args:
262
+ args = last_args
263
+ if lastStraight:
264
+ yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]])
265
+ else:
266
+ yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]])
267
+
268
+ @staticmethod
269
+ def vhcurveto(args):
270
+ l = len(args)
271
+ if l < 4 or l % 8 not in {0, 1, 4, 5}:
272
+ raise ValueError(args)
273
+ last_args = None
274
+ if l % 2 == 1:
275
+ lastStraight = l % 8 == 5
276
+ args, last_args = args[:-5], args[-5:]
277
+ it = _everyN(args, 4)
278
+ try:
279
+ while True:
280
+ args = next(it)
281
+ yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0])
282
+ args = next(it)
283
+ yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]])
284
+ except StopIteration:
285
+ pass
286
+ if last_args:
287
+ args = last_args
288
+ if lastStraight:
289
+ yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]])
290
+ else:
291
+ yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]])
292
+
293
+ @staticmethod
294
+ def rcurveline(args):
295
+ l = len(args)
296
+ if l < 8 or l % 6 != 2:
297
+ raise ValueError(args)
298
+ args, last_args = args[:-2], args[-2:]
299
+ for args in _everyN(args, 6):
300
+ yield ("rrcurveto", args)
301
+ yield ("rlineto", last_args)
302
+
303
+ @staticmethod
304
+ def rlinecurve(args):
305
+ l = len(args)
306
+ if l < 8 or l % 2 != 0:
307
+ raise ValueError(args)
308
+ args, last_args = args[:-6], args[-6:]
309
+ for args in _everyN(args, 2):
310
+ yield ("rlineto", args)
311
+ yield ("rrcurveto", last_args)
312
+
313
+
314
+ def _convertBlendOpToArgs(blendList):
315
+ # args is list of blend op args. Since we are supporting
316
+ # recursive blend op calls, some of these args may also
317
+ # be a list of blend op args, and need to be converted before
318
+ # we convert the current list.
319
+ if any([isinstance(arg, list) for arg in blendList]):
320
+ args = [
321
+ i
322
+ for e in blendList
323
+ for i in (_convertBlendOpToArgs(e) if isinstance(e, list) else [e])
324
+ ]
325
+ else:
326
+ args = blendList
327
+
328
+ # We now know that blendList contains a blend op argument list, even if
329
+ # some of the args are lists that each contain a blend op argument list.
330
+ # Convert from:
331
+ # [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn]
332
+ # to:
333
+ # [ [x0] + [delta tuple for x0],
334
+ # ...,
335
+ # [xn] + [delta tuple for xn] ]
336
+ numBlends = args[-1]
337
+ # Can't use args.pop() when the args are being used in a nested list
338
+ # comprehension. See calling context
339
+ args = args[:-1]
340
+
341
+ l = len(args)
342
+ numRegions = l // numBlends - 1
343
+ if not (numBlends * (numRegions + 1) == l):
344
+ raise ValueError(blendList)
345
+
346
+ defaultArgs = [[arg] for arg in args[:numBlends]]
347
+ deltaArgs = args[numBlends:]
348
+ numDeltaValues = len(deltaArgs)
349
+ deltaList = [
350
+ deltaArgs[i : i + numRegions] for i in range(0, numDeltaValues, numRegions)
351
+ ]
352
+ blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)]
353
+ return blend_args
354
+
355
+
356
+ def generalizeCommands(commands, ignoreErrors=False):
357
+ result = []
358
+ mapping = _GeneralizerDecombinerCommandsMap
359
+ for op, args in commands:
360
+ # First, generalize any blend args in the arg list.
361
+ if any([isinstance(arg, list) for arg in args]):
362
+ try:
363
+ args = [
364
+ n
365
+ for arg in args
366
+ for n in (
367
+ _convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg]
368
+ )
369
+ ]
370
+ except ValueError:
371
+ if ignoreErrors:
372
+ # Store op as data, such that consumers of commands do not have to
373
+ # deal with incorrect number of arguments.
374
+ result.append(("", args))
375
+ result.append(("", [op]))
376
+ else:
377
+ raise
378
+
379
+ func = getattr(mapping, op, None)
380
+ if func is None:
381
+ result.append((op, args))
382
+ continue
383
+ try:
384
+ for command in func(args):
385
+ result.append(command)
386
+ except ValueError:
387
+ if ignoreErrors:
388
+ # Store op as data, such that consumers of commands do not have to
389
+ # deal with incorrect number of arguments.
390
+ result.append(("", args))
391
+ result.append(("", [op]))
392
+ else:
393
+ raise
394
+ return result
395
+
396
+
397
+ def generalizeProgram(program, getNumRegions=None, **kwargs):
398
+ return commandsToProgram(
399
+ generalizeCommands(programToCommands(program, getNumRegions), **kwargs)
400
+ )
401
+
402
+
403
+ def _categorizeVector(v):
404
+ """
405
+ Takes X,Y vector v and returns one of r, h, v, or 0 depending on which
406
+ of X and/or Y are zero, plus tuple of nonzero ones. If both are zero,
407
+ it returns a single zero still.
408
+
409
+ >>> _categorizeVector((0,0))
410
+ ('0', (0,))
411
+ >>> _categorizeVector((1,0))
412
+ ('h', (1,))
413
+ >>> _categorizeVector((0,2))
414
+ ('v', (2,))
415
+ >>> _categorizeVector((1,2))
416
+ ('r', (1, 2))
417
+ """
418
+ if not v[0]:
419
+ if not v[1]:
420
+ return "0", v[:1]
421
+ else:
422
+ return "v", v[1:]
423
+ else:
424
+ if not v[1]:
425
+ return "h", v[:1]
426
+ else:
427
+ return "r", v
428
+
429
+
430
+ def _mergeCategories(a, b):
431
+ if a == "0":
432
+ return b
433
+ if b == "0":
434
+ return a
435
+ if a == b:
436
+ return a
437
+ return None
438
+
439
+
440
+ def _negateCategory(a):
441
+ if a == "h":
442
+ return "v"
443
+ if a == "v":
444
+ return "h"
445
+ assert a in "0r"
446
+ return a
447
+
448
+
449
+ def _convertToBlendCmds(args):
450
+ # return a list of blend commands, and
451
+ # the remaining non-blended args, if any.
452
+ num_args = len(args)
453
+ stack_use = 0
454
+ new_args = []
455
+ i = 0
456
+ while i < num_args:
457
+ arg = args[i]
458
+ i += 1
459
+ if not isinstance(arg, list):
460
+ new_args.append(arg)
461
+ stack_use += 1
462
+ else:
463
+ prev_stack_use = stack_use
464
+ # The arg is a tuple of blend values.
465
+ # These are each (master 0,delta 1..delta n, 1)
466
+ # Combine as many successive tuples as we can,
467
+ # up to the max stack limit.
468
+ num_sources = len(arg) - 1
469
+ blendlist = [arg]
470
+ stack_use += 1 + num_sources # 1 for the num_blends arg
471
+
472
+ # if we are here, max stack is the CFF2 max stack.
473
+ # I use the CFF2 max stack limit here rather than
474
+ # the 'maxstack' chosen by the client, as the default
475
+ # maxstack may have been used unintentionally. For all
476
+ # the other operators, this just produces a little less
477
+ # optimization, but here it puts a hard (and low) limit
478
+ # on the number of source fonts that can be used.
479
+ #
480
+ # Make sure the stack depth does not exceed (maxstack - 1), so
481
+ # that subroutinizer can insert subroutine calls at any point.
482
+ while (
483
+ (i < num_args)
484
+ and isinstance(args[i], list)
485
+ and stack_use + num_sources < maxStackLimit
486
+ ):
487
+ blendlist.append(args[i])
488
+ i += 1
489
+ stack_use += num_sources
490
+ # blendList now contains as many single blend tuples as can be
491
+ # combined without exceeding the CFF2 stack limit.
492
+ num_blends = len(blendlist)
493
+ # append the 'num_blends' default font values
494
+ blend_args = []
495
+ for arg in blendlist:
496
+ blend_args.append(arg[0])
497
+ for arg in blendlist:
498
+ assert arg[-1] == 1
499
+ blend_args.extend(arg[1:-1])
500
+ blend_args.append(num_blends)
501
+ new_args.append(blend_args)
502
+ stack_use = prev_stack_use + num_blends
503
+
504
+ return new_args
505
+
506
+
507
+ def _addArgs(a, b):
508
+ if isinstance(b, list):
509
+ if isinstance(a, list):
510
+ if len(a) != len(b) or a[-1] != b[-1]:
511
+ raise ValueError()
512
+ return [_addArgs(va, vb) for va, vb in zip(a[:-1], b[:-1])] + [a[-1]]
513
+ else:
514
+ a, b = b, a
515
+ if isinstance(a, list):
516
+ assert a[-1] == 1
517
+ return [_addArgs(a[0], b)] + a[1:]
518
+ return a + b
519
+
520
+
521
+ def _argsStackUse(args):
522
+ stackLen = 0
523
+ maxLen = 0
524
+ for arg in args:
525
+ if type(arg) is list:
526
+ # Blended arg
527
+ maxLen = max(maxLen, stackLen + _argsStackUse(arg))
528
+ stackLen += arg[-1]
529
+ else:
530
+ stackLen += 1
531
+ return max(stackLen, maxLen)
532
+
533
+
534
+ def specializeCommands(
535
+ commands,
536
+ ignoreErrors=False,
537
+ generalizeFirst=True,
538
+ preserveTopology=False,
539
+ maxstack=48,
540
+ ):
541
+ # We perform several rounds of optimizations. They are carefully ordered and are:
542
+ #
543
+ # 0. Generalize commands.
544
+ # This ensures that they are in our expected simple form, with each line/curve only
545
+ # having arguments for one segment, and using the generic form (rlineto/rrcurveto).
546
+ # If caller is sure the input is in this form, they can turn off generalization to
547
+ # save time.
548
+ #
549
+ # 1. Combine successive rmoveto operations.
550
+ #
551
+ # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
552
+ # We specialize into some, made-up, variants as well, which simplifies following
553
+ # passes.
554
+ #
555
+ # 3. Merge or delete redundant operations, to the extent requested.
556
+ # OpenType spec declares point numbers in CFF undefined. As such, we happily
557
+ # change topology. If client relies on point numbers (in GPOS anchors, or for
558
+ # hinting purposes(what?)) they can turn this off.
559
+ #
560
+ # 4. Peephole optimization to revert back some of the h/v variants back into their
561
+ # original "relative" operator (rline/rrcurveto) if that saves a byte.
562
+ #
563
+ # 5. Combine adjacent operators when possible, minding not to go over max stack size.
564
+ #
565
+ # 6. Resolve any remaining made-up operators into real operators.
566
+ #
567
+ # I have convinced myself that this produces optimal bytecode (except for, possibly
568
+ # one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-)
569
+ # A dynamic-programming approach can do the same but would be significantly slower.
570
+ #
571
+ # 7. For any args which are blend lists, convert them to a blend command.
572
+
573
+ # 0. Generalize commands.
574
+ if generalizeFirst:
575
+ commands = generalizeCommands(commands, ignoreErrors=ignoreErrors)
576
+ else:
577
+ commands = list(commands) # Make copy since we modify in-place later.
578
+
579
+ # 1. Combine successive rmoveto operations.
580
+ for i in range(len(commands) - 1, 0, -1):
581
+ if "rmoveto" == commands[i][0] == commands[i - 1][0]:
582
+ v1, v2 = commands[i - 1][1], commands[i][1]
583
+ commands[i - 1] = (
584
+ "rmoveto",
585
+ [_addArgs(v1[0], v2[0]), _addArgs(v1[1], v2[1])],
586
+ )
587
+ del commands[i]
588
+
589
+ # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
590
+ #
591
+ # We, in fact, specialize into more, made-up, variants that special-case when both
592
+ # X and Y components are zero. This simplifies the following optimization passes.
593
+ # This case is rare, but OCD does not let me skip it.
594
+ #
595
+ # After this round, we will have four variants that use the following mnemonics:
596
+ #
597
+ # - 'r' for relative, ie. non-zero X and non-zero Y,
598
+ # - 'h' for horizontal, ie. zero X and non-zero Y,
599
+ # - 'v' for vertical, ie. non-zero X and zero Y,
600
+ # - '0' for zeros, ie. zero X and zero Y.
601
+ #
602
+ # The '0' pseudo-operators are not part of the spec, but help simplify the following
603
+ # optimization rounds. We resolve them at the end. So, after this, we will have four
604
+ # moveto and four lineto variants:
605
+ #
606
+ # - 0moveto, 0lineto
607
+ # - hmoveto, hlineto
608
+ # - vmoveto, vlineto
609
+ # - rmoveto, rlineto
610
+ #
611
+ # and sixteen curveto variants. For example, a '0hcurveto' operator means a curve
612
+ # dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1, and dy3 are zero but not dx3.
613
+ # An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3.
614
+ #
615
+ # There are nine different variants of curves without the '0'. Those nine map exactly
616
+ # to the existing curve variants in the spec: rrcurveto, and the four variants hhcurveto,
617
+ # vvcurveto, hvcurveto, and vhcurveto each cover two cases, one with an odd number of
618
+ # arguments and one without. Eg. an hhcurveto with an extra argument (odd number of
619
+ # arguments) is in fact an rhcurveto. The operators in the spec are designed such that
620
+ # all four of rhcurveto, rvcurveto, hrcurveto, and vrcurveto are encodable for one curve.
621
+ #
622
+ # Of the curve types with '0', the 00curveto is equivalent to a lineto variant. The rest
623
+ # of the curve types with a 0 need to be encoded as a h or v variant. Ie. a '0' can be
624
+ # thought of a "don't care" and can be used as either an 'h' or a 'v'. As such, we always
625
+ # encode a number 0 as argument when we use a '0' variant. Later on, we can just substitute
626
+ # the '0' with either 'h' or 'v' and it works.
627
+ #
628
+ # When we get to curve splines however, things become more complicated... XXX finish this.
629
+ # There's one more complexity with splines. If one side of the spline is not horizontal or
630
+ # vertical (or zero), ie. if it's 'r', then it limits which spline types we can encode.
631
+ # Only hhcurveto and vvcurveto operators can encode a spline starting with 'r', and
632
+ # only hvcurveto and vhcurveto operators can encode a spline ending with 'r'.
633
+ # This limits our merge opportunities later.
634
+ #
635
+ for i in range(len(commands)):
636
+ op, args = commands[i]
637
+
638
+ if op in {"rmoveto", "rlineto"}:
639
+ c, args = _categorizeVector(args)
640
+ commands[i] = c + op[1:], args
641
+ continue
642
+
643
+ if op == "rrcurveto":
644
+ c1, args1 = _categorizeVector(args[:2])
645
+ c2, args2 = _categorizeVector(args[-2:])
646
+ commands[i] = c1 + c2 + "curveto", args1 + args[2:4] + args2
647
+ continue
648
+
649
+ # 3. Merge or delete redundant operations, to the extent requested.
650
+ #
651
+ # TODO
652
+ # A 0moveto that comes before all other path operations can be removed.
653
+ # though I find conflicting evidence for this.
654
+ #
655
+ # TODO
656
+ # "If hstem and vstem hints are both declared at the beginning of a
657
+ # CharString, and this sequence is followed directly by the hintmask or
658
+ # cntrmask operators, then the vstem hint operator (or, if applicable,
659
+ # the vstemhm operator) need not be included."
660
+ #
661
+ # "The sequence and form of a CFF2 CharString program may be represented as:
662
+ # {hs* vs* cm* hm* mt subpath}? {mt subpath}*"
663
+ #
664
+ # https://www.microsoft.com/typography/otspec/cff2charstr.htm#section3.1
665
+ #
666
+ # For Type2 CharStrings the sequence is:
667
+ # w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar"
668
+
669
+ # Some other redundancies change topology (point numbers).
670
+ if not preserveTopology:
671
+ for i in range(len(commands) - 1, -1, -1):
672
+ op, args = commands[i]
673
+
674
+ # A 00curveto is demoted to a (specialized) lineto.
675
+ if op == "00curveto":
676
+ assert len(args) == 4
677
+ c, args = _categorizeVector(args[1:3])
678
+ op = c + "lineto"
679
+ commands[i] = op, args
680
+ # and then...
681
+
682
+ # A 0lineto can be deleted.
683
+ if op == "0lineto":
684
+ del commands[i]
685
+ continue
686
+
687
+ # Merge adjacent hlineto's and vlineto's.
688
+ # In CFF2 charstrings from variable fonts, each
689
+ # arg item may be a list of blendable values, one from
690
+ # each source font.
691
+ if i and op in {"hlineto", "vlineto"} and (op == commands[i - 1][0]):
692
+ _, other_args = commands[i - 1]
693
+ assert len(args) == 1 and len(other_args) == 1
694
+ try:
695
+ new_args = [_addArgs(args[0], other_args[0])]
696
+ except ValueError:
697
+ continue
698
+ commands[i - 1] = (op, new_args)
699
+ del commands[i]
700
+ continue
701
+
702
+ # 4. Peephole optimization to revert back some of the h/v variants back into their
703
+ # original "relative" operator (rline/rrcurveto) if that saves a byte.
704
+ for i in range(1, len(commands) - 1):
705
+ op, args = commands[i]
706
+ prv, nxt = commands[i - 1][0], commands[i + 1][0]
707
+
708
+ if op in {"0lineto", "hlineto", "vlineto"} and prv == nxt == "rlineto":
709
+ assert len(args) == 1
710
+ args = [0, args[0]] if op[0] == "v" else [args[0], 0]
711
+ commands[i] = ("rlineto", args)
712
+ continue
713
+
714
+ if op[2:] == "curveto" and len(args) == 5 and prv == nxt == "rrcurveto":
715
+ assert (op[0] == "r") ^ (op[1] == "r")
716
+ if op[0] == "v":
717
+ pos = 0
718
+ elif op[0] != "r":
719
+ pos = 1
720
+ elif op[1] == "v":
721
+ pos = 4
722
+ else:
723
+ pos = 5
724
+ # Insert, while maintaining the type of args (can be tuple or list).
725
+ args = args[:pos] + type(args)((0,)) + args[pos:]
726
+ commands[i] = ("rrcurveto", args)
727
+ continue
728
+
729
+ # 5. Combine adjacent operators when possible, minding not to go over max stack size.
730
+ stackUse = _argsStackUse(commands[-1][1]) if commands else 0
731
+ for i in range(len(commands) - 1, 0, -1):
732
+ op1, args1 = commands[i - 1]
733
+ op2, args2 = commands[i]
734
+ new_op = None
735
+
736
+ # Merge logic...
737
+ if {op1, op2} <= {"rlineto", "rrcurveto"}:
738
+ if op1 == op2:
739
+ new_op = op1
740
+ else:
741
+ l = len(args2)
742
+ if op2 == "rrcurveto" and l == 6:
743
+ new_op = "rlinecurve"
744
+ elif l == 2:
745
+ new_op = "rcurveline"
746
+
747
+ elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}:
748
+ new_op = op2
749
+
750
+ elif {op1, op2} == {"vlineto", "hlineto"}:
751
+ new_op = op1
752
+
753
+ elif "curveto" == op1[2:] == op2[2:]:
754
+ d0, d1 = op1[:2]
755
+ d2, d3 = op2[:2]
756
+
757
+ if d1 == "r" or d2 == "r" or d0 == d3 == "r":
758
+ continue
759
+
760
+ d = _mergeCategories(d1, d2)
761
+ if d is None:
762
+ continue
763
+ if d0 == "r":
764
+ d = _mergeCategories(d, d3)
765
+ if d is None:
766
+ continue
767
+ new_op = "r" + d + "curveto"
768
+ elif d3 == "r":
769
+ d0 = _mergeCategories(d0, _negateCategory(d))
770
+ if d0 is None:
771
+ continue
772
+ new_op = d0 + "r" + "curveto"
773
+ else:
774
+ d0 = _mergeCategories(d0, d3)
775
+ if d0 is None:
776
+ continue
777
+ new_op = d0 + d + "curveto"
778
+
779
+ # Make sure the stack depth does not exceed (maxstack - 1), so
780
+ # that subroutinizer can insert subroutine calls at any point.
781
+ args1StackUse = _argsStackUse(args1)
782
+ combinedStackUse = max(args1StackUse, len(args1) + stackUse)
783
+ if new_op and combinedStackUse < maxstack:
784
+ commands[i - 1] = (new_op, args1 + args2)
785
+ del commands[i]
786
+ stackUse = combinedStackUse
787
+ else:
788
+ stackUse = args1StackUse
789
+
790
+ # 6. Resolve any remaining made-up operators into real operators.
791
+ for i in range(len(commands)):
792
+ op, args = commands[i]
793
+
794
+ if op in {"0moveto", "0lineto"}:
795
+ commands[i] = "h" + op[1:], args
796
+ continue
797
+
798
+ if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}:
799
+ l = len(args)
800
+
801
+ op0, op1 = op[:2]
802
+ if (op0 == "r") ^ (op1 == "r"):
803
+ assert l % 2 == 1
804
+ if op0 == "0":
805
+ op0 = "h"
806
+ if op1 == "0":
807
+ op1 = "h"
808
+ if op0 == "r":
809
+ op0 = op1
810
+ if op1 == "r":
811
+ op1 = _negateCategory(op0)
812
+ assert {op0, op1} <= {"h", "v"}, (op0, op1)
813
+
814
+ if l % 2:
815
+ if op0 != op1: # vhcurveto / hvcurveto
816
+ if (op0 == "h") ^ (l % 8 == 1):
817
+ # Swap last two args order
818
+ args = args[:-2] + args[-1:] + args[-2:-1]
819
+ else: # hhcurveto / vvcurveto
820
+ if op0 == "h": # hhcurveto
821
+ # Swap first two args order
822
+ args = args[1:2] + args[:1] + args[2:]
823
+
824
+ commands[i] = op0 + op1 + "curveto", args
825
+ continue
826
+
827
+ # 7. For any series of args which are blend lists, convert the series to a single blend arg.
828
+ for i in range(len(commands)):
829
+ op, args = commands[i]
830
+ if any(isinstance(arg, list) for arg in args):
831
+ commands[i] = op, _convertToBlendCmds(args)
832
+
833
+ return commands
834
+
835
+
836
+ def specializeProgram(program, getNumRegions=None, **kwargs):
837
+ return commandsToProgram(
838
+ specializeCommands(programToCommands(program, getNumRegions), **kwargs)
839
+ )
840
+
841
+
842
+ if __name__ == "__main__":
843
+ import sys
844
+
845
+ if len(sys.argv) == 1:
846
+ import doctest
847
+
848
+ sys.exit(doctest.testmod().failed)
849
+
850
+ import argparse
851
+
852
+ parser = argparse.ArgumentParser(
853
+ "fonttools cffLib.specializer",
854
+ description="CFF CharString generalizer/specializer",
855
+ )
856
+ parser.add_argument("program", metavar="command", nargs="*", help="Commands.")
857
+ parser.add_argument(
858
+ "--num-regions",
859
+ metavar="NumRegions",
860
+ nargs="*",
861
+ default=None,
862
+ help="Number of variable-font regions for blend opertaions.",
863
+ )
864
+ parser.add_argument(
865
+ "--font",
866
+ metavar="FONTFILE",
867
+ default=None,
868
+ help="CFF2 font to specialize.",
869
+ )
870
+ parser.add_argument(
871
+ "-o",
872
+ "--output-file",
873
+ type=str,
874
+ help="Output font file name.",
875
+ )
876
+
877
+ options = parser.parse_args(sys.argv[1:])
878
+
879
+ if options.program:
880
+ getNumRegions = (
881
+ None
882
+ if options.num_regions is None
883
+ else lambda vsIndex: int(
884
+ options.num_regions[0 if vsIndex is None else vsIndex]
885
+ )
886
+ )
887
+
888
+ program = stringToProgram(options.program)
889
+ print("Program:")
890
+ print(programToString(program))
891
+ commands = programToCommands(program, getNumRegions)
892
+ print("Commands:")
893
+ print(commands)
894
+ program2 = commandsToProgram(commands)
895
+ print("Program from commands:")
896
+ print(programToString(program2))
897
+ assert program == program2
898
+ print("Generalized program:")
899
+ print(programToString(generalizeProgram(program, getNumRegions)))
900
+ print("Specialized program:")
901
+ print(programToString(specializeProgram(program, getNumRegions)))
902
+
903
+ if options.font:
904
+ from fontTools.ttLib import TTFont
905
+
906
+ font = TTFont(options.font)
907
+ cff2 = font["CFF2"].cff.topDictIndex[0]
908
+ charstrings = cff2.CharStrings
909
+ for glyphName in charstrings.keys():
910
+ charstring = charstrings[glyphName]
911
+ charstring.decompile()
912
+ getNumRegions = charstring.private.getNumRegions
913
+ charstring.program = specializeProgram(
914
+ charstring.program, getNumRegions, maxstack=maxStackLimit
915
+ )
916
+
917
+ if options.output_file is None:
918
+ from fontTools.misc.cliTools import makeOutputFileName
919
+
920
+ outfile = makeOutputFileName(
921
+ options.font, overWrite=True, suffix=".specialized"
922
+ )
923
+ else:
924
+ outfile = options.output_file
925
+ if outfile:
926
+ print("Saving", outfile)
927
+ font.save(outfile)
lib/python3.12/site-packages/fontTools/cffLib/transforms.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.misc.psCharStrings import (
2
+ SimpleT2Decompiler,
3
+ T2WidthExtractor,
4
+ calcSubrBias,
5
+ )
6
+
7
+
8
+ def _uniq_sort(l):
9
+ return sorted(set(l))
10
+
11
+
12
+ class StopHintCountEvent(Exception):
13
+ pass
14
+
15
+
16
+ class _DesubroutinizingT2Decompiler(SimpleT2Decompiler):
17
+ stop_hintcount_ops = (
18
+ "op_hintmask",
19
+ "op_cntrmask",
20
+ "op_rmoveto",
21
+ "op_hmoveto",
22
+ "op_vmoveto",
23
+ )
24
+
25
+ def __init__(self, localSubrs, globalSubrs, private=None):
26
+ SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
27
+
28
+ def execute(self, charString):
29
+ self.need_hintcount = True # until proven otherwise
30
+ for op_name in self.stop_hintcount_ops:
31
+ setattr(self, op_name, self.stop_hint_count)
32
+
33
+ if hasattr(charString, "_desubroutinized"):
34
+ # If a charstring has already been desubroutinized, we will still
35
+ # need to execute it if we need to count hints in order to
36
+ # compute the byte length for mask arguments, and haven't finished
37
+ # counting hints pairs.
38
+ if self.need_hintcount and self.callingStack:
39
+ try:
40
+ SimpleT2Decompiler.execute(self, charString)
41
+ except StopHintCountEvent:
42
+ del self.callingStack[-1]
43
+ return
44
+
45
+ charString._patches = []
46
+ SimpleT2Decompiler.execute(self, charString)
47
+ desubroutinized = charString.program[:]
48
+ for idx, expansion in reversed(charString._patches):
49
+ assert idx >= 2
50
+ assert desubroutinized[idx - 1] in [
51
+ "callsubr",
52
+ "callgsubr",
53
+ ], desubroutinized[idx - 1]
54
+ assert type(desubroutinized[idx - 2]) == int
55
+ if expansion[-1] == "return":
56
+ expansion = expansion[:-1]
57
+ desubroutinized[idx - 2 : idx] = expansion
58
+ if not self.private.in_cff2:
59
+ if "endchar" in desubroutinized:
60
+ # Cut off after first endchar
61
+ desubroutinized = desubroutinized[
62
+ : desubroutinized.index("endchar") + 1
63
+ ]
64
+
65
+ charString._desubroutinized = desubroutinized
66
+ del charString._patches
67
+
68
+ def op_callsubr(self, index):
69
+ subr = self.localSubrs[self.operandStack[-1] + self.localBias]
70
+ SimpleT2Decompiler.op_callsubr(self, index)
71
+ self.processSubr(index, subr)
72
+
73
+ def op_callgsubr(self, index):
74
+ subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
75
+ SimpleT2Decompiler.op_callgsubr(self, index)
76
+ self.processSubr(index, subr)
77
+
78
+ def stop_hint_count(self, *args):
79
+ self.need_hintcount = False
80
+ for op_name in self.stop_hintcount_ops:
81
+ setattr(self, op_name, None)
82
+ cs = self.callingStack[-1]
83
+ if hasattr(cs, "_desubroutinized"):
84
+ raise StopHintCountEvent()
85
+
86
+ def op_hintmask(self, index):
87
+ SimpleT2Decompiler.op_hintmask(self, index)
88
+ if self.need_hintcount:
89
+ self.stop_hint_count()
90
+
91
+ def processSubr(self, index, subr):
92
+ cs = self.callingStack[-1]
93
+ if not hasattr(cs, "_desubroutinized"):
94
+ cs._patches.append((index, subr._desubroutinized))
95
+
96
+
97
+ def desubroutinizeCharString(cs):
98
+ """Desubroutinize a charstring in-place."""
99
+ cs.decompile()
100
+ subrs = getattr(cs.private, "Subrs", [])
101
+ decompiler = _DesubroutinizingT2Decompiler(subrs, cs.globalSubrs, cs.private)
102
+ decompiler.execute(cs)
103
+ cs.program = cs._desubroutinized
104
+ del cs._desubroutinized
105
+
106
+
107
+ def desubroutinize(cff):
108
+ for fontName in cff.fontNames:
109
+ font = cff[fontName]
110
+ cs = font.CharStrings
111
+ for c in cs.values():
112
+ desubroutinizeCharString(c)
113
+ # Delete all the local subrs
114
+ if hasattr(font, "FDArray"):
115
+ for fd in font.FDArray:
116
+ pd = fd.Private
117
+ if hasattr(pd, "Subrs"):
118
+ del pd.Subrs
119
+ if "Subrs" in pd.rawDict:
120
+ del pd.rawDict["Subrs"]
121
+ else:
122
+ pd = font.Private
123
+ if hasattr(pd, "Subrs"):
124
+ del pd.Subrs
125
+ if "Subrs" in pd.rawDict:
126
+ del pd.rawDict["Subrs"]
127
+ # as well as the global subrs
128
+ cff.GlobalSubrs.clear()
129
+
130
+
131
+ class _MarkingT2Decompiler(SimpleT2Decompiler):
132
+ def __init__(self, localSubrs, globalSubrs, private):
133
+ SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
134
+ for subrs in [localSubrs, globalSubrs]:
135
+ if subrs and not hasattr(subrs, "_used"):
136
+ subrs._used = set()
137
+
138
+ def op_callsubr(self, index):
139
+ self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
140
+ SimpleT2Decompiler.op_callsubr(self, index)
141
+
142
+ def op_callgsubr(self, index):
143
+ self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
144
+ SimpleT2Decompiler.op_callgsubr(self, index)
145
+
146
+
147
+ class _DehintingT2Decompiler(T2WidthExtractor):
148
+ class Hints(object):
149
+ def __init__(self):
150
+ # Whether calling this charstring produces any hint stems
151
+ # Note that if a charstring starts with hintmask, it will
152
+ # have has_hint set to True, because it *might* produce an
153
+ # implicit vstem if called under certain conditions.
154
+ self.has_hint = False
155
+ # Index to start at to drop all hints
156
+ self.last_hint = 0
157
+ # Index up to which we know more hints are possible.
158
+ # Only relevant if status is 0 or 1.
159
+ self.last_checked = 0
160
+ # The status means:
161
+ # 0: after dropping hints, this charstring is empty
162
+ # 1: after dropping hints, there may be more hints
163
+ # continuing after this, or there might be
164
+ # other things. Not clear yet.
165
+ # 2: no more hints possible after this charstring
166
+ self.status = 0
167
+ # Has hintmask instructions; not recursive
168
+ self.has_hintmask = False
169
+ # List of indices of calls to empty subroutines to remove.
170
+ self.deletions = []
171
+
172
+ pass
173
+
174
+ def __init__(
175
+ self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
176
+ ):
177
+ self._css = css
178
+ T2WidthExtractor.__init__(
179
+ self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
180
+ )
181
+ self.private = private
182
+
183
+ def execute(self, charString):
184
+ old_hints = charString._hints if hasattr(charString, "_hints") else None
185
+ charString._hints = self.Hints()
186
+
187
+ T2WidthExtractor.execute(self, charString)
188
+
189
+ hints = charString._hints
190
+
191
+ if hints.has_hint or hints.has_hintmask:
192
+ self._css.add(charString)
193
+
194
+ if hints.status != 2:
195
+ # Check from last_check, make sure we didn't have any operators.
196
+ for i in range(hints.last_checked, len(charString.program) - 1):
197
+ if isinstance(charString.program[i], str):
198
+ hints.status = 2
199
+ break
200
+ else:
201
+ hints.status = 1 # There's *something* here
202
+ hints.last_checked = len(charString.program)
203
+
204
+ if old_hints:
205
+ assert hints.__dict__ == old_hints.__dict__
206
+
207
+ def op_callsubr(self, index):
208
+ subr = self.localSubrs[self.operandStack[-1] + self.localBias]
209
+ T2WidthExtractor.op_callsubr(self, index)
210
+ self.processSubr(index, subr)
211
+
212
+ def op_callgsubr(self, index):
213
+ subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
214
+ T2WidthExtractor.op_callgsubr(self, index)
215
+ self.processSubr(index, subr)
216
+
217
+ def op_hstem(self, index):
218
+ T2WidthExtractor.op_hstem(self, index)
219
+ self.processHint(index)
220
+
221
+ def op_vstem(self, index):
222
+ T2WidthExtractor.op_vstem(self, index)
223
+ self.processHint(index)
224
+
225
+ def op_hstemhm(self, index):
226
+ T2WidthExtractor.op_hstemhm(self, index)
227
+ self.processHint(index)
228
+
229
+ def op_vstemhm(self, index):
230
+ T2WidthExtractor.op_vstemhm(self, index)
231
+ self.processHint(index)
232
+
233
+ def op_hintmask(self, index):
234
+ rv = T2WidthExtractor.op_hintmask(self, index)
235
+ self.processHintmask(index)
236
+ return rv
237
+
238
+ def op_cntrmask(self, index):
239
+ rv = T2WidthExtractor.op_cntrmask(self, index)
240
+ self.processHintmask(index)
241
+ return rv
242
+
243
+ def processHintmask(self, index):
244
+ cs = self.callingStack[-1]
245
+ hints = cs._hints
246
+ hints.has_hintmask = True
247
+ if hints.status != 2:
248
+ # Check from last_check, see if we may be an implicit vstem
249
+ for i in range(hints.last_checked, index - 1):
250
+ if isinstance(cs.program[i], str):
251
+ hints.status = 2
252
+ break
253
+ else:
254
+ # We are an implicit vstem
255
+ hints.has_hint = True
256
+ hints.last_hint = index + 1
257
+ hints.status = 0
258
+ hints.last_checked = index + 1
259
+
260
+ def processHint(self, index):
261
+ cs = self.callingStack[-1]
262
+ hints = cs._hints
263
+ hints.has_hint = True
264
+ hints.last_hint = index
265
+ hints.last_checked = index
266
+
267
+ def processSubr(self, index, subr):
268
+ cs = self.callingStack[-1]
269
+ hints = cs._hints
270
+ subr_hints = subr._hints
271
+
272
+ # Check from last_check, make sure we didn't have
273
+ # any operators.
274
+ if hints.status != 2:
275
+ for i in range(hints.last_checked, index - 1):
276
+ if isinstance(cs.program[i], str):
277
+ hints.status = 2
278
+ break
279
+ hints.last_checked = index
280
+
281
+ if hints.status != 2:
282
+ if subr_hints.has_hint:
283
+ hints.has_hint = True
284
+
285
+ # Decide where to chop off from
286
+ if subr_hints.status == 0:
287
+ hints.last_hint = index
288
+ else:
289
+ hints.last_hint = index - 2 # Leave the subr call in
290
+
291
+ elif subr_hints.status == 0:
292
+ hints.deletions.append(index)
293
+
294
+ hints.status = max(hints.status, subr_hints.status)
295
+
296
+
297
+ def _cs_subset_subroutines(charstring, subrs, gsubrs):
298
+ p = charstring.program
299
+ for i in range(1, len(p)):
300
+ if p[i] == "callsubr":
301
+ assert isinstance(p[i - 1], int)
302
+ p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
303
+ elif p[i] == "callgsubr":
304
+ assert isinstance(p[i - 1], int)
305
+ p[i - 1] = (
306
+ gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
307
+ )
308
+
309
+
310
+ def _cs_drop_hints(charstring):
311
+ hints = charstring._hints
312
+
313
+ if hints.deletions:
314
+ p = charstring.program
315
+ for idx in reversed(hints.deletions):
316
+ del p[idx - 2 : idx]
317
+
318
+ if hints.has_hint:
319
+ assert not hints.deletions or hints.last_hint <= hints.deletions[0]
320
+ charstring.program = charstring.program[hints.last_hint :]
321
+ if not charstring.program:
322
+ # TODO CFF2 no need for endchar.
323
+ charstring.program.append("endchar")
324
+ if hasattr(charstring, "width"):
325
+ # Insert width back if needed
326
+ if charstring.width != charstring.private.defaultWidthX:
327
+ # For CFF2 charstrings, this should never happen
328
+ assert (
329
+ charstring.private.defaultWidthX is not None
330
+ ), "CFF2 CharStrings must not have an initial width value"
331
+ charstring.program.insert(
332
+ 0, charstring.width - charstring.private.nominalWidthX
333
+ )
334
+
335
+ if hints.has_hintmask:
336
+ i = 0
337
+ p = charstring.program
338
+ while i < len(p):
339
+ if p[i] in ["hintmask", "cntrmask"]:
340
+ assert i + 1 <= len(p)
341
+ del p[i : i + 2]
342
+ continue
343
+ i += 1
344
+
345
+ assert len(charstring.program)
346
+
347
+ del charstring._hints
348
+
349
+
350
+ def remove_hints(cff, *, removeUnusedSubrs: bool = True):
351
+ for fontname in cff.keys():
352
+ font = cff[fontname]
353
+ cs = font.CharStrings
354
+ # This can be tricky, but doesn't have to. What we do is:
355
+ #
356
+ # - Run all used glyph charstrings and recurse into subroutines,
357
+ # - For each charstring (including subroutines), if it has any
358
+ # of the hint stem operators, we mark it as such.
359
+ # Upon returning, for each charstring we note all the
360
+ # subroutine calls it makes that (recursively) contain a stem,
361
+ # - Dropping hinting then consists of the following two ops:
362
+ # * Drop the piece of the program in each charstring before the
363
+ # last call to a stem op or a stem-calling subroutine,
364
+ # * Drop all hintmask operations.
365
+ # - It's trickier... A hintmask right after hints and a few numbers
366
+ # will act as an implicit vstemhm. As such, we track whether
367
+ # we have seen any non-hint operators so far and do the right
368
+ # thing, recursively... Good luck understanding that :(
369
+ css = set()
370
+ for c in cs.values():
371
+ c.decompile()
372
+ subrs = getattr(c.private, "Subrs", [])
373
+ decompiler = _DehintingT2Decompiler(
374
+ css,
375
+ subrs,
376
+ c.globalSubrs,
377
+ c.private.nominalWidthX,
378
+ c.private.defaultWidthX,
379
+ c.private,
380
+ )
381
+ decompiler.execute(c)
382
+ c.width = decompiler.width
383
+ for charstring in css:
384
+ _cs_drop_hints(charstring)
385
+ del css
386
+
387
+ # Drop font-wide hinting values
388
+ all_privs = []
389
+ if hasattr(font, "FDArray"):
390
+ all_privs.extend(fd.Private for fd in font.FDArray)
391
+ else:
392
+ all_privs.append(font.Private)
393
+ for priv in all_privs:
394
+ for k in [
395
+ "BlueValues",
396
+ "OtherBlues",
397
+ "FamilyBlues",
398
+ "FamilyOtherBlues",
399
+ "BlueScale",
400
+ "BlueShift",
401
+ "BlueFuzz",
402
+ "StemSnapH",
403
+ "StemSnapV",
404
+ "StdHW",
405
+ "StdVW",
406
+ "ForceBold",
407
+ "LanguageGroup",
408
+ "ExpansionFactor",
409
+ ]:
410
+ if hasattr(priv, k):
411
+ setattr(priv, k, None)
412
+ if removeUnusedSubrs:
413
+ remove_unused_subroutines(cff)
414
+
415
+
416
+ def _pd_delete_empty_subrs(private_dict):
417
+ if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
418
+ if "Subrs" in private_dict.rawDict:
419
+ del private_dict.rawDict["Subrs"]
420
+ del private_dict.Subrs
421
+
422
+
423
+ def remove_unused_subroutines(cff):
424
+ for fontname in cff.keys():
425
+ font = cff[fontname]
426
+ cs = font.CharStrings
427
+ # Renumber subroutines to remove unused ones
428
+
429
+ # Mark all used subroutines
430
+ for c in cs.values():
431
+ subrs = getattr(c.private, "Subrs", [])
432
+ decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
433
+ decompiler.execute(c)
434
+
435
+ all_subrs = [font.GlobalSubrs]
436
+ if hasattr(font, "FDArray"):
437
+ all_subrs.extend(
438
+ fd.Private.Subrs
439
+ for fd in font.FDArray
440
+ if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
441
+ )
442
+ elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
443
+ all_subrs.append(font.Private.Subrs)
444
+
445
+ subrs = set(subrs) # Remove duplicates
446
+
447
+ # Prepare
448
+ for subrs in all_subrs:
449
+ if not hasattr(subrs, "_used"):
450
+ subrs._used = set()
451
+ subrs._used = _uniq_sort(subrs._used)
452
+ subrs._old_bias = calcSubrBias(subrs)
453
+ subrs._new_bias = calcSubrBias(subrs._used)
454
+
455
+ # Renumber glyph charstrings
456
+ for c in cs.values():
457
+ subrs = getattr(c.private, "Subrs", None)
458
+ _cs_subset_subroutines(c, subrs, font.GlobalSubrs)
459
+
460
+ # Renumber subroutines themselves
461
+ for subrs in all_subrs:
462
+ if subrs == font.GlobalSubrs:
463
+ if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
464
+ local_subrs = font.Private.Subrs
465
+ elif (
466
+ hasattr(font, "FDArray")
467
+ and len(font.FDArray) == 1
468
+ and hasattr(font.FDArray[0].Private, "Subrs")
469
+ ):
470
+ # Technically we shouldn't do this. But I've run into fonts that do it.
471
+ local_subrs = font.FDArray[0].Private.Subrs
472
+ else:
473
+ local_subrs = None
474
+ else:
475
+ local_subrs = subrs
476
+
477
+ subrs.items = [subrs.items[i] for i in subrs._used]
478
+ if hasattr(subrs, "file"):
479
+ del subrs.file
480
+ if hasattr(subrs, "offsets"):
481
+ del subrs.offsets
482
+
483
+ for subr in subrs.items:
484
+ _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs)
485
+
486
+ # Delete local SubrsIndex if empty
487
+ if hasattr(font, "FDArray"):
488
+ for fd in font.FDArray:
489
+ _pd_delete_empty_subrs(fd.Private)
490
+ else:
491
+ _pd_delete_empty_subrs(font.Private)
492
+
493
+ # Cleanup
494
+ for subrs in all_subrs:
495
+ del subrs._used, subrs._old_bias, subrs._new_bias
lib/python3.12/site-packages/fontTools/cffLib/width.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """T2CharString glyph width optimizer.
4
+
5
+ CFF glyphs whose width equals the CFF Private dictionary's ``defaultWidthX``
6
+ value do not need to specify their width in their charstring, saving bytes.
7
+ This module determines the optimum ``defaultWidthX`` and ``nominalWidthX``
8
+ values for a font, when provided with a list of glyph widths."""
9
+
10
+ from fontTools.ttLib import TTFont
11
+ from collections import defaultdict
12
+ from operator import add
13
+ from functools import reduce
14
+
15
+
16
+ __all__ = ["optimizeWidths", "main"]
17
+
18
+
19
+ class missingdict(dict):
20
+ def __init__(self, missing_func):
21
+ self.missing_func = missing_func
22
+
23
+ def __missing__(self, v):
24
+ return self.missing_func(v)
25
+
26
+
27
+ def cumSum(f, op=add, start=0, decreasing=False):
28
+ keys = sorted(f.keys())
29
+ minx, maxx = keys[0], keys[-1]
30
+
31
+ total = reduce(op, f.values(), start)
32
+
33
+ if decreasing:
34
+ missing = lambda x: start if x > maxx else total
35
+ domain = range(maxx, minx - 1, -1)
36
+ else:
37
+ missing = lambda x: start if x < minx else total
38
+ domain = range(minx, maxx + 1)
39
+
40
+ out = missingdict(missing)
41
+
42
+ v = start
43
+ for x in domain:
44
+ v = op(v, f[x])
45
+ out[x] = v
46
+
47
+ return out
48
+
49
+
50
+ def byteCost(widths, default, nominal):
51
+ if not hasattr(widths, "items"):
52
+ d = defaultdict(int)
53
+ for w in widths:
54
+ d[w] += 1
55
+ widths = d
56
+
57
+ cost = 0
58
+ for w, freq in widths.items():
59
+ if w == default:
60
+ continue
61
+ diff = abs(w - nominal)
62
+ if diff <= 107:
63
+ cost += freq
64
+ elif diff <= 1131:
65
+ cost += freq * 2
66
+ else:
67
+ cost += freq * 5
68
+ return cost
69
+
70
+
71
+ def optimizeWidthsBruteforce(widths):
72
+ """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts."""
73
+
74
+ d = defaultdict(int)
75
+ for w in widths:
76
+ d[w] += 1
77
+
78
+ # Maximum number of bytes using default can possibly save
79
+ maxDefaultAdvantage = 5 * max(d.values())
80
+
81
+ minw, maxw = min(widths), max(widths)
82
+ domain = list(range(minw, maxw + 1))
83
+
84
+ bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain)
85
+
86
+ bestCost = len(widths) * 5 + 1
87
+ for nominal in domain:
88
+ if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage:
89
+ continue
90
+ for default in domain:
91
+ cost = byteCost(widths, default, nominal)
92
+ if cost < bestCost:
93
+ bestCost = cost
94
+ bestDefault = default
95
+ bestNominal = nominal
96
+
97
+ return bestDefault, bestNominal
98
+
99
+
100
+ def optimizeWidths(widths):
101
+ """Given a list of glyph widths, or dictionary mapping glyph width to number of
102
+ glyphs having that, returns a tuple of best CFF default and nominal glyph widths.
103
+
104
+ This algorithm is linear in UPEM+numGlyphs."""
105
+
106
+ if not hasattr(widths, "items"):
107
+ d = defaultdict(int)
108
+ for w in widths:
109
+ d[w] += 1
110
+ widths = d
111
+
112
+ keys = sorted(widths.keys())
113
+ minw, maxw = keys[0], keys[-1]
114
+ domain = list(range(minw, maxw + 1))
115
+
116
+ # Cumulative sum/max forward/backward.
117
+ cumFrqU = cumSum(widths, op=add)
118
+ cumMaxU = cumSum(widths, op=max)
119
+ cumFrqD = cumSum(widths, op=add, decreasing=True)
120
+ cumMaxD = cumSum(widths, op=max, decreasing=True)
121
+
122
+ # Cost per nominal choice, without default consideration.
123
+ nomnCostU = missingdict(
124
+ lambda x: cumFrqU[x] + cumFrqU[x - 108] + cumFrqU[x - 1132] * 3
125
+ )
126
+ nomnCostD = missingdict(
127
+ lambda x: cumFrqD[x] + cumFrqD[x + 108] + cumFrqD[x + 1132] * 3
128
+ )
129
+ nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x])
130
+
131
+ # Cost-saving per nominal choice, by best default choice.
132
+ dfltCostU = missingdict(
133
+ lambda x: max(cumMaxU[x], cumMaxU[x - 108] * 2, cumMaxU[x - 1132] * 5)
134
+ )
135
+ dfltCostD = missingdict(
136
+ lambda x: max(cumMaxD[x], cumMaxD[x + 108] * 2, cumMaxD[x + 1132] * 5)
137
+ )
138
+ dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x]))
139
+
140
+ # Combined cost per nominal choice.
141
+ bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x])
142
+
143
+ # Best nominal.
144
+ nominal = min(domain, key=lambda x: bestCost[x])
145
+
146
+ # Work back the best default.
147
+ bestC = bestCost[nominal]
148
+ dfltC = nomnCost[nominal] - bestCost[nominal]
149
+ ends = []
150
+ if dfltC == dfltCostU[nominal]:
151
+ starts = [nominal, nominal - 108, nominal - 1132]
152
+ for start in starts:
153
+ while cumMaxU[start] and cumMaxU[start] == cumMaxU[start - 1]:
154
+ start -= 1
155
+ ends.append(start)
156
+ else:
157
+ starts = [nominal, nominal + 108, nominal + 1132]
158
+ for start in starts:
159
+ while cumMaxD[start] and cumMaxD[start] == cumMaxD[start + 1]:
160
+ start += 1
161
+ ends.append(start)
162
+ default = min(ends, key=lambda default: byteCost(widths, default, nominal))
163
+
164
+ return default, nominal
165
+
166
+
167
+ def main(args=None):
168
+ """Calculate optimum defaultWidthX/nominalWidthX values"""
169
+
170
+ import argparse
171
+
172
+ parser = argparse.ArgumentParser(
173
+ "fonttools cffLib.width",
174
+ description=main.__doc__,
175
+ )
176
+ parser.add_argument(
177
+ "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files"
178
+ )
179
+ parser.add_argument(
180
+ "-b",
181
+ "--brute-force",
182
+ dest="brute",
183
+ action="store_true",
184
+ help="Use brute-force approach (VERY slow)",
185
+ )
186
+
187
+ args = parser.parse_args(args)
188
+
189
+ for fontfile in args.inputs:
190
+ font = TTFont(fontfile)
191
+ hmtx = font["hmtx"]
192
+ widths = [m[0] for m in hmtx.metrics.values()]
193
+ if args.brute:
194
+ default, nominal = optimizeWidthsBruteforce(widths)
195
+ else:
196
+ default, nominal = optimizeWidths(widths)
197
+ print(
198
+ "glyphs=%d default=%d nominal=%d byteCost=%d"
199
+ % (len(widths), default, nominal, byteCost(widths, default, nominal))
200
+ )
201
+
202
+
203
+ if __name__ == "__main__":
204
+ import sys
205
+
206
+ if len(sys.argv) == 1:
207
+ import doctest
208
+
209
+ sys.exit(doctest.testmod().failed)
210
+ main()
lib/python3.12/site-packages/fontTools/config/__init__.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Define all configuration options that can affect the working of fontTools
3
+ modules. E.g. optimization levels of varLib IUP, otlLib GPOS compression level,
4
+ etc. If this file gets too big, split it into smaller files per-module.
5
+
6
+ An instance of the Config class can be attached to a TTFont object, so that
7
+ the various modules can access their configuration options from it.
8
+ """
9
+
10
+ from textwrap import dedent
11
+
12
+ from fontTools.misc.configTools import *
13
+
14
+
15
+ class Config(AbstractConfig):
16
+ options = Options()
17
+
18
+
19
+ OPTIONS = Config.options
20
+
21
+
22
+ Config.register_option(
23
+ name="fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
24
+ help=dedent(
25
+ """\
26
+ GPOS Lookup type 2 (PairPos) compression level:
27
+ 0 = do not attempt to compact PairPos lookups;
28
+ 1 to 8 = create at most 1 to 8 new subtables for each existing
29
+ subtable, provided that it would yield a 50%% file size saving;
30
+ 9 = create as many new subtables as needed to yield a file size saving.
31
+ Default: 0.
32
+
33
+ This compaction aims to save file size, by splitting large class
34
+ kerning subtables (Format 2) that contain many zero values into
35
+ smaller and denser subtables. It's a trade-off between the overhead
36
+ of several subtables versus the sparseness of one big subtable.
37
+
38
+ See the pull request: https://github.com/fonttools/fonttools/pull/2326
39
+ """
40
+ ),
41
+ default=0,
42
+ parse=int,
43
+ validate=lambda v: v in range(10),
44
+ )
45
+
46
+ Config.register_option(
47
+ name="fontTools.ttLib.tables.otBase:USE_HARFBUZZ_REPACKER",
48
+ help=dedent(
49
+ """\
50
+ FontTools tries to use the HarfBuzz Repacker to serialize GPOS/GSUB tables
51
+ if the uharfbuzz python bindings are importable, otherwise falls back to its
52
+ slower, less efficient serializer. Set to False to always use the latter.
53
+ Set to True to explicitly request the HarfBuzz Repacker (will raise an
54
+ error if uharfbuzz cannot be imported).
55
+ """
56
+ ),
57
+ default=None,
58
+ parse=Option.parse_optional_bool,
59
+ validate=Option.validate_optional_bool,
60
+ )
61
+
62
+ Config.register_option(
63
+ name="fontTools.otlLib.builder:WRITE_GPOS7",
64
+ help=dedent(
65
+ """\
66
+ macOS before 13.2 didn’t support GPOS LookupType 7 (non-chaining
67
+ ContextPos lookups), so FontTools.otlLib.builder disables a file size
68
+ optimisation that would use LookupType 7 instead of 8 when there is no
69
+ chaining (no prefix or suffix). Set to True to enable the optimization.
70
+ """
71
+ ),
72
+ default=False,
73
+ parse=Option.parse_optional_bool,
74
+ validate=Option.validate_optional_bool,
75
+ )
76
+
77
+ Config.register_option(
78
+ name="fontTools.ttLib:OPTIMIZE_FONT_SPEED",
79
+ help=dedent(
80
+ """\
81
+ Enable optimizations that prioritize speed over file size. This
82
+ mainly affects how glyf table and gvar / VARC tables are compiled.
83
+ The produced fonts will be larger, but rendering performance will
84
+ be improved with HarfBuzz and other text layout engines.
85
+ """
86
+ ),
87
+ default=False,
88
+ parse=Option.parse_optional_bool,
89
+ validate=Option.validate_optional_bool,
90
+ )
lib/python3.12/site-packages/fontTools/config/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (3.73 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__init__.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+ from fontTools import ttLib
6
+ import fontTools.merge.base
7
+ from fontTools.merge.cmap import (
8
+ computeMegaGlyphOrder,
9
+ computeMegaCmap,
10
+ renameCFFCharStrings,
11
+ )
12
+ from fontTools.merge.layout import layoutPreMerge, layoutPostMerge
13
+ from fontTools.merge.options import Options
14
+ import fontTools.merge.tables
15
+ from fontTools.misc.loggingTools import Timer
16
+ from functools import reduce
17
+ import sys
18
+ import logging
19
+
20
+
21
+ log = logging.getLogger("fontTools.merge")
22
+ timer = Timer(logger=logging.getLogger(__name__ + ".timer"), level=logging.INFO)
23
+
24
+
25
+ class Merger(object):
26
+ """Font merger.
27
+
28
+ This class merges multiple files into a single OpenType font, taking into
29
+ account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and
30
+ cross-font metrics (for example ``hhea.ascent`` is set to the maximum value
31
+ across all the fonts).
32
+
33
+ If multiple glyphs map to the same Unicode value, and the glyphs are considered
34
+ sufficiently different (that is, they differ in any of paths, widths, or
35
+ height), then subsequent glyphs are renamed and a lookup in the ``locl``
36
+ feature will be created to disambiguate them. For example, if the arguments
37
+ are an Arabic font and a Latin font and both contain a set of parentheses,
38
+ the Latin glyphs will be renamed to ``parenleft.1`` and ``parenright.1``,
39
+ and a lookup will be inserted into the to ``locl`` feature (creating it if
40
+ necessary) under the ``latn`` script to substitute ``parenleft`` with
41
+ ``parenleft.1`` etc.
42
+
43
+ Restrictions:
44
+
45
+ - All fonts must have the same units per em.
46
+ - If duplicate glyph disambiguation takes place as described above then the
47
+ fonts must have a ``GSUB`` table.
48
+
49
+ Attributes:
50
+ options: Currently unused.
51
+ """
52
+
53
+ def __init__(self, options=None):
54
+ if not options:
55
+ options = Options()
56
+
57
+ self.options = options
58
+
59
+ def _openFonts(self, fontfiles):
60
+ fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
61
+ for font, fontfile in zip(fonts, fontfiles):
62
+ font._merger__fontfile = fontfile
63
+ font._merger__name = font["name"].getDebugName(4)
64
+ return fonts
65
+
66
+ def merge(self, fontfiles):
67
+ """Merges fonts together.
68
+
69
+ Args:
70
+ fontfiles: A list of file names to be merged
71
+
72
+ Returns:
73
+ A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on
74
+ this to write it out to an OTF file.
75
+ """
76
+ #
77
+ # Settle on a mega glyph order.
78
+ #
79
+ fonts = self._openFonts(fontfiles)
80
+ glyphOrders = [list(font.getGlyphOrder()) for font in fonts]
81
+ computeMegaGlyphOrder(self, glyphOrders)
82
+
83
+ # Take first input file sfntVersion
84
+ sfntVersion = fonts[0].sfntVersion
85
+
86
+ # Reload fonts and set new glyph names on them.
87
+ fonts = self._openFonts(fontfiles)
88
+ for font, glyphOrder in zip(fonts, glyphOrders):
89
+ font.setGlyphOrder(glyphOrder)
90
+ if "CFF " in font:
91
+ renameCFFCharStrings(self, glyphOrder, font["CFF "])
92
+
93
+ cmaps = [font["cmap"] for font in fonts]
94
+ self.duplicateGlyphsPerFont = [{} for _ in fonts]
95
+ computeMegaCmap(self, cmaps)
96
+
97
+ mega = ttLib.TTFont(sfntVersion=sfntVersion)
98
+ mega.setGlyphOrder(self.glyphOrder)
99
+
100
+ for font in fonts:
101
+ self._preMerge(font)
102
+
103
+ self.fonts = fonts
104
+
105
+ allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
106
+ allTags.remove("GlyphOrder")
107
+
108
+ for tag in sorted(allTags):
109
+ if tag in self.options.drop_tables:
110
+ continue
111
+
112
+ with timer("merge '%s'" % tag):
113
+ tables = [font.get(tag, NotImplemented) for font in fonts]
114
+
115
+ log.info("Merging '%s'.", tag)
116
+ clazz = ttLib.getTableClass(tag)
117
+ table = clazz(tag).merge(self, tables)
118
+ # XXX Clean this up and use: table = mergeObjects(tables)
119
+
120
+ if table is not NotImplemented and table is not False:
121
+ mega[tag] = table
122
+ log.info("Merged '%s'.", tag)
123
+ else:
124
+ log.info("Dropped '%s'.", tag)
125
+
126
+ del self.duplicateGlyphsPerFont
127
+ del self.fonts
128
+
129
+ self._postMerge(mega)
130
+
131
+ return mega
132
+
133
+ def mergeObjects(self, returnTable, logic, tables):
134
+ # Right now we don't use self at all. Will use in the future
135
+ # for options and logging.
136
+
137
+ allKeys = set.union(
138
+ set(),
139
+ *(vars(table).keys() for table in tables if table is not NotImplemented),
140
+ )
141
+ for key in allKeys:
142
+ log.info(" %s", key)
143
+ try:
144
+ mergeLogic = logic[key]
145
+ except KeyError:
146
+ try:
147
+ mergeLogic = logic["*"]
148
+ except KeyError:
149
+ raise Exception(
150
+ "Don't know how to merge key %s of class %s"
151
+ % (key, returnTable.__class__.__name__)
152
+ )
153
+ if mergeLogic is NotImplemented:
154
+ continue
155
+ value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
156
+ if value is not NotImplemented:
157
+ setattr(returnTable, key, value)
158
+
159
+ return returnTable
160
+
161
+ def _preMerge(self, font):
162
+ layoutPreMerge(font)
163
+
164
+ def _postMerge(self, font):
165
+ layoutPostMerge(font)
166
+
167
+ if "OS/2" in font:
168
+ # https://github.com/fonttools/fonttools/issues/2538
169
+ # TODO: Add an option to disable this?
170
+ font["OS/2"].recalcAvgCharWidth(font)
171
+
172
+
173
+ __all__ = ["Options", "Merger", "main"]
174
+
175
+
176
+ @timer("make one with everything (TOTAL TIME)")
177
+ def main(args=None):
178
+ """Merge multiple fonts into one"""
179
+ from fontTools import configLogger
180
+
181
+ if args is None:
182
+ args = sys.argv[1:]
183
+
184
+ options = Options()
185
+ args = options.parse_opts(args)
186
+ fontfiles = []
187
+ if options.input_file:
188
+ with open(options.input_file) as inputfile:
189
+ fontfiles = [
190
+ line.strip()
191
+ for line in inputfile.readlines()
192
+ if not line.lstrip().startswith("#")
193
+ ]
194
+ for g in args:
195
+ fontfiles.append(g)
196
+
197
+ if len(fontfiles) < 1:
198
+ print(
199
+ "usage: fonttools merge [font1 ... fontN] [--input-file=filelist.txt] [--output-file=merged.ttf] [--import-file=tables.ttx]",
200
+ file=sys.stderr,
201
+ )
202
+ print(
203
+ " [--drop-tables=tags] [--verbose] [--timing]",
204
+ file=sys.stderr,
205
+ )
206
+ print("", file=sys.stderr)
207
+ print(" font1 ... fontN Files to merge.", file=sys.stderr)
208
+ print(
209
+ " --input-file=<filename> Read files to merge from a text file, each path new line. # Comment lines allowed.",
210
+ file=sys.stderr,
211
+ )
212
+ print(
213
+ " --output-file=<filename> Specify output file name (default: merged.ttf).",
214
+ file=sys.stderr,
215
+ )
216
+ print(
217
+ " --import-file=<filename> TTX file to import after merging. This can be used to set metadata.",
218
+ file=sys.stderr,
219
+ )
220
+ print(
221
+ " --drop-tables=<table tags> Comma separated list of table tags to skip, case sensitive.",
222
+ file=sys.stderr,
223
+ )
224
+ print(
225
+ " --verbose Output progress information.",
226
+ file=sys.stderr,
227
+ )
228
+ print(" --timing Output progress timing.", file=sys.stderr)
229
+ return 1
230
+
231
+ configLogger(level=logging.INFO if options.verbose else logging.WARNING)
232
+ if options.timing:
233
+ timer.logger.setLevel(logging.DEBUG)
234
+ else:
235
+ timer.logger.disabled = True
236
+
237
+ merger = Merger(options=options)
238
+ font = merger.merge(fontfiles)
239
+
240
+ if options.import_file:
241
+ font.importXML(options.import_file)
242
+
243
+ with timer("compile and save font"):
244
+ font.save(options.output_file)
245
+
246
+
247
+ if __name__ == "__main__":
248
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/merge/__main__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import sys
2
+ from fontTools.merge import main
3
+
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
lib/python3.12/site-packages/fontTools/merge/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (11.5 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/__main__.cpython-312.pyc ADDED
Binary file (372 Bytes). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/base.cpython-312.pyc ADDED
Binary file (3.89 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/cmap.cpython-312.pyc ADDED
Binary file (6.43 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/layout.cpython-312.pyc ADDED
Binary file (23.7 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/options.cpython-312.pyc ADDED
Binary file (3.24 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/tables.cpython-312.pyc ADDED
Binary file (13 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/unicode.cpython-312.pyc ADDED
Binary file (1.8 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/__pycache__/util.cpython-312.pyc ADDED
Binary file (7.57 kB). View file
 
lib/python3.12/site-packages/fontTools/merge/base.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+ from fontTools.ttLib.tables.DefaultTable import DefaultTable
6
+ import logging
7
+
8
+
9
+ log = logging.getLogger("fontTools.merge")
10
+
11
+
12
+ def add_method(*clazzes, **kwargs):
13
+ """Returns a decorator function that adds a new method to one or
14
+ more classes."""
15
+ allowDefault = kwargs.get("allowDefaultTable", False)
16
+
17
+ def wrapper(method):
18
+ done = []
19
+ for clazz in clazzes:
20
+ if clazz in done:
21
+ continue # Support multiple names of a clazz
22
+ done.append(clazz)
23
+ assert allowDefault or clazz != DefaultTable, "Oops, table class not found."
24
+ assert (
25
+ method.__name__ not in clazz.__dict__
26
+ ), "Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__)
27
+ setattr(clazz, method.__name__, method)
28
+ return None
29
+
30
+ return wrapper
31
+
32
+
33
+ def mergeObjects(lst):
34
+ lst = [item for item in lst if item is not NotImplemented]
35
+ if not lst:
36
+ return NotImplemented
37
+ lst = [item for item in lst if item is not None]
38
+ if not lst:
39
+ return None
40
+
41
+ clazz = lst[0].__class__
42
+ assert all(type(item) == clazz for item in lst), lst
43
+
44
+ logic = clazz.mergeMap
45
+ returnTable = clazz()
46
+ returnDict = {}
47
+
48
+ allKeys = set.union(set(), *(vars(table).keys() for table in lst))
49
+ for key in allKeys:
50
+ try:
51
+ mergeLogic = logic[key]
52
+ except KeyError:
53
+ try:
54
+ mergeLogic = logic["*"]
55
+ except KeyError:
56
+ raise Exception(
57
+ "Don't know how to merge key %s of class %s" % (key, clazz.__name__)
58
+ )
59
+ if mergeLogic is NotImplemented:
60
+ continue
61
+ value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
62
+ if value is not NotImplemented:
63
+ returnDict[key] = value
64
+
65
+ returnTable.__dict__ = returnDict
66
+
67
+ return returnTable
68
+
69
+
70
+ @add_method(DefaultTable, allowDefaultTable=True)
71
+ def merge(self, m, tables):
72
+ if not hasattr(self, "mergeMap"):
73
+ log.info("Don't know how to merge '%s'.", self.tableTag)
74
+ return NotImplemented
75
+
76
+ logic = self.mergeMap
77
+
78
+ if isinstance(logic, dict):
79
+ return m.mergeObjects(self, self.mergeMap, tables)
80
+ else:
81
+ return logic(tables)
lib/python3.12/site-packages/fontTools/merge/cmap.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+ from fontTools.merge.unicode import is_Default_Ignorable
6
+ from fontTools.pens.recordingPen import DecomposingRecordingPen
7
+ import logging
8
+
9
+
10
+ log = logging.getLogger("fontTools.merge")
11
+
12
+
13
+ def computeMegaGlyphOrder(merger, glyphOrders):
14
+ """Modifies passed-in glyphOrders to reflect new glyph names.
15
+ Stores merger.glyphOrder."""
16
+ megaOrder = {}
17
+ for glyphOrder in glyphOrders:
18
+ for i, glyphName in enumerate(glyphOrder):
19
+ if glyphName in megaOrder:
20
+ n = megaOrder[glyphName]
21
+ while (glyphName + "." + repr(n)) in megaOrder:
22
+ n += 1
23
+ megaOrder[glyphName] = n
24
+ glyphName += "." + repr(n)
25
+ glyphOrder[i] = glyphName
26
+ megaOrder[glyphName] = 1
27
+ merger.glyphOrder = megaOrder = list(megaOrder.keys())
28
+
29
+
30
+ def _glyphsAreSame(
31
+ glyphSet1,
32
+ glyphSet2,
33
+ glyph1,
34
+ glyph2,
35
+ advanceTolerance=0.05,
36
+ advanceToleranceEmpty=0.20,
37
+ ):
38
+ pen1 = DecomposingRecordingPen(glyphSet1)
39
+ pen2 = DecomposingRecordingPen(glyphSet2)
40
+ g1 = glyphSet1[glyph1]
41
+ g2 = glyphSet2[glyph2]
42
+ g1.draw(pen1)
43
+ g2.draw(pen2)
44
+ if pen1.value != pen2.value:
45
+ return False
46
+ # Allow more width tolerance for glyphs with no ink
47
+ tolerance = advanceTolerance if pen1.value else advanceToleranceEmpty
48
+ # TODO Warn if advances not the same but within tolerance.
49
+ if abs(g1.width - g2.width) > g1.width * tolerance:
50
+ return False
51
+ if hasattr(g1, "height") and g1.height is not None:
52
+ if abs(g1.height - g2.height) > g1.height * tolerance:
53
+ return False
54
+ return True
55
+
56
+
57
+ def computeMegaUvs(merger, uvsTables):
58
+ """Returns merged UVS subtable (cmap format=14)."""
59
+ uvsDict = {}
60
+ cmap = merger.cmap
61
+ for table in uvsTables:
62
+ for variationSelector, uvsMapping in table.uvsDict.items():
63
+ if variationSelector not in uvsDict:
64
+ uvsDict[variationSelector] = {}
65
+ for unicodeValue, glyphName in uvsMapping:
66
+ if cmap.get(unicodeValue) == glyphName:
67
+ # this is a default variation
68
+ glyphName = None
69
+ # prefer previous glyph id if both fonts defined UVS
70
+ if unicodeValue not in uvsDict[variationSelector]:
71
+ uvsDict[variationSelector][unicodeValue] = glyphName
72
+
73
+ for variationSelector in uvsDict:
74
+ uvsDict[variationSelector] = [*uvsDict[variationSelector].items()]
75
+
76
+ return uvsDict
77
+
78
+
79
+ # Valid (format, platformID, platEncID) triplets for cmap subtables containing
80
+ # Unicode BMP-only and Unicode Full Repertoire semantics.
81
+ # Cf. OpenType spec for "Platform specific encodings":
82
+ # https://docs.microsoft.com/en-us/typography/opentype/spec/name
83
+ class _CmapUnicodePlatEncodings:
84
+ BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
85
+ FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
86
+ UVS = {(14, 0, 5)}
87
+
88
+
89
+ def computeMegaCmap(merger, cmapTables):
90
+ """Sets merger.cmap and merger.uvsDict."""
91
+
92
+ # TODO Handle format=14.
93
+ # Only merge format 4 and 12 Unicode subtables, ignores all other subtables
94
+ # If there is a format 12 table for a font, ignore the format 4 table of it
95
+ chosenCmapTables = []
96
+ chosenUvsTables = []
97
+ for fontIdx, table in enumerate(cmapTables):
98
+ format4 = None
99
+ format12 = None
100
+ format14 = None
101
+ for subtable in table.tables:
102
+ properties = (subtable.format, subtable.platformID, subtable.platEncID)
103
+ if properties in _CmapUnicodePlatEncodings.BMP:
104
+ format4 = subtable
105
+ elif properties in _CmapUnicodePlatEncodings.FullRepertoire:
106
+ format12 = subtable
107
+ elif properties in _CmapUnicodePlatEncodings.UVS:
108
+ format14 = subtable
109
+ else:
110
+ log.warning(
111
+ "Dropped cmap subtable from font '%s':\t"
112
+ "format %2s, platformID %2s, platEncID %2s",
113
+ fontIdx,
114
+ subtable.format,
115
+ subtable.platformID,
116
+ subtable.platEncID,
117
+ )
118
+ if format12 is not None:
119
+ chosenCmapTables.append((format12, fontIdx))
120
+ elif format4 is not None:
121
+ chosenCmapTables.append((format4, fontIdx))
122
+
123
+ if format14 is not None:
124
+ chosenUvsTables.append(format14)
125
+
126
+ # Build the unicode mapping
127
+ merger.cmap = cmap = {}
128
+ fontIndexForGlyph = {}
129
+ glyphSets = [None for f in merger.fonts] if hasattr(merger, "fonts") else None
130
+
131
+ for table, fontIdx in chosenCmapTables:
132
+ # handle duplicates
133
+ for uni, gid in table.cmap.items():
134
+ oldgid = cmap.get(uni, None)
135
+ if oldgid is None:
136
+ cmap[uni] = gid
137
+ fontIndexForGlyph[gid] = fontIdx
138
+ elif is_Default_Ignorable(uni) or uni in (0x25CC,): # U+25CC DOTTED CIRCLE
139
+ continue
140
+ elif oldgid != gid:
141
+ # Char previously mapped to oldgid, now to gid.
142
+ # Record, to fix up in GSUB 'locl' later.
143
+ if merger.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None:
144
+ if glyphSets is not None:
145
+ oldFontIdx = fontIndexForGlyph[oldgid]
146
+ for idx in (fontIdx, oldFontIdx):
147
+ if glyphSets[idx] is None:
148
+ glyphSets[idx] = merger.fonts[idx].getGlyphSet()
149
+ # if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid):
150
+ # continue
151
+ merger.duplicateGlyphsPerFont[fontIdx][oldgid] = gid
152
+ elif merger.duplicateGlyphsPerFont[fontIdx][oldgid] != gid:
153
+ # Char previously mapped to oldgid but oldgid is already remapped to a different
154
+ # gid, because of another Unicode character.
155
+ # TODO: Try harder to do something about these.
156
+ log.warning(
157
+ "Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid
158
+ )
159
+
160
+ merger.uvsDict = computeMegaUvs(merger, chosenUvsTables)
161
+
162
+
163
+ def renameCFFCharStrings(merger, glyphOrder, cffTable):
164
+ """Rename topDictIndex charStrings based on glyphOrder."""
165
+ td = cffTable.cff.topDictIndex[0]
166
+
167
+ charStrings = {}
168
+ for i, v in enumerate(td.CharStrings.charStrings.values()):
169
+ glyphName = glyphOrder[i]
170
+ charStrings[glyphName] = v
171
+ td.CharStrings.charStrings = charStrings
172
+
173
+ td.charset = list(glyphOrder)
lib/python3.12/site-packages/fontTools/merge/layout.py ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+ from fontTools import ttLib
6
+ from fontTools.ttLib.tables.DefaultTable import DefaultTable
7
+ from fontTools.ttLib.tables import otTables
8
+ from fontTools.merge.base import add_method, mergeObjects
9
+ from fontTools.merge.util import *
10
+ import logging
11
+
12
+
13
+ log = logging.getLogger("fontTools.merge")
14
+
15
+
16
+ def mergeLookupLists(lst):
17
+ # TODO Do smarter merge.
18
+ return sumLists(lst)
19
+
20
+
21
+ def mergeFeatures(lst):
22
+ assert lst
23
+ self = otTables.Feature()
24
+ self.FeatureParams = None
25
+ self.LookupListIndex = mergeLookupLists(
26
+ [l.LookupListIndex for l in lst if l.LookupListIndex]
27
+ )
28
+ self.LookupCount = len(self.LookupListIndex)
29
+ return self
30
+
31
+
32
+ def mergeFeatureLists(lst):
33
+ d = {}
34
+ for l in lst:
35
+ for f in l:
36
+ tag = f.FeatureTag
37
+ if tag not in d:
38
+ d[tag] = []
39
+ d[tag].append(f.Feature)
40
+ ret = []
41
+ for tag in sorted(d.keys()):
42
+ rec = otTables.FeatureRecord()
43
+ rec.FeatureTag = tag
44
+ rec.Feature = mergeFeatures(d[tag])
45
+ ret.append(rec)
46
+ return ret
47
+
48
+
49
+ def mergeLangSyses(lst):
50
+ assert lst
51
+
52
+ # TODO Support merging ReqFeatureIndex
53
+ assert all(l.ReqFeatureIndex == 0xFFFF for l in lst)
54
+
55
+ self = otTables.LangSys()
56
+ self.LookupOrder = None
57
+ self.ReqFeatureIndex = 0xFFFF
58
+ self.FeatureIndex = mergeFeatureLists(
59
+ [l.FeatureIndex for l in lst if l.FeatureIndex]
60
+ )
61
+ self.FeatureCount = len(self.FeatureIndex)
62
+ return self
63
+
64
+
65
+ def mergeScripts(lst):
66
+ assert lst
67
+
68
+ if len(lst) == 1:
69
+ return lst[0]
70
+ langSyses = {}
71
+ for sr in lst:
72
+ for lsr in sr.LangSysRecord:
73
+ if lsr.LangSysTag not in langSyses:
74
+ langSyses[lsr.LangSysTag] = []
75
+ langSyses[lsr.LangSysTag].append(lsr.LangSys)
76
+ lsrecords = []
77
+ for tag, langSys_list in sorted(langSyses.items()):
78
+ lsr = otTables.LangSysRecord()
79
+ lsr.LangSys = mergeLangSyses(langSys_list)
80
+ lsr.LangSysTag = tag
81
+ lsrecords.append(lsr)
82
+
83
+ self = otTables.Script()
84
+ self.LangSysRecord = lsrecords
85
+ self.LangSysCount = len(lsrecords)
86
+ dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys]
87
+ if dfltLangSyses:
88
+ self.DefaultLangSys = mergeLangSyses(dfltLangSyses)
89
+ else:
90
+ self.DefaultLangSys = None
91
+ return self
92
+
93
+
94
+ def mergeScriptRecords(lst):
95
+ d = {}
96
+ for l in lst:
97
+ for s in l:
98
+ tag = s.ScriptTag
99
+ if tag not in d:
100
+ d[tag] = []
101
+ d[tag].append(s.Script)
102
+ ret = []
103
+ for tag in sorted(d.keys()):
104
+ rec = otTables.ScriptRecord()
105
+ rec.ScriptTag = tag
106
+ rec.Script = mergeScripts(d[tag])
107
+ ret.append(rec)
108
+ return ret
109
+
110
+
111
+ otTables.ScriptList.mergeMap = {
112
+ "ScriptCount": lambda lst: None, # TODO
113
+ "ScriptRecord": mergeScriptRecords,
114
+ }
115
+ otTables.BaseScriptList.mergeMap = {
116
+ "BaseScriptCount": lambda lst: None, # TODO
117
+ # TODO: Merge duplicate entries
118
+ "BaseScriptRecord": lambda lst: sorted(
119
+ sumLists(lst), key=lambda s: s.BaseScriptTag
120
+ ),
121
+ }
122
+
123
+ otTables.FeatureList.mergeMap = {
124
+ "FeatureCount": sum,
125
+ "FeatureRecord": lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag),
126
+ }
127
+
128
+ otTables.LookupList.mergeMap = {
129
+ "LookupCount": sum,
130
+ "Lookup": sumLists,
131
+ }
132
+
133
+ otTables.Coverage.mergeMap = {
134
+ "Format": min,
135
+ "glyphs": sumLists,
136
+ }
137
+
138
+ otTables.ClassDef.mergeMap = {
139
+ "Format": min,
140
+ "classDefs": sumDicts,
141
+ }
142
+
143
+ otTables.LigCaretList.mergeMap = {
144
+ "Coverage": mergeObjects,
145
+ "LigGlyphCount": sum,
146
+ "LigGlyph": sumLists,
147
+ }
148
+
149
+ otTables.AttachList.mergeMap = {
150
+ "Coverage": mergeObjects,
151
+ "GlyphCount": sum,
152
+ "AttachPoint": sumLists,
153
+ }
154
+
155
+ # XXX Renumber MarkFilterSets of lookups
156
+ otTables.MarkGlyphSetsDef.mergeMap = {
157
+ "MarkSetTableFormat": equal,
158
+ "MarkSetCount": sum,
159
+ "Coverage": sumLists,
160
+ }
161
+
162
+ otTables.Axis.mergeMap = {
163
+ "*": mergeObjects,
164
+ }
165
+
166
+ # XXX Fix BASE table merging
167
+ otTables.BaseTagList.mergeMap = {
168
+ "BaseTagCount": sum,
169
+ "BaselineTag": sumLists,
170
+ }
171
+
172
+ otTables.GDEF.mergeMap = otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = (
173
+ otTables.BASE.mergeMap
174
+ ) = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = {
175
+ "*": mergeObjects,
176
+ "Version": max,
177
+ }
178
+
179
+ ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass("GSUB").mergeMap = (
180
+ ttLib.getTableClass("GPOS").mergeMap
181
+ ) = ttLib.getTableClass("BASE").mergeMap = ttLib.getTableClass(
182
+ "JSTF"
183
+ ).mergeMap = ttLib.getTableClass(
184
+ "MATH"
185
+ ).mergeMap = {
186
+ "tableTag": onlyExisting(equal), # XXX clean me up
187
+ "table": mergeObjects,
188
+ }
189
+
190
+
191
+ @add_method(ttLib.getTableClass("GSUB"))
192
+ def merge(self, m, tables):
193
+ assert len(tables) == len(m.duplicateGlyphsPerFont)
194
+ for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
195
+ if not dups:
196
+ continue
197
+ if table is None or table is NotImplemented:
198
+ log.warning(
199
+ "Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s",
200
+ m.fonts[i]._merger__name,
201
+ dups,
202
+ )
203
+ continue
204
+
205
+ synthFeature = None
206
+ synthLookup = None
207
+ for script in table.table.ScriptList.ScriptRecord:
208
+ if script.ScriptTag == "DFLT":
209
+ continue # XXX
210
+ for langsys in [script.Script.DefaultLangSys] + [
211
+ l.LangSys for l in script.Script.LangSysRecord
212
+ ]:
213
+ if langsys is None:
214
+ continue # XXX Create!
215
+ feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"]
216
+ assert len(feature) <= 1
217
+ if feature:
218
+ feature = feature[0]
219
+ else:
220
+ if not synthFeature:
221
+ synthFeature = otTables.FeatureRecord()
222
+ synthFeature.FeatureTag = "locl"
223
+ f = synthFeature.Feature = otTables.Feature()
224
+ f.FeatureParams = None
225
+ f.LookupCount = 0
226
+ f.LookupListIndex = []
227
+ table.table.FeatureList.FeatureRecord.append(synthFeature)
228
+ table.table.FeatureList.FeatureCount += 1
229
+ feature = synthFeature
230
+ langsys.FeatureIndex.append(feature)
231
+ langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag)
232
+
233
+ if not synthLookup:
234
+ subtable = otTables.SingleSubst()
235
+ subtable.mapping = dups
236
+ synthLookup = otTables.Lookup()
237
+ synthLookup.LookupFlag = 0
238
+ synthLookup.LookupType = 1
239
+ synthLookup.SubTableCount = 1
240
+ synthLookup.SubTable = [subtable]
241
+ if table.table.LookupList is None:
242
+ # mtiLib uses None as default value for LookupList,
243
+ # while feaLib points to an empty array with count 0
244
+ # TODO: make them do the same
245
+ table.table.LookupList = otTables.LookupList()
246
+ table.table.LookupList.Lookup = []
247
+ table.table.LookupList.LookupCount = 0
248
+ table.table.LookupList.Lookup.append(synthLookup)
249
+ table.table.LookupList.LookupCount += 1
250
+
251
+ if feature.Feature.LookupListIndex[:1] != [synthLookup]:
252
+ feature.Feature.LookupListIndex[:0] = [synthLookup]
253
+ feature.Feature.LookupCount += 1
254
+
255
+ DefaultTable.merge(self, m, tables)
256
+ return self
257
+
258
+
259
+ @add_method(
260
+ otTables.SingleSubst,
261
+ otTables.MultipleSubst,
262
+ otTables.AlternateSubst,
263
+ otTables.LigatureSubst,
264
+ otTables.ReverseChainSingleSubst,
265
+ otTables.SinglePos,
266
+ otTables.PairPos,
267
+ otTables.CursivePos,
268
+ otTables.MarkBasePos,
269
+ otTables.MarkLigPos,
270
+ otTables.MarkMarkPos,
271
+ )
272
+ def mapLookups(self, lookupMap):
273
+ pass
274
+
275
+
276
+ # Copied and trimmed down from subset.py
277
+ @add_method(
278
+ otTables.ContextSubst,
279
+ otTables.ChainContextSubst,
280
+ otTables.ContextPos,
281
+ otTables.ChainContextPos,
282
+ )
283
+ def __merge_classify_context(self):
284
+ class ContextHelper(object):
285
+ def __init__(self, klass, Format):
286
+ if klass.__name__.endswith("Subst"):
287
+ Typ = "Sub"
288
+ Type = "Subst"
289
+ else:
290
+ Typ = "Pos"
291
+ Type = "Pos"
292
+ if klass.__name__.startswith("Chain"):
293
+ Chain = "Chain"
294
+ else:
295
+ Chain = ""
296
+ ChainTyp = Chain + Typ
297
+
298
+ self.Typ = Typ
299
+ self.Type = Type
300
+ self.Chain = Chain
301
+ self.ChainTyp = ChainTyp
302
+
303
+ self.LookupRecord = Type + "LookupRecord"
304
+
305
+ if Format == 1:
306
+ self.Rule = ChainTyp + "Rule"
307
+ self.RuleSet = ChainTyp + "RuleSet"
308
+ elif Format == 2:
309
+ self.Rule = ChainTyp + "ClassRule"
310
+ self.RuleSet = ChainTyp + "ClassSet"
311
+
312
+ if self.Format not in [1, 2, 3]:
313
+ return None # Don't shoot the messenger; let it go
314
+ if not hasattr(self.__class__, "_merge__ContextHelpers"):
315
+ self.__class__._merge__ContextHelpers = {}
316
+ if self.Format not in self.__class__._merge__ContextHelpers:
317
+ helper = ContextHelper(self.__class__, self.Format)
318
+ self.__class__._merge__ContextHelpers[self.Format] = helper
319
+ return self.__class__._merge__ContextHelpers[self.Format]
320
+
321
+
322
+ @add_method(
323
+ otTables.ContextSubst,
324
+ otTables.ChainContextSubst,
325
+ otTables.ContextPos,
326
+ otTables.ChainContextPos,
327
+ )
328
+ def mapLookups(self, lookupMap):
329
+ c = self.__merge_classify_context()
330
+
331
+ if self.Format in [1, 2]:
332
+ for rs in getattr(self, c.RuleSet):
333
+ if not rs:
334
+ continue
335
+ for r in getattr(rs, c.Rule):
336
+ if not r:
337
+ continue
338
+ for ll in getattr(r, c.LookupRecord):
339
+ if not ll:
340
+ continue
341
+ ll.LookupListIndex = lookupMap[ll.LookupListIndex]
342
+ elif self.Format == 3:
343
+ for ll in getattr(self, c.LookupRecord):
344
+ if not ll:
345
+ continue
346
+ ll.LookupListIndex = lookupMap[ll.LookupListIndex]
347
+ else:
348
+ assert 0, "unknown format: %s" % self.Format
349
+
350
+
351
+ @add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
352
+ def mapLookups(self, lookupMap):
353
+ if self.Format == 1:
354
+ self.ExtSubTable.mapLookups(lookupMap)
355
+ else:
356
+ assert 0, "unknown format: %s" % self.Format
357
+
358
+
359
+ @add_method(otTables.Lookup)
360
+ def mapLookups(self, lookupMap):
361
+ for st in self.SubTable:
362
+ if not st:
363
+ continue
364
+ st.mapLookups(lookupMap)
365
+
366
+
367
+ @add_method(otTables.LookupList)
368
+ def mapLookups(self, lookupMap):
369
+ for l in self.Lookup:
370
+ if not l:
371
+ continue
372
+ l.mapLookups(lookupMap)
373
+
374
+
375
+ @add_method(otTables.Lookup)
376
+ def mapMarkFilteringSets(self, markFilteringSetMap):
377
+ if self.LookupFlag & 0x0010:
378
+ self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet]
379
+
380
+
381
+ @add_method(otTables.LookupList)
382
+ def mapMarkFilteringSets(self, markFilteringSetMap):
383
+ for l in self.Lookup:
384
+ if not l:
385
+ continue
386
+ l.mapMarkFilteringSets(markFilteringSetMap)
387
+
388
+
389
+ @add_method(otTables.Feature)
390
+ def mapLookups(self, lookupMap):
391
+ self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
392
+
393
+
394
+ @add_method(otTables.FeatureList)
395
+ def mapLookups(self, lookupMap):
396
+ for f in self.FeatureRecord:
397
+ if not f or not f.Feature:
398
+ continue
399
+ f.Feature.mapLookups(lookupMap)
400
+
401
+
402
+ @add_method(otTables.DefaultLangSys, otTables.LangSys)
403
+ def mapFeatures(self, featureMap):
404
+ self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
405
+ if self.ReqFeatureIndex != 65535:
406
+ self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
407
+
408
+
409
+ @add_method(otTables.Script)
410
+ def mapFeatures(self, featureMap):
411
+ if self.DefaultLangSys:
412
+ self.DefaultLangSys.mapFeatures(featureMap)
413
+ for l in self.LangSysRecord:
414
+ if not l or not l.LangSys:
415
+ continue
416
+ l.LangSys.mapFeatures(featureMap)
417
+
418
+
419
+ @add_method(otTables.ScriptList)
420
+ def mapFeatures(self, featureMap):
421
+ for s in self.ScriptRecord:
422
+ if not s or not s.Script:
423
+ continue
424
+ s.Script.mapFeatures(featureMap)
425
+
426
+
427
+ def layoutPreMerge(font):
428
+ # Map indices to references
429
+
430
+ GDEF = font.get("GDEF")
431
+ GSUB = font.get("GSUB")
432
+ GPOS = font.get("GPOS")
433
+
434
+ for t in [GSUB, GPOS]:
435
+ if not t:
436
+ continue
437
+
438
+ if t.table.LookupList:
439
+ lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)}
440
+ t.table.LookupList.mapLookups(lookupMap)
441
+ t.table.FeatureList.mapLookups(lookupMap)
442
+
443
+ if (
444
+ GDEF
445
+ and GDEF.table.Version >= 0x00010002
446
+ and GDEF.table.MarkGlyphSetsDef
447
+ ):
448
+ markFilteringSetMap = {
449
+ i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage)
450
+ }
451
+ t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
452
+
453
+ if t.table.FeatureList and t.table.ScriptList:
454
+ featureMap = {i: v for i, v in enumerate(t.table.FeatureList.FeatureRecord)}
455
+ t.table.ScriptList.mapFeatures(featureMap)
456
+
457
+ # TODO FeatureParams nameIDs
458
+
459
+
460
+ def layoutPostMerge(font):
461
+ # Map references back to indices
462
+
463
+ GDEF = font.get("GDEF")
464
+ GSUB = font.get("GSUB")
465
+ GPOS = font.get("GPOS")
466
+
467
+ for t in [GSUB, GPOS]:
468
+ if not t:
469
+ continue
470
+
471
+ if t.table.FeatureList and t.table.ScriptList:
472
+ # Collect unregistered (new) features.
473
+ featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord)
474
+ t.table.ScriptList.mapFeatures(featureMap)
475
+
476
+ # Record used features.
477
+ featureMap = AttendanceRecordingIdentityDict(
478
+ t.table.FeatureList.FeatureRecord
479
+ )
480
+ t.table.ScriptList.mapFeatures(featureMap)
481
+ usedIndices = featureMap.s
482
+
483
+ # Remove unused features
484
+ t.table.FeatureList.FeatureRecord = [
485
+ f
486
+ for i, f in enumerate(t.table.FeatureList.FeatureRecord)
487
+ if i in usedIndices
488
+ ]
489
+
490
+ # Map back to indices.
491
+ featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord)
492
+ t.table.ScriptList.mapFeatures(featureMap)
493
+
494
+ t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord)
495
+
496
+ if t.table.LookupList:
497
+ # Collect unregistered (new) lookups.
498
+ lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup)
499
+ t.table.FeatureList.mapLookups(lookupMap)
500
+ t.table.LookupList.mapLookups(lookupMap)
501
+
502
+ # Record used lookups.
503
+ lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup)
504
+ t.table.FeatureList.mapLookups(lookupMap)
505
+ t.table.LookupList.mapLookups(lookupMap)
506
+ usedIndices = lookupMap.s
507
+
508
+ # Remove unused lookups
509
+ t.table.LookupList.Lookup = [
510
+ l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices
511
+ ]
512
+
513
+ # Map back to indices.
514
+ lookupMap = NonhashableDict(t.table.LookupList.Lookup)
515
+ t.table.FeatureList.mapLookups(lookupMap)
516
+ t.table.LookupList.mapLookups(lookupMap)
517
+
518
+ t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup)
519
+
520
+ if GDEF and GDEF.table.Version >= 0x00010002:
521
+ markFilteringSetMap = NonhashableDict(
522
+ GDEF.table.MarkGlyphSetsDef.Coverage
523
+ )
524
+ t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
525
+
526
+ # TODO FeatureParams nameIDs
lib/python3.12/site-packages/fontTools/merge/options.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+
6
+ class Options(object):
7
+ class UnknownOptionError(Exception):
8
+ pass
9
+
10
+ def __init__(self, **kwargs):
11
+ self.verbose = False
12
+ self.timing = False
13
+ self.drop_tables = []
14
+ self.input_file = None
15
+ self.output_file = "merged.ttf"
16
+ self.import_file = None
17
+
18
+ self.set(**kwargs)
19
+
20
+ def set(self, **kwargs):
21
+ for k, v in kwargs.items():
22
+ if not hasattr(self, k):
23
+ raise self.UnknownOptionError("Unknown option '%s'" % k)
24
+ setattr(self, k, v)
25
+
26
+ def parse_opts(self, argv, ignore_unknown=[]):
27
+ ret = []
28
+ opts = {}
29
+ for a in argv:
30
+ orig_a = a
31
+ if not a.startswith("--"):
32
+ ret.append(a)
33
+ continue
34
+ a = a[2:]
35
+ i = a.find("=")
36
+ op = "="
37
+ if i == -1:
38
+ if a.startswith("no-"):
39
+ k = a[3:]
40
+ v = False
41
+ else:
42
+ k = a
43
+ v = True
44
+ else:
45
+ k = a[:i]
46
+ if k[-1] in "-+":
47
+ op = k[-1] + "=" # Ops is '-=' or '+=' now.
48
+ k = k[:-1]
49
+ v = a[i + 1 :]
50
+ ok = k
51
+ k = k.replace("-", "_")
52
+ if not hasattr(self, k):
53
+ if ignore_unknown is True or ok in ignore_unknown:
54
+ ret.append(orig_a)
55
+ continue
56
+ else:
57
+ raise self.UnknownOptionError("Unknown option '%s'" % a)
58
+
59
+ ov = getattr(self, k)
60
+ if isinstance(ov, bool):
61
+ v = bool(v)
62
+ elif isinstance(ov, int):
63
+ v = int(v)
64
+ elif isinstance(ov, list):
65
+ vv = v.split(",")
66
+ if vv == [""]:
67
+ vv = []
68
+ vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
69
+ if op == "=":
70
+ v = vv
71
+ elif op == "+=":
72
+ v = ov
73
+ v.extend(vv)
74
+ elif op == "-=":
75
+ v = ov
76
+ for x in vv:
77
+ if x in v:
78
+ v.remove(x)
79
+ else:
80
+ assert 0
81
+
82
+ opts[k] = v
83
+ self.set(**opts)
84
+
85
+ return ret
lib/python3.12/site-packages/fontTools/merge/tables.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+ from fontTools import ttLib, cffLib
6
+ from fontTools.misc.psCharStrings import T2WidthExtractor
7
+ from fontTools.ttLib.tables.DefaultTable import DefaultTable
8
+ from fontTools.merge.base import add_method, mergeObjects
9
+ from fontTools.merge.cmap import computeMegaCmap
10
+ from fontTools.merge.util import *
11
+ import logging
12
+
13
+
14
+ log = logging.getLogger("fontTools.merge")
15
+
16
+
17
+ ttLib.getTableClass("maxp").mergeMap = {
18
+ "*": max,
19
+ "tableTag": equal,
20
+ "tableVersion": equal,
21
+ "numGlyphs": sum,
22
+ "maxStorage": first,
23
+ "maxFunctionDefs": first,
24
+ "maxInstructionDefs": first,
25
+ # TODO When we correctly merge hinting data, update these values:
26
+ # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
27
+ }
28
+
29
+ headFlagsMergeBitMap = {
30
+ "size": 16,
31
+ "*": bitwise_or,
32
+ 1: bitwise_and, # Baseline at y = 0
33
+ 2: bitwise_and, # lsb at x = 0
34
+ 3: bitwise_and, # Force ppem to integer values. FIXME?
35
+ 5: bitwise_and, # Font is vertical
36
+ 6: lambda bit: 0, # Always set to zero
37
+ 11: bitwise_and, # Font data is 'lossless'
38
+ 13: bitwise_and, # Optimized for ClearType
39
+ 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
40
+ 15: lambda bit: 0, # Always set to zero
41
+ }
42
+
43
+ ttLib.getTableClass("head").mergeMap = {
44
+ "tableTag": equal,
45
+ "tableVersion": max,
46
+ "fontRevision": max,
47
+ "checkSumAdjustment": lambda lst: 0, # We need *something* here
48
+ "magicNumber": equal,
49
+ "flags": mergeBits(headFlagsMergeBitMap),
50
+ "unitsPerEm": equal,
51
+ "created": current_time,
52
+ "modified": current_time,
53
+ "xMin": min,
54
+ "yMin": min,
55
+ "xMax": max,
56
+ "yMax": max,
57
+ "macStyle": first,
58
+ "lowestRecPPEM": max,
59
+ "fontDirectionHint": lambda lst: 2,
60
+ "indexToLocFormat": first,
61
+ "glyphDataFormat": equal,
62
+ }
63
+
64
+ ttLib.getTableClass("hhea").mergeMap = {
65
+ "*": equal,
66
+ "tableTag": equal,
67
+ "tableVersion": max,
68
+ "ascent": max,
69
+ "descent": min,
70
+ "lineGap": max,
71
+ "advanceWidthMax": max,
72
+ "minLeftSideBearing": min,
73
+ "minRightSideBearing": min,
74
+ "xMaxExtent": max,
75
+ "caretSlopeRise": first,
76
+ "caretSlopeRun": first,
77
+ "caretOffset": first,
78
+ "numberOfHMetrics": recalculate,
79
+ }
80
+
81
+ ttLib.getTableClass("vhea").mergeMap = {
82
+ "*": equal,
83
+ "tableTag": equal,
84
+ "tableVersion": max,
85
+ "ascent": max,
86
+ "descent": min,
87
+ "lineGap": max,
88
+ "advanceHeightMax": max,
89
+ "minTopSideBearing": min,
90
+ "minBottomSideBearing": min,
91
+ "yMaxExtent": max,
92
+ "caretSlopeRise": first,
93
+ "caretSlopeRun": first,
94
+ "caretOffset": first,
95
+ "numberOfVMetrics": recalculate,
96
+ }
97
+
98
+ os2FsTypeMergeBitMap = {
99
+ "size": 16,
100
+ "*": lambda bit: 0,
101
+ 1: bitwise_or, # no embedding permitted
102
+ 2: bitwise_and, # allow previewing and printing documents
103
+ 3: bitwise_and, # allow editing documents
104
+ 8: bitwise_or, # no subsetting permitted
105
+ 9: bitwise_or, # no embedding of outlines permitted
106
+ }
107
+
108
+
109
+ def mergeOs2FsType(lst):
110
+ lst = list(lst)
111
+ if all(item == 0 for item in lst):
112
+ return 0
113
+
114
+ # Compute least restrictive logic for each fsType value
115
+ for i in range(len(lst)):
116
+ # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
117
+ if lst[i] & 0x000C:
118
+ lst[i] &= ~0x0002
119
+ # set bit 2 (allow previewing) if bit 3 is set (allow editing)
120
+ elif lst[i] & 0x0008:
121
+ lst[i] |= 0x0004
122
+ # set bits 2 and 3 if everything is allowed
123
+ elif lst[i] == 0:
124
+ lst[i] = 0x000C
125
+
126
+ fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
127
+ # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
128
+ if fsType & 0x0002:
129
+ fsType &= ~0x000C
130
+ return fsType
131
+
132
+
133
+ ttLib.getTableClass("OS/2").mergeMap = {
134
+ "*": first,
135
+ "tableTag": equal,
136
+ "version": max,
137
+ "xAvgCharWidth": first, # Will be recalculated at the end on the merged font
138
+ "fsType": mergeOs2FsType, # Will be overwritten
139
+ "panose": first, # FIXME: should really be the first Latin font
140
+ "ulUnicodeRange1": bitwise_or,
141
+ "ulUnicodeRange2": bitwise_or,
142
+ "ulUnicodeRange3": bitwise_or,
143
+ "ulUnicodeRange4": bitwise_or,
144
+ "fsFirstCharIndex": min,
145
+ "fsLastCharIndex": max,
146
+ "sTypoAscender": max,
147
+ "sTypoDescender": min,
148
+ "sTypoLineGap": max,
149
+ "usWinAscent": max,
150
+ "usWinDescent": max,
151
+ # Version 1
152
+ "ulCodePageRange1": onlyExisting(bitwise_or),
153
+ "ulCodePageRange2": onlyExisting(bitwise_or),
154
+ # Version 2, 3, 4
155
+ "sxHeight": onlyExisting(max),
156
+ "sCapHeight": onlyExisting(max),
157
+ "usDefaultChar": onlyExisting(first),
158
+ "usBreakChar": onlyExisting(first),
159
+ "usMaxContext": onlyExisting(max),
160
+ # version 5
161
+ "usLowerOpticalPointSize": onlyExisting(min),
162
+ "usUpperOpticalPointSize": onlyExisting(max),
163
+ }
164
+
165
+
166
+ @add_method(ttLib.getTableClass("OS/2"))
167
+ def merge(self, m, tables):
168
+ DefaultTable.merge(self, m, tables)
169
+ if self.version < 2:
170
+ # bits 8 and 9 are reserved and should be set to zero
171
+ self.fsType &= ~0x0300
172
+ if self.version >= 3:
173
+ # Only one of bits 1, 2, and 3 may be set. We already take
174
+ # care of bit 1 implications in mergeOs2FsType. So unset
175
+ # bit 2 if bit 3 is already set.
176
+ if self.fsType & 0x0008:
177
+ self.fsType &= ~0x0004
178
+ return self
179
+
180
+
181
+ ttLib.getTableClass("post").mergeMap = {
182
+ "*": first,
183
+ "tableTag": equal,
184
+ "formatType": max,
185
+ "isFixedPitch": min,
186
+ "minMemType42": max,
187
+ "maxMemType42": lambda lst: 0,
188
+ "minMemType1": max,
189
+ "maxMemType1": lambda lst: 0,
190
+ "mapping": onlyExisting(sumDicts),
191
+ "extraNames": lambda lst: [],
192
+ }
193
+
194
+ ttLib.getTableClass("vmtx").mergeMap = ttLib.getTableClass("hmtx").mergeMap = {
195
+ "tableTag": equal,
196
+ "metrics": sumDicts,
197
+ }
198
+
199
+ ttLib.getTableClass("name").mergeMap = {
200
+ "tableTag": equal,
201
+ "names": first, # FIXME? Does mixing name records make sense?
202
+ }
203
+
204
+ ttLib.getTableClass("loca").mergeMap = {
205
+ "*": recalculate,
206
+ "tableTag": equal,
207
+ }
208
+
209
+ ttLib.getTableClass("glyf").mergeMap = {
210
+ "tableTag": equal,
211
+ "glyphs": sumDicts,
212
+ "glyphOrder": sumLists,
213
+ "_reverseGlyphOrder": recalculate,
214
+ "axisTags": equal,
215
+ }
216
+
217
+
218
+ @add_method(ttLib.getTableClass("glyf"))
219
+ def merge(self, m, tables):
220
+ for i, table in enumerate(tables):
221
+ for g in table.glyphs.values():
222
+ if i:
223
+ # Drop hints for all but first font, since
224
+ # we don't map functions / CVT values.
225
+ g.removeHinting()
226
+ # Expand composite glyphs to load their
227
+ # composite glyph names.
228
+ if g.isComposite():
229
+ g.expand(table)
230
+ return DefaultTable.merge(self, m, tables)
231
+
232
+
233
+ ttLib.getTableClass("prep").mergeMap = lambda self, lst: first(lst)
234
+ ttLib.getTableClass("fpgm").mergeMap = lambda self, lst: first(lst)
235
+ ttLib.getTableClass("cvt ").mergeMap = lambda self, lst: first(lst)
236
+ ttLib.getTableClass("gasp").mergeMap = lambda self, lst: first(
237
+ lst
238
+ ) # FIXME? Appears irreconcilable
239
+
240
+
241
+ @add_method(ttLib.getTableClass("CFF "))
242
+ def merge(self, m, tables):
243
+ if any(hasattr(table.cff[0], "FDSelect") for table in tables):
244
+ raise NotImplementedError("Merging CID-keyed CFF tables is not supported yet")
245
+
246
+ for table in tables:
247
+ table.cff.desubroutinize()
248
+
249
+ newcff = tables[0]
250
+ newfont = newcff.cff[0]
251
+ private = newfont.Private
252
+ newDefaultWidthX, newNominalWidthX = private.defaultWidthX, private.nominalWidthX
253
+ storedNamesStrings = []
254
+ glyphOrderStrings = []
255
+ glyphOrder = set(newfont.getGlyphOrder())
256
+
257
+ for name in newfont.strings.strings:
258
+ if name not in glyphOrder:
259
+ storedNamesStrings.append(name)
260
+ else:
261
+ glyphOrderStrings.append(name)
262
+
263
+ chrset = list(newfont.charset)
264
+ newcs = newfont.CharStrings
265
+ log.debug("FONT 0 CharStrings: %d.", len(newcs))
266
+
267
+ for i, table in enumerate(tables[1:], start=1):
268
+ font = table.cff[0]
269
+ defaultWidthX, nominalWidthX = (
270
+ font.Private.defaultWidthX,
271
+ font.Private.nominalWidthX,
272
+ )
273
+ widthsDiffer = (
274
+ defaultWidthX != newDefaultWidthX or nominalWidthX != newNominalWidthX
275
+ )
276
+ font.Private = private
277
+ fontGlyphOrder = set(font.getGlyphOrder())
278
+ for name in font.strings.strings:
279
+ if name in fontGlyphOrder:
280
+ glyphOrderStrings.append(name)
281
+ cs = font.CharStrings
282
+ gs = table.cff.GlobalSubrs
283
+ log.debug("Font %d CharStrings: %d.", i, len(cs))
284
+ chrset.extend(font.charset)
285
+ if newcs.charStringsAreIndexed:
286
+ for i, name in enumerate(cs.charStrings, start=len(newcs)):
287
+ newcs.charStrings[name] = i
288
+ newcs.charStringsIndex.items.append(None)
289
+ for name in cs.charStrings:
290
+ if widthsDiffer:
291
+ c = cs[name]
292
+ defaultWidthXToken = object()
293
+ extractor = T2WidthExtractor([], [], nominalWidthX, defaultWidthXToken)
294
+ extractor.execute(c)
295
+ width = extractor.width
296
+ if width is not defaultWidthXToken:
297
+ # The following will be wrong if the width is added
298
+ # by a subroutine. Ouch!
299
+ c.program.pop(0)
300
+ else:
301
+ width = defaultWidthX
302
+ if width != newDefaultWidthX:
303
+ c.program.insert(0, width - newNominalWidthX)
304
+ newcs[name] = cs[name]
305
+
306
+ newfont.charset = chrset
307
+ newfont.numGlyphs = len(chrset)
308
+ newfont.strings.strings = glyphOrderStrings + storedNamesStrings
309
+
310
+ return newcff
311
+
312
+
313
+ @add_method(ttLib.getTableClass("cmap"))
314
+ def merge(self, m, tables):
315
+ if not hasattr(m, "cmap"):
316
+ computeMegaCmap(m, tables)
317
+ cmap = m.cmap
318
+
319
+ cmapBmpOnly = {uni: gid for uni, gid in cmap.items() if uni <= 0xFFFF}
320
+ self.tables = []
321
+ module = ttLib.getTableModule("cmap")
322
+ if len(cmapBmpOnly) != len(cmap):
323
+ # format-12 required.
324
+ cmapTable = module.cmap_classes[12](12)
325
+ cmapTable.platformID = 3
326
+ cmapTable.platEncID = 10
327
+ cmapTable.language = 0
328
+ cmapTable.cmap = cmap
329
+ self.tables.append(cmapTable)
330
+ # always create format-4
331
+ cmapTable = module.cmap_classes[4](4)
332
+ cmapTable.platformID = 3
333
+ cmapTable.platEncID = 1
334
+ cmapTable.language = 0
335
+ cmapTable.cmap = cmapBmpOnly
336
+ # ordered by platform then encoding
337
+ self.tables.insert(0, cmapTable)
338
+
339
+ uvsDict = m.uvsDict
340
+ if uvsDict:
341
+ # format-14
342
+ uvsTable = module.cmap_classes[14](14)
343
+ uvsTable.platformID = 0
344
+ uvsTable.platEncID = 5
345
+ uvsTable.language = 0
346
+ uvsTable.cmap = {}
347
+ uvsTable.uvsDict = uvsDict
348
+ # ordered by platform then encoding
349
+ self.tables.insert(0, uvsTable)
350
+ self.tableVersion = 0
351
+ self.numSubTables = len(self.tables)
352
+ return self
lib/python3.12/site-packages/fontTools/merge/unicode.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2021 Behdad Esfahbod. All Rights Reserved.
2
+
3
+
4
+ def is_Default_Ignorable(u):
5
+ # http://www.unicode.org/reports/tr44/#Default_Ignorable_Code_Point
6
+ #
7
+ # TODO Move me to unicodedata module and autogenerate.
8
+ #
9
+ # Unicode 14.0:
10
+ # $ grep '; Default_Ignorable_Code_Point ' DerivedCoreProperties.txt | sed 's/;.*#/#/'
11
+ # 00AD # Cf SOFT HYPHEN
12
+ # 034F # Mn COMBINING GRAPHEME JOINER
13
+ # 061C # Cf ARABIC LETTER MARK
14
+ # 115F..1160 # Lo [2] HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER
15
+ # 17B4..17B5 # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA
16
+ # 180B..180D # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE
17
+ # 180E # Cf MONGOLIAN VOWEL SEPARATOR
18
+ # 180F # Mn MONGOLIAN FREE VARIATION SELECTOR FOUR
19
+ # 200B..200F # Cf [5] ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK
20
+ # 202A..202E # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
21
+ # 2060..2064 # Cf [5] WORD JOINER..INVISIBLE PLUS
22
+ # 2065 # Cn <reserved-2065>
23
+ # 2066..206F # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES
24
+ # 3164 # Lo HANGUL FILLER
25
+ # FE00..FE0F # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16
26
+ # FEFF # Cf ZERO WIDTH NO-BREAK SPACE
27
+ # FFA0 # Lo HALFWIDTH HANGUL FILLER
28
+ # FFF0..FFF8 # Cn [9] <reserved-FFF0>..<reserved-FFF8>
29
+ # 1BCA0..1BCA3 # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP
30
+ # 1D173..1D17A # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE
31
+ # E0000 # Cn <reserved-E0000>
32
+ # E0001 # Cf LANGUAGE TAG
33
+ # E0002..E001F # Cn [30] <reserved-E0002>..<reserved-E001F>
34
+ # E0020..E007F # Cf [96] TAG SPACE..CANCEL TAG
35
+ # E0080..E00FF # Cn [128] <reserved-E0080>..<reserved-E00FF>
36
+ # E0100..E01EF # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256
37
+ # E01F0..E0FFF # Cn [3600] <reserved-E01F0>..<reserved-E0FFF>
38
+ return (
39
+ u == 0x00AD
40
+ or u == 0x034F # Cf SOFT HYPHEN
41
+ or u == 0x061C # Mn COMBINING GRAPHEME JOINER
42
+ or 0x115F <= u <= 0x1160 # Cf ARABIC LETTER MARK
43
+ or 0x17B4 # Lo [2] HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER
44
+ <= u
45
+ <= 0x17B5
46
+ or 0x180B # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA
47
+ <= u
48
+ <= 0x180D
49
+ or u # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE
50
+ == 0x180E
51
+ or u == 0x180F # Cf MONGOLIAN VOWEL SEPARATOR
52
+ or 0x200B <= u <= 0x200F # Mn MONGOLIAN FREE VARIATION SELECTOR FOUR
53
+ or 0x202A <= u <= 0x202E # Cf [5] ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK
54
+ or 0x2060 # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
55
+ <= u
56
+ <= 0x2064
57
+ or u == 0x2065 # Cf [5] WORD JOINER..INVISIBLE PLUS
58
+ or 0x2066 <= u <= 0x206F # Cn <reserved-2065>
59
+ or u == 0x3164 # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES
60
+ or 0xFE00 <= u <= 0xFE0F # Lo HANGUL FILLER
61
+ or u == 0xFEFF # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16
62
+ or u == 0xFFA0 # Cf ZERO WIDTH NO-BREAK SPACE
63
+ or 0xFFF0 <= u <= 0xFFF8 # Lo HALFWIDTH HANGUL FILLER
64
+ or 0x1BCA0 <= u <= 0x1BCA3 # Cn [9] <reserved-FFF0>..<reserved-FFF8>
65
+ or 0x1D173 # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP
66
+ <= u
67
+ <= 0x1D17A
68
+ or u == 0xE0000 # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE
69
+ or u == 0xE0001 # Cn <reserved-E0000>
70
+ or 0xE0002 <= u <= 0xE001F # Cf LANGUAGE TAG
71
+ or 0xE0020 <= u <= 0xE007F # Cn [30] <reserved-E0002>..<reserved-E001F>
72
+ or 0xE0080 <= u <= 0xE00FF # Cf [96] TAG SPACE..CANCEL TAG
73
+ or 0xE0100 <= u <= 0xE01EF # Cn [128] <reserved-E0080>..<reserved-E00FF>
74
+ or 0xE01F0 # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256
75
+ <= u
76
+ <= 0xE0FFF
77
+ or False # Cn [3600] <reserved-E01F0>..<reserved-E0FFF>
78
+ )
lib/python3.12/site-packages/fontTools/merge/util.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
+
5
+ from fontTools.misc.timeTools import timestampNow
6
+ from fontTools.ttLib.tables.DefaultTable import DefaultTable
7
+ from functools import reduce
8
+ import operator
9
+ import logging
10
+
11
+
12
+ log = logging.getLogger("fontTools.merge")
13
+
14
+
15
+ # General utility functions for merging values from different fonts
16
+
17
+
18
+ def equal(lst):
19
+ lst = list(lst)
20
+ t = iter(lst)
21
+ first = next(t)
22
+ assert all(item == first for item in t), "Expected all items to be equal: %s" % lst
23
+ return first
24
+
25
+
26
+ def first(lst):
27
+ return next(iter(lst))
28
+
29
+
30
+ def recalculate(lst):
31
+ return NotImplemented
32
+
33
+
34
+ def current_time(lst):
35
+ return timestampNow()
36
+
37
+
38
+ def bitwise_and(lst):
39
+ return reduce(operator.and_, lst)
40
+
41
+
42
+ def bitwise_or(lst):
43
+ return reduce(operator.or_, lst)
44
+
45
+
46
+ def avg_int(lst):
47
+ lst = list(lst)
48
+ return sum(lst) // len(lst)
49
+
50
+
51
+ def onlyExisting(func):
52
+ """Returns a filter func that when called with a list,
53
+ only calls func on the non-NotImplemented items of the list,
54
+ and only so if there's at least one item remaining.
55
+ Otherwise returns NotImplemented."""
56
+
57
+ def wrapper(lst):
58
+ items = [item for item in lst if item is not NotImplemented]
59
+ return func(items) if items else NotImplemented
60
+
61
+ return wrapper
62
+
63
+
64
+ def sumLists(lst):
65
+ l = []
66
+ for item in lst:
67
+ l.extend(item)
68
+ return l
69
+
70
+
71
+ def sumDicts(lst):
72
+ d = {}
73
+ for item in lst:
74
+ d.update(item)
75
+ return d
76
+
77
+
78
+ def mergeBits(bitmap):
79
+ def wrapper(lst):
80
+ lst = list(lst)
81
+ returnValue = 0
82
+ for bitNumber in range(bitmap["size"]):
83
+ try:
84
+ mergeLogic = bitmap[bitNumber]
85
+ except KeyError:
86
+ try:
87
+ mergeLogic = bitmap["*"]
88
+ except KeyError:
89
+ raise Exception("Don't know how to merge bit %s" % bitNumber)
90
+ shiftedBit = 1 << bitNumber
91
+ mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
92
+ returnValue |= mergedValue << bitNumber
93
+ return returnValue
94
+
95
+ return wrapper
96
+
97
+
98
+ class AttendanceRecordingIdentityDict(object):
99
+ """A dictionary-like object that records indices of items actually accessed
100
+ from a list."""
101
+
102
+ def __init__(self, lst):
103
+ self.l = lst
104
+ self.d = {id(v): i for i, v in enumerate(lst)}
105
+ self.s = set()
106
+
107
+ def __getitem__(self, v):
108
+ self.s.add(self.d[id(v)])
109
+ return v
110
+
111
+
112
+ class GregariousIdentityDict(object):
113
+ """A dictionary-like object that welcomes guests without reservations and
114
+ adds them to the end of the guest list."""
115
+
116
+ def __init__(self, lst):
117
+ self.l = lst
118
+ self.s = set(id(v) for v in lst)
119
+
120
+ def __getitem__(self, v):
121
+ if id(v) not in self.s:
122
+ self.s.add(id(v))
123
+ self.l.append(v)
124
+ return v
125
+
126
+
127
+ class NonhashableDict(object):
128
+ """A dictionary-like object mapping objects to values."""
129
+
130
+ def __init__(self, keys, values=None):
131
+ if values is None:
132
+ self.d = {id(v): i for i, v in enumerate(keys)}
133
+ else:
134
+ self.d = {id(k): v for k, v in zip(keys, values)}
135
+
136
+ def __getitem__(self, k):
137
+ return self.d[id(k)]
138
+
139
+ def __setitem__(self, k, v):
140
+ self.d[id(k)] = v
141
+
142
+ def __delitem__(self, k):
143
+ del self.d[id(k)]
lib/python3.12/site-packages/fontTools/misc/__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/misc/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (276 Bytes). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/arrayTools.cpython-312.pyc ADDED
Binary file (15 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/bezierTools.cpython-312.pyc ADDED
Binary file (56.9 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/classifyTools.cpython-312.pyc ADDED
Binary file (6.75 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/cliTools.cpython-312.pyc ADDED
Binary file (2.58 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/configTools.cpython-312.pyc ADDED
Binary file (15.9 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/cython.cpython-312.pyc ADDED
Binary file (1.17 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/dictTools.cpython-312.pyc ADDED
Binary file (3.93 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/eexec.cpython-312.pyc ADDED
Binary file (4.48 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/encodingTools.cpython-312.pyc ADDED
Binary file (1.85 kB). View file
 
lib/python3.12/site-packages/fontTools/misc/__pycache__/enumTools.cpython-312.pyc ADDED
Binary file (978 Bytes). View file