Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/fontTools/__pycache__/__main__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/__pycache__/afmLib.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/__pycache__/fontBuilder.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/__pycache__/help.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/__pycache__/tfmLib.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/__pycache__/ttx.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/__pycache__/unicode.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/CFF2ToCFF.py +258 -0
- lib/python3.12/site-packages/fontTools/cffLib/CFFToCFF2.py +305 -0
- lib/python3.12/site-packages/fontTools/cffLib/__init__.py +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFFToCFF2.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/__pycache__/specializer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/__pycache__/transforms.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/__pycache__/width.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/cffLib/specializer.py +927 -0
- lib/python3.12/site-packages/fontTools/cffLib/transforms.py +495 -0
- lib/python3.12/site-packages/fontTools/cffLib/width.py +210 -0
- lib/python3.12/site-packages/fontTools/config/__init__.py +90 -0
- lib/python3.12/site-packages/fontTools/config/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__init__.py +248 -0
- lib/python3.12/site-packages/fontTools/merge/__main__.py +6 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/__main__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/base.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/cmap.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/layout.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/options.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/tables.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/unicode.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/__pycache__/util.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/merge/base.py +81 -0
- lib/python3.12/site-packages/fontTools/merge/cmap.py +173 -0
- lib/python3.12/site-packages/fontTools/merge/layout.py +526 -0
- lib/python3.12/site-packages/fontTools/merge/options.py +85 -0
- lib/python3.12/site-packages/fontTools/merge/tables.py +352 -0
- lib/python3.12/site-packages/fontTools/merge/unicode.py +78 -0
- lib/python3.12/site-packages/fontTools/merge/util.py +143 -0
- lib/python3.12/site-packages/fontTools/misc/__init__.py +1 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/arrayTools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/bezierTools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/classifyTools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/cliTools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/configTools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/cython.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/dictTools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/eexec.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/misc/__pycache__/encodingTools.cpython-312.pyc +0 -0
- 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
|
|
|