diff options
author | Alexander Smirnov <alex@ydb.tech> | 2024-11-29 13:32:04 +0000 |
---|---|---|
committer | Alexander Smirnov <alex@ydb.tech> | 2024-11-29 13:32:04 +0000 |
commit | 03fc6a84cd47911dfcc873669a69da6cbcc308d1 (patch) | |
tree | 650bf49d8971dbf6bafa9dd0488b8305a5579092 /contrib/python | |
parent | 991917c6cbd9969fcb3741d24d0fa0d2e0cfaba0 (diff) | |
parent | 56a560baa86b52c66ce622414579975930421950 (diff) | |
download | ydb-03fc6a84cd47911dfcc873669a69da6cbcc308d1.tar.gz |
Merge branch 'rightlib' into mergelibs-241129-1330
Diffstat (limited to 'contrib/python')
27 files changed, 572 insertions, 206 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index 419722ab40..8ad26257c5 100644 --- a/contrib/python/fonttools/.dist-info/METADATA +++ b/contrib/python/fonttools/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: fonttools -Version: 4.54.1 +Version: 4.55.0 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Text Processing :: Fonts Classifier: Topic :: Multimedia :: Graphics @@ -376,6 +377,24 @@ Have fun! Changelog ~~~~~~~~~ +4.55.0 (released 2024-11-14) +---------------------------- + + +- [cffLib.specializer] Adjust stack use calculation (#3689) +- [varLib] Lets not add mac names if the rest of name doesn't have them (#3688) +- [ttLib.reorderGlyphs] Update CFF table charstrings and charset (#3682) +- [cffLib.specializer] Add cmdline to specialize a CFF2 font (#3675, #3679) +- [CFF2] Lift uint16 VariationStore.length limitation (#3674) +- [subset] consider variation selectors subsetting cmap14 (#3672) +- [varLib.interpolatable] Support CFF2 fonts (#3670) +- Set isfinal to true in XML parser for proper resource cleanup (#3669) +- [removeOverlaps] Fix CFF CharString width (#3659) +- [glyf] Add optimizeSize option (#3657) +- Python 3.13 support (#3656) +- [TupleVariation] Optimize for loading speed, not size (#3650, #3653) + + 4.54.1 (released 2024-09-24) ---------------------------- diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 0ec8b4ecfd..502ca5cec6 100644 --- a/contrib/python/fonttools/fontTools/__init__.py +++ b/contrib/python/fonttools/fontTools/__init__.py @@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.54.1" +version = __version__ = "4.55.0" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py b/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py index 689412ce2b..f929cc9686 100644 --- a/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py +++ b/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py @@ -32,9 +32,10 @@ def _convertCFF2ToCFF(cff, otFont): cff.major = 1 - topDictData = TopDictIndex(None, isCFF2=True) + topDictData = TopDictIndex(None) for item in cff.topDictIndex: # Iterate over, such that all are decompiled + item.cff2GetGlyphOrder = None topDictData.append(item) cff.topDictIndex = topDictData topDict = topDictData[0] @@ -99,6 +100,21 @@ def _convertCFF2ToCFF(cff, otFont): if width != private.defaultWidthX: cs.program.insert(0, width - private.nominalWidthX) + mapping = { + name: ("cid" + str(n) if n else ".notdef") + for n, name in enumerate(topDict.charset) + } + topDict.charset = [ + "cid" + str(n) if n else ".notdef" for n in range(len(topDict.charset)) + ] + charStrings.charStrings = { + mapping[name]: v for name, v in charStrings.charStrings.items() + } + + # I'm not sure why the following is *not* necessary. And it breaks + # the output if I add it. + # topDict.ROS = ("Adobe", "Identity", 0) + def convertCFF2ToCFF(font, *, updatePostTable=True): cff = font["CFF2"].cff diff --git a/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py b/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py index 37463a5b9b..2555f0b242 100644 --- a/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py +++ b/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py @@ -81,7 +81,7 @@ def _convertCFFToCFF2(cff, otFont): thisLocalSubrs = ( localSubrs[fdIndex] - if fdIndex + if fdIndex is not None else ( getattr(topDict.Private, "Subrs", []) if hasattr(topDict, "Private") @@ -104,9 +104,10 @@ def _convertCFFToCFF2(cff, otFont): # just pop the first number since it may be a subroutine call. # Instead, when seeing that, we embed the subroutine and recurse. # If this ever happened, we later prune unused subroutines. - while program[1] in ["callsubr", "callgsubr"]: + while len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]: removeUnusedSubrs = True subrNumber = program.pop(0) + assert isinstance(subrNumber, int), subrNumber op = program.pop(0) bias = extractor.localBias if op == "callsubr" else extractor.globalBias subrNumber += bias @@ -114,6 +115,7 @@ def _convertCFFToCFF2(cff, otFont): subrProgram = subrSet[subrNumber].program program[:0] = subrProgram # Now pop the actual width + assert len(program) >= 1, program program.pop(0) if program and program[-1] == "endchar": diff --git a/contrib/python/fonttools/fontTools/cffLib/__init__.py b/contrib/python/fonttools/fontTools/cffLib/__init__.py index c192ec77af..d75e23b750 100644 --- a/contrib/python/fonttools/fontTools/cffLib/__init__.py +++ b/contrib/python/fonttools/fontTools/cffLib/__init__.py @@ -1,7 +1,7 @@ """cffLib: read/write Adobe CFF fonts -OpenType fonts with PostScript outlines contain a completely independent -font file, Adobe's *Compact Font Format*. So dealing with OpenType fonts +OpenType fonts with PostScript outlines embed a completely independent +font file in Adobe's *Compact Font Format*. So dealing with OpenType fonts requires also dealing with CFF. This module allows you to read and write fonts written in the CFF format. @@ -867,7 +867,11 @@ class VarStoreData(object): if self.file: # read data in from file. Assume position is correct. length = readCard16(self.file) - self.data = self.file.read(length) + # https://github.com/fonttools/fonttools/issues/3673 + if length == 65535: + self.data = self.file.read() + else: + self.data = self.file.read(length) globalState = {} reader = OTTableReader(self.data, globalState) self.otVarStore = ot.VarStore() @@ -1956,7 +1960,8 @@ class VarStoreCompiler(object): self.parent = parent if not varStoreData.data: varStoreData.compile() - data = [packCard16(len(varStoreData.data)), varStoreData.data] + varStoreDataLen = min(0xFFFF, len(varStoreData.data)) + data = [packCard16(varStoreDataLen), varStoreData.data] self.data = bytesjoin(data) def setPos(self, pos, endPos): @@ -2281,7 +2286,7 @@ class DictCompiler(object): # For PrivateDict BlueValues, the default font # values are absolute, not relative. # Must convert these back to relative coordinates - # befor writing to CFF2. + # before writing to CFF2. defaultValue = value[i][0] firstList[i] = defaultValue - prevVal prevVal = defaultValue diff --git a/contrib/python/fonttools/fontTools/cffLib/specializer.py b/contrib/python/fonttools/fontTools/cffLib/specializer.py index bb7f89e4ff..5fddcb67dd 100644 --- a/contrib/python/fonttools/fontTools/cffLib/specializer.py +++ b/contrib/python/fonttools/fontTools/cffLib/specializer.py @@ -80,8 +80,9 @@ def programToCommands(program, getNumRegions=None): numBlendArgs = numBlends * numSourceFonts + 1 # replace first blend op by a list of the blend ops. stack[-numBlendArgs:] = [stack[-numBlendArgs:]] - lenBlendStack += numBlends + len(stack) - 1 - lastBlendIndex = len(stack) + lenStack = len(stack) + lenBlendStack += numBlends + lenStack - 1 + lastBlendIndex = lenStack # if a blend op exists, this is or will be a CFF2 charstring. continue @@ -153,9 +154,10 @@ def commandsToProgram(commands): def _everyN(el, n): """Group the list el into groups of size n""" - if len(el) % n != 0: + l = len(el) + if l % n != 0: raise ValueError(el) - for i in range(0, len(el), n): + for i in range(0, l, n): yield el[i : i + n] @@ -218,9 +220,10 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod def hhcurveto(args): - if len(args) < 4 or len(args) % 4 > 1: + l = len(args) + if l < 4 or l % 4 > 1: raise ValueError(args) - if len(args) % 2 == 1: + if l % 2 == 1: yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0]) args = args[5:] for args in _everyN(args, 4): @@ -228,9 +231,10 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod def vvcurveto(args): - if len(args) < 4 or len(args) % 4 > 1: + l = len(args) + if l < 4 or l % 4 > 1: raise ValueError(args) - if len(args) % 2 == 1: + if l % 2 == 1: yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]]) args = args[5:] for args in _everyN(args, 4): @@ -238,11 +242,12 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod def hvcurveto(args): - if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: + l = len(args) + if l < 4 or l % 8 not in {0, 1, 4, 5}: raise ValueError(args) last_args = None - if len(args) % 2 == 1: - lastStraight = len(args) % 8 == 5 + if l % 2 == 1: + lastStraight = l % 8 == 5 args, last_args = args[:-5], args[-5:] it = _everyN(args, 4) try: @@ -262,11 +267,12 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod def vhcurveto(args): - if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: + l = len(args) + if l < 4 or l % 8 not in {0, 1, 4, 5}: raise ValueError(args) last_args = None - if len(args) % 2 == 1: - lastStraight = len(args) % 8 == 5 + if l % 2 == 1: + lastStraight = l % 8 == 5 args, last_args = args[:-5], args[-5:] it = _everyN(args, 4) try: @@ -286,7 +292,8 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod def rcurveline(args): - if len(args) < 8 or len(args) % 6 != 2: + l = len(args) + if l < 8 or l % 6 != 2: raise ValueError(args) args, last_args = args[:-2], args[-2:] for args in _everyN(args, 6): @@ -295,7 +302,8 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod def rlinecurve(args): - if len(args) < 8 or len(args) % 2 != 0: + l = len(args) + if l < 8 or l % 2 != 0: raise ValueError(args) args, last_args = args[:-6], args[-6:] for args in _everyN(args, 2): @@ -330,8 +338,9 @@ def _convertBlendOpToArgs(blendList): # comprehension. See calling context args = args[:-1] - numRegions = len(args) // numBlends - 1 - if not (numBlends * (numRegions + 1) == len(args)): + l = len(args) + numRegions = l // numBlends - 1 + if not (numBlends * (numRegions + 1) == l): raise ValueError(blendList) defaultArgs = [[arg] for arg in args[:numBlends]] @@ -368,7 +377,7 @@ def generalizeCommands(commands, ignoreErrors=False): raise func = getattr(mapping, op, None) - if not func: + if func is None: result.append((op, args)) continue try: @@ -446,9 +455,9 @@ def _convertToBlendCmds(args): i = 0 while i < num_args: arg = args[i] + i += 1 if not isinstance(arg, list): new_args.append(arg) - i += 1 stack_use += 1 else: prev_stack_use = stack_use @@ -458,21 +467,26 @@ def _convertToBlendCmds(args): # up to the max stack limit. num_sources = len(arg) - 1 blendlist = [arg] - i += 1 stack_use += 1 + num_sources # 1 for the num_blends arg - while (i < num_args) and isinstance(args[i], list): + + # if we are here, max stack is the CFF2 max stack. + # I use the CFF2 max stack limit here rather than + # the 'maxstack' chosen by the client, as the default + # maxstack may have been used unintentionally. For all + # the other operators, this just produces a little less + # optimization, but here it puts a hard (and low) limit + # on the number of source fonts that can be used. + # + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + while ( + (i < num_args) + and isinstance(args[i], list) + and stack_use + num_sources < maxStackLimit + ): blendlist.append(args[i]) i += 1 stack_use += num_sources - if stack_use + num_sources > maxStackLimit: - # if we are here, max stack is the CFF2 max stack. - # I use the CFF2 max stack limit here rather than - # the 'maxstack' chosen by the client, as the default - # maxstack may have been used unintentionally. For all - # the other operators, this just produces a little less - # optimization, but here it puts a hard (and low) limit - # on the number of source fonts that can be used. - break # blendList now contains as many single blend tuples as can be # combined without exceeding the CFF2 stack limit. num_blends = len(blendlist) @@ -504,6 +518,19 @@ def _addArgs(a, b): return a + b +def _argsStackUse(args): + stackLen = 0 + maxLen = 0 + for arg in args: + if type(arg) is list: + # Blended arg + maxLen = max(maxLen, stackLen + _argsStackUse(arg)) + stackLen += arg[-1] + else: + stackLen += 1 + return max(stackLen, maxLen) + + def specializeCommands( commands, ignoreErrors=False, @@ -697,6 +724,7 @@ def specializeCommands( continue # 5. Combine adjacent operators when possible, minding not to go over max stack size. + stackUse = _argsStackUse(commands[-1][1]) if commands else 0 for i in range(len(commands) - 1, 0, -1): op1, args1 = commands[i - 1] op2, args2 = commands[i] @@ -707,9 +735,10 @@ def specializeCommands( if op1 == op2: new_op = op1 else: - if op2 == "rrcurveto" and len(args2) == 6: + l = len(args2) + if op2 == "rrcurveto" and l == 6: new_op = "rlinecurve" - elif len(args2) == 2: + elif l == 2: new_op = "rcurveline" elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}: @@ -746,9 +775,14 @@ def specializeCommands( # Make sure the stack depth does not exceed (maxstack - 1), so # that subroutinizer can insert subroutine calls at any point. - if new_op and len(args1) + len(args2) < maxstack: + args1StackUse = _argsStackUse(args1) + combinedStackUse = max(args1StackUse, len(args1) + stackUse) + if new_op and combinedStackUse < maxstack: commands[i - 1] = (new_op, args1 + args2) del commands[i] + stackUse = combinedStackUse + else: + stackUse = args1StackUse # 6. Resolve any remaining made-up operators into real operators. for i in range(len(commands)): @@ -759,9 +793,11 @@ def specializeCommands( continue if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}: + l = len(args) + op0, op1 = op[:2] if (op0 == "r") ^ (op1 == "r"): - assert len(args) % 2 == 1 + assert l % 2 == 1 if op0 == "0": op0 = "h" if op1 == "0": @@ -772,9 +808,9 @@ def specializeCommands( op1 = _negateCategory(op0) assert {op0, op1} <= {"h", "v"}, (op0, op1) - if len(args) % 2: + if l % 2: if op0 != op1: # vhcurveto / hvcurveto - if (op0 == "h") ^ (len(args) % 8 == 1): + if (op0 == "h") ^ (l % 8 == 1): # Swap last two args order args = args[:-2] + args[-1:] + args[-2:-1] else: # hhcurveto / vvcurveto @@ -822,26 +858,67 @@ if __name__ == "__main__": default=None, help="Number of variable-font regions for blend opertaions.", ) + parser.add_argument( + "--font", + metavar="FONTFILE", + default=None, + help="CFF2 font to specialize.", + ) + parser.add_argument( + "-o", + "--output-file", + type=str, + help="Output font file name.", + ) options = parser.parse_args(sys.argv[1:]) - getNumRegions = ( - None - if options.num_regions is None - else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex]) - ) - - program = stringToProgram(options.program) - print("Program:") - print(programToString(program)) - commands = programToCommands(program, getNumRegions) - print("Commands:") - print(commands) - program2 = commandsToProgram(commands) - print("Program from commands:") - print(programToString(program2)) - assert program == program2 - print("Generalized program:") - print(programToString(generalizeProgram(program, getNumRegions))) - print("Specialized program:") - print(programToString(specializeProgram(program, getNumRegions))) + if options.program: + getNumRegions = ( + None + if options.num_regions is None + else lambda vsIndex: int( + options.num_regions[0 if vsIndex is None else vsIndex] + ) + ) + + program = stringToProgram(options.program) + print("Program:") + print(programToString(program)) + commands = programToCommands(program, getNumRegions) + print("Commands:") + print(commands) + program2 = commandsToProgram(commands) + print("Program from commands:") + print(programToString(program2)) + assert program == program2 + print("Generalized program:") + print(programToString(generalizeProgram(program, getNumRegions))) + print("Specialized program:") + print(programToString(specializeProgram(program, getNumRegions))) + + if options.font: + from fontTools.ttLib import TTFont + + font = TTFont(options.font) + cff2 = font["CFF2"].cff.topDictIndex[0] + charstrings = cff2.CharStrings + for glyphName in charstrings.keys(): + charstring = charstrings[glyphName] + charstring.decompile() + getNumRegions = charstring.private.getNumRegions + charstring.program = specializeProgram( + charstring.program, getNumRegions, maxstack=maxStackLimit + ) + + if options.output_file is None: + from fontTools.misc.cliTools import makeOutputFileName + + outfile = makeOutputFileName( + options.font, overWrite=True, suffix=".specialized" + ) + else: + outfile = options.output_file + if outfile: + print("Saving", outfile) + font.save(outfile) diff --git a/contrib/python/fonttools/fontTools/cffLib/transforms.py b/contrib/python/fonttools/fontTools/cffLib/transforms.py index 91f6999fe6..5b474a7cd8 100644 --- a/contrib/python/fonttools/fontTools/cffLib/transforms.py +++ b/contrib/python/fonttools/fontTools/cffLib/transforms.py @@ -457,6 +457,8 @@ def remove_unused_subroutines(cff): if subrs == font.GlobalSubrs: if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): local_subrs = font.Private.Subrs + elif hasattr(font, "FDArray") and len(font.FDArray) == 1: + local_subrs = font.FDArray[0].Private.Subrs else: local_subrs = None else: diff --git a/contrib/python/fonttools/fontTools/fontBuilder.py b/contrib/python/fonttools/fontTools/fontBuilder.py index 16b7ee167d..d4af38fba4 100644 --- a/contrib/python/fonttools/fontTools/fontBuilder.py +++ b/contrib/python/fonttools/fontTools/fontBuilder.py @@ -918,7 +918,15 @@ class FontBuilder(object): """ from .otlLib.builder import buildStatTable - buildStatTable(self.font, axes, locations, elidedFallbackName) + assert "name" in self.font, "name must to be set up first" + + buildStatTable( + self.font, + axes, + locations, + elidedFallbackName, + macNames=any(nr.platformID == 1 for nr in self.font["name"].names), + ) def buildCmapSubTable(cmapping, format, platformID, platEncID): @@ -938,6 +946,15 @@ def addFvar(font, axes, instances): fvar = newTable("fvar") nameTable = font["name"] + # if there are not currently any mac names don't add them here, that's inconsistent + # https://github.com/fonttools/fonttools/issues/683 + macNames = any(nr.platformID == 1 for nr in getattr(nameTable, "names", ())) + + # we have all the best ways to express mac names + platforms = ((3, 1, 0x409),) + if macNames: + platforms = ((1, 0, 0),) + platforms + for axis_def in axes: axis = Axis() @@ -963,7 +980,7 @@ def addFvar(font, axes, instances): if isinstance(name, str): name = dict(en=name) - axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font) + axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font, mac=macNames) fvar.axes.append(axis) for instance in instances: @@ -980,9 +997,11 @@ def addFvar(font, axes, instances): name = dict(en=name) inst = NamedInstance() - inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font) + inst.subfamilyNameID = nameTable.addMultilingualName( + name, ttFont=font, mac=macNames + ) if psname is not None: - inst.postscriptNameID = nameTable.addName(psname) + inst.postscriptNameID = nameTable.addName(psname, platforms=platforms) inst.coordinates = coordinates fvar.instances.append(inst) diff --git a/contrib/python/fonttools/fontTools/misc/testTools.py b/contrib/python/fonttools/fontTools/misc/testTools.py index be6116132d..7d78721485 100644 --- a/contrib/python/fonttools/fontTools/misc/testTools.py +++ b/contrib/python/fonttools/fontTools/misc/testTools.py @@ -38,7 +38,7 @@ def parseXML(xmlSnippet): % type(xmlSnippet).__name__ ) xml += b"</root>" - reader.parser.Parse(xml, 0) + reader.parser.Parse(xml, 1) return reader.root[2] diff --git a/contrib/python/fonttools/fontTools/subset/__init__.py b/contrib/python/fonttools/fontTools/subset/__init__.py index 99556d49e1..8458edc359 100644 --- a/contrib/python/fonttools/fontTools/subset/__init__.py +++ b/contrib/python/fonttools/fontTools/subset/__init__.py @@ -2873,7 +2873,9 @@ def closure_glyphs(self, s): # Close glyphs for table in tables: if table.format == 14: - for cmap in table.uvsDict.values(): + for varSelector, cmap in table.uvsDict.items(): + if varSelector not in s.unicodes_requested: + continue glyphs = {g for u, g in cmap if u in s.unicodes_requested} if None in glyphs: glyphs.remove(None) @@ -2928,6 +2930,7 @@ def subset_glyphs(self, s): if g in s.glyphs_requested or u in s.unicodes_requested ] for v, l in t.uvsDict.items() + if v in s.unicodes_requested } t.uvsDict = {v: l for v, l in t.uvsDict.items() if l} elif t.isUnicode(): @@ -3797,6 +3800,8 @@ def main(args=None): for t in font["cmap"].tables: if t.isUnicode(): unicodes.extend(t.cmap.keys()) + if t.format == 14: + unicodes.extend(t.uvsDict.keys()) assert "" not in glyphs log.info("Text: '%s'" % text) diff --git a/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py b/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py index 312b56b294..6dadf4aa52 100644 --- a/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py +++ b/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py @@ -87,7 +87,11 @@ def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: def _charString_from_SkPath( path: pathops.Path, charString: T2CharString ) -> T2CharString: - t2Pen = T2CharStringPen(width=charString.width, glyphSet=None) + if charString.width == charString.private.defaultWidthX: + width = None + else: + width = charString.width - charString.private.nominalWidthX + t2Pen = T2CharStringPen(width=width, glyphSet=None) path.draw(t2Pen) return t2Pen.getCharString(charString.private, charString.globalSubrs) diff --git a/contrib/python/fonttools/fontTools/ttLib/reorderGlyphs.py b/contrib/python/fonttools/fontTools/ttLib/reorderGlyphs.py index 3221261f16..fd950ba0e3 100644 --- a/contrib/python/fonttools/fontTools/ttLib/reorderGlyphs.py +++ b/contrib/python/fonttools/fontTools/ttLib/reorderGlyphs.py @@ -19,9 +19,7 @@ from typing import ( Deque, Iterable, List, - NamedTuple, Tuple, - Union, ) @@ -276,3 +274,11 @@ def reorderGlyphs(font: ttLib.TTFont, new_glyph_order: List[str]): reorder_key = (type(value), getattr(value, "Format", None)) for reorder in _REORDER_RULES.get(reorder_key, []): reorder.apply(font, value) + + if "CFF " in font: + cff_table = font["CFF "] + charstrings = cff_table.cff.topDictIndex[0].CharStrings.charStrings + cff_table.cff.topDictIndex[0].charset = new_glyph_order + cff_table.cff.topDictIndex[0].CharStrings.charStrings = { + k: charstrings.get(k) for k in new_glyph_order + } diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py index a98bca2e0e..bd6217e2ed 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py @@ -129,7 +129,9 @@ class TupleVariation(object): else: log.warning("bad delta format: %s" % ", ".join(sorted(attrs.keys()))) - def compile(self, axisTags, sharedCoordIndices={}, pointData=None): + def compile( + self, axisTags, sharedCoordIndices={}, pointData=None, *, optimizeSize=True + ): assert set(self.axes.keys()) <= set(axisTags), ( "Unknown axis tag found.", self.axes.keys(), @@ -161,7 +163,7 @@ class TupleVariation(object): flags |= PRIVATE_POINT_NUMBERS auxData.append(pointData) - auxData.append(self.compileDeltas()) + auxData.append(self.compileDeltas(optimizeSize=optimizeSize)) auxData = b"".join(auxData) tupleData.insert(0, struct.pack(">HH", len(auxData), flags)) @@ -322,7 +324,7 @@ class TupleVariation(object): ) return (result, pos) - def compileDeltas(self): + def compileDeltas(self, optimizeSize=True): deltaX = [] deltaY = [] if self.getCoordWidth() == 2: @@ -337,12 +339,12 @@ class TupleVariation(object): continue deltaX.append(c) bytearr = bytearray() - self.compileDeltaValues_(deltaX, bytearr) - self.compileDeltaValues_(deltaY, bytearr) + self.compileDeltaValues_(deltaX, bytearr, optimizeSize=optimizeSize) + self.compileDeltaValues_(deltaY, bytearr, optimizeSize=optimizeSize) return bytearr @staticmethod - def compileDeltaValues_(deltas, bytearr=None): + def compileDeltaValues_(deltas, bytearr=None, *, optimizeSize=True): """[value1, value2, value3, ...] --> bytearray Emits a sequence of runs. Each run starts with a @@ -360,18 +362,40 @@ class TupleVariation(object): """ # Explaining the format because the 'gvar' spec is hard to understand. if bytearr is None: bytearr = bytearray() + pos = 0 numDeltas = len(deltas) - while pos < numDeltas: - value = deltas[pos] - if value == 0: + + if optimizeSize: + while pos < numDeltas: + value = deltas[pos] + if value == 0: + pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) + elif -128 <= value <= 127: + pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) + elif -32768 <= value <= 32767: + pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) + else: + pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) + else: + minVal, maxVal = min(deltas), max(deltas) + if minVal == 0 == maxVal: pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) - elif -128 <= value <= 127: - pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) - elif -32768 <= value <= 32767: - pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) + elif -128 <= minVal <= maxVal <= 127: + pos = TupleVariation.encodeDeltaRunAsBytes_( + deltas, pos, bytearr, optimizeSize=False + ) + elif -32768 <= minVal <= maxVal <= 32767: + pos = TupleVariation.encodeDeltaRunAsWords_( + deltas, pos, bytearr, optimizeSize=False + ) else: - pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) + pos = TupleVariation.encodeDeltaRunAsLongs_( + deltas, pos, bytearr, optimizeSize=False + ) + + assert pos == numDeltas, (pos, numDeltas) + return bytearr @staticmethod @@ -389,7 +413,7 @@ class TupleVariation(object): return pos @staticmethod - def encodeDeltaRunAsBytes_(deltas, offset, bytearr): + def encodeDeltaRunAsBytes_(deltas, offset, bytearr, optimizeSize=True): pos = offset numDeltas = len(deltas) while pos < numDeltas: @@ -404,7 +428,12 @@ class TupleVariation(object): # (04 0F 0F 00 0F 0F) when storing the zero value # literally, but 7 bytes (01 0F 0F 80 01 0F 0F) # when starting a new run. - if value == 0 and pos + 1 < numDeltas and deltas[pos + 1] == 0: + if ( + optimizeSize + and value == 0 + and pos + 1 < numDeltas + and deltas[pos + 1] == 0 + ): break pos += 1 runLength = pos - offset @@ -419,7 +448,7 @@ class TupleVariation(object): return pos @staticmethod - def encodeDeltaRunAsWords_(deltas, offset, bytearr): + def encodeDeltaRunAsWords_(deltas, offset, bytearr, optimizeSize=True): pos = offset numDeltas = len(deltas) while pos < numDeltas: @@ -432,7 +461,7 @@ class TupleVariation(object): # storing the zero literally (42 66 66 00 00 77 77), # and equally 7 bytes when starting a new run # (40 66 66 80 40 77 77). - if value == 0: + if optimizeSize and value == 0: break # Within a word-encoded run of deltas, a single value @@ -442,7 +471,8 @@ class TupleVariation(object): # the value literally (42 66 66 00 02 77 77), but 8 bytes # when starting a new run (40 66 66 00 02 40 77 77). if ( - (-128 <= value <= 127) + optimizeSize + and (-128 <= value <= 127) and pos + 1 < numDeltas and (-128 <= deltas[pos + 1] <= 127) ): @@ -470,12 +500,12 @@ class TupleVariation(object): return pos @staticmethod - def encodeDeltaRunAsLongs_(deltas, offset, bytearr): + def encodeDeltaRunAsLongs_(deltas, offset, bytearr, optimizeSize=True): pos = offset numDeltas = len(deltas) while pos < numDeltas: value = deltas[pos] - if -32768 <= value <= 32767: + if optimizeSize and -32768 <= value <= 32767: break pos += 1 runLength = pos - offset @@ -677,7 +707,13 @@ def compileSharedTuples( def compileTupleVariationStore( - variations, pointCount, axisTags, sharedTupleIndices, useSharedPoints=True + variations, + pointCount, + axisTags, + sharedTupleIndices, + useSharedPoints=True, + *, + optimizeSize=True, ): # pointCount is actually unused. Keeping for API compat. del pointCount @@ -733,7 +769,9 @@ def compileTupleVariationStore( ] for v, p in zip(variations, pointDatas): - thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p) + thisTuple, thisData = v.compile( + axisTags, sharedTupleIndices, pointData=p, optimizeSize=optimizeSize + ) tuples.append(thisTuple) data.append(thisData) diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py index fa11cf8f47..bc7d4bf1e4 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py @@ -713,7 +713,9 @@ class Glyph(object): else: self.decompileCoordinates(data) - def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): + def compile( + self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=None + ): if hasattr(self, "data"): if recalcBBoxes: # must unpack glyph in order to recalculate bounding box @@ -730,7 +732,9 @@ class Glyph(object): if self.isComposite(): data = data + self.compileComponents(glyfTable) else: - data = data + self.compileCoordinates() + if optimizeSize is None: + optimizeSize = getattr(glyfTable, "optimizeSize", True) + data = data + self.compileCoordinates(optimizeSize=optimizeSize) return data def toXML(self, writer, ttFont): @@ -976,7 +980,7 @@ class Glyph(object): data = data + struct.pack(">h", len(instructions)) + instructions return data - def compileCoordinates(self): + def compileCoordinates(self, *, optimizeSize=True): assert len(self.coordinates) == len(self.flags) data = [] endPtsOfContours = array.array("H", self.endPtsOfContours) @@ -991,9 +995,12 @@ class Glyph(object): deltas.toInt() deltas.absoluteToRelative() - # TODO(behdad): Add a configuration option for this? - deltas = self.compileDeltasGreedy(self.flags, deltas) - # deltas = self.compileDeltasOptimal(self.flags, deltas) + if optimizeSize: + # TODO(behdad): Add a configuration option for this? + deltas = self.compileDeltasGreedy(self.flags, deltas) + # deltas = self.compileDeltasOptimal(self.flags, deltas) + else: + deltas = self.compileDeltasForSpeed(self.flags, deltas) data.extend(deltas) return b"".join(data) @@ -1110,6 +1117,63 @@ class Glyph(object): return (compressedFlags, compressedXs, compressedYs) + def compileDeltasForSpeed(self, flags, deltas): + # uses widest representation needed, for all deltas. + compressedFlags = bytearray() + compressedXs = bytearray() + compressedYs = bytearray() + + # Compute the necessary width for each axis + xs = [d[0] for d in deltas] + ys = [d[1] for d in deltas] + minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys) + xZero = minX == 0 and maxX == 0 + yZero = minY == 0 and maxY == 0 + xShort = -255 <= minX <= maxX <= 255 + yShort = -255 <= minY <= maxY <= 255 + + lastflag = None + repeat = 0 + for flag, (x, y) in zip(flags, deltas): + # Oh, the horrors of TrueType + # do x + if xZero: + flag = flag | flagXsame + elif xShort: + flag = flag | flagXShort + if x > 0: + flag = flag | flagXsame + else: + x = -x + compressedXs.append(x) + else: + compressedXs.extend(struct.pack(">h", x)) + # do y + if yZero: + flag = flag | flagYsame + elif yShort: + flag = flag | flagYShort + if y > 0: + flag = flag | flagYsame + else: + y = -y + compressedYs.append(y) + else: + compressedYs.extend(struct.pack(">h", y)) + # handle repeating flags + if flag == lastflag and repeat != 255: + repeat = repeat + 1 + if repeat == 1: + compressedFlags.append(flag) + else: + compressedFlags[-2] = flag | flagRepeat + compressedFlags[-1] = repeat + else: + repeat = 0 + compressedFlags.append(flag) + lastflag = flag + return (compressedFlags, compressedXs, compressedYs) + def recalcBounds(self, glyfTable, *, boundsDone=None): """Recalculates the bounds of the glyph. @@ -1404,6 +1468,7 @@ class Glyph(object): pen.addComponent(glyphName, transform) return + self.expand(glyfTable) coordinates, endPts, flags = self.getCoordinates(glyfTable) if offset: coordinates = coordinates.copy() diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py index 044f65f716..cdc9ef8e76 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_v_a_r.py @@ -85,6 +85,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable): def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): result = [] glyf = ttFont["glyf"] + optimizeSize = getattr(self, "optimizeSize", True) for glyphName in ttFont.getGlyphOrder(): variations = self.variations.get(glyphName, []) if not variations: @@ -93,7 +94,11 @@ class table__g_v_a_r(DefaultTable.DefaultTable): pointCountUnused = 0 # pointCount is actually unused by compileGlyph result.append( compileGlyph_( - variations, pointCountUnused, axisTags, sharedCoordIndices + variations, + pointCountUnused, + axisTags, + sharedCoordIndices, + optimizeSize=optimizeSize, ) ) return result @@ -248,9 +253,11 @@ class table__g_v_a_r(DefaultTable.DefaultTable): return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS -def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices): +def compileGlyph_( + variations, pointCount, axisTags, sharedCoordIndices, *, optimizeSize=True +): tupleVariationCount, tuples, data = tv.compileTupleVariationStore( - variations, pointCount, axisTags, sharedCoordIndices + variations, pointCount, axisTags, sharedCoordIndices, optimizeSize=optimizeSize ) if tupleVariationCount == 0: return b"" diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py index 656836bd3c..8068540301 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py @@ -2016,6 +2016,7 @@ converterMapping = { # type class "int8": Int8, "int16": Short, + "int32": Long, "uint8": UInt8, "uint16": UShort, "uint24": UInt24, diff --git a/contrib/python/fonttools/fontTools/ufoLib/__init__.py b/contrib/python/fonttools/fontTools/ufoLib/__init__.py index aa57beede2..42c06734ea 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/__init__.py +++ b/contrib/python/fonttools/fontTools/ufoLib/__init__.py @@ -1,35 +1,35 @@ """ A library for importing .ufo files and their descendants. -Refer to http://unifiedfontobject.com for the UFO specification. +Refer to http://unifiedfontobject.org for the UFO specification. -The UFOReader and UFOWriter classes support versions 1, 2 and 3 -of the specification. +The main interfaces are the :class:`.UFOReader` and :class:`.UFOWriter` +classes, which support versions 1, 2, and 3 of the UFO specification. -Sets that list the font info attribute names for the fontinfo.plist -formats are available for external use. These are: +Set variables are available for external use that list the font +info attribute names for the `fontinfo.plist` formats. These are: -- fontInfoAttributesVersion1 -- fontInfoAttributesVersion2 -- fontInfoAttributesVersion3 +- :obj:`.fontInfoAttributesVersion1` +- :obj:`.fontInfoAttributesVersion2` +- :obj:`.fontInfoAttributesVersion3` -A set listing the fontinfo.plist attributes that were deprecated +A set listing the `fontinfo.plist` attributes that were deprecated in version 2 is available for external use: -- deprecatedFontInfoAttributesVersion2 +- :obj:`.deprecatedFontInfoAttributesVersion2` -Functions that do basic validation on values for fontinfo.plist +Functions that do basic validation on values for `fontinfo.plist` are available for external use. These are -- validateFontInfoVersion2ValueForAttribute -- validateFontInfoVersion3ValueForAttribute +- :func:`.validateFontInfoVersion2ValueForAttribute` +- :func:`.validateFontInfoVersion3ValueForAttribute` Value conversion functions are available for converting -fontinfo.plist values between the possible format versions. +`fontinfo.plist` values between the possible format versions. -- convertFontInfoValueForAttributeFromVersion1ToVersion2 -- convertFontInfoValueForAttributeFromVersion2ToVersion1 -- convertFontInfoValueForAttributeFromVersion2ToVersion3 -- convertFontInfoValueForAttributeFromVersion3ToVersion2 +- :func:`.convertFontInfoValueForAttributeFromVersion1ToVersion2` +- :func:`.convertFontInfoValueForAttributeFromVersion2ToVersion1` +- :func:`.convertFontInfoValueForAttributeFromVersion2ToVersion3` +- :func:`.convertFontInfoValueForAttributeFromVersion3ToVersion2` """ import os @@ -201,8 +201,12 @@ class _UFOBaseIO: class UFOReader(_UFOBaseIO): - """ - Read the various components of the .ufo. + """Read the various components of a .ufo. + + Attributes: + path: An `os.PathLike` object pointing to the .ufo. + validate: A boolean indicating if the data read should be + validated. Defaults to `True`. By default read data is validated. Set ``validate`` to ``False`` to not validate the data. @@ -884,20 +888,27 @@ class UFOReader(_UFOBaseIO): class UFOWriter(UFOReader): - """ - Write the various components of the .ufo. + """Write the various components of a .ufo. + + Attributes: + path: An `os.PathLike` object pointing to the .ufo. + formatVersion: the UFO format version as a tuple of integers (major, minor), + or as a single integer for the major digit only (minor is implied to be 0). + By default, the latest formatVersion will be used; currently it is 3.0, + which is equivalent to formatVersion=(3, 0). + fileCreator: The creator of the .ufo file. Defaults to + `com.github.fonttools.ufoLib`. + structure: The internal structure of the .ufo file: either `ZIP` or `PACKAGE`. + validate: A boolean indicating if the data read should be validated. Defaults + to `True`. By default, the written data will be validated before writing. Set ``validate`` to ``False`` if you do not want to validate the data. Validation can also be overriden - on a per method level if desired. - - The ``formatVersion`` argument allows to specify the UFO format version as a tuple - of integers (major, minor), or as a single integer for the major digit only (minor - is implied as 0). By default the latest formatVersion will be used; currently it's - 3.0, which is equivalent to formatVersion=(3, 0). + on a per-method level if desired. - An UnsupportedUFOFormat exception is raised if the requested UFO formatVersion is - not supported. + Raises: + UnsupportedUFOFormat: An exception indicating that the requested UFO + formatVersion is not supported. """ def __init__( diff --git a/contrib/python/fonttools/fontTools/ufoLib/glifLib.py b/contrib/python/fonttools/fontTools/ufoLib/glifLib.py index 62e87db0df..abbda49146 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/glifLib.py +++ b/contrib/python/fonttools/fontTools/ufoLib/glifLib.py @@ -1191,8 +1191,12 @@ def _readGlyphFromTreeFormat1( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": + v = element.get("hex") + if v is None: + raise GlifLibError( + "A unicode element is missing its required hex attribute." + ) try: - v = element.get("hex") v = int(v, 16) if v not in unicodes: unicodes.append(v) @@ -1254,8 +1258,12 @@ def _readGlyphFromTreeFormat2( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": + v = element.get("hex") + if v is None: + raise GlifLibError( + "A unicode element is missing its required hex attribute." + ) try: - v = element.get("hex") v = int(v, 16) if v not in unicodes: unicodes.append(v) @@ -1757,7 +1765,7 @@ class _BaseParser: parser = ParserCreate() parser.StartElementHandler = self.startElementHandler parser.EndElementHandler = self.endElementHandler - parser.Parse(text) + parser.Parse(text, 1) def startElementHandler(self, name, attrs): self._elementStack.append(name) diff --git a/contrib/python/fonttools/fontTools/ufoLib/plistlib.py b/contrib/python/fonttools/fontTools/ufoLib/plistlib.py index 38bb266b21..0242e9d26f 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/plistlib.py +++ b/contrib/python/fonttools/fontTools/ufoLib/plistlib.py @@ -1,5 +1,5 @@ """DEPRECATED - This module is kept here only as a backward compatibility shim -for the old ufoLib.plistlib module, which was moved to fontTools.misc.plistlib. +for the old `ufoLib.plistlib` module, which was moved to :class:`fontTools.misc.plistlib`. Please use the latter instead. """ diff --git a/contrib/python/fonttools/fontTools/ufoLib/pointPen.py b/contrib/python/fonttools/fontTools/ufoLib/pointPen.py index baef9a583e..4a8126cd64 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/pointPen.py +++ b/contrib/python/fonttools/fontTools/ufoLib/pointPen.py @@ -1,5 +1,5 @@ """DEPRECATED - This module is kept here only as a backward compatibility shim -for the old ufoLib.pointPen module, which was moved to fontTools.pens.pointPen. +for the old `ufoLib.pointPen` module, which was moved to :class:`fontTools.pens.pointPen`. Please use the latter instead. """ diff --git a/contrib/python/fonttools/fontTools/varLib/__init__.py b/contrib/python/fonttools/fontTools/varLib/__init__.py index 36b1851cba..43a16da5a9 100644 --- a/contrib/python/fonttools/fontTools/varLib/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/__init__.py @@ -85,6 +85,15 @@ def _add_fvar(font, axes, instances: List[InstanceDescriptor]): fvar = newTable("fvar") nameTable = font["name"] + # if there are not currently any mac names don't add them here, that's inconsistent + # https://github.com/fonttools/fonttools/issues/683 + macNames = any(nr.platformID == 1 for nr in getattr(nameTable, "names", ())) + + # we have all the best ways to express mac names + platforms = ((3, 1, 0x409),) + if macNames: + platforms = ((1, 0, 0),) + platforms + for a in axes.values(): axis = Axis() axis.axisTag = Tag(a.tag) @@ -95,7 +104,7 @@ def _add_fvar(font, axes, instances: List[InstanceDescriptor]): a.maximum, ) axis.axisNameID = nameTable.addMultilingualName( - a.labelNames, font, minNameID=256 + a.labelNames, font, minNameID=256, mac=macNames ) axis.flags = int(a.hidden) fvar.axes.append(axis) @@ -121,10 +130,12 @@ def _add_fvar(font, axes, instances: List[InstanceDescriptor]): psname = instance.postScriptFontName inst = NamedInstance() - inst.subfamilyNameID = nameTable.addMultilingualName(localisedStyleName) + inst.subfamilyNameID = nameTable.addMultilingualName( + localisedStyleName, mac=macNames + ) if psname is not None: psname = tostr(psname) - inst.postscriptNameID = nameTable.addName(psname) + inst.postscriptNameID = nameTable.addName(psname, platforms=platforms) inst.coordinates = { axes[k].tag: axes[k].map_backward(v) for k, v in coordinates.items() } diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py index 60dbfeb7fc..c5d7ecf525 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py @@ -693,7 +693,7 @@ def main(args=None): from fontTools import configLogger - configLogger(level=("INFO" if args.verbose else "ERROR")) + configLogger(level=("INFO" if args.verbose else "WARNING")) if args.debug: configLogger(level="DEBUG") @@ -750,41 +750,43 @@ def main(args=None): for k, vv in axis_triples.items() } - elif args.inputs[0].endswith(".ttf"): + elif args.inputs[0].endswith(".ttf") or args.inputs[0].endswith(".otf"): from fontTools.ttLib import TTFont + # Is variable font? + font = TTFont(args.inputs[0]) upem = font["head"].unitsPerEm - if "gvar" in font: - # Is variable font - - fvar = font["fvar"] - axisMapping = {} - for axis in fvar.axes: - axisMapping[axis.axisTag] = { - -1: axis.minValue, - 0: axis.defaultValue, - 1: axis.maxValue, - } - normalized = False - if "avar" in font: - avar = font["avar"] - if getattr(avar.table, "VarStore", None): - axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping} - normalized = True - else: - for axisTag, segments in avar.segments.items(): - fvarMapping = axisMapping[axisTag].copy() - for location, value in segments.items(): - axisMapping[axisTag][value] = piecewiseLinearMap( - location, fvarMapping - ) + fvar = font["fvar"] + axisMapping = {} + for axis in fvar.axes: + axisMapping[axis.axisTag] = { + -1: axis.minValue, + 0: axis.defaultValue, + 1: axis.maxValue, + } + normalized = False + if "avar" in font: + avar = font["avar"] + if getattr(avar.table, "VarStore", None): + axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping} + normalized = True + else: + for axisTag, segments in avar.segments.items(): + fvarMapping = axisMapping[axisTag].copy() + for location, value in segments.items(): + axisMapping[axisTag][value] = piecewiseLinearMap( + location, fvarMapping + ) + + # Gather all glyphs at their "master" locations + ttGlyphSets = {} + glyphsets = defaultdict(dict) + + if "gvar" in font: gvar = font["gvar"] glyf = font["glyf"] - # Gather all glyphs at their "master" locations - ttGlyphSets = {} - glyphsets = defaultdict(dict) if glyphs is None: glyphs = sorted(gvar.variations.keys()) @@ -806,32 +808,87 @@ def main(args=None): glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf ) - names = ["''"] - fonts = [font.getGlyphSet()] - locations = [{}] - axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())} - for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)): - name = ( - "'" - + " ".join( - "%s=%s" - % ( - k, - floatToFixedToStr( - piecewiseLinearMap(v, axisMapping[k]), 14 - ), - ) - for k, v in locTuple + elif "CFF2" in font: + fvarAxes = font["fvar"].axes + cff2 = font["CFF2"].cff.topDictIndex[0] + charstrings = cff2.CharStrings + + if glyphs is None: + glyphs = sorted(charstrings.keys()) + for glyphname in glyphs: + cs = charstrings[glyphname] + private = cs.private + + # Extract vsindex for the glyph + vsindices = {getattr(private, "vsindex", 0)} + vsindex = getattr(private, "vsindex", 0) + last_op = 0 + # The spec says vsindex can only appear once and must be the first + # operator in the charstring, but we support multiple. + # https://github.com/harfbuzz/boring-expansion-spec/issues/158 + for op in enumerate(cs.program): + if op == "blend": + vsindices.add(vsindex) + elif op == "vsindex": + assert isinstance(last_op, int) + vsindex = last_op + last_op = op + + if not hasattr(private, "vstore"): + continue + + varStore = private.vstore.otVarStore + for vsindex in vsindices: + varData = varStore.VarData[vsindex] + for regionIndex in varData.VarRegionIndex: + region = varStore.VarRegionList.Region[regionIndex] + + locDict = {} + loc = [] + for axisIndex, axis in enumerate(region.VarRegionAxis): + tag = fvarAxes[axisIndex].axisTag + val = axis.PeakCoord + locDict[tag] = val + loc.append((tag, val)) + + locTuple = tuple(loc) + if locTuple not in ttGlyphSets: + ttGlyphSets[locTuple] = font.getGlyphSet( + location=locDict, + normalized=True, + recalcBounds=False, + ) + + glyphset = glyphsets[locTuple] + glyphset[glyphname] = ttGlyphSets[locTuple][glyphname] + + names = ["''"] + fonts = [font.getGlyphSet()] + locations = [{}] + axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())} + for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)): + name = ( + "'" + + " ".join( + "%s=%s" + % ( + k, + floatToFixedToStr( + piecewiseLinearMap(v, axisMapping[k]), 14 + ), ) - + "'" + for k, v in locTuple ) - if normalized: - name += " (normalized)" - names.append(name) - fonts.append(glyphsets[locTuple]) - locations.append(dict(locTuple)) - args.ignore_missing = True - args.inputs = [] + + "'" + ) + if normalized: + name += " (normalized)" + names.append(name) + fonts.append(glyphsets[locTuple]) + locations.append(dict(locTuple)) + + args.ignore_missing = True + args.inputs = [] if not locations: locations = [{} for _ in fonts] @@ -854,6 +911,10 @@ def main(args=None): names.append(basename(filename).rsplit(".", 1)[0]) + if len(fonts) < 2: + log.warning("Font file does not seem to be variable. Nothing to check.") + return + glyphsets = [] for font in fonts: if hasattr(font, "getGlyphSet"): diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py b/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py index 9edb1afcb5..3688529798 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py @@ -55,10 +55,10 @@ def test_contour_order(glyph0, glyph1): m1GreenReversed = [(-m[0],) + m[1:] for m in m1Green] ( matching_control_reversed, - matching_cost_control_reversed, - identity_cost_control_reversed, - ) = matching_for_vectors(m0Control, m1ControlReversed) - done = matching_cost_control_reversed == identity_cost_control_reversed + matching_cost_green_reversed, + identity_cost_green_reversed, + ) = matching_for_vectors(m0Green, m1GreenReversed) + done = matching_cost_green_reversed == identity_cost_green_reversed if not done: # Otherwise, use the worst of the two matchings. diff --git a/contrib/python/fonttools/fontTools/varLib/multiVarStore.py b/contrib/python/fonttools/fontTools/varLib/multiVarStore.py index f24a6e6f75..2d074a353d 100644 --- a/contrib/python/fonttools/fontTools/varLib/multiVarStore.py +++ b/contrib/python/fonttools/fontTools/varLib/multiVarStore.py @@ -50,7 +50,7 @@ class OnlineMultiVarStoreBuilder(object): self._cache = None self._data = None - def finish(self, optimize=True): + def finish(self): self._regionList.RegionCount = len(self._regionList.Region) self._store.MultiVarDataCount = len(self._store.MultiVarData) return self._store diff --git a/contrib/python/fonttools/fontTools/varLib/mutator.py b/contrib/python/fonttools/fontTools/varLib/mutator.py index 80e46bb244..f9f9379026 100644 --- a/contrib/python/fonttools/fontTools/varLib/mutator.py +++ b/contrib/python/fonttools/fontTools/varLib/mutator.py @@ -408,7 +408,9 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): if set(excludedUnicodeLangIDs) == set(range(len((varfont["ltag"].tags)))): del varfont["ltag"] varfont["name"].names[:] = [ - n for n in varfont["name"].names if n.nameID not in exclude + n + for n in varfont["name"].names + if n.nameID < 256 or n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: diff --git a/contrib/python/fonttools/fontTools/varLib/stat.py b/contrib/python/fonttools/fontTools/varLib/stat.py index 46c9498dc7..eacbf580ae 100644 --- a/contrib/python/fonttools/fontTools/varLib/stat.py +++ b/contrib/python/fonttools/fontTools/varLib/stat.py @@ -39,11 +39,18 @@ def buildVFStatTable(ttFont: TTFont, doc: DesignSpaceDocument, vfName: str) -> N region = getVFUserRegion(doc, vf) + # if there are not currently any mac names don't add them here, that's inconsistent + # https://github.com/fonttools/fonttools/issues/683 + macNames = any( + nr.platformID == 1 for nr in getattr(ttFont.get("name"), "names", ()) + ) + return fontTools.otlLib.builder.buildStatTable( ttFont, getStatAxes(doc, region), getStatLocations(doc, region), doc.elidedFallbackName if doc.elidedFallbackName is not None else 2, + macNames=macNames, ) diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index 402baf8a09..874cdd1b3b 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.54.1) +VERSION(4.55.0) LICENSE(MIT) |