diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2024-06-08 08:51:43 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2024-06-08 09:02:27 +0300 |
commit | c3763910c6f878b17102c94e0b62cab57c9954b8 (patch) | |
tree | 19de35706beeb1adc158f13ca87110e110f1a753 /contrib | |
parent | 63965219325c32d9b72b466b89fc613295746336 (diff) | |
download | ydb-c3763910c6f878b17102c94e0b62cab57c9954b8.tar.gz |
Intermediate changes
Diffstat (limited to 'contrib')
60 files changed, 3199 insertions, 1449 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index 60b6e6df80..6a00aa9e47 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.51.0 +Version: 4.52.1 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -375,6 +375,33 @@ Have fun! Changelog ~~~~~~~~~ +4.52.1 (released 2024-05-24) +---------------------------- + +- Fixed a small syntax error in the reStructuredText-formatted NEWS.rst file + which caused the upload to PyPI to fail for 4.52.0. No other code changes. + +4.52.0 (released 2024-05-24) +---------------------------- + +- Added support for the new ``VARC`` (Variable Composite) table that is being + proposed to OpenType spec (#3395). For more info: + https://github.com/harfbuzz/boring-expansion-spec/blob/main/VARC.md +- [ttLib.__main__] Fixed decompiling all tables (90fed08). +- [feaLib] Don't reference the same lookup index multiple times within the same + feature record, it is only applied once anyway (#3520). +- [cffLib] Moved methods to desubroutinize, remove hints and unused subroutines + from subset module to cffLib (#3517). +- [varLib.instancer] Added support for partial-instancing CFF2 tables! Also, added + method to down-convert from CFF2 to CFF 1.0, and CLI entry points to convert + CFF<->CFF2 (#3506). +- [subset] Prune unused user name IDs even with --name-IDs='*' (#3410). +- [ttx] use GNU-style getopt to intermix options and positional arguments (#3509). +- [feaLib.variableScalar] Fixed ``value_at_location()`` method (#3491) +- [psCharStrings] Shorten output of ``encodeFloat`` (#3492). +- [bezierTools] Fix infinite-recursion in ``calcCubicArcLength`` (#3502). +- [avar2] Implement ``avar2`` support in ``TTFont.getGlyphSet()`` (#3473). + 4.51.0 (released 2024-04-05) ---------------------------- diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 5621f391f9..56dd7303e0 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.51.0" +version = __version__ = "4.52.1" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py b/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py new file mode 100644 index 0000000000..5dc48a7fcb --- /dev/null +++ b/contrib/python/fonttools/fontTools/cffLib/CFF2ToCFF.py @@ -0,0 +1,165 @@ +"""CFF2 to CFF converter.""" + +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.cliTools import makeOutputFileName +from fontTools.cffLib import TopDictIndex, buildOrder, topDictOperators +from .width import optimizeWidths +from collections import defaultdict +import logging + + +__all__ = ["convertCFF2ToCFF", "main"] + + +log = logging.getLogger("fontTools.cffLib") + + +def _convertCFF2ToCFF(cff, otFont): + """Converts this object from CFF2 format to CFF format. This conversion + is done 'in-place'. The conversion cannot be reversed. + + The CFF2 font cannot be variable. (TODO Accept those and convert to the + default instance?) + + This assumes a decompiled CFF table. (i.e. that the object has been + filled via :meth:`decompile` and e.g. not loaded from XML.)""" + + cff.major = 1 + + topDictData = TopDictIndex(None, isCFF2=True) + for item in cff.topDictIndex: + # Iterate over, such that all are decompiled + topDictData.append(item) + cff.topDictIndex = topDictData + topDict = topDictData[0] + + if hasattr(topDict, "VarStore"): + raise ValueError("Variable CFF2 font cannot be converted to CFF format.") + + if hasattr(topDict, "Private"): + privateDict = topDict.Private + else: + privateDict = None + opOrder = buildOrder(topDictOperators) + topDict.order = opOrder + + fdArray = topDict.FDArray + charStrings = topDict.CharStrings + + for cs in charStrings.values(): + cs.decompile() + cs.program.append("endchar") + for subrSets in [cff.GlobalSubrs] + [ + getattr(fd.Private, "Subrs", []) for fd in fdArray + ]: + for cs in subrSets: + cs.program.append("return") + + # Add (optimal) width to CharStrings that need it. + widths = defaultdict(list) + metrics = otFont["hmtx"].metrics + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + if fdIndex == None: + fdIndex = 0 + widths[fdIndex].append(metrics[glyphName][0]) + for fdIndex, widthList in widths.items(): + bestDefault, bestNominal = optimizeWidths(widthList) + private = fdArray[fdIndex].Private + private.defaultWidthX = bestDefault + private.nominalWidthX = bestNominal + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + if fdIndex == None: + fdIndex = 0 + private = fdArray[fdIndex].Private + width = metrics[glyphName][0] + if width != private.defaultWidthX: + cs.program.insert(0, width - private.nominalWidthX) + + +def convertCFF2ToCFF(font, *, updatePostTable=True): + cff = font["CFF2"].cff + _convertCFF2ToCFF(cff, font) + del font["CFF2"] + table = font["CFF "] = newTable("CFF ") + table.cff = cff + + if updatePostTable and "post" in font: + # Only version supported for fonts with CFF table is 0x00030000 not 0x20000 + post = font["post"] + if post.formatType == 2.0: + post.formatType = 3.0 + + +def main(args=None): + """Convert CFF OTF font to CFF2 OTF font""" + if args is None: + import sys + + args = sys.argv[1:] + + import argparse + + parser = argparse.ArgumentParser( + "fonttools cffLib.CFFToCFF2", + description="Upgrade a CFF font to CFF2.", + ) + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input OTF file with CFF table." + ) + parser.add_argument( + "-o", + "--output", + metavar="OUTPUT.ttf", + default=None, + help="Output instance OTF file (default: INPUT-CFF2.ttf).", + ) + parser.add_argument( + "--no-recalc-timestamp", + dest="recalc_timestamp", + action="store_false", + help="Don't set the output font's timestamp to the current time.", + ) + loggingGroup = parser.add_mutually_exclusive_group(required=False) + loggingGroup.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely." + ) + loggingGroup.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off." + ) + options = parser.parse_args(args) + + from fontTools import configLogger + + configLogger( + level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO") + ) + + import os + + infile = options.input + if not os.path.isfile(infile): + parser.error("No such file '{}'".format(infile)) + + outfile = ( + makeOutputFileName(infile, overWrite=True, suffix="-CFF") + if not options.output + else options.output + ) + + font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False) + + convertCFF2ToCFF(font) + + log.info( + "Saving %s", + outfile, + ) + font.save(outfile) + + +if __name__ == "__main__": + import sys + + sys.exit(main(sys.argv[1:])) diff --git a/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py b/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py new file mode 100644 index 0000000000..78347c6667 --- /dev/null +++ b/contrib/python/fonttools/fontTools/cffLib/CFFToCFF2.py @@ -0,0 +1,287 @@ +"""CFF to CFF2 converter.""" + +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.cliTools import makeOutputFileName +from fontTools.misc.psCharStrings import T2WidthExtractor +from fontTools.cffLib import ( + TopDictIndex, + FDArrayIndex, + FontDict, + buildOrder, + topDictOperators, + privateDictOperators, + topDictOperators2, + privateDictOperators2, +) +from io import BytesIO +import logging + +__all__ = ["convertCFFToCFF2", "main"] + + +log = logging.getLogger("fontTools.cffLib") + + +class _NominalWidthUsedError(Exception): + def __add__(self, other): + raise self + + def __radd__(self, other): + raise self + + +def _convertCFFToCFF2(cff, otFont): + """Converts this object from CFF format to CFF2 format. This conversion + is done 'in-place'. The conversion cannot be reversed. + + This assumes a decompiled CFF table. (i.e. that the object has been + filled via :meth:`decompile` and e.g. not loaded from XML.)""" + + # Clean up T2CharStrings + + topDict = cff.topDictIndex[0] + fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None + charStrings = topDict.CharStrings + globalSubrs = cff.GlobalSubrs + localSubrs = [getattr(fd.Private, "Subrs", []) for fd in fdArray] if fdArray else [] + + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + cs.decompile() + + # Clean up subroutines first + for subrs in [globalSubrs] + localSubrs: + for subr in subrs: + program = subr.program + i = j = len(program) + try: + i = program.index("return") + except ValueError: + pass + try: + j = program.index("endchar") + except ValueError: + pass + program[min(i, j) :] = [] + + # Clean up glyph charstrings + removeUnusedSubrs = False + nominalWidthXError = _NominalWidthUsedError() + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + program = cs.program + if fdIndex == None: + fdIndex = 0 + + # Intentionally use custom type for nominalWidthX, such that any + # CharString that has an explicit width encoded will throw back to us. + extractor = T2WidthExtractor( + localSubrs[fdIndex] if localSubrs else [], + globalSubrs, + nominalWidthXError, + 0, + ) + try: + extractor.execute(cs) + except _NominalWidthUsedError: + # Program has explicit width. We want to drop it, but can't + # 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"]: + removeUnusedSubrs = True + subrNumber = program.pop(0) + op = program.pop(0) + bias = extractor.localBias if op == "callsubr" else extractor.globalBias + subrNumber += bias + subrSet = localSubrs[fdIndex] if op == "callsubr" else globalSubrs + subrProgram = subrSet[subrNumber].program + program[:0] = subrProgram + # Now pop the actual width + program.pop(0) + + if program and program[-1] == "endchar": + program.pop() + + if removeUnusedSubrs: + cff.remove_unused_subroutines() + + # Upconvert TopDict + + cff.major = 2 + cff2GetGlyphOrder = cff.otFont.getGlyphOrder + topDictData = TopDictIndex(None, cff2GetGlyphOrder) + for item in cff.topDictIndex: + # Iterate over, such that all are decompiled + topDictData.append(item) + cff.topDictIndex = topDictData + topDict = topDictData[0] + if hasattr(topDict, "Private"): + privateDict = topDict.Private + else: + privateDict = None + opOrder = buildOrder(topDictOperators2) + topDict.order = opOrder + topDict.cff2GetGlyphOrder = cff2GetGlyphOrder + + if not hasattr(topDict, "FDArray"): + fdArray = topDict.FDArray = FDArrayIndex() + fdArray.strings = None + fdArray.GlobalSubrs = topDict.GlobalSubrs + topDict.GlobalSubrs.fdArray = fdArray + charStrings = topDict.CharStrings + if charStrings.charStringsAreIndexed: + charStrings.charStringsIndex.fdArray = fdArray + else: + charStrings.fdArray = fdArray + fontDict = FontDict() + fontDict.setCFF2(True) + fdArray.append(fontDict) + fontDict.Private = privateDict + privateOpOrder = buildOrder(privateDictOperators2) + if privateDict is not None: + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in privateDict.rawDict: + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + else: + # clean up the PrivateDicts in the fdArray + fdArray = topDict.FDArray + privateOpOrder = buildOrder(privateDictOperators2) + for fontDict in fdArray: + fontDict.setCFF2(True) + for key in list(fontDict.rawDict.keys()): + if key not in fontDict.order: + del fontDict.rawDict[key] + if hasattr(fontDict, key): + delattr(fontDict, key) + + privateDict = fontDict.Private + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in list(privateDict.rawDict.keys()): + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + + # Now delete up the deprecated topDict operators from CFF 1.0 + for entry in topDictOperators: + key = entry[1] + # We seem to need to keep the charset operator for now, + # or we fail to compile with some fonts, like AdditionFont.otf. + # I don't know which kind of CFF font those are. But keeping + # charset seems to work. It will be removed when we save and + # read the font again. + # + # AdditionFont.otf has <Encoding name="StandardEncoding"/>. + if key == "charset": + continue + if key not in opOrder: + if key in topDict.rawDict: + del topDict.rawDict[key] + if hasattr(topDict, key): + delattr(topDict, key) + + # TODO(behdad): What does the following comment even mean? Both CFF and CFF2 + # use the same T2Charstring class. I *think* what it means is that the CharStrings + # were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc + # on them. At least that's what I understand. It's probably safe to remove this + # and just set vstore where needed. + # + # See comment above about charset as well. + + # At this point, the Subrs and Charstrings are all still T2Charstring class + # easiest to fix this by compiling, then decompiling again + file = BytesIO() + cff.compile(file, otFont, isCFF2=True) + file.seek(0) + cff.decompile(file, otFont, isCFF2=True) + + +def convertCFFToCFF2(font): + cff = font["CFF "].cff + del font["CFF "] + _convertCFFToCFF2(cff, font) + table = font["CFF2"] = newTable("CFF2") + table.cff = cff + + +def main(args=None): + """Convert CFF OTF font to CFF2 OTF font""" + if args is None: + import sys + + args = sys.argv[1:] + + import argparse + + parser = argparse.ArgumentParser( + "fonttools cffLib.CFFToCFF2", + description="Upgrade a CFF font to CFF2.", + ) + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input OTF file with CFF table." + ) + parser.add_argument( + "-o", + "--output", + metavar="OUTPUT.ttf", + default=None, + help="Output instance OTF file (default: INPUT-CFF2.ttf).", + ) + parser.add_argument( + "--no-recalc-timestamp", + dest="recalc_timestamp", + action="store_false", + help="Don't set the output font's timestamp to the current time.", + ) + loggingGroup = parser.add_mutually_exclusive_group(required=False) + loggingGroup.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely." + ) + loggingGroup.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off." + ) + options = parser.parse_args(args) + + from fontTools import configLogger + + configLogger( + level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO") + ) + + import os + + infile = options.input + if not os.path.isfile(infile): + parser.error("No such file '{}'".format(infile)) + + outfile = ( + makeOutputFileName(infile, overWrite=True, suffix="-CFF2") + if not options.output + else options.output + ) + + font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False) + + convertCFFToCFF2(font) + + log.info( + "Saving %s", + outfile, + ) + font.save(outfile) + + +if __name__ == "__main__": + import sys + + sys.exit(main(sys.argv[1:])) diff --git a/contrib/python/fonttools/fontTools/cffLib/__init__.py b/contrib/python/fonttools/fontTools/cffLib/__init__.py index 0ad41c5674..9cfdebaa11 100644 --- a/contrib/python/fonttools/fontTools/cffLib/__init__.py +++ b/contrib/python/fonttools/fontTools/cffLib/__init__.py @@ -45,96 +45,6 @@ maxStackLimit = 513 # maxstack operator has been deprecated. max stack is now always 513. -class StopHintCountEvent(Exception): - pass - - -class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): - stop_hintcount_ops = ( - "op_hintmask", - "op_cntrmask", - "op_rmoveto", - "op_hmoveto", - "op_vmoveto", - ) - - def __init__(self, localSubrs, globalSubrs, private=None): - psCharStrings.SimpleT2Decompiler.__init__( - self, localSubrs, globalSubrs, private - ) - - def execute(self, charString): - self.need_hintcount = True # until proven otherwise - for op_name in self.stop_hintcount_ops: - setattr(self, op_name, self.stop_hint_count) - - if hasattr(charString, "_desubroutinized"): - # If a charstring has already been desubroutinized, we will still - # need to execute it if we need to count hints in order to - # compute the byte length for mask arguments, and haven't finished - # counting hints pairs. - if self.need_hintcount and self.callingStack: - try: - psCharStrings.SimpleT2Decompiler.execute(self, charString) - except StopHintCountEvent: - del self.callingStack[-1] - return - - charString._patches = [] - psCharStrings.SimpleT2Decompiler.execute(self, charString) - desubroutinized = charString.program[:] - for idx, expansion in reversed(charString._patches): - assert idx >= 2 - assert desubroutinized[idx - 1] in [ - "callsubr", - "callgsubr", - ], desubroutinized[idx - 1] - assert type(desubroutinized[idx - 2]) == int - if expansion[-1] == "return": - expansion = expansion[:-1] - desubroutinized[idx - 2 : idx] = expansion - if not self.private.in_cff2: - if "endchar" in desubroutinized: - # Cut off after first endchar - desubroutinized = desubroutinized[ - : desubroutinized.index("endchar") + 1 - ] - else: - if not len(desubroutinized) or desubroutinized[-1] != "return": - desubroutinized.append("return") - - charString._desubroutinized = desubroutinized - del charString._patches - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1] + self.localBias] - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - self.processSubr(index, subr) - - def stop_hint_count(self, *args): - self.need_hintcount = False - for op_name in self.stop_hintcount_ops: - setattr(self, op_name, None) - cs = self.callingStack[-1] - if hasattr(cs, "_desubroutinized"): - raise StopHintCountEvent() - - def op_hintmask(self, index): - psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) - if self.need_hintcount: - self.stop_hint_count() - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - if not hasattr(cs, "_desubroutinized"): - cs._patches.append((index, subr._desubroutinized)) - - class CFFFontSet(object): """A CFF font "file" can contain more than one font, although this is extremely rare (and not allowed within OpenType fonts). @@ -389,115 +299,29 @@ class CFFFontSet(object): self.minor = int(attrs["value"]) def convertCFFToCFF2(self, otFont): - """Converts this object from CFF format to CFF2 format. This conversion - is done 'in-place'. The conversion cannot be reversed. - - This assumes a decompiled CFF table. (i.e. that the object has been - filled via :meth:`decompile`.)""" - self.major = 2 - cff2GetGlyphOrder = self.otFont.getGlyphOrder - topDictData = TopDictIndex(None, cff2GetGlyphOrder) - topDictData.items = self.topDictIndex.items - self.topDictIndex = topDictData - topDict = topDictData[0] - if hasattr(topDict, "Private"): - privateDict = topDict.Private - else: - privateDict = None - opOrder = buildOrder(topDictOperators2) - topDict.order = opOrder - topDict.cff2GetGlyphOrder = cff2GetGlyphOrder - for entry in topDictOperators: - key = entry[1] - if key not in opOrder: - if key in topDict.rawDict: - del topDict.rawDict[key] - if hasattr(topDict, key): - delattr(topDict, key) - - if not hasattr(topDict, "FDArray"): - fdArray = topDict.FDArray = FDArrayIndex() - fdArray.strings = None - fdArray.GlobalSubrs = topDict.GlobalSubrs - topDict.GlobalSubrs.fdArray = fdArray - charStrings = topDict.CharStrings - if charStrings.charStringsAreIndexed: - charStrings.charStringsIndex.fdArray = fdArray - else: - charStrings.fdArray = fdArray - fontDict = FontDict() - fontDict.setCFF2(True) - fdArray.append(fontDict) - fontDict.Private = privateDict - privateOpOrder = buildOrder(privateDictOperators2) - for entry in privateDictOperators: - key = entry[1] - if key not in privateOpOrder: - if key in privateDict.rawDict: - # print "Removing private dict", key - del privateDict.rawDict[key] - if hasattr(privateDict, key): - delattr(privateDict, key) - # print "Removing privateDict attr", key - else: - # clean up the PrivateDicts in the fdArray - fdArray = topDict.FDArray - privateOpOrder = buildOrder(privateDictOperators2) - for fontDict in fdArray: - fontDict.setCFF2(True) - for key in fontDict.rawDict.keys(): - if key not in fontDict.order: - del fontDict.rawDict[key] - if hasattr(fontDict, key): - delattr(fontDict, key) - - privateDict = fontDict.Private - for entry in privateDictOperators: - key = entry[1] - if key not in privateOpOrder: - if key in privateDict.rawDict: - # print "Removing private dict", key - del privateDict.rawDict[key] - if hasattr(privateDict, key): - delattr(privateDict, key) - # print "Removing privateDict attr", key - # At this point, the Subrs and Charstrings are all still T2Charstring class - # easiest to fix this by compiling, then decompiling again - file = BytesIO() - self.compile(file, otFont, isCFF2=True) - file.seek(0) - self.decompile(file, otFont, isCFF2=True) + from .CFFToCFF2 import _convertCFFToCFF2 + + _convertCFFToCFF2(self, otFont) + + def convertCFF2ToCFF(self, otFont): + from .CFF2ToCFF import _convertCFF2ToCFF + + _convertCFF2ToCFF(self, otFont) def desubroutinize(self): - for fontName in self.fontNames: - font = self[fontName] - cs = font.CharStrings - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DesubroutinizingT2Decompiler( - subrs, c.globalSubrs, c.private - ) - decompiler.execute(c) - c.program = c._desubroutinized - del c._desubroutinized - # Delete all the local subrs - if hasattr(font, "FDArray"): - for fd in font.FDArray: - pd = fd.Private - if hasattr(pd, "Subrs"): - del pd.Subrs - if "Subrs" in pd.rawDict: - del pd.rawDict["Subrs"] - else: - pd = font.Private - if hasattr(pd, "Subrs"): - del pd.Subrs - if "Subrs" in pd.rawDict: - del pd.rawDict["Subrs"] - # as well as the global subrs - self.GlobalSubrs.clear() + from .transforms import desubroutinize + + desubroutinize(self) + + def remove_hints(self): + from .transforms import remove_hints + + remove_hints(self) + + def remove_unused_subroutines(self): + from .transforms import remove_unused_subroutines + + remove_unused_subroutines(self) class CFFWriter(object): @@ -764,8 +588,8 @@ class Index(object): compilerClass = IndexCompiler def __init__(self, file=None, isCFF2=None): - assert (isCFF2 is None) == (file is None) self.items = [] + self.offsets = offsets = [] name = self.__class__.__name__ if file is None: return @@ -782,7 +606,6 @@ class Index(object): offSize = readCard8(file) log.log(DEBUG, " index count: %s offSize: %s", count, offSize) assert offSize <= 4, "offSize too large: %s" % offSize - self.offsets = offsets = [] pad = b"\0" * (4 - offSize) for index in range(count + 1): chunk = file.read(offSize) @@ -960,7 +783,6 @@ class TopDictIndex(Index): compilerClass = TopDictIndexCompiler def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, isCFF2=None): - assert (isCFF2 is None) == (file is None) self.cff2GetGlyphOrder = cff2GetGlyphOrder if file is not None and isCFF2: self._isCFF2 = isCFF2 @@ -1050,6 +872,7 @@ class VarStoreData(object): reader = OTTableReader(self.data, globalState) self.otVarStore = ot.VarStore() self.otVarStore.decompile(reader, self.font) + self.data = None return self def compile(self): @@ -2860,9 +2683,11 @@ class PrivateDict(BaseDict): # Provide dummy values. This avoids needing to provide # an isCFF2 state in a lot of places. self.nominalWidthX = self.defaultWidthX = None + self._isCFF2 = True else: self.defaults = buildDefaults(privateDictOperators) self.order = buildOrder(privateDictOperators) + self._isCFF2 = False @property def in_cff2(self): diff --git a/contrib/python/fonttools/fontTools/cffLib/specializer.py b/contrib/python/fonttools/fontTools/cffLib/specializer.py index efc15af704..bb7f89e4ff 100644 --- a/contrib/python/fonttools/fontTools/cffLib/specializer.py +++ b/contrib/python/fonttools/fontTools/cffLib/specializer.py @@ -43,10 +43,8 @@ def programToCommands(program, getNumRegions=None): hintmask/cntrmask argument, as well as stray arguments at the end of the program (🤷). 'getNumRegions' may be None, or a callable object. It must return the - number of regions. 'getNumRegions' takes a single argument, vsindex. If - the vsindex argument is None, getNumRegions returns the default number - of regions for the charstring, else it returns the numRegions for - the vsindex. + number of regions. 'getNumRegions' takes a single argument, vsindex. It + returns the numRegions for the vsindex. The Charstring may or may not start with a width value. If the first non-blend operator has an odd number of arguments, then the first argument is a width, and is popped off. This is complicated with blend operators, as @@ -61,7 +59,7 @@ def programToCommands(program, getNumRegions=None): """ seenWidthOp = False - vsIndex = None + vsIndex = 0 lenBlendStack = 0 lastBlendIndex = 0 commands = [] @@ -813,7 +811,7 @@ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( - "fonttools cffLib.specialer", + "fonttools cffLib.specializer", description="CFF CharString generalizer/specializer", ) parser.add_argument("program", metavar="command", nargs="*", help="Commands.") diff --git a/contrib/python/fonttools/fontTools/cffLib/transforms.py b/contrib/python/fonttools/fontTools/cffLib/transforms.py new file mode 100644 index 0000000000..0772d85e21 --- /dev/null +++ b/contrib/python/fonttools/fontTools/cffLib/transforms.py @@ -0,0 +1,482 @@ +from fontTools.misc.psCharStrings import ( + SimpleT2Decompiler, + T2WidthExtractor, + calcSubrBias, +) + + +def _uniq_sort(l): + return sorted(set(l)) + + +class StopHintCountEvent(Exception): + pass + + +class _DesubroutinizingT2Decompiler(SimpleT2Decompiler): + stop_hintcount_ops = ( + "op_hintmask", + "op_cntrmask", + "op_rmoveto", + "op_hmoveto", + "op_vmoveto", + ) + + def __init__(self, localSubrs, globalSubrs, private=None): + SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) + + def execute(self, charString): + self.need_hintcount = True # until proven otherwise + for op_name in self.stop_hintcount_ops: + setattr(self, op_name, self.stop_hint_count) + + if hasattr(charString, "_desubroutinized"): + # If a charstring has already been desubroutinized, we will still + # need to execute it if we need to count hints in order to + # compute the byte length for mask arguments, and haven't finished + # counting hints pairs. + if self.need_hintcount and self.callingStack: + try: + SimpleT2Decompiler.execute(self, charString) + except StopHintCountEvent: + del self.callingStack[-1] + return + + charString._patches = [] + SimpleT2Decompiler.execute(self, charString) + desubroutinized = charString.program[:] + for idx, expansion in reversed(charString._patches): + assert idx >= 2 + assert desubroutinized[idx - 1] in [ + "callsubr", + "callgsubr", + ], desubroutinized[idx - 1] + assert type(desubroutinized[idx - 2]) == int + if expansion[-1] == "return": + expansion = expansion[:-1] + desubroutinized[idx - 2 : idx] = expansion + if not self.private.in_cff2: + if "endchar" in desubroutinized: + # Cut off after first endchar + desubroutinized = desubroutinized[ + : desubroutinized.index("endchar") + 1 + ] + + charString._desubroutinized = desubroutinized + del charString._patches + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1] + self.localBias] + SimpleT2Decompiler.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] + SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) + + def stop_hint_count(self, *args): + self.need_hintcount = False + for op_name in self.stop_hintcount_ops: + setattr(self, op_name, None) + cs = self.callingStack[-1] + if hasattr(cs, "_desubroutinized"): + raise StopHintCountEvent() + + def op_hintmask(self, index): + SimpleT2Decompiler.op_hintmask(self, index) + if self.need_hintcount: + self.stop_hint_count() + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + if not hasattr(cs, "_desubroutinized"): + cs._patches.append((index, subr._desubroutinized)) + + +def desubroutinize(cff): + for fontName in cff.fontNames: + font = cff[fontName] + cs = font.CharStrings + for c in cs.values(): + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private) + decompiler.execute(c) + c.program = c._desubroutinized + del c._desubroutinized + # Delete all the local subrs + if hasattr(font, "FDArray"): + for fd in font.FDArray: + pd = fd.Private + if hasattr(pd, "Subrs"): + del pd.Subrs + if "Subrs" in pd.rawDict: + del pd.rawDict["Subrs"] + else: + pd = font.Private + if hasattr(pd, "Subrs"): + del pd.Subrs + if "Subrs" in pd.rawDict: + del pd.rawDict["Subrs"] + # as well as the global subrs + cff.GlobalSubrs.clear() + + +class _MarkingT2Decompiler(SimpleT2Decompiler): + def __init__(self, localSubrs, globalSubrs, private): + SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) + for subrs in [localSubrs, globalSubrs]: + if subrs and not hasattr(subrs, "_used"): + subrs._used = set() + + def op_callsubr(self, index): + self.localSubrs._used.add(self.operandStack[-1] + self.localBias) + SimpleT2Decompiler.op_callsubr(self, index) + + def op_callgsubr(self, index): + self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias) + SimpleT2Decompiler.op_callgsubr(self, index) + + +class _DehintingT2Decompiler(T2WidthExtractor): + class Hints(object): + def __init__(self): + # Whether calling this charstring produces any hint stems + # Note that if a charstring starts with hintmask, it will + # have has_hint set to True, because it *might* produce an + # implicit vstem if called under certain conditions. + self.has_hint = False + # Index to start at to drop all hints + self.last_hint = 0 + # Index up to which we know more hints are possible. + # Only relevant if status is 0 or 1. + self.last_checked = 0 + # The status means: + # 0: after dropping hints, this charstring is empty + # 1: after dropping hints, there may be more hints + # continuing after this, or there might be + # other things. Not clear yet. + # 2: no more hints possible after this charstring + self.status = 0 + # Has hintmask instructions; not recursive + self.has_hintmask = False + # List of indices of calls to empty subroutines to remove. + self.deletions = [] + + pass + + def __init__( + self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None + ): + self._css = css + T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX + ) + self.private = private + + def execute(self, charString): + old_hints = charString._hints if hasattr(charString, "_hints") else None + charString._hints = self.Hints() + + T2WidthExtractor.execute(self, charString) + + hints = charString._hints + + if hints.has_hint or hints.has_hintmask: + self._css.add(charString) + + if hints.status != 2: + # Check from last_check, make sure we didn't have any operators. + for i in range(hints.last_checked, len(charString.program) - 1): + if isinstance(charString.program[i], str): + hints.status = 2 + break + else: + hints.status = 1 # There's *something* here + hints.last_checked = len(charString.program) + + if old_hints: + assert hints.__dict__ == old_hints.__dict__ + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1] + self.localBias] + T2WidthExtractor.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] + T2WidthExtractor.op_callgsubr(self, index) + self.processSubr(index, subr) + + def op_hstem(self, index): + T2WidthExtractor.op_hstem(self, index) + self.processHint(index) + + def op_vstem(self, index): + T2WidthExtractor.op_vstem(self, index) + self.processHint(index) + + def op_hstemhm(self, index): + T2WidthExtractor.op_hstemhm(self, index) + self.processHint(index) + + def op_vstemhm(self, index): + T2WidthExtractor.op_vstemhm(self, index) + self.processHint(index) + + def op_hintmask(self, index): + rv = T2WidthExtractor.op_hintmask(self, index) + self.processHintmask(index) + return rv + + def op_cntrmask(self, index): + rv = T2WidthExtractor.op_cntrmask(self, index) + self.processHintmask(index) + return rv + + def processHintmask(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hintmask = True + if hints.status != 2: + # Check from last_check, see if we may be an implicit vstem + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + else: + # We are an implicit vstem + hints.has_hint = True + hints.last_hint = index + 1 + hints.status = 0 + hints.last_checked = index + 1 + + def processHint(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hint = True + hints.last_hint = index + hints.last_checked = index + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + hints = cs._hints + subr_hints = subr._hints + + # Check from last_check, make sure we didn't have + # any operators. + if hints.status != 2: + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + hints.last_checked = index + + if hints.status != 2: + if subr_hints.has_hint: + hints.has_hint = True + + # Decide where to chop off from + if subr_hints.status == 0: + hints.last_hint = index + else: + hints.last_hint = index - 2 # Leave the subr call in + + elif subr_hints.status == 0: + hints.deletions.append(index) + + hints.status = max(hints.status, subr_hints.status) + + +def _cs_subset_subroutines(charstring, subrs, gsubrs): + p = charstring.program + for i in range(1, len(p)): + if p[i] == "callsubr": + assert isinstance(p[i - 1], int) + p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias + elif p[i] == "callgsubr": + assert isinstance(p[i - 1], int) + p[i - 1] = ( + gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias + ) + + +def _cs_drop_hints(charstring): + hints = charstring._hints + + if hints.deletions: + p = charstring.program + for idx in reversed(hints.deletions): + del p[idx - 2 : idx] + + if hints.has_hint: + assert not hints.deletions or hints.last_hint <= hints.deletions[0] + charstring.program = charstring.program[hints.last_hint :] + if not charstring.program: + # TODO CFF2 no need for endchar. + charstring.program.append("endchar") + if hasattr(charstring, "width"): + # Insert width back if needed + if charstring.width != charstring.private.defaultWidthX: + # For CFF2 charstrings, this should never happen + assert ( + charstring.private.defaultWidthX is not None + ), "CFF2 CharStrings must not have an initial width value" + charstring.program.insert( + 0, charstring.width - charstring.private.nominalWidthX + ) + + if hints.has_hintmask: + i = 0 + p = charstring.program + while i < len(p): + if p[i] in ["hintmask", "cntrmask"]: + assert i + 1 <= len(p) + del p[i : i + 2] + continue + i += 1 + + assert len(charstring.program) + + del charstring._hints + + +def remove_hints(cff): + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + # This can be tricky, but doesn't have to. What we do is: + # + # - Run all used glyph charstrings and recurse into subroutines, + # - For each charstring (including subroutines), if it has any + # of the hint stem operators, we mark it as such. + # Upon returning, for each charstring we note all the + # subroutine calls it makes that (recursively) contain a stem, + # - Dropping hinting then consists of the following two ops: + # * Drop the piece of the program in each charstring before the + # last call to a stem op or a stem-calling subroutine, + # * Drop all hintmask operations. + # - It's trickier... A hintmask right after hints and a few numbers + # will act as an implicit vstemhm. As such, we track whether + # we have seen any non-hint operators so far and do the right + # thing, recursively... Good luck understanding that :( + css = set() + for c in cs.values(): + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DehintingT2Decompiler( + css, + subrs, + c.globalSubrs, + c.private.nominalWidthX, + c.private.defaultWidthX, + c.private, + ) + decompiler.execute(c) + c.width = decompiler.width + for charstring in css: + _cs_drop_hints(charstring) + del css + + # Drop font-wide hinting values + all_privs = [] + if hasattr(font, "FDArray"): + all_privs.extend(fd.Private for fd in font.FDArray) + else: + all_privs.append(font.Private) + for priv in all_privs: + for k in [ + "BlueValues", + "OtherBlues", + "FamilyBlues", + "FamilyOtherBlues", + "BlueScale", + "BlueShift", + "BlueFuzz", + "StemSnapH", + "StemSnapV", + "StdHW", + "StdVW", + "ForceBold", + "LanguageGroup", + "ExpansionFactor", + ]: + if hasattr(priv, k): + setattr(priv, k, None) + remove_unused_subroutines(cff) + + +def _pd_delete_empty_subrs(private_dict): + if hasattr(private_dict, "Subrs") and not private_dict.Subrs: + if "Subrs" in private_dict.rawDict: + del private_dict.rawDict["Subrs"] + del private_dict.Subrs + + +def remove_unused_subroutines(cff): + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + # Renumber subroutines to remove unused ones + + # Mark all used subroutines + for c in cs.values(): + subrs = getattr(c.private, "Subrs", []) + decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) + decompiler.execute(c) + + all_subrs = [font.GlobalSubrs] + if hasattr(font, "FDArray"): + all_subrs.extend( + fd.Private.Subrs + for fd in font.FDArray + if hasattr(fd.Private, "Subrs") and fd.Private.Subrs + ) + elif hasattr(font.Private, "Subrs") and font.Private.Subrs: + all_subrs.append(font.Private.Subrs) + + subrs = set(subrs) # Remove duplicates + + # Prepare + for subrs in all_subrs: + if not hasattr(subrs, "_used"): + subrs._used = set() + subrs._used = _uniq_sort(subrs._used) + subrs._old_bias = calcSubrBias(subrs) + subrs._new_bias = calcSubrBias(subrs._used) + + # Renumber glyph charstrings + for c in cs.values(): + subrs = getattr(c.private, "Subrs", None) + _cs_subset_subroutines(c, subrs, font.GlobalSubrs) + + # Renumber subroutines themselves + for subrs in all_subrs: + if subrs == font.GlobalSubrs: + if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): + local_subrs = font.Private.Subrs + else: + local_subrs = None + else: + local_subrs = subrs + + subrs.items = [subrs.items[i] for i in subrs._used] + if hasattr(subrs, "file"): + del subrs.file + if hasattr(subrs, "offsets"): + del subrs.offsets + + for subr in subrs.items: + _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs) + + # Delete local SubrsIndex if empty + if hasattr(font, "FDArray"): + for fd in font.FDArray: + _pd_delete_empty_subrs(fd.Private) + else: + _pd_delete_empty_subrs(font.Private) + + # Cleanup + for subrs in all_subrs: + del subrs._used, subrs._old_bias, subrs._new_bias diff --git a/contrib/python/fonttools/fontTools/cffLib/width.py b/contrib/python/fonttools/fontTools/cffLib/width.py index 0ba3ed39bf..78ff27e4fd 100644 --- a/contrib/python/fonttools/fontTools/cffLib/width.py +++ b/contrib/python/fonttools/fontTools/cffLib/width.py @@ -13,6 +13,9 @@ from operator import add from functools import reduce +__all__ = ["optimizeWidths", "main"] + + class missingdict(dict): def __init__(self, missing_func): self.missing_func = missing_func diff --git a/contrib/python/fonttools/fontTools/cu2qu/__main__.py b/contrib/python/fonttools/fontTools/cu2qu/__main__.py index 084bf8f960..5205ffeef9 100644 --- a/contrib/python/fonttools/fontTools/cu2qu/__main__.py +++ b/contrib/python/fonttools/fontTools/cu2qu/__main__.py @@ -1,5 +1,5 @@ import sys -from .cli import main +from .cli import _main as main if __name__ == "__main__": diff --git a/contrib/python/fonttools/fontTools/cu2qu/benchmark.py b/contrib/python/fonttools/fontTools/cu2qu/benchmark.py index 2ab1e966b1..007f75d887 100644 --- a/contrib/python/fonttools/fontTools/cu2qu/benchmark.py +++ b/contrib/python/fonttools/fontTools/cu2qu/benchmark.py @@ -45,7 +45,6 @@ def run_benchmark(module, function, setup_suffix="", repeat=5, number=1000): def main(): - """Benchmark the cu2qu algorithm performance.""" run_benchmark("cu2qu", "curve_to_quadratic") run_benchmark("cu2qu", "curves_to_quadratic") diff --git a/contrib/python/fonttools/fontTools/cu2qu/cli.py b/contrib/python/fonttools/fontTools/cu2qu/cli.py index 9144043ff1..ddc6450200 100644 --- a/contrib/python/fonttools/fontTools/cu2qu/cli.py +++ b/contrib/python/fonttools/fontTools/cu2qu/cli.py @@ -64,7 +64,7 @@ def _copytree(input_path, output_path): shutil.copytree(input_path, output_path) -def main(args=None): +def _main(args=None): """Convert a UFO font from cubic to quadratic curves""" parser = argparse.ArgumentParser(prog="cu2qu") parser.add_argument("--version", action="version", version=fontTools.__version__) diff --git a/contrib/python/fonttools/fontTools/feaLib/builder.py b/contrib/python/fonttools/fontTools/feaLib/builder.py index 7921a3f179..a91381ddc1 100644 --- a/contrib/python/fonttools/fontTools/feaLib/builder.py +++ b/contrib/python/fonttools/fontTools/feaLib/builder.py @@ -880,8 +880,13 @@ class Builder(object): # l.lookup_index will be None when a lookup is not needed # for the table under construction. For example, substitution # rules will have no lookup_index while building GPOS tables. + # We also deduplicate lookup indices, as they only get applied once + # within a given feature: + # https://github.com/fonttools/fonttools/issues/2946 lookup_indices = tuple( - [l.lookup_index for l in lookups if l.lookup_index is not None] + dict.fromkeys( + l.lookup_index for l in lookups if l.lookup_index is not None + ) ) size_feature = tag == "GPOS" and feature_tag == "size" diff --git a/contrib/python/fonttools/fontTools/feaLib/variableScalar.py b/contrib/python/fonttools/fontTools/feaLib/variableScalar.py index c97b435429..96e7c07566 100644 --- a/contrib/python/fonttools/fontTools/feaLib/variableScalar.py +++ b/contrib/python/fonttools/fontTools/feaLib/variableScalar.py @@ -75,10 +75,11 @@ class VariableScalar: return self.values[key] def value_at_location(self, location, model_cache=None, avar=None): - loc = location + loc = Location(location) if loc in self.values.keys(): return self.values[loc] values = list(self.values.values()) + loc = dict(self._normalized_location(loc)) return self.model(model_cache, avar).interpolateFromMasters(loc, values) def model(self, model_cache=None, avar=None): diff --git a/contrib/python/fonttools/fontTools/fontBuilder.py b/contrib/python/fonttools/fontTools/fontBuilder.py index dd57a0507d..16b7ee167d 100644 --- a/contrib/python/fonttools/fontTools/fontBuilder.py +++ b/contrib/python/fonttools/fontTools/fontBuilder.py @@ -656,11 +656,7 @@ class FontBuilder(object): if validateGlyphFormat and self.font["head"].glyphDataFormat == 0: for name, g in glyphs.items(): - if g.isVarComposite(): - raise ValueError( - f"Glyph {name!r} is a variable composite, but glyphDataFormat=0" - ) - elif g.numberOfContours > 0 and any(f & flagCubic for f in g.flags): + if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags): raise ValueError( f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; " "either convert to quadratics with cu2qu or set glyphDataFormat=1." diff --git a/contrib/python/fonttools/fontTools/help.py b/contrib/python/fonttools/fontTools/help.py index 2a238de3d6..4331a26dde 100644 --- a/contrib/python/fonttools/fontTools/help.py +++ b/contrib/python/fonttools/fontTools/help.py @@ -20,7 +20,8 @@ def main(): continue try: description = imports.main.__doc__ - if description: + # Cython modules seem to return "main()" as the docstring + if description and description != "main()": pkg = pkg.replace("fontTools.", "").replace(".__main__", "") # show the docstring's first line only descriptions[pkg] = description.splitlines()[0] diff --git a/contrib/python/fonttools/fontTools/merge/tables.py b/contrib/python/fonttools/fontTools/merge/tables.py index 57ed64d337..208a5099ff 100644 --- a/contrib/python/fonttools/fontTools/merge/tables.py +++ b/contrib/python/fonttools/fontTools/merge/tables.py @@ -225,7 +225,7 @@ def merge(self, m, tables): g.removeHinting() # Expand composite glyphs to load their # composite glyph names. - if g.isComposite() or g.isVarComposite(): + if g.isComposite(): g.expand(table) return DefaultTable.merge(self, m, tables) @@ -294,6 +294,8 @@ def merge(self, m, tables): extractor.execute(c) width = extractor.width if width is not defaultWidthXToken: + # The following will be wrong if the width is added + # by a subroutine. Ouch! c.program.pop(0) else: width = defaultWidthX diff --git a/contrib/python/fonttools/fontTools/misc/bezierTools.py b/contrib/python/fonttools/fontTools/misc/bezierTools.py index a1a707b098..5411ff99fd 100644 --- a/contrib/python/fonttools/fontTools/misc/bezierTools.py +++ b/contrib/python/fonttools/fontTools/misc/bezierTools.py @@ -18,6 +18,9 @@ except (AttributeError, ImportError): COMPILED = False +EPSILON = 1e-9 + + Intersection = namedtuple("Intersection", ["pt", "t1", "t2"]) @@ -92,7 +95,7 @@ def _split_cubic_into_two(p0, p1, p2, p3): def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): arch = abs(p0 - p3) box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3) - if arch * mult >= box: + if arch * mult + EPSILON >= box: return (arch + box) * 0.5 else: one, two = _split_cubic_into_two(p0, p1, p2, p3) diff --git a/contrib/python/fonttools/fontTools/misc/iterTools.py b/contrib/python/fonttools/fontTools/misc/iterTools.py new file mode 100644 index 0000000000..d7b8305322 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/iterTools.py @@ -0,0 +1,12 @@ +from itertools import * + +# Python 3.12: +if "batched" not in globals(): + # https://docs.python.org/3/library/itertools.html#itertools.batched + def batched(iterable, n): + # batched('ABCDEFG', 3) --> ABC DEF G + if n < 1: + raise ValueError("n must be at least one") + it = iter(iterable) + while batch := tuple(islice(it, n)): + yield batch diff --git a/contrib/python/fonttools/fontTools/misc/lazyTools.py b/contrib/python/fonttools/fontTools/misc/lazyTools.py new file mode 100644 index 0000000000..91cb80c992 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/lazyTools.py @@ -0,0 +1,42 @@ +from collections import UserDict, UserList + +__all__ = ["LazyDict", "LazyList"] + + +class LazyDict(UserDict): + def __init__(self, data): + super().__init__() + self.data = data + + def __getitem__(self, k): + v = self.data[k] + if callable(v): + v = v(k) + self.data[k] = v + return v + + +class LazyList(UserList): + def __getitem__(self, k): + if isinstance(k, slice): + indices = range(*k.indices(len(self))) + return [self[i] for i in indices] + v = self.data[k] + if callable(v): + v = v(k) + self.data[k] = v + return v + + def __add__(self, other): + if isinstance(other, LazyList): + other = list(other) + elif isinstance(other, list): + pass + else: + return NotImplemented + return list(self) + other + + def __radd__(self, other): + if not isinstance(other, list): + return NotImplemented + return other + list(self) diff --git a/contrib/python/fonttools/fontTools/misc/psCharStrings.py b/contrib/python/fonttools/fontTools/misc/psCharStrings.py index cc9ca01c7f..5d881c5816 100644 --- a/contrib/python/fonttools/fontTools/misc/psCharStrings.py +++ b/contrib/python/fonttools/fontTools/misc/psCharStrings.py @@ -275,6 +275,24 @@ def encodeFloat(f): s = s[1:] elif s[:3] == "-0.": s = "-" + s[2:] + elif s.endswith("000"): + significantDigits = s.rstrip("0") + s = "%sE%d" % (significantDigits, len(s) - len(significantDigits)) + else: + dotIndex = s.find(".") + eIndex = s.find("E") + if dotIndex != -1 and eIndex != -1: + integerPart = s[:dotIndex] + fractionalPart = s[dotIndex + 1 : eIndex] + exponent = int(s[eIndex + 1 :]) + newExponent = exponent - len(fractionalPart) + if newExponent == 1: + s = "%s%s0" % (integerPart, fractionalPart) + else: + s = "%s%sE%d" % (integerPart, fractionalPart, newExponent) + if s.startswith((".0", "-.0")): + sign, s = s.split(".", 1) + s = "%s%sE-%d" % (sign, s.lstrip("0"), len(s)) nibbles = [] while s: c = s[0] @@ -286,6 +304,8 @@ def encodeFloat(f): c = "E-" elif c2 == "+": s = s[1:] + if s.startswith("0"): + s = s[1:] nibbles.append(realNibblesDict[c]) nibbles.append(0xF) if len(nibbles) % 2: diff --git a/contrib/python/fonttools/fontTools/misc/sstruct.py b/contrib/python/fonttools/fontTools/misc/sstruct.py index d35bc9a5c8..92be275b89 100644 --- a/contrib/python/fonttools/fontTools/misc/sstruct.py +++ b/contrib/python/fonttools/fontTools/misc/sstruct.py @@ -64,7 +64,10 @@ def pack(fmt, obj): elements = [] if not isinstance(obj, dict): obj = obj.__dict__ - for name in names: + string_index = formatstring + if formatstring.startswith(">"): + string_index = formatstring[1:] + for ix, name in enumerate(names.keys()): value = obj[name] if name in fixes: # fixed point conversion @@ -72,6 +75,13 @@ def pack(fmt, obj): elif isinstance(value, str): value = tobytes(value) elements.append(value) + # Check it fits + try: + struct.pack(names[name], value) + except Exception as e: + raise ValueError( + "Value %s does not fit in format %s for %s" % (value, names[name], name) + ) from e data = struct.pack(*(formatstring,) + tuple(elements)) return data @@ -87,7 +97,7 @@ def unpack(fmt, data, obj=None): d = obj.__dict__ elements = struct.unpack(formatstring, data) for i in range(len(names)): - name = names[i] + name = list(names.keys())[i] value = elements[i] if name in fixes: # fixed point conversion @@ -141,7 +151,7 @@ def getformat(fmt, keep_pad_byte=False): except KeyError: lines = re.split("[\n;]", fmt) formatstring = "" - names = [] + names = {} fixes = {} for line in lines: if _emptyRE.match(line): @@ -158,7 +168,7 @@ def getformat(fmt, keep_pad_byte=False): name = m.group(1) formatchar = m.group(2) if keep_pad_byte or formatchar != "x": - names.append(name) + names[name] = formatchar if m.group(3): # fixed point before = int(m.group(3)) @@ -167,9 +177,10 @@ def getformat(fmt, keep_pad_byte=False): if bits not in [8, 16, 32]: raise Error("fixed point must be 8, 16 or 32 bits long") formatchar = _fixedpointmappings[bits] + names[name] = formatchar assert m.group(5) == "F" fixes[name] = after - formatstring = formatstring + formatchar + formatstring += formatchar _formatcache[fmt] = formatstring, names, fixes return formatstring, names, fixes diff --git a/contrib/python/fonttools/fontTools/misc/transform.py b/contrib/python/fonttools/fontTools/misc/transform.py index 0f9f3a5d8b..9025b79ec1 100644 --- a/contrib/python/fonttools/fontTools/misc/transform.py +++ b/contrib/python/fonttools/fontTools/misc/transform.py @@ -422,6 +422,19 @@ class DecomposedTransform: tCenterX: float = 0 tCenterY: float = 0 + def __bool__(self): + return ( + self.translateX != 0 + or self.translateY != 0 + or self.rotation != 0 + or self.scaleX != 1 + or self.scaleY != 1 + or self.skewX != 0 + or self.skewY != 0 + or self.tCenterX != 0 + or self.tCenterY != 0 + ) + @classmethod def fromTransform(self, transform): # Adapted from an answer on diff --git a/contrib/python/fonttools/fontTools/pens/svgPathPen.py b/contrib/python/fonttools/fontTools/pens/svgPathPen.py index 29d41a8029..8231603f8a 100644 --- a/contrib/python/fonttools/fontTools/pens/svgPathPen.py +++ b/contrib/python/fonttools/fontTools/pens/svgPathPen.py @@ -2,7 +2,7 @@ from typing import Callable from fontTools.pens.basePen import BasePen -def pointToString(pt, ntos=str): +def pointToString(pt, ntos): return " ".join(ntos(i) for i in pt) @@ -37,7 +37,13 @@ class SVGPathPen(BasePen): print(tpen.getCommands()) """ - def __init__(self, glyphSet, ntos: Callable[[float], str] = str): + def __init__( + self, + glyphSet, + ntos: Callable[[float], str] = ( + lambda x: ("%.2f" % x) if x != int(x) else str(int(x)) + ), + ): BasePen.__init__(self, glyphSet) self._commands = [] self._lastCommand = None diff --git a/contrib/python/fonttools/fontTools/qu2cu/__main__.py b/contrib/python/fonttools/fontTools/qu2cu/__main__.py index 27728cc7aa..7c85f61b41 100644 --- a/contrib/python/fonttools/fontTools/qu2cu/__main__.py +++ b/contrib/python/fonttools/fontTools/qu2cu/__main__.py @@ -1,6 +1,6 @@ import sys -from .cli import main +from .cli import _main as main if __name__ == "__main__": diff --git a/contrib/python/fonttools/fontTools/qu2cu/benchmark.py b/contrib/python/fonttools/fontTools/qu2cu/benchmark.py index cee55f5e7d..f28ad88cf9 100644 --- a/contrib/python/fonttools/fontTools/qu2cu/benchmark.py +++ b/contrib/python/fonttools/fontTools/qu2cu/benchmark.py @@ -48,7 +48,6 @@ def run_benchmark(module, function, setup_suffix="", repeat=25, number=1): def main(): - """Benchmark the qu2cu algorithm performance.""" run_benchmark("qu2cu", "quadratic_to_curves") diff --git a/contrib/python/fonttools/fontTools/qu2cu/cli.py b/contrib/python/fonttools/fontTools/qu2cu/cli.py index a07fd6dcd0..101e938a6f 100644 --- a/contrib/python/fonttools/fontTools/qu2cu/cli.py +++ b/contrib/python/fonttools/fontTools/qu2cu/cli.py @@ -42,7 +42,7 @@ def _font_to_cubic(input_path, output_path=None, **kwargs): font.save(output_path) -def main(args=None): +def _main(args=None): """Convert an OpenType font from quadratic to cubic curves""" parser = argparse.ArgumentParser(prog="qu2cu") parser.add_argument("--version", action="version", version=fontTools.__version__) diff --git a/contrib/python/fonttools/fontTools/subset/__init__.py b/contrib/python/fonttools/fontTools/subset/__init__.py index 250a07ef1a..4aa60ad842 100644 --- a/contrib/python/fonttools/fontTools/subset/__init__.py +++ b/contrib/python/fonttools/fontTools/subset/__init__.py @@ -14,7 +14,7 @@ from fontTools.misc.cliTools import makeOutputFileName from fontTools.subset.util import _add_method, _uniq_sort from fontTools.subset.cff import * from fontTools.subset.svg import * -from fontTools.varLib import varStore # for subset_varidxes +from fontTools.varLib import varStore, multiVarStore # For monkey-patching from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor import sys import struct @@ -2630,6 +2630,88 @@ def closure_glyphs(self, s): s.glyphs.update(variants) +@_add_method(ttLib.getTableClass("VARC")) +def subset_glyphs(self, s): + indices = self.table.Coverage.subset(s.glyphs) + self.table.VarCompositeGlyphs.VarCompositeGlyph = _list_subset( + self.table.VarCompositeGlyphs.VarCompositeGlyph, indices + ) + return bool(self.table.VarCompositeGlyphs.VarCompositeGlyph) + + +@_add_method(ttLib.getTableClass("VARC")) +def closure_glyphs(self, s): + if self.table.VarCompositeGlyphs is None: + return + + glyphMap = {glyphName: i for i, glyphName in enumerate(self.table.Coverage.glyphs)} + glyphRecords = self.table.VarCompositeGlyphs.VarCompositeGlyph + + glyphs = s.glyphs + covered = set() + new = set(glyphs) + while new: + oldNew = new + new = set() + for glyphName in oldNew: + if glyphName in covered: + continue + idx = glyphMap.get(glyphName) + if idx is None: + continue + glyph = glyphRecords[idx] + for comp in glyph.components: + name = comp.glyphName + glyphs.add(name) + if name not in covered: + new.add(name) + + +@_add_method(ttLib.getTableClass("VARC")) +def prune_post_subset(self, font, options): + table = self.table + + store = table.MultiVarStore + if store is not None: + usedVarIdxes = set() + table.collect_varidxes(usedVarIdxes) + varidx_map = store.subset_varidxes(usedVarIdxes) + table.remap_varidxes(varidx_map) + + axisIndicesList = table.AxisIndicesList.Item + if axisIndicesList is not None: + usedIndices = set() + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.axisIndicesIndex is not None: + usedIndices.add(comp.axisIndicesIndex) + usedIndices = sorted(usedIndices) + table.AxisIndicesList.Item = _list_subset(axisIndicesList, usedIndices) + mapping = {old: new for new, old in enumerate(usedIndices)} + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.axisIndicesIndex is not None: + comp.axisIndicesIndex = mapping[comp.axisIndicesIndex] + + conditionList = table.ConditionList + if conditionList is not None: + conditionTables = conditionList.ConditionTable + usedIndices = set() + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.conditionIndex is not None: + usedIndices.add(comp.conditionIndex) + usedIndices = sorted(usedIndices) + conditionList.ConditionTable = _list_subset(conditionTables, usedIndices) + mapping = {old: new for new, old in enumerate(usedIndices)} + for glyph in table.VarCompositeGlyphs.VarCompositeGlyph: + for comp in glyph.components: + if comp.conditionIndex is not None: + comp.conditionIndex = mapping[comp.conditionIndex] + + return True + + @_add_method(ttLib.getTableClass("MATH")) def closure_glyphs(self, s): if self.table.MathVariants: @@ -2913,8 +2995,9 @@ def prune_post_subset(self, font, options): visitor = NameRecordVisitor() visitor.visit(font) nameIDs = set(options.name_IDs) | visitor.seen - if "*" not in options.name_IDs: - self.names = [n for n in self.names if n.nameID in nameIDs] + if "*" in options.name_IDs: + nameIDs |= {n.nameID for n in self.names if n.nameID < 256} + self.names = [n for n in self.names if n.nameID in nameIDs] if not options.name_legacy: # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman # entry for Latin and no Unicode names. @@ -3297,33 +3380,33 @@ class Subsetter(object): self.glyphs.add(font.getGlyphName(i)) log.info("Added first four glyphs to subset") - if self.options.layout_closure and "GSUB" in font: - with timer("close glyph list over 'GSUB'"): + if "MATH" in font: + with timer("close glyph list over 'MATH'"): log.info( - "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs) + "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs) ) log.glyphs(self.glyphs, font=font) - font["GSUB"].closure_glyphs(self) + font["MATH"].closure_glyphs(self) self.glyphs.intersection_update(realGlyphs) log.info( - "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs) + "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs) ) log.glyphs(self.glyphs, font=font) - self.glyphs_gsubed = frozenset(self.glyphs) + self.glyphs_mathed = frozenset(self.glyphs) - if "MATH" in font: - with timer("close glyph list over 'MATH'"): + if self.options.layout_closure and "GSUB" in font: + with timer("close glyph list over 'GSUB'"): log.info( - "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs) + "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs) ) log.glyphs(self.glyphs, font=font) - font["MATH"].closure_glyphs(self) + font["GSUB"].closure_glyphs(self) self.glyphs.intersection_update(realGlyphs) log.info( - "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs) + "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs) ) log.glyphs(self.glyphs, font=font) - self.glyphs_mathed = frozenset(self.glyphs) + self.glyphs_gsubed = frozenset(self.glyphs) for table in ("COLR", "bsln"): if table in font: @@ -3344,6 +3427,20 @@ class Subsetter(object): log.glyphs(self.glyphs, font=font) setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs)) + if "VARC" in font: + with timer("close glyph list over 'VARC'"): + log.info( + "Closing glyph list over 'VARC': %d glyphs before", len(self.glyphs) + ) + log.glyphs(self.glyphs, font=font) + font["VARC"].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info( + "Closed glyph list over 'VARC': %d glyphs after", len(self.glyphs) + ) + log.glyphs(self.glyphs, font=font) + self.glyphs_glyfed = frozenset(self.glyphs) + if "glyf" in font: with timer("close glyph list over 'glyf'"): log.info( diff --git a/contrib/python/fonttools/fontTools/subset/cff.py b/contrib/python/fonttools/fontTools/subset/cff.py index 03fc565b31..cef0a36864 100644 --- a/contrib/python/fonttools/fontTools/subset/cff.py +++ b/contrib/python/fonttools/fontTools/subset/cff.py @@ -132,227 +132,6 @@ def subset_glyphs(self, s): return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) -@_add_method(psCharStrings.T2CharString) -def subset_subroutines(self, subrs, gsubrs): - p = self.program - for i in range(1, len(p)): - if p[i] == "callsubr": - assert isinstance(p[i - 1], int) - p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias - elif p[i] == "callgsubr": - assert isinstance(p[i - 1], int) - p[i - 1] = ( - gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias - ) - - -@_add_method(psCharStrings.T2CharString) -def drop_hints(self): - hints = self._hints - - if hints.deletions: - p = self.program - for idx in reversed(hints.deletions): - del p[idx - 2 : idx] - - if hints.has_hint: - assert not hints.deletions or hints.last_hint <= hints.deletions[0] - self.program = self.program[hints.last_hint :] - if not self.program: - # TODO CFF2 no need for endchar. - self.program.append("endchar") - if hasattr(self, "width"): - # Insert width back if needed - if self.width != self.private.defaultWidthX: - # For CFF2 charstrings, this should never happen - assert ( - self.private.defaultWidthX is not None - ), "CFF2 CharStrings must not have an initial width value" - self.program.insert(0, self.width - self.private.nominalWidthX) - - if hints.has_hintmask: - i = 0 - p = self.program - while i < len(p): - if p[i] in ["hintmask", "cntrmask"]: - assert i + 1 <= len(p) - del p[i : i + 2] - continue - i += 1 - - assert len(self.program) - - del self._hints - - -class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): - def __init__(self, localSubrs, globalSubrs, private): - psCharStrings.SimpleT2Decompiler.__init__( - self, localSubrs, globalSubrs, private - ) - for subrs in [localSubrs, globalSubrs]: - if subrs and not hasattr(subrs, "_used"): - subrs._used = set() - - def op_callsubr(self, index): - self.localSubrs._used.add(self.operandStack[-1] + self.localBias) - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - - def op_callgsubr(self, index): - self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias) - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - - -class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): - class Hints(object): - def __init__(self): - # Whether calling this charstring produces any hint stems - # Note that if a charstring starts with hintmask, it will - # have has_hint set to True, because it *might* produce an - # implicit vstem if called under certain conditions. - self.has_hint = False - # Index to start at to drop all hints - self.last_hint = 0 - # Index up to which we know more hints are possible. - # Only relevant if status is 0 or 1. - self.last_checked = 0 - # The status means: - # 0: after dropping hints, this charstring is empty - # 1: after dropping hints, there may be more hints - # continuing after this, or there might be - # other things. Not clear yet. - # 2: no more hints possible after this charstring - self.status = 0 - # Has hintmask instructions; not recursive - self.has_hintmask = False - # List of indices of calls to empty subroutines to remove. - self.deletions = [] - - pass - - def __init__( - self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None - ): - self._css = css - psCharStrings.T2WidthExtractor.__init__( - self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX - ) - self.private = private - - def execute(self, charString): - old_hints = charString._hints if hasattr(charString, "_hints") else None - charString._hints = self.Hints() - - psCharStrings.T2WidthExtractor.execute(self, charString) - - hints = charString._hints - - if hints.has_hint or hints.has_hintmask: - self._css.add(charString) - - if hints.status != 2: - # Check from last_check, make sure we didn't have any operators. - for i in range(hints.last_checked, len(charString.program) - 1): - if isinstance(charString.program[i], str): - hints.status = 2 - break - else: - hints.status = 1 # There's *something* here - hints.last_checked = len(charString.program) - - if old_hints: - assert hints.__dict__ == old_hints.__dict__ - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1] + self.localBias] - psCharStrings.T2WidthExtractor.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] - psCharStrings.T2WidthExtractor.op_callgsubr(self, index) - self.processSubr(index, subr) - - def op_hstem(self, index): - psCharStrings.T2WidthExtractor.op_hstem(self, index) - self.processHint(index) - - def op_vstem(self, index): - psCharStrings.T2WidthExtractor.op_vstem(self, index) - self.processHint(index) - - def op_hstemhm(self, index): - psCharStrings.T2WidthExtractor.op_hstemhm(self, index) - self.processHint(index) - - def op_vstemhm(self, index): - psCharStrings.T2WidthExtractor.op_vstemhm(self, index) - self.processHint(index) - - def op_hintmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) - self.processHintmask(index) - return rv - - def op_cntrmask(self, index): - rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) - self.processHintmask(index) - return rv - - def processHintmask(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hintmask = True - if hints.status != 2: - # Check from last_check, see if we may be an implicit vstem - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - else: - # We are an implicit vstem - hints.has_hint = True - hints.last_hint = index + 1 - hints.status = 0 - hints.last_checked = index + 1 - - def processHint(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hint = True - hints.last_hint = index - hints.last_checked = index - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - hints = cs._hints - subr_hints = subr._hints - - # Check from last_check, make sure we didn't have - # any operators. - if hints.status != 2: - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break - hints.last_checked = index - - if hints.status != 2: - if subr_hints.has_hint: - hints.has_hint = True - - # Decide where to chop off from - if subr_hints.status == 0: - hints.last_hint = index - else: - hints.last_hint = index - 2 # Leave the subr call in - - elif subr_hints.status == 0: - hints.deletions.append(index) - - hints.status = max(hints.status, subr_hints.status) - - @_add_method(ttLib.getTableClass("CFF ")) def prune_post_subset(self, ttfFont, options): cff = self.cff @@ -381,13 +160,6 @@ def prune_post_subset(self, ttfFont, options): return True -def _delete_empty_subrs(private_dict): - if hasattr(private_dict, "Subrs") and not private_dict.Subrs: - if "Subrs" in private_dict.rawDict: - del private_dict.rawDict["Subrs"] - del private_dict.Subrs - - @deprecateFunction( "use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning ) @@ -396,141 +168,17 @@ def desubroutinize(self): self.cff.desubroutinize() +@deprecateFunction( + "use 'CFFFontSet.remove_hints()' instead", category=DeprecationWarning +) @_add_method(ttLib.getTableClass("CFF ")) def remove_hints(self): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - # This can be tricky, but doesn't have to. What we do is: - # - # - Run all used glyph charstrings and recurse into subroutines, - # - For each charstring (including subroutines), if it has any - # of the hint stem operators, we mark it as such. - # Upon returning, for each charstring we note all the - # subroutine calls it makes that (recursively) contain a stem, - # - Dropping hinting then consists of the following two ops: - # * Drop the piece of the program in each charstring before the - # last call to a stem op or a stem-calling subroutine, - # * Drop all hintmask operations. - # - It's trickier... A hintmask right after hints and a few numbers - # will act as an implicit vstemhm. As such, we track whether - # we have seen any non-hint operators so far and do the right - # thing, recursively... Good luck understanding that :( - css = set() - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DehintingT2Decompiler( - css, - subrs, - c.globalSubrs, - c.private.nominalWidthX, - c.private.defaultWidthX, - c.private, - ) - decompiler.execute(c) - c.width = decompiler.width - for charstring in css: - charstring.drop_hints() - del css - - # Drop font-wide hinting values - all_privs = [] - if hasattr(font, "FDArray"): - all_privs.extend(fd.Private for fd in font.FDArray) - else: - all_privs.append(font.Private) - for priv in all_privs: - for k in [ - "BlueValues", - "OtherBlues", - "FamilyBlues", - "FamilyOtherBlues", - "BlueScale", - "BlueShift", - "BlueFuzz", - "StemSnapH", - "StemSnapV", - "StdHW", - "StdVW", - "ForceBold", - "LanguageGroup", - "ExpansionFactor", - ]: - if hasattr(priv, k): - setattr(priv, k, None) - self.remove_unused_subroutines() + self.cff.remove_hints() +@deprecateFunction( + "use 'CFFFontSet.remove_unused_subroutines' instead", category=DeprecationWarning +) @_add_method(ttLib.getTableClass("CFF ")) def remove_unused_subroutines(self): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - # Renumber subroutines to remove unused ones - - # Mark all used subroutines - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) - decompiler.execute(c) - - all_subrs = [font.GlobalSubrs] - if hasattr(font, "FDArray"): - all_subrs.extend( - fd.Private.Subrs - for fd in font.FDArray - if hasattr(fd.Private, "Subrs") and fd.Private.Subrs - ) - elif hasattr(font.Private, "Subrs") and font.Private.Subrs: - all_subrs.append(font.Private.Subrs) - - subrs = set(subrs) # Remove duplicates - - # Prepare - for subrs in all_subrs: - if not hasattr(subrs, "_used"): - subrs._used = set() - subrs._used = _uniq_sort(subrs._used) - subrs._old_bias = psCharStrings.calcSubrBias(subrs) - subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) - - # Renumber glyph charstrings - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", None) - c.subset_subroutines(subrs, font.GlobalSubrs) - - # Renumber subroutines themselves - for subrs in all_subrs: - if subrs == font.GlobalSubrs: - if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): - local_subrs = font.Private.Subrs - else: - local_subrs = None - else: - local_subrs = subrs - - subrs.items = [subrs.items[i] for i in subrs._used] - if hasattr(subrs, "file"): - del subrs.file - if hasattr(subrs, "offsets"): - del subrs.offsets - - for subr in subrs.items: - subr.subset_subroutines(local_subrs, font.GlobalSubrs) - - # Delete local SubrsIndex if empty - if hasattr(font, "FDArray"): - for fd in font.FDArray: - _delete_empty_subrs(fd.Private) - else: - _delete_empty_subrs(font.Private) - - # Cleanup - for subrs in all_subrs: - del subrs._used, subrs._old_bias, subrs._new_bias + self.cff.remove_unused_subroutines() diff --git a/contrib/python/fonttools/fontTools/ttLib/__main__.py b/contrib/python/fonttools/fontTools/ttLib/__main__.py index 2733444d8b..a7ba7c76a9 100644 --- a/contrib/python/fonttools/fontTools/ttLib/__main__.py +++ b/contrib/python/fonttools/fontTools/ttLib/__main__.py @@ -77,7 +77,7 @@ def main(args=None): outFile = options.output lazy = options.lazy flavor = options.flavor - tables = options.table if options.table is not None else [] + tables = options.table if options.table is not None else ["*"] fonts = [] for f in options.font: @@ -88,9 +88,10 @@ def main(args=None): collection = TTCollection(f, lazy=lazy) fonts.extend(collection.fonts) - for font in fonts: - for table in tables if "*" not in tables else font.keys(): - font[table] # Decompiles + if lazy is False: + for font in fonts: + for table in tables if "*" not in tables else font.keys(): + font[table] # Decompiles if outFile is not None: if len(fonts) == 1: diff --git a/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py b/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py index 4795320669..ea13d4734b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py +++ b/contrib/python/fonttools/fontTools/ttLib/removeOverlaps.py @@ -226,6 +226,8 @@ def removeOverlaps( def main(args=None): + """Simplify glyphs in TTFont by merging overlapping contours.""" + import sys if args is None: diff --git a/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py b/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py index 2909bfcb2c..68709825b3 100644 --- a/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py +++ b/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py @@ -10,8 +10,10 @@ import fontTools.ttLib.tables.otTables as otTables from fontTools.cffLib import VarStoreData import fontTools.cffLib.specializer as cffSpecializer from fontTools.varLib import builder # for VarData.calculateNumShorts +from fontTools.varLib.multiVarStore import OnlineMultiVarStoreBuilder +from fontTools.misc.vector import Vector from fontTools.misc.fixedTools import otRound -from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags +from fontTools.misc.iterTools import batched __all__ = ["scale_upem", "ScalerVisitor"] @@ -123,13 +125,6 @@ def visit(visitor, obj, attr, glyphs): component.y = visitor.scale(component.y) continue - if g.isVarComposite(): - for component in g.components: - for attr in ("translateX", "translateY", "tCenterX", "tCenterY"): - v = getattr(component.transform, attr) - setattr(component.transform, attr, visitor.scale(v)) - continue - if hasattr(g, "coordinates"): coordinates = g.coordinates for i, (x, y) in enumerate(coordinates): @@ -138,57 +133,105 @@ def visit(visitor, obj, attr, glyphs): @ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations") def visit(visitor, obj, attr, variations): - # VarComposites are a pain to handle :-( glyfTable = visitor.font["glyf"] for glyphName, varlist in variations.items(): glyph = glyfTable[glyphName] - isVarComposite = glyph.isVarComposite() for var in varlist: coordinates = var.coordinates + for i, xy in enumerate(coordinates): + if xy is None: + continue + coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - if not isVarComposite: - for i, xy in enumerate(coordinates): - if xy is None: - continue - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - continue - # VarComposite glyph - - i = 0 - for component in glyph.components: - if component.flags & VarComponentFlags.AXES_HAVE_VARIATION: - i += len(component.location) - if component.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X - | VarComponentFlags.HAVE_TRANSLATE_Y - ): - xy = coordinates[i] - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - i += 1 - if component.flags & VarComponentFlags.HAVE_ROTATION: - i += 1 - if component.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - i += 1 - if component.flags & ( - VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y - ): - i += 1 - if component.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - xy = coordinates[i] - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) - i += 1 - - # Phantom points - assert i + 4 == len(coordinates) - for i in range(i, len(coordinates)): - xy = coordinates[i] - coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) +@ScalerVisitor.register_attr(ttLib.getTableClass("VARC"), "table") +def visit(visitor, obj, attr, varc): + # VarComposite variations are a pain + + fvar = visitor.font["fvar"] + fvarAxes = [a.axisTag for a in fvar.axes] + + store = varc.MultiVarStore + storeBuilder = OnlineMultiVarStoreBuilder(fvarAxes) + + for g in varc.VarCompositeGlyphs.VarCompositeGlyph: + for component in g.components: + t = component.transform + t.translateX = visitor.scale(t.translateX) + t.translateY = visitor.scale(t.translateY) + t.tCenterX = visitor.scale(t.tCenterX) + t.tCenterY = visitor.scale(t.tCenterY) + + if component.axisValuesVarIndex != otTables.NO_VARIATION_INDEX: + varIdx = component.axisValuesVarIndex + # TODO Move this code duplicated below to MultiVarStore.__getitem__, + # or a getDeltasAndSupports(). + if varIdx != otTables.NO_VARIATION_INDEX: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + varData = store.MultiVarData[major] + vec = varData.Item[minor] + storeBuilder.setSupports(store.get_supports(major, fvar.axes)) + if vec: + m = len(vec) // varData.VarRegionCount + vec = list(batched(vec, m)) + vec = [Vector(v) for v in vec] + component.axisValuesVarIndex = storeBuilder.storeDeltas(vec) + else: + component.axisValuesVarIndex = otTables.NO_VARIATION_INDEX + + if component.transformVarIndex != otTables.NO_VARIATION_INDEX: + varIdx = component.transformVarIndex + if varIdx != otTables.NO_VARIATION_INDEX: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + vec = varData.Item[varIdx & 0xFFFF] + major = varIdx >> 16 + minor = varIdx & 0xFFFF + varData = store.MultiVarData[major] + vec = varData.Item[minor] + storeBuilder.setSupports(store.get_supports(major, fvar.axes)) + if vec: + m = len(vec) // varData.VarRegionCount + flags = component.flags + vec = list(batched(vec, m)) + newVec = [] + for v in vec: + v = list(v) + i = 0 + ## Scale translate & tCenter + if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_X: + v[i] = visitor.scale(v[i]) + i += 1 + if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_Y: + v[i] = visitor.scale(v[i]) + i += 1 + if flags & otTables.VarComponentFlags.HAVE_ROTATION: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SCALE_X: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SCALE_Y: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SKEW_X: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_SKEW_Y: + i += 1 + if flags & otTables.VarComponentFlags.HAVE_TCENTER_X: + v[i] = visitor.scale(v[i]) + i += 1 + if flags & otTables.VarComponentFlags.HAVE_TCENTER_Y: + v[i] = visitor.scale(v[i]) + i += 1 + + newVec.append(Vector(v)) + vec = newVec + + component.transformVarIndex = storeBuilder.storeDeltas(vec) + else: + component.transformVarIndex = otTables.NO_VARIATION_INDEX + + varc.MultiVarStore = storeBuilder.finish() @ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables") diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py index 6046d9100b..23d57964f6 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py @@ -298,9 +298,9 @@ class BitmapSizeTable(object): # cares about in terms of XML creation. def _getXMLMetricNames(self): dataNames = sstruct.getformat(bitmapSizeTableFormatPart1)[1] - dataNames = dataNames + sstruct.getformat(bitmapSizeTableFormatPart2)[1] + dataNames = {**dataNames, **sstruct.getformat(bitmapSizeTableFormatPart2)[1]} # Skip the first 3 data names because they are byte offsets and counts. - return dataNames[3:] + return list(dataNames.keys())[3:] def toXML(self, writer, ttFont): writer.begintag("bitmapSizeTable") diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py index 027ac15342..a98bca2e0e 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py @@ -22,6 +22,8 @@ PRIVATE_POINT_NUMBERS = 0x2000 DELTAS_ARE_ZERO = 0x80 DELTAS_ARE_WORDS = 0x40 +DELTAS_ARE_LONGS = 0xC0 +DELTAS_SIZE_MASK = 0xC0 DELTA_RUN_COUNT_MASK = 0x3F POINTS_ARE_WORDS = 0x80 @@ -366,8 +368,10 @@ class TupleVariation(object): pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) elif -128 <= value <= 127: pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) - else: + elif -32768 <= value <= 32767: pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) + else: + pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) return bytearr @staticmethod @@ -420,6 +424,7 @@ class TupleVariation(object): numDeltas = len(deltas) while pos < numDeltas: value = deltas[pos] + # Within a word-encoded run of deltas, it is easiest # to start a new run (with a different encoding) # whenever we encounter a zero value. For example, @@ -442,6 +447,10 @@ class TupleVariation(object): and (-128 <= deltas[pos + 1] <= 127) ): break + + if not (-32768 <= value <= 32767): + break + pos += 1 runLength = pos - offset while runLength >= 64: @@ -461,18 +470,47 @@ class TupleVariation(object): return pos @staticmethod - def decompileDeltas_(numDeltas, data, offset): + def encodeDeltaRunAsLongs_(deltas, offset, bytearr): + pos = offset + numDeltas = len(deltas) + while pos < numDeltas: + value = deltas[pos] + if -32768 <= value <= 32767: + break + pos += 1 + runLength = pos - offset + while runLength >= 64: + bytearr.append(DELTAS_ARE_LONGS | 63) + a = array.array("i", deltas[offset : offset + 64]) + if sys.byteorder != "big": + a.byteswap() + bytearr.extend(a) + offset += 64 + runLength -= 64 + if runLength: + bytearr.append(DELTAS_ARE_LONGS | (runLength - 1)) + a = array.array("i", deltas[offset:pos]) + if sys.byteorder != "big": + a.byteswap() + bytearr.extend(a) + return pos + + @staticmethod + def decompileDeltas_(numDeltas, data, offset=0): """(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)""" result = [] pos = offset - while len(result) < numDeltas: + while len(result) < numDeltas if numDeltas is not None else pos < len(data): runHeader = data[pos] pos += 1 numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1 - if (runHeader & DELTAS_ARE_ZERO) != 0: + if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_ZERO: result.extend([0] * numDeltasInRun) else: - if (runHeader & DELTAS_ARE_WORDS) != 0: + if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_LONGS: + deltas = array.array("i") + deltasSize = numDeltasInRun * 4 + elif (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS: deltas = array.array("h") deltasSize = numDeltasInRun * 2 else: @@ -481,10 +519,10 @@ class TupleVariation(object): deltas.frombytes(data[pos : pos + deltasSize]) if sys.byteorder != "big": deltas.byteswap() - assert len(deltas) == numDeltasInRun + assert len(deltas) == numDeltasInRun, (len(deltas), numDeltasInRun) pos += deltasSize result.extend(deltas) - assert len(result) == numDeltas + assert numDeltas is None or len(result) == numDeltas return (result, pos) @staticmethod diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py b/contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py new file mode 100644 index 0000000000..5a00887160 --- /dev/null +++ b/contrib/python/fonttools/fontTools/ttLib/tables/V_A_R_C_.py @@ -0,0 +1,5 @@ +from .otBase import BaseTTXConverter + + +class table_V_A_R_C_(BaseTTXConverter): + pass diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py index 39039cf73a..6ea4132b3d 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_a_v_a_r.py @@ -6,6 +6,9 @@ from fontTools.misc.fixedTools import ( strToFixedToFloat as str2fl, ) from fontTools.misc.textTools import bytesjoin, safeEval +from fontTools.misc.roundTools import otRound +from fontTools.varLib.models import piecewiseLinearMap +from fontTools.varLib.varStore import VarStoreInstancer, NO_VARIATION_INDEX from fontTools.ttLib import TTLibError from . import DefaultTable from . import otTables @@ -74,9 +77,10 @@ class table__a_v_a_r(BaseTTXConverter): def decompile(self, data, ttFont): super().decompile(data, ttFont) - assert self.table.Version >= 0x00010000 self.majorVersion = self.table.Version >> 16 self.minorVersion = self.table.Version & 0xFFFF + if self.majorVersion not in (1, 2): + raise NotImplementedError("Unknown avar table version") axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] for axis in axisTags: self.segments[axis] = {} @@ -136,3 +140,48 @@ class table__a_v_a_r(BaseTTXConverter): segment[fromValue] = toValue else: super().fromXML(name, attrs, content, ttFont) + + def renormalizeLocation(self, location, font): + + if self.majorVersion not in (1, 2): + raise NotImplementedError("Unknown avar table version") + + avarSegments = self.segments + mappedLocation = {} + for axisTag, value in location.items(): + avarMapping = avarSegments.get(axisTag, None) + if avarMapping is not None: + value = piecewiseLinearMap(value, avarMapping) + mappedLocation[axisTag] = value + + if self.majorVersion < 2: + return mappedLocation + + # Version 2 + + varIdxMap = self.table.VarIdxMap + varStore = self.table.VarStore + axes = font["fvar"].axes + if varStore is not None: + instancer = VarStoreInstancer(varStore, axes, mappedLocation) + + coords = list(fl2fi(mappedLocation.get(axis.axisTag, 0), 14) for axis in axes) + + out = [] + for varIdx, v in enumerate(coords): + + if varIdxMap is not None: + varIdx = varIdxMap[varIdx] + + if varStore is not None: + delta = instancer[varIdx] + v += otRound(delta) + v = min(max(v, -(1 << 14)), +(1 << 14)) + + out.append(v) + + mappedLocation = { + axis.axisTag: fi2fl(v, 14) for v, axis in zip(out, axes) if v != 0 + } + + return mappedLocation diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py index 062a9aa429..a3bdacd4cc 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py @@ -110,6 +110,9 @@ class table__f_v_a_r(DefaultTable.DefaultTable): instance.fromXML(name, attrs, content, ttFont) self.instances.append(instance) + def getAxes(self): + return {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in self.axes} + class Axis(object): def __init__(self): 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 683912be97..fa11cf8f47 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 @@ -424,29 +424,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable): for c in glyph.components ], ) - elif glyph.isVarComposite(): - coords = [] - controls = [] - - for component in glyph.components: - ( - componentCoords, - componentControls, - ) = component.getCoordinatesAndControls() - coords.extend(componentCoords) - controls.extend(componentControls) - - coords = GlyphCoordinates(coords) - - controls = _GlyphControls( - numberOfContours=glyph.numberOfContours, - endPts=list(range(len(coords))), - flags=None, - components=[ - (c.glyphName, getattr(c, "flags", None)) for c in glyph.components - ], - ) - else: coords, endPts, flags = glyph.getCoordinates(self) coords = coords.copy() @@ -492,10 +469,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable): for p, comp in zip(coord, glyph.components): if hasattr(comp, "x"): comp.x, comp.y = p - elif glyph.isVarComposite(): - for comp in glyph.components: - coord = comp.setCoordinates(coord) - assert not coord elif glyph.numberOfContours == 0: assert len(coord) == 0 else: @@ -737,8 +710,6 @@ class Glyph(object): return if self.isComposite(): self.decompileComponents(data, glyfTable) - elif self.isVarComposite(): - self.decompileVarComponents(data, glyfTable) else: self.decompileCoordinates(data) @@ -758,8 +729,6 @@ class Glyph(object): data = sstruct.pack(glyphHeaderFormat, self) if self.isComposite(): data = data + self.compileComponents(glyfTable) - elif self.isVarComposite(): - data = data + self.compileVarComponents(glyfTable) else: data = data + self.compileCoordinates() return data @@ -769,10 +738,6 @@ class Glyph(object): for compo in self.components: compo.toXML(writer, ttFont) haveInstructions = hasattr(self, "program") - elif self.isVarComposite(): - for compo in self.components: - compo.toXML(writer, ttFont) - haveInstructions = False else: last = 0 for i in range(self.numberOfContours): @@ -842,15 +807,6 @@ class Glyph(object): component = GlyphComponent() self.components.append(component) component.fromXML(name, attrs, content, ttFont) - elif name == "varComponent": - if self.numberOfContours > 0: - raise ttLib.TTLibError("can't mix composites and contours in glyph") - self.numberOfContours = -2 - if not hasattr(self, "components"): - self.components = [] - component = GlyphVarComponent() - self.components.append(component) - component.fromXML(name, attrs, content, ttFont) elif name == "instructions": self.program = ttProgram.Program() for element in content: @@ -860,7 +816,7 @@ class Glyph(object): self.program.fromXML(name, attrs, content, ttFont) def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): - assert self.isComposite() or self.isVarComposite() + assert self.isComposite() nContours = 0 nPoints = 0 initialMaxComponentDepth = maxComponentDepth @@ -904,13 +860,6 @@ class Glyph(object): len(data), ) - def decompileVarComponents(self, data, glyfTable): - self.components = [] - while len(data) >= GlyphVarComponent.MIN_SIZE: - component = GlyphVarComponent() - data = component.decompile(data, glyfTable) - self.components.append(component) - def decompileCoordinates(self, data): endPtsOfContours = array.array("H") endPtsOfContours.frombytes(data[: 2 * self.numberOfContours]) @@ -1027,9 +976,6 @@ class Glyph(object): data = data + struct.pack(">h", len(instructions)) + instructions return data - def compileVarComponents(self, glyfTable): - return b"".join(c.compile(glyfTable) for c in self.components) - def compileCoordinates(self): assert len(self.coordinates) == len(self.flags) data = [] @@ -1231,13 +1177,6 @@ class Glyph(object): else: return self.numberOfContours == -1 - def isVarComposite(self): - """Test whether a glyph has variable components""" - if hasattr(self, "data"): - return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False - else: - return self.numberOfContours == -2 - def getCoordinates(self, glyfTable): """Return the coordinates, end points and flags @@ -1308,8 +1247,6 @@ class Glyph(object): allCoords.extend(coordinates) allFlags.extend(flags) return allCoords, allEndPts, allFlags - elif self.isVarComposite(): - raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs") else: return GlyphCoordinates(), [], bytearray() @@ -1319,12 +1256,8 @@ class Glyph(object): This method can be used on simple glyphs (in which case it returns an empty list) or composite glyphs. """ - if hasattr(self, "data") and self.isVarComposite(): - # TODO(VarComposite) Add implementation without expanding glyph - self.expand(glyfTable) - if not hasattr(self, "data"): - if self.isComposite() or self.isVarComposite(): + if self.isComposite(): return [c.glyphName for c in self.components] else: return [] @@ -1367,8 +1300,6 @@ class Glyph(object): if self.isComposite(): if hasattr(self, "program"): del self.program - elif self.isVarComposite(): - pass # Doesn't have hinting else: self.program = ttProgram.Program() self.program.fromBytecode([]) @@ -1450,13 +1381,6 @@ class Glyph(object): i += 2 + instructionLen # Remove padding data = data[:i] - elif self.isVarComposite(): - i = 0 - MIN_SIZE = GlyphVarComponent.MIN_SIZE - while len(data[i : i + MIN_SIZE]) >= MIN_SIZE: - size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE]) - i += size - data = data[:i] self.data = data @@ -1942,391 +1866,6 @@ class GlyphComponent(object): return result if result is NotImplemented else not result -# -# Variable Composite glyphs -# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md -# - - -class VarComponentFlags(IntFlag): - USE_MY_METRICS = 0x0001 - AXIS_INDICES_ARE_SHORT = 0x0002 - UNIFORM_SCALE = 0x0004 - HAVE_TRANSLATE_X = 0x0008 - HAVE_TRANSLATE_Y = 0x0010 - HAVE_ROTATION = 0x0020 - HAVE_SCALE_X = 0x0040 - HAVE_SCALE_Y = 0x0080 - HAVE_SKEW_X = 0x0100 - HAVE_SKEW_Y = 0x0200 - HAVE_TCENTER_X = 0x0400 - HAVE_TCENTER_Y = 0x0800 - GID_IS_24BIT = 0x1000 - AXES_HAVE_VARIATION = 0x2000 - RESET_UNSPECIFIED_AXES = 0x4000 - - -VarComponentTransformMappingValues = namedtuple( - "VarComponentTransformMappingValues", - ["flag", "fractionalBits", "scale", "defaultValue"], -) - -VAR_COMPONENT_TRANSFORM_MAPPING = { - "translateX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 - ), - "translateY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 - ), - "rotation": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_ROTATION, 12, 180, 0 - ), - "scaleX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SCALE_X, 10, 1, 1 - ), - "scaleY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1 - ), - "skewX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SKEW_X, 12, -180, 0 - ), - "skewY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0 - ), - "tCenterX": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0 - ), - "tCenterY": VarComponentTransformMappingValues( - VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0 - ), -} - - -class GlyphVarComponent(object): - MIN_SIZE = 5 - - def __init__(self): - self.location = {} - self.transform = DecomposedTransform() - - @staticmethod - def getSize(data): - size = 5 - flags = struct.unpack(">H", data[:2])[0] - numAxes = int(data[2]) - - if flags & VarComponentFlags.GID_IS_24BIT: - size += 1 - - size += numAxes - if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: - size += 2 * numAxes - else: - axisIndices = array.array("B", data[:numAxes]) - size += numAxes - - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - if flags & mapping_values.flag: - size += 2 - - return size - - def decompile(self, data, glyfTable): - flags = struct.unpack(">H", data[:2])[0] - self.flags = int(flags) - data = data[2:] - - numAxes = int(data[0]) - data = data[1:] - - if flags & VarComponentFlags.GID_IS_24BIT: - glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0]) - data = data[3:] - flags ^= VarComponentFlags.GID_IS_24BIT - else: - glyphID = int(struct.unpack(">H", data[:2])[0]) - data = data[2:] - self.glyphName = glyfTable.getGlyphName(int(glyphID)) - - if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: - axisIndices = array.array("H", data[: 2 * numAxes]) - if sys.byteorder != "big": - axisIndices.byteswap() - data = data[2 * numAxes :] - flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT - else: - axisIndices = array.array("B", data[:numAxes]) - data = data[numAxes:] - assert len(axisIndices) == numAxes - axisIndices = list(axisIndices) - - axisValues = array.array("h", data[: 2 * numAxes]) - if sys.byteorder != "big": - axisValues.byteswap() - data = data[2 * numAxes :] - assert len(axisValues) == numAxes - axisValues = [fi2fl(v, 14) for v in axisValues] - - self.location = { - glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues) - } - - def read_transform_component(data, values): - if flags & values.flag: - return ( - data[2:], - fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits) - * values.scale, - ) - else: - return data, values.defaultValue - - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - data, value = read_transform_component(data, mapping_values) - setattr(self.transform, attr_name, value) - - if flags & VarComponentFlags.UNIFORM_SCALE: - if flags & VarComponentFlags.HAVE_SCALE_X and not ( - flags & VarComponentFlags.HAVE_SCALE_Y - ): - self.transform.scaleY = self.transform.scaleX - flags |= VarComponentFlags.HAVE_SCALE_Y - flags ^= VarComponentFlags.UNIFORM_SCALE - - return data - - def compile(self, glyfTable): - data = b"" - - if not hasattr(self, "flags"): - flags = 0 - # Calculate optimal transform component flags - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = getattr(self.transform, attr_name) - if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi( - mapping.defaultValue / mapping.scale, mapping.fractionalBits - ): - flags |= mapping.flag - else: - flags = self.flags - - if ( - flags & VarComponentFlags.HAVE_SCALE_X - and flags & VarComponentFlags.HAVE_SCALE_Y - and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10) - ): - flags |= VarComponentFlags.UNIFORM_SCALE - flags ^= VarComponentFlags.HAVE_SCALE_Y - - numAxes = len(self.location) - - data = data + struct.pack(">B", numAxes) - - glyphID = glyfTable.getGlyphID(self.glyphName) - if glyphID > 65535: - flags |= VarComponentFlags.GID_IS_24BIT - data = data + struct.pack(">L", glyphID)[1:] - else: - data = data + struct.pack(">H", glyphID) - - axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()] - if all(a <= 255 for a in axisIndices): - axisIndices = array.array("B", axisIndices) - else: - axisIndices = array.array("H", axisIndices) - if sys.byteorder != "big": - axisIndices.byteswap() - flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT - data = data + bytes(axisIndices) - - axisValues = self.location.values() - axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) - if sys.byteorder != "big": - axisValues.byteswap() - data = data + bytes(axisValues) - - def write_transform_component(data, value, values): - if flags & values.flag: - return data + struct.pack( - ">h", fl2fi(value / values.scale, values.fractionalBits) - ) - else: - return data - - for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - value = getattr(self.transform, attr_name) - data = write_transform_component(data, value, mapping_values) - - return struct.pack(">H", flags) + data - - def toXML(self, writer, ttFont): - attrs = [("glyphName", self.glyphName)] - - if hasattr(self, "flags"): - attrs = attrs + [("flags", hex(self.flags))] - - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - v = getattr(self.transform, attr_name) - if v != mapping.defaultValue: - attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) - - writer.begintag("varComponent", attrs) - writer.newline() - - writer.begintag("location") - writer.newline() - for tag, v in self.location.items(): - writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))]) - writer.newline() - writer.endtag("location") - writer.newline() - - writer.endtag("varComponent") - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - self.glyphName = attrs["glyphName"] - - if "flags" in attrs: - self.flags = safeEval(attrs["flags"]) - - for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): - if attr_name not in attrs: - continue - v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) - setattr(self.transform, attr_name, v) - - for c in content: - if not isinstance(c, tuple): - continue - name, attrs, content = c - if name != "location": - continue - for c in content: - if not isinstance(c, tuple): - continue - name, attrs, content = c - assert name == "axis" - assert not content - self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14) - - def getPointCount(self): - assert hasattr(self, "flags"), "VarComponent with variations must have flags" - - count = 0 - - if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: - count += len(self.location) - - if self.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y - ): - count += 1 - if self.flags & VarComponentFlags.HAVE_ROTATION: - count += 1 - if self.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - count += 1 - if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): - count += 1 - if self.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - count += 1 - - return count - - def getCoordinatesAndControls(self): - coords = [] - controls = [] - - if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: - for tag, v in self.location.items(): - controls.append(tag) - coords.append((fl2fi(v, 14), 0)) - - if self.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y - ): - controls.append("translate") - coords.append((self.transform.translateX, self.transform.translateY)) - if self.flags & VarComponentFlags.HAVE_ROTATION: - controls.append("rotation") - coords.append((fl2fi(self.transform.rotation / 180, 12), 0)) - if self.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - controls.append("scale") - coords.append( - (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10)) - ) - if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): - controls.append("skew") - coords.append( - ( - fl2fi(self.transform.skewX / -180, 12), - fl2fi(self.transform.skewY / 180, 12), - ) - ) - if self.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - controls.append("tCenter") - coords.append((self.transform.tCenterX, self.transform.tCenterY)) - - return coords, controls - - def setCoordinates(self, coords): - i = 0 - - if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: - newLocation = {} - for tag in self.location: - newLocation[tag] = fi2fl(coords[i][0], 14) - i += 1 - self.location = newLocation - - self.transform = DecomposedTransform() - if self.flags & ( - VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y - ): - self.transform.translateX, self.transform.translateY = coords[i] - i += 1 - if self.flags & VarComponentFlags.HAVE_ROTATION: - self.transform.rotation = fi2fl(coords[i][0], 12) * 180 - i += 1 - if self.flags & ( - VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y - ): - self.transform.scaleX, self.transform.scaleY = fi2fl( - coords[i][0], 10 - ), fi2fl(coords[i][1], 10) - i += 1 - if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): - self.transform.skewX, self.transform.skewY = ( - fi2fl(coords[i][0], 12) * -180, - fi2fl(coords[i][1], 12) * 180, - ) - i += 1 - if self.flags & ( - VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y - ): - self.transform.tCenterX, self.transform.tCenterY = coords[i] - i += 1 - - return coords[i:] - - def __eq__(self, other): - if type(self) != type(other): - return NotImplemented - return self.__dict__ == other.__dict__ - - def __ne__(self, other): - result = self.__eq__(other) - return result if result is NotImplemented else not result - - class GlyphCoordinates(object): """A list of glyph coordinates. 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 11485bf09a..044f65f716 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 @@ -1,7 +1,8 @@ -from collections import UserDict, deque +from collections import deque from functools import partial from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval +from fontTools.misc.lazyTools import LazyDict from . import DefaultTable import array import itertools @@ -39,19 +40,6 @@ GVAR_HEADER_FORMAT = """ GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) -class _LazyDict(UserDict): - def __init__(self, data): - super().__init__() - self.data = data - - def __getitem__(self, k): - v = self.data[k] - if callable(v): - v = v() - self.data[k] = v - return v - - class table__g_v_a_r(DefaultTable.DefaultTable): dependencies = ["fvar", "glyf"] @@ -116,11 +104,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable): sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) assert len(glyphs) == self.glyphCount assert len(axisTags) == self.axisCount - offsets = self.decompileOffsets_( - data[GVAR_HEADER_SIZE:], - tableFormat=(self.flags & 1), - glyphCount=self.glyphCount, - ) sharedCoords = tv.decompileSharedTuples( axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples ) @@ -128,20 +111,35 @@ class table__g_v_a_r(DefaultTable.DefaultTable): offsetToData = self.offsetToGlyphVariationData glyf = ttFont["glyf"] - def decompileVarGlyph(glyphName, gid): - gvarData = data[ - offsetToData + offsets[gid] : offsetToData + offsets[gid + 1] - ] - if not gvarData: - return [] - glyph = glyf[glyphName] - numPointsInGlyph = self.getNumPoints_(glyph) - return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData) + def get_read_item(): + reverseGlyphMap = ttFont.getReverseGlyphMap() + tableFormat = self.flags & 1 + + def read_item(glyphName): + gid = reverseGlyphMap[glyphName] + offsetSize = 2 if tableFormat == 0 else 4 + startOffset = GVAR_HEADER_SIZE + offsetSize * gid + endOffset = startOffset + offsetSize * 2 + offsets = table__g_v_a_r.decompileOffsets_( + data[startOffset:endOffset], + tableFormat=tableFormat, + glyphCount=1, + ) + gvarData = data[offsetToData + offsets[0] : offsetToData + offsets[1]] + if not gvarData: + return [] + glyph = glyf[glyphName] + numPointsInGlyph = self.getNumPoints_(glyph) + return decompileGlyph_( + numPointsInGlyph, sharedCoords, axisTags, gvarData + ) + + return read_item + + read_item = get_read_item() + l = LazyDict({glyphs[gid]: read_item for gid in range(self.glyphCount)}) - for gid in range(self.glyphCount): - glyphName = glyphs[gid] - variations[glyphName] = partial(decompileVarGlyph, glyphName, gid) - self.variations = _LazyDict(variations) + self.variations = l if ttFont.lazy is False: # Be lazy for None and True self.ensureDecompiled() @@ -245,11 +243,6 @@ class table__g_v_a_r(DefaultTable.DefaultTable): if glyph.isComposite(): return len(glyph.components) + NUM_PHANTOM_POINTS - elif glyph.isVarComposite(): - count = 0 - for component in glyph.components: - count += component.getPointCount() - return count + NUM_PHANTOM_POINTS else: # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute. return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py index 5884cef45f..39c0c9e39b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py @@ -21,10 +21,7 @@ class table__l_o_c_a(DefaultTable.DefaultTable): if sys.byteorder != "big": locations.byteswap() if not longFormat: - l = array.array("I") - for i in range(len(locations)): - l.append(locations[i] * 2) - locations = l + locations = array.array("I", (2 * l for l in locations)) if len(locations) < (ttFont["maxp"].numGlyphs + 1): log.warning( "corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d", diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py b/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py index f0e6c33ae3..95b6ab9335 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py @@ -127,7 +127,7 @@ class table__m_a_x_p(DefaultTable.DefaultTable): formatstring, names, fixes = sstruct.getformat(maxpFormat_0_5) if self.tableVersion != 0x00005000: formatstring, names_1_0, fixes = sstruct.getformat(maxpFormat_1_0_add) - names = names + names_1_0 + names = {**names, **names_1_0} for name in names: value = getattr(self, name) if name == "tableVersion": diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py b/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py index 53abd13b48..8df7c236b1 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py @@ -1146,7 +1146,10 @@ class BaseTable(object): except KeyError: raise # XXX on KeyError, raise nice error value = conv.xmlRead(attrs, content, font) - if conv.repeat: + # Some manually-written tables have a conv.repeat of "" + # to represent lists. Hence comparing to None here to + # allow those lists to be read correctly from XML. + if conv.repeat is not None: seq = getattr(self, conv.name, None) if seq is None: seq = [] diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py index a2f672567e..656836bd3c 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py @@ -6,8 +6,10 @@ from fontTools.misc.fixedTools import ( ensureVersionIsLong as fi2ve, versionToFixed as ve2fi, ) +from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval +from fontTools.misc.lazyTools import LazyList from fontTools.ttLib import getSearchRange from .otBase import ( CountReference, @@ -18,6 +20,7 @@ from .otBase import ( ) from .otTables import ( lookupTypes, + VarCompositeGlyph, AATStateTable, AATState, AATAction, @@ -29,8 +32,9 @@ from .otTables import ( CompositeMode as _CompositeMode, NO_VARIATION_INDEX, ) -from itertools import zip_longest +from itertools import zip_longest, accumulate from functools import partial +from types import SimpleNamespace import re import struct from typing import Optional @@ -78,7 +82,7 @@ def buildConverters(tableSpec, tableNamespace): conv = converterClass(name, repeat, aux, description=descr) if conv.tableClass: - # A "template" such as OffsetTo(AType) knowss the table class already + # A "template" such as OffsetTo(AType) knows the table class already tableClass = conv.tableClass elif tp in ("MortChain", "MortSubtable", "MorxChain"): tableClass = tableNamespace.get(tp) @@ -105,46 +109,6 @@ def buildConverters(tableSpec, tableNamespace): return converters, convertersByName -class _MissingItem(tuple): - __slots__ = () - - -try: - from collections import UserList -except ImportError: - from UserList import UserList - - -class _LazyList(UserList): - def __getslice__(self, i, j): - return self.__getitem__(slice(i, j)) - - def __getitem__(self, k): - if isinstance(k, slice): - indices = range(*k.indices(len(self))) - return [self[i] for i in indices] - item = self.data[k] - if isinstance(item, _MissingItem): - self.reader.seek(self.pos + item[0] * self.recordSize) - item = self.conv.read(self.reader, self.font, {}) - self.data[k] = item - return item - - def __add__(self, other): - if isinstance(other, _LazyList): - other = list(other) - elif isinstance(other, list): - pass - else: - return NotImplemented - return list(self) + other - - def __radd__(self, other): - if not isinstance(other, list): - return NotImplemented - return other + list(self) - - class BaseConverter(object): """Base class for converter objects. Apart from the constructor, this is an abstract class.""" @@ -176,6 +140,7 @@ class BaseConverter(object): "AxisCount", "BaseGlyphRecordCount", "LayerRecordCount", + "AxisIndicesList", ] self.description = description @@ -192,14 +157,21 @@ class BaseConverter(object): l.append(self.read(reader, font, tableDict)) return l else: - l = _LazyList() - l.reader = reader.copy() - l.pos = l.reader.pos - l.font = font - l.conv = self - l.recordSize = recordSize - l.extend(_MissingItem([i]) for i in range(count)) + + def get_read_item(): + reader_copy = reader.copy() + pos = reader.pos + + def read_item(i): + reader_copy.seek(pos + i * recordSize) + return self.read(reader_copy, font, {}) + + return read_item + + read_item = get_read_item() + l = LazyList(read_item for i in range(count)) reader.advance(count * recordSize) + return l def getRecordSize(self, reader): @@ -1833,6 +1805,169 @@ class VarDataValue(BaseConverter): return safeEval(attrs["value"]) +class TupleValues: + def read(self, data, font): + return TupleVariation.decompileDeltas_(None, data)[0] + + def write(self, writer, font, tableDict, values, repeatIndex=None): + return bytes(TupleVariation.compileDeltaValues_(values)) + + def xmlRead(self, attrs, content, font): + return safeEval(attrs["value"]) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.newline() + + +class CFF2Index(BaseConverter): + def __init__( + self, + name, + repeat, + aux, + tableClass=None, + *, + itemClass=None, + itemConverterClass=None, + description="", + ): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + self._itemClass = itemClass + self._converter = ( + itemConverterClass() if itemConverterClass is not None else None + ) + + def read(self, reader, font, tableDict): + count = reader.readULong() + if count == 0: + return [] + offSize = reader.readUInt8() + + def getReadArray(reader, offSize): + return { + 1: reader.readUInt8Array, + 2: reader.readUShortArray, + 3: reader.readUInt24Array, + 4: reader.readULongArray, + }[offSize] + + readArray = getReadArray(reader, offSize) + + lazy = font.lazy is not False and count > 8 + if not lazy: + offsets = readArray(count + 1) + items = [] + lastOffset = offsets.pop(0) + reader.readData(lastOffset - 1) # In case first offset is not 1 + + for offset in offsets: + assert lastOffset <= offset + item = reader.readData(offset - lastOffset) + + if self._itemClass is not None: + obj = self._itemClass() + obj.decompile(item, font, reader.localState) + item = obj + elif self._converter is not None: + item = self._converter.read(item, font) + + items.append(item) + lastOffset = offset + return items + else: + + def get_read_item(): + reader_copy = reader.copy() + offset_pos = reader.pos + data_pos = offset_pos + (count + 1) * offSize - 1 + readArray = getReadArray(reader_copy, offSize) + + def read_item(i): + reader_copy.seek(offset_pos + i * offSize) + offsets = readArray(2) + reader_copy.seek(data_pos + offsets[0]) + item = reader_copy.readData(offsets[1] - offsets[0]) + + if self._itemClass is not None: + obj = self._itemClass() + obj.decompile(item, font, reader_copy.localState) + item = obj + elif self._converter is not None: + item = self._converter.read(item, font) + return item + + return read_item + + read_item = get_read_item() + l = LazyList([read_item] * count) + + # TODO: Advance reader + + return l + + def write(self, writer, font, tableDict, values, repeatIndex=None): + items = values + + writer.writeULong(len(items)) + if not len(items): + return + + if self._itemClass is not None: + items = [item.compile(font) for item in items] + elif self._converter is not None: + items = [ + self._converter.write(writer, font, tableDict, item, i) + for i, item in enumerate(items) + ] + + offsets = [len(item) for item in items] + offsets = list(accumulate(offsets, initial=1)) + + lastOffset = offsets[-1] + offSize = ( + 1 + if lastOffset < 0x100 + else 2 if lastOffset < 0x10000 else 3 if lastOffset < 0x1000000 else 4 + ) + writer.writeUInt8(offSize) + + writeArray = { + 1: writer.writeUInt8Array, + 2: writer.writeUShortArray, + 3: writer.writeUInt24Array, + 4: writer.writeULongArray, + }[offSize] + + writeArray(offsets) + for item in items: + writer.writeData(item) + + def xmlRead(self, attrs, content, font): + if self._itemClass is not None: + obj = self._itemClass() + obj.fromXML(None, attrs, content, font) + return obj + elif self._converter is not None: + return self._converter.xmlRead(attrs, content, font) + else: + raise NotImplementedError() + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + if self._itemClass is not None: + for i, item in enumerate(value): + item.toXML(xmlWriter, font, [("index", i)], name) + elif self._converter is not None: + for i, item in enumerate(value): + self._converter.xmlWrite( + xmlWriter, font, item, name, attrs + [("index", i)] + ) + else: + raise NotImplementedError() + + class LookupFlag(UShort): def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) @@ -1910,6 +2045,8 @@ converterMapping = { "ExtendMode": ExtendMode, "CompositeMode": CompositeMode, "STATFlags": STATFlags, + "TupleList": partial(CFF2Index, itemConverterClass=TupleValues), + "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), # AAT "CIDGlyphMap": CIDGlyphMap, "GlyphCIDMap": GlyphCIDMap, diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otData.py b/contrib/python/fonttools/fontTools/ttLib/tables/otData.py index 56716824ec..3a01f5934f 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otData.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otData.py @@ -3169,6 +3169,25 @@ otData = [ ], ), ( + "ConditionList", + [ + ( + "uint32", + "ConditionCount", + None, + None, + "Number of condition tables in the ConditionTable array", + ), + ( + "LOffset", + "ConditionTable", + "ConditionCount", + 0, + "Array of offset to condition tables, from the beginning of the ConditionList table.", + ), + ], + ), + ( "ConditionSet", [ ( @@ -3183,7 +3202,7 @@ otData = [ "ConditionTable", "ConditionCount", 0, - "Array of condition tables.", + "Array of offset to condition tables, from the beginning of the ConditionSet table.", ), ], ), @@ -3215,6 +3234,79 @@ otData = [ ], ), ( + "ConditionTableFormat2", + [ + ("uint16", "Format", None, None, "Format, = 2"), + ( + "int16", + "DefaultValue", + None, + None, + "Value at default instance.", + ), + ( + "uint32", + "VarIdx", + None, + None, + "Variation index to vary the value based on current designspace location.", + ), + ], + ), + ( + "ConditionTableFormat3", + [ + ("uint16", "Format", None, None, "Format, = 3"), + ( + "uint8", + "ConditionCount", + None, + None, + "Index for the variation axis within the fvar table, base 0.", + ), + ( + "Offset24", + "ConditionTable", + "ConditionCount", + 0, + "Array of condition tables for this conjunction (AND) expression.", + ), + ], + ), + ( + "ConditionTableFormat4", + [ + ("uint16", "Format", None, None, "Format, = 4"), + ( + "uint8", + "ConditionCount", + None, + None, + "Index for the variation axis within the fvar table, base 0.", + ), + ( + "Offset24", + "ConditionTable", + "ConditionCount", + 0, + "Array of condition tables for this disjunction (OR) expression.", + ), + ], + ), + ( + "ConditionTableFormat5", + [ + ("uint16", "Format", None, None, "Format, = 5"), + ( + "Offset24", + "ConditionTable", + None, + None, + "Condition to negate.", + ), + ], + ), + ( "FeatureTableSubstitution", [ ( @@ -3322,6 +3414,78 @@ otData = [ ("VarIdxMapValue", "mapping", "", 0, "Array of compressed data"), ], ), + # MultiVariationStore + ( + "SparseVarRegionAxis", + [ + ("uint16", "AxisIndex", None, None, ""), + ("F2Dot14", "StartCoord", None, None, ""), + ("F2Dot14", "PeakCoord", None, None, ""), + ("F2Dot14", "EndCoord", None, None, ""), + ], + ), + ( + "SparseVarRegion", + [ + ("uint16", "SparseRegionCount", None, None, ""), + ("struct", "SparseVarRegionAxis", "SparseRegionCount", 0, ""), + ], + ), + ( + "SparseVarRegionList", + [ + ("uint16", "RegionCount", None, None, ""), + ("LOffsetTo(SparseVarRegion)", "Region", "RegionCount", 0, ""), + ], + ), + ( + "MultiVarData", + [ + ("uint8", "Format", None, None, "Set to 1."), + ("uint16", "VarRegionCount", None, None, ""), + ("uint16", "VarRegionIndex", "VarRegionCount", 0, ""), + ("TupleList", "Item", "", 0, ""), + ], + ), + ( + "MultiVarStore", + [ + ("uint16", "Format", None, None, "Set to 1."), + ("LOffset", "SparseVarRegionList", None, None, ""), + ("uint16", "MultiVarDataCount", None, None, ""), + ("LOffset", "MultiVarData", "MultiVarDataCount", 0, ""), + ], + ), + # VariableComposites + ( + "VARC", + [ + ( + "Version", + "Version", + None, + None, + "Version of the HVAR table-initially = 0x00010000", + ), + ("LOffset", "Coverage", None, None, ""), + ("LOffset", "MultiVarStore", None, None, "(may be NULL)"), + ("LOffset", "ConditionList", None, None, "(may be NULL)"), + ("LOffset", "AxisIndicesList", None, None, "(may be NULL)"), + ("LOffset", "VarCompositeGlyphs", None, None, ""), + ], + ), + ( + "AxisIndicesList", + [ + ("TupleList", "Item", "", 0, ""), + ], + ), + ( + "VarCompositeGlyphs", + [ + ("VarCompositeGlyphList", "VarCompositeGlyph", "", None, ""), + ], + ), # Glyph advance variations ( "HVAR", diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py index 3505f42337..bc7fbad915 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py @@ -11,11 +11,13 @@ from functools import reduce from math import radians import itertools from collections import defaultdict, namedtuple +from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables.otTraverse import dfs_base_table from fontTools.misc.arrayTools import quantizeRect from fontTools.misc.roundTools import otRound -from fontTools.misc.transform import Transform, Identity +from fontTools.misc.transform import Transform, Identity, DecomposedTransform from fontTools.misc.textTools import bytesjoin, pad, safeEval +from fontTools.misc.vector import Vector from fontTools.pens.boundsPen import ControlBoundsPen from fontTools.pens.transformPen import TransformPen from .otBase import ( @@ -25,9 +27,18 @@ from .otBase import ( CountReference, getFormatSwitchingBaseTableClass, ) +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, + floatToFixed as fl2fi, + floatToFixedToStr as fl2str, + strToFixedToFloat as str2fl, +) from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY import logging import struct +import array +import sys +from enum import IntFlag from typing import TYPE_CHECKING, Iterator, List, Optional, Set if TYPE_CHECKING: @@ -37,6 +48,389 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +class VarComponentFlags(IntFlag): + RESET_UNSPECIFIED_AXES = 1 << 0 + + HAVE_AXES = 1 << 1 + + AXIS_VALUES_HAVE_VARIATION = 1 << 2 + TRANSFORM_HAS_VARIATION = 1 << 3 + + HAVE_TRANSLATE_X = 1 << 4 + HAVE_TRANSLATE_Y = 1 << 5 + HAVE_ROTATION = 1 << 6 + + HAVE_CONDITION = 1 << 7 + + HAVE_SCALE_X = 1 << 8 + HAVE_SCALE_Y = 1 << 9 + HAVE_TCENTER_X = 1 << 10 + HAVE_TCENTER_Y = 1 << 11 + + GID_IS_24BIT = 1 << 12 + + HAVE_SKEW_X = 1 << 13 + HAVE_SKEW_Y = 1 << 14 + + RESERVED_MASK = (1 << 32) - (1 << 15) + + +VarTransformMappingValues = namedtuple( + "VarTransformMappingValues", + ["flag", "fractionalBits", "scale", "defaultValue"], +) + +VAR_TRANSFORM_MAPPING = { + "translateX": VarTransformMappingValues( + VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 + ), + "translateY": VarTransformMappingValues( + VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 + ), + "rotation": VarTransformMappingValues(VarComponentFlags.HAVE_ROTATION, 12, 180, 0), + "scaleX": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_X, 10, 1, 1), + "scaleY": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1), + "skewX": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_X, 12, -180, 0), + "skewY": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0), + "tCenterX": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0), + "tCenterY": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0), +} + +# Probably should be somewhere in fontTools.misc +_packer = { + 1: lambda v: struct.pack(">B", v), + 2: lambda v: struct.pack(">H", v), + 3: lambda v: struct.pack(">L", v)[1:], + 4: lambda v: struct.pack(">L", v), +} +_unpacker = { + 1: lambda v: struct.unpack(">B", v)[0], + 2: lambda v: struct.unpack(">H", v)[0], + 3: lambda v: struct.unpack(">L", b"\0" + v)[0], + 4: lambda v: struct.unpack(">L", v)[0], +} + + +def _read_uint32var(data, i): + """Read a variable-length number from data starting at index i. + + Return the number and the next index. + """ + + b0 = data[i] + if b0 < 0x80: + return b0, i + 1 + elif b0 < 0xC0: + return (b0 - 0x80) << 8 | data[i + 1], i + 2 + elif b0 < 0xE0: + return (b0 - 0xC0) << 16 | data[i + 1] << 8 | data[i + 2], i + 3 + elif b0 < 0xF0: + return (b0 - 0xE0) << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[ + i + 3 + ], i + 4 + else: + return (b0 - 0xF0) << 32 | data[i + 1] << 24 | data[i + 2] << 16 | data[ + i + 3 + ] << 8 | data[i + 4], i + 5 + + +def _write_uint32var(v): + """Write a variable-length number. + + Return the data. + """ + if v < 0x80: + return struct.pack(">B", v) + elif v < 0x4000: + return struct.pack(">H", (v | 0x8000)) + elif v < 0x200000: + return struct.pack(">L", (v | 0xC00000))[1:] + elif v < 0x10000000: + return struct.pack(">L", (v | 0xE0000000)) + else: + return struct.pack(">B", 0xF0) + struct.pack(">L", v) + + +class VarComponent: + def __init__(self): + self.populateDefaults() + + def populateDefaults(self, propagator=None): + self.flags = 0 + self.glyphName = None + self.conditionIndex = None + self.axisIndicesIndex = None + self.axisValues = () + self.axisValuesVarIndex = NO_VARIATION_INDEX + self.transformVarIndex = NO_VARIATION_INDEX + self.transform = DecomposedTransform() + + def decompile(self, data, font, localState): + i = 0 + self.flags, i = _read_uint32var(data, i) + flags = self.flags + + gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2 + glyphID = _unpacker[gidSize](data[i : i + gidSize]) + i += gidSize + self.glyphName = font.glyphOrder[glyphID] + + if flags & VarComponentFlags.HAVE_CONDITION: + self.conditionIndex, i = _read_uint32var(data, i) + + if flags & VarComponentFlags.HAVE_AXES: + self.axisIndicesIndex, i = _read_uint32var(data, i) + else: + self.axisIndicesIndex = None + + if self.axisIndicesIndex is None: + numAxes = 0 + else: + axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex] + numAxes = len(axisIndices) + + if flags & VarComponentFlags.HAVE_AXES: + axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i) + self.axisValues = tuple(fi2fl(v, 14) for v in axisValues) + else: + self.axisValues = () + assert len(self.axisValues) == numAxes + + if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION: + self.axisValuesVarIndex, i = _read_uint32var(data, i) + else: + self.axisValuesVarIndex = NO_VARIATION_INDEX + if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION: + self.transformVarIndex, i = _read_uint32var(data, i) + else: + self.transformVarIndex = NO_VARIATION_INDEX + + self.transform = DecomposedTransform() + + def read_transform_component(values): + nonlocal i + if flags & values.flag: + v = ( + fi2fl( + struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits + ) + * values.scale + ) + i += 2 + return v + else: + return values.defaultValue + + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): + value = read_transform_component(mapping_values) + setattr(self.transform, attr_name, value) + + if not (flags & VarComponentFlags.HAVE_SCALE_Y): + self.transform.scaleY = self.transform.scaleX + + n = flags & VarComponentFlags.RESERVED_MASK + while n: + _, i = _read_uint32var(data, i) + n &= n - 1 + + return data[i:] + + def compile(self, font): + data = [] + + flags = self.flags + + glyphID = font.getGlyphID(self.glyphName) + if glyphID > 65535: + flags |= VarComponentFlags.GID_IS_24BIT + data.append(_packer[3](glyphID)) + else: + flags &= ~VarComponentFlags.GID_IS_24BIT + data.append(_packer[2](glyphID)) + + if self.conditionIndex is not None: + flags |= VarComponentFlags.HAVE_CONDITION + data.append(_write_uint32var(self.conditionIndex)) + + numAxes = len(self.axisValues) + + if numAxes: + flags |= VarComponentFlags.HAVE_AXES + data.append(_write_uint32var(self.axisIndicesIndex)) + data.append( + TupleVariation.compileDeltaValues_( + [fl2fi(v, 14) for v in self.axisValues] + ) + ) + else: + flags &= ~VarComponentFlags.HAVE_AXES + + if self.axisValuesVarIndex != NO_VARIATION_INDEX: + flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + data.append(_write_uint32var(self.axisValuesVarIndex)) + else: + flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION + if self.transformVarIndex != NO_VARIATION_INDEX: + flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION + data.append(_write_uint32var(self.transformVarIndex)) + else: + flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION + + def write_transform_component(value, values): + if flags & values.flag: + return struct.pack( + ">h", fl2fi(value / values.scale, values.fractionalBits) + ) + else: + return b"" + + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): + value = getattr(self.transform, attr_name) + data.append(write_transform_component(value, mapping_values)) + + return _write_uint32var(flags) + bytesjoin(data) + + def toXML(self, writer, ttFont, attrs): + writer.begintag("VarComponent", attrs) + writer.newline() + + def write(name, value, attrs=()): + if value is not None: + writer.simpletag(name, (("value", value),) + attrs) + writer.newline() + + write("glyphName", self.glyphName) + + if self.conditionIndex is not None: + write("conditionIndex", self.conditionIndex) + if self.axisIndicesIndex is not None: + write("axisIndicesIndex", self.axisIndicesIndex) + if ( + self.axisIndicesIndex is not None + or self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + ): + if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES: + attrs = (("resetUnspecifiedAxes", 1),) + else: + attrs = () + write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues], attrs) + + if self.axisValuesVarIndex != NO_VARIATION_INDEX: + write("axisValuesVarIndex", self.axisValuesVarIndex) + if self.transformVarIndex != NO_VARIATION_INDEX: + write("transformVarIndex", self.transformVarIndex) + + # Only write transform components that are specified in the + # flags, even if they are the default value. + for attr_name, mapping in VAR_TRANSFORM_MAPPING.items(): + if not (self.flags & mapping.flag): + continue + v = getattr(self.transform, attr_name) + write(attr_name, fl2str(v, mapping.fractionalBits)) + + writer.endtag("VarComponent") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + content = [c for c in content if isinstance(c, tuple)] + + self.populateDefaults() + + for name, attrs, content in content: + assert not content + v = attrs["value"] + + if name == "glyphName": + self.glyphName = v + elif name == "conditionIndex": + self.conditionIndex = safeEval(v) + elif name == "axisIndicesIndex": + self.axisIndicesIndex = safeEval(v) + elif name == "axisValues": + self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v)) + if safeEval(attrs.get("resetUnspecifiedAxes", "0")): + self.flags |= VarComponentFlags.RESET_UNSPECIFIED_AXES + elif name == "axisValuesVarIndex": + self.axisValuesVarIndex = safeEval(v) + elif name == "transformVarIndex": + self.transformVarIndex = safeEval(v) + elif name in VAR_TRANSFORM_MAPPING: + setattr( + self.transform, + name, + safeEval(v), + ) + self.flags |= VAR_TRANSFORM_MAPPING[name].flag + else: + assert False, name + + def applyTransformDeltas(self, deltas): + i = 0 + + def read_transform_component_delta(values): + nonlocal i + if self.flags & values.flag: + v = fi2fl(deltas[i], values.fractionalBits) * values.scale + i += 1 + return v + else: + return 0 + + for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items(): + value = read_transform_component_delta(mapping_values) + setattr( + self.transform, attr_name, getattr(self.transform, attr_name) + value + ) + + if not (self.flags & VarComponentFlags.HAVE_SCALE_Y): + self.transform.scaleY = self.transform.scaleX + + assert i == len(deltas), (i, len(deltas)) + + def __eq__(self, other): + if type(self) != type(other): + return NotImplemented + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result + + +class VarCompositeGlyph: + def __init__(self, components=None): + self.components = components if components is not None else [] + + def decompile(self, data, font, localState): + self.components = [] + while data: + component = VarComponent() + data = component.decompile(data, font, localState) + self.components.append(component) + + def compile(self, font): + data = [] + for component in self.components: + data.append(component.compile(font)) + return bytesjoin(data) + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.begintag("VarCompositeGlyph", attrs) + xmlWriter.newline() + for i, component in enumerate(self.components): + component.toXML(xmlWriter, font, [("index", i)]) + xmlWriter.endtag("VarCompositeGlyph") + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + content = [c for c in content if isinstance(c, tuple)] + for name, attrs, content in content: + assert name == "VarComponent" + component = VarComponent() + component.fromXML(name, attrs, content, font) + self.components.append(component) + + class AATStateTable(object): def __init__(self): self.GlyphClasses = {} # GlyphID --> GlyphClass @@ -703,6 +1097,9 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): assert inner <= 0xFFFF mapping.insert(index, (outer << 16) | inner) + def __getitem__(self, i): + return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX + class VarIdxMap(BaseTable): def populateDefaults(self, propagator=None): @@ -755,6 +1152,9 @@ class VarIdxMap(BaseTable): assert inner <= 0xFFFF mapping[glyph] = (outer << 16) | inner + def __getitem__(self, glyphName): + return self.mapping.get(glyphName, NO_VARIATION_INDEX) + class VarRegionList(BaseTable): def preWrite(self, font): diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py index 52e048b5f1..f4a539678b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py @@ -4,7 +4,12 @@ from fontTools.misc.configTools import AbstractConfig from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.misc.loggingTools import deprecateArgument from fontTools.ttLib import TTLibError -from fontTools.ttLib.ttGlyphSet import _TTGlyph, _TTGlyphSetCFF, _TTGlyphSetGlyf +from fontTools.ttLib.ttGlyphSet import ( + _TTGlyph, + _TTGlyphSetCFF, + _TTGlyphSetGlyf, + _TTGlyphSetVARC, +) from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter from io import BytesIO, StringIO, UnsupportedOperation import os @@ -537,7 +542,7 @@ class TTFont(object): # # Not enough names found in the 'post' table. # Can happen when 'post' format 1 is improperly used on a font that - # has more than 258 glyphs (the lenght of 'standardGlyphOrder'). + # has more than 258 glyphs (the length of 'standardGlyphOrder'). # log.warning( "Not enough names found in the 'post' table, generating them from cmap instead" @@ -764,12 +769,16 @@ class TTFont(object): location = None if location and not normalized: location = self.normalizeLocation(location) + glyphSet = None if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): - return _TTGlyphSetCFF(self, location) + glyphSet = _TTGlyphSetCFF(self, location) elif "glyf" in self: - return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) + glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) else: raise TTLibError("Font contains no outlines") + if "VARC" in self: + glyphSet = _TTGlyphSetVARC(self, location, glyphSet) + return glyphSet def normalizeLocation(self, location): """Normalize a ``location`` from the font's defined axes space (also @@ -781,26 +790,15 @@ class TTFont(object): Raises ``TTLibError`` if the font is not a variable font. """ - from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap + from fontTools.varLib.models import normalizeLocation if "fvar" not in self: raise TTLibError("Not a variable font") - axes = { - a.axisTag: (a.minValue, a.defaultValue, a.maxValue) - for a in self["fvar"].axes - } + axes = self["fvar"].getAxes() location = normalizeLocation(location, axes) if "avar" in self: - avar = self["avar"] - avarSegments = avar.segments - mappedLocation = {} - for axisTag, value in location.items(): - avarMapping = avarSegments.get(axisTag, None) - if avarMapping is not None: - value = piecewiseLinearMap(value, avarMapping) - mappedLocation[axisTag] = value - location = mappedLocation + location = self["avar"].renormalizeLocation(location, self) return location def getBestCmap( diff --git a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py index b4beb3e766..446c81e7db 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py @@ -3,11 +3,12 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from contextlib import contextmanager -from copy import copy +from copy import copy, deepcopy from types import SimpleNamespace -from fontTools.misc.fixedTools import otRound +from fontTools.misc.vector import Vector +from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl from fontTools.misc.loggingTools import deprecateFunction -from fontTools.misc.transform import Transform +from fontTools.misc.transform import Transform, DecomposedTransform from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.pens.recordingPen import ( DecomposingRecordingPen, @@ -103,6 +104,16 @@ class _TTGlyphSetGlyf(_TTGlyphSet): return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) +class _TTGlyphSetGlyf(_TTGlyphSet): + def __init__(self, font, location, recalcBounds=True): + self.glyfTable = font["glyf"] + super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) + self.gvarTable = font.get("gvar") + + def __getitem__(self, glyphName): + return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) + + class _TTGlyphSetCFF(_TTGlyphSet): def __init__(self, font, location): tableTag = "CFF2" if "CFF2" in font else "CFF " @@ -123,6 +134,19 @@ class _TTGlyphSetCFF(_TTGlyphSet): return _TTGlyphCFF(self, glyphName) +class _TTGlyphSetVARC(_TTGlyphSet): + def __init__(self, font, location, glyphSet): + self.glyphSet = glyphSet + super().__init__(font, location, glyphSet) + self.varcTable = font["VARC"].table + + def __getitem__(self, glyphName): + varc = self.varcTable + if glyphName not in varc.Coverage.glyphs: + return self.glyphSet[glyphName] + return _TTGlyphVARC(self, glyphName) + + class _TTGlyph(ABC): """Glyph object that supports the Pen protocol, meaning that it has .draw() and .drawPoints() methods that take a pen object as their only @@ -178,10 +202,6 @@ class _TTGlyphGlyf(_TTGlyph): if depth: offset = 0 # Offset should only apply at top-level - if glyph.isVarComposite(): - self._drawVarComposite(glyph, pen, False) - return - glyph.draw(pen, self.glyphSet.glyfTable, offset) def drawPoints(self, pen): @@ -194,35 +214,8 @@ class _TTGlyphGlyf(_TTGlyph): if depth: offset = 0 # Offset should only apply at top-level - if glyph.isVarComposite(): - self._drawVarComposite(glyph, pen, True) - return - glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) - def _drawVarComposite(self, glyph, pen, isPointPen): - from fontTools.ttLib.tables._g_l_y_f import ( - VarComponentFlags, - VAR_COMPONENT_TRANSFORM_MAPPING, - ) - - for comp in glyph.components: - with self.glyphSet.pushLocation( - comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES - ): - try: - pen.addVarComponent( - comp.glyphName, comp.transform, self.glyphSet.rawLocation - ) - except AttributeError: - t = comp.transform.toTransform() - if isPointPen: - tPen = TransformPointPen(pen, t) - self.glyphSet[comp.glyphName].drawPoints(tPen) - else: - tPen = TransformPen(pen, t) - self.glyphSet[comp.glyphName].draw(tPen) - def _getGlyphAndOffset(self): if self.glyphSet.location and self.glyphSet.gvarTable is not None: glyph = self._getGlyphInstance() @@ -283,6 +276,128 @@ class _TTGlyphCFF(_TTGlyph): self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) +def _evaluateCondition(condition, fvarAxes, location, instancer): + if condition.Format == 1: + # ConditionAxisRange + axisIndex = condition.AxisIndex + axisTag = fvarAxes[axisIndex].axisTag + axisValue = location.get(axisTag, 0) + minValue = condition.FilterRangeMinValue + maxValue = condition.FilterRangeMaxValue + return minValue <= axisValue <= maxValue + elif condition.Format == 2: + # ConditionValue + value = condition.DefaultValue + value += instancer[condition.VarIdx][0] + return value > 0 + elif condition.Format == 3: + # ConditionAnd + for subcondition in condition.ConditionTable: + if not _evaluateCondition(subcondition, fvarAxes, location, instancer): + return False + return True + elif condition.Format == 4: + # ConditionOr + for subcondition in condition.ConditionTable: + if _evaluateCondition(subcondition, fvarAxes, location, instancer): + return True + return False + elif condition.Format == 5: + # ConditionNegate + return not _evaluateCondition( + condition.conditionTable, fvarAxes, location, instancer + ) + else: + return False # Unkonwn condition format + + +class _TTGlyphVARC(_TTGlyph): + def _draw(self, pen, isPointPen): + """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details + how that works. + """ + from fontTools.ttLib.tables.otTables import ( + VarComponentFlags, + NO_VARIATION_INDEX, + ) + + glyphSet = self.glyphSet + varc = glyphSet.varcTable + idx = varc.Coverage.glyphs.index(self.name) + glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx] + + from fontTools.varLib.multiVarStore import MultiVarStoreInstancer + from fontTools.varLib.varStore import VarStoreInstancer + + fvarAxes = glyphSet.font["fvar"].axes + instancer = MultiVarStoreInstancer( + varc.MultiVarStore, fvarAxes, self.glyphSet.location + ) + + for comp in glyph.components: + + if comp.flags & VarComponentFlags.HAVE_CONDITION: + condition = varc.ConditionList.ConditionTable[comp.conditionIndex] + if not _evaluateCondition( + condition, fvarAxes, self.glyphSet.location, instancer + ): + continue + + location = {} + if comp.axisIndicesIndex is not None: + axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] + axisValues = Vector(comp.axisValues) + if comp.axisValuesVarIndex != NO_VARIATION_INDEX: + axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14) + assert len(axisIndices) == len(axisValues), ( + len(axisIndices), + len(axisValues), + ) + location = { + fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) + } + + if comp.transformVarIndex != NO_VARIATION_INDEX: + deltas = instancer[comp.transformVarIndex] + comp = deepcopy(comp) + comp.applyTransformDeltas(deltas) + transform = comp.transform + + reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + with self.glyphSet.glyphSet.pushLocation(location, reset): + with self.glyphSet.pushLocation(location, reset): + shouldDecompose = self.name == comp.glyphName + + if not shouldDecompose: + try: + pen.addVarComponent( + comp.glyphName, transform, self.glyphSet.rawLocation + ) + except AttributeError: + shouldDecompose = True + + if shouldDecompose: + t = transform.toTransform() + compGlyphSet = ( + self.glyphSet + if comp.glyphName != self.name + else glyphSet.glyphSet + ) + g = compGlyphSet[comp.glyphName] + if isPointPen: + tPen = TransformPointPen(pen, t) + g.drawPoints(tPen) + else: + tPen = TransformPen(pen, t) + g.draw(tPen) + + def draw(self, pen): + self._draw(pen, False) + + def drawPoints(self, pen): + self._draw(pen, True) + + def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): # Handle phantom points for (left, right, top, bottom) positions. assert len(coord) >= 4 @@ -300,11 +415,6 @@ def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): for p, comp in zip(coord, glyph.components): if hasattr(comp, "x"): comp.x, comp.y = p - elif glyph.isVarComposite(): - glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy - for comp in glyph.components: - coord = comp.setCoordinates(coord) - assert not coord elif glyph.numberOfContours == 0: assert len(coord) == 0 else: diff --git a/contrib/python/fonttools/fontTools/ttLib/woff2.py b/contrib/python/fonttools/fontTools/ttLib/woff2.py index 9da2f7e6d6..03667e834b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/woff2.py +++ b/contrib/python/fonttools/fontTools/ttLib/woff2.py @@ -1017,8 +1017,6 @@ class WOFF2GlyfTable(getTableClass("glyf")): return elif glyph.isComposite(): self._encodeComponents(glyph) - elif glyph.isVarComposite(): - raise NotImplementedError else: self._encodeCoordinates(glyph) self._encodeOverlapSimpleFlag(glyph, glyphID) diff --git a/contrib/python/fonttools/fontTools/ttx.py b/contrib/python/fonttools/fontTools/ttx.py index e7a068748b..0adda52d74 100644 --- a/contrib/python/fonttools/fontTools/ttx.py +++ b/contrib/python/fonttools/fontTools/ttx.py @@ -375,7 +375,7 @@ def guessFileType(fileName): def parseOptions(args): - rawOptions, files = getopt.getopt( + rawOptions, files = getopt.gnu_getopt( args, "ld:o:fvqht:x:sgim:z:baey:", [ diff --git a/contrib/python/fonttools/fontTools/varLib/__init__.py b/contrib/python/fonttools/fontTools/varLib/__init__.py index 1e0f2ec2f4..6d0e00ee10 100644 --- a/contrib/python/fonttools/fontTools/varLib/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/__init__.py @@ -845,9 +845,10 @@ def _add_CFF2(varFont, model, master_fonts): glyphOrder = varFont.getGlyphOrder() if "CFF2" not in varFont: - from .cff import convertCFFtoCFF2 + from fontTools.cffLib.CFFToCFF2 import convertCFFToCFF2 + + convertCFFToCFF2(varFont) - convertCFFtoCFF2(varFont) ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping) # re-ordering the master list simplifies building the CFF2 data item lists. merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder) diff --git a/contrib/python/fonttools/fontTools/varLib/builder.py b/contrib/python/fonttools/fontTools/varLib/builder.py index 94cc5bf063..456c34c4dd 100644 --- a/contrib/python/fonttools/fontTools/varLib/builder.py +++ b/contrib/python/fonttools/fontTools/varLib/builder.py @@ -10,6 +10,13 @@ def buildVarRegionAxis(axisSupport): return self +def buildSparseVarRegionAxis(axisIndex, axisSupport): + self = ot.SparseVarRegionAxis() + self.AxisIndex = axisIndex + self.StartCoord, self.PeakCoord, self.EndCoord = [float(v) for v in axisSupport] + return self + + def buildVarRegion(support, axisTags): assert all(tag in axisTags for tag in support.keys()), ( "Unknown axis tag found.", @@ -23,6 +30,24 @@ def buildVarRegion(support, axisTags): return self +def buildSparseVarRegion(support, axisTags): + assert all(tag in axisTags for tag in support.keys()), ( + "Unknown axis tag found.", + support, + axisTags, + ) + self = ot.SparseVarRegion() + self.SparseVarRegionAxis = [] + for i, tag in enumerate(axisTags): + if tag not in support: + continue + self.SparseVarRegionAxis.append( + buildSparseVarRegionAxis(i, support.get(tag, (0, 0, 0))) + ) + self.SparseRegionCount = len(self.SparseVarRegionAxis) + return self + + def buildVarRegionList(supports, axisTags): self = ot.VarRegionList() self.RegionAxisCount = len(axisTags) @@ -33,6 +58,16 @@ def buildVarRegionList(supports, axisTags): return self +def buildSparseVarRegionList(supports, axisTags): + self = ot.SparseVarRegionList() + self.RegionAxisCount = len(axisTags) + self.Region = [] + for support in supports: + self.Region.append(buildSparseVarRegion(support, axisTags)) + self.RegionCount = len(self.Region) + return self + + def _reorderItem(lst, mapping): return [lst[i] for i in mapping] @@ -130,6 +165,29 @@ def buildVarStore(varRegionList, varDataList): return self +def buildMultiVarData(varRegionIndices, items): + self = ot.MultiVarData() + self.Format = 1 + self.VarRegionIndex = list(varRegionIndices) + regionCount = self.VarRegionCount = len(self.VarRegionIndex) + records = self.Item = [] + if items: + for item in items: + assert len(item) == regionCount + records.append(list(item)) + self.ItemCount = len(self.Item) + return self + + +def buildMultiVarStore(varRegionList, multiVarDataList): + self = ot.MultiVarStore() + self.Format = 1 + self.SparseVarRegionList = varRegionList + self.MultiVarData = list(multiVarDataList) + self.MultiVarDataCount = len(self.MultiVarData) + return self + + # Variation helpers diff --git a/contrib/python/fonttools/fontTools/varLib/cff.py b/contrib/python/fonttools/fontTools/varLib/cff.py index 52e6a8848d..393c793e36 100644 --- a/contrib/python/fonttools/fontTools/varLib/cff.py +++ b/contrib/python/fonttools/fontTools/varLib/cff.py @@ -49,95 +49,6 @@ def addCFFVarStore(varFont, varModel, varDataList, masterSupports): fontDict.Private.vstore = topDict.VarStore -def lib_convertCFFToCFF2(cff, otFont): - # This assumes a decompiled CFF table. - cff2GetGlyphOrder = cff.otFont.getGlyphOrder - topDictData = TopDictIndex(None, cff2GetGlyphOrder, None) - topDictData.items = cff.topDictIndex.items - cff.topDictIndex = topDictData - topDict = topDictData[0] - if hasattr(topDict, "Private"): - privateDict = topDict.Private - else: - privateDict = None - opOrder = buildOrder(topDictOperators2) - topDict.order = opOrder - topDict.cff2GetGlyphOrder = cff2GetGlyphOrder - if not hasattr(topDict, "FDArray"): - fdArray = topDict.FDArray = FDArrayIndex() - fdArray.strings = None - fdArray.GlobalSubrs = topDict.GlobalSubrs - topDict.GlobalSubrs.fdArray = fdArray - charStrings = topDict.CharStrings - if charStrings.charStringsAreIndexed: - charStrings.charStringsIndex.fdArray = fdArray - else: - charStrings.fdArray = fdArray - fontDict = FontDict() - fontDict.setCFF2(True) - fdArray.append(fontDict) - fontDict.Private = privateDict - privateOpOrder = buildOrder(privateDictOperators2) - if privateDict is not None: - for entry in privateDictOperators: - key = entry[1] - if key not in privateOpOrder: - if key in privateDict.rawDict: - # print "Removing private dict", key - del privateDict.rawDict[key] - if hasattr(privateDict, key): - delattr(privateDict, key) - # print "Removing privateDict attr", key - else: - # clean up the PrivateDicts in the fdArray - fdArray = topDict.FDArray - privateOpOrder = buildOrder(privateDictOperators2) - for fontDict in fdArray: - fontDict.setCFF2(True) - for key in list(fontDict.rawDict.keys()): - if key not in fontDict.order: - del fontDict.rawDict[key] - if hasattr(fontDict, key): - delattr(fontDict, key) - - privateDict = fontDict.Private - for entry in privateDictOperators: - key = entry[1] - if key not in privateOpOrder: - if key in privateDict.rawDict: - # print "Removing private dict", key - del privateDict.rawDict[key] - if hasattr(privateDict, key): - delattr(privateDict, key) - # print "Removing privateDict attr", key - # Now delete up the deprecated topDict operators from CFF 1.0 - for entry in topDictOperators: - key = entry[1] - if key not in opOrder: - if key in topDict.rawDict: - del topDict.rawDict[key] - if hasattr(topDict, key): - delattr(topDict, key) - - # At this point, the Subrs and Charstrings are all still T2Charstring class - # easiest to fix this by compiling, then decompiling again - cff.major = 2 - file = BytesIO() - cff.compile(file, otFont, isCFF2=True) - file.seek(0) - cff.decompile(file, otFont, isCFF2=True) - - -def convertCFFtoCFF2(varFont): - # Convert base font to a single master CFF2 font. - cffTable = varFont["CFF "] - lib_convertCFFToCFF2(cffTable.cff, varFont) - newCFF2 = newTable("CFF2") - newCFF2.cff = cffTable.cff - varFont["CFF2"] = newCFF2 - del varFont["CFF "] - - def conv_to_int(num): if isinstance(num, float) and num.is_integer(): return int(num) diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py index c5de81cad0..f8c43187c7 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py @@ -89,7 +89,7 @@ from fontTools.misc.fixedTools import ( otRound, ) from fontTools.varLib.models import normalizeValue, piecewiseLinearMap -from fontTools.ttLib import TTFont +from fontTools.ttLib import TTFont, newTable from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables import _g_l_y_f from fontTools import varLib @@ -97,6 +97,13 @@ from fontTools import varLib # we import the `subset` module because we use the `prune_lookups` method on the GSUB # table class, and that method is only defined dynamically upon importing `subset` from fontTools import subset # noqa: F401 +from fontTools.cffLib import privateDictOperators2 +from fontTools.cffLib.specializer import ( + programToCommands, + commandsToProgram, + specializeCommands, + generalizeCommands, +) from fontTools.varLib import builder from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.merger import MutatorMerger @@ -104,6 +111,7 @@ from fontTools.varLib.instancer import names from .featureVars import instantiateFeatureVariations from fontTools.misc.cliTools import makeOutputFileName from fontTools.varLib.instancer import solver +from fontTools.ttLib.tables.otTables import VarComponentFlags import collections import dataclasses from contextlib import contextmanager @@ -458,6 +466,42 @@ class OverlapMode(IntEnum): REMOVE_AND_IGNORE_ERRORS = 3 +def instantiateVARC(varfont, axisLimits): + log.info("Instantiating VARC tables") + + # TODO(behdad) My confidence in this function is rather low; + # It needs more testing. Specially with partial-instancing, + # I don't think it currently works. + + varc = varfont["VARC"].table + fvarAxes = varfont["fvar"].axes if "fvar" in varfont else [] + + location = axisLimits.pinnedLocation() + axisMap = [i for i, axis in enumerate(fvarAxes) if axis.axisTag not in location] + reverseAxisMap = {i: j for j, i in enumerate(axisMap)} + + if varc.AxisIndicesList: + axisIndicesList = varc.AxisIndicesList.Item + for i, axisIndices in enumerate(axisIndicesList): + if any(fvarAxes[j].axisTag in axisLimits for j in axisIndices): + raise NotImplementedError( + "Instancing across VarComponent axes is not supported." + ) + axisIndicesList[i] = [reverseAxisMap[j] for j in axisIndices] + + store = varc.MultiVarStore + if store: + for region in store.SparseVarRegionList.Region: + newRegionAxis = [] + for regionRecord in region.SparseVarRegionAxis: + tag = fvarAxes[regionRecord.AxisIndex].axisTag + if tag in axisLimits: + raise NotImplementedError( + "Instancing across VarComponent axes is not supported." + ) + regionRecord.AxisIndex = reverseAxisMap[regionRecord.AxisIndex] + + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None ): @@ -566,6 +610,259 @@ def changeTupleVariationAxisLimit(var, axisTag, axisLimit): return out +def instantiateCFF2( + varfont, + axisLimits, + *, + round=round, + specialize=True, + generalize=False, + downgrade=False, +): + # The algorithm here is rather simple: + # + # Take all blend operations and store their deltas in the (otherwise empty) + # CFF2 VarStore. Then, instantiate the VarStore with the given axis limits, + # and read back the new deltas. This is done for both the CharStrings and + # the Private dicts. + # + # Then prune unused things and possibly drop the VarStore if it's empty. + # In which case, downgrade to CFF table if requested. + + log.info("Instantiating CFF2 table") + + fvarAxes = varfont["fvar"].axes + + cff = varfont["CFF2"].cff + topDict = cff.topDictIndex[0] + varStore = topDict.VarStore.otVarStore + if not varStore: + if downgrade: + from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF + + convertCFF2ToCFF(varfont) + return + + cff.desubroutinize() + + def getNumRegions(vsindex): + return varStore.VarData[vsindex if vsindex is not None else 0].VarRegionCount + + charStrings = topDict.CharStrings.values() + + # Gather all unique private dicts + uniquePrivateDicts = set() + privateDicts = [] + for fd in topDict.FDArray: + if fd.Private not in uniquePrivateDicts: + uniquePrivateDicts.add(fd.Private) + privateDicts.append(fd.Private) + + allCommands = [] + for cs in charStrings: + assert cs.private.vstore.otVarStore is varStore # Or in many places!! + commands = programToCommands(cs.program, getNumRegions=getNumRegions) + if generalize: + commands = generalizeCommands(commands) + if specialize: + commands = specializeCommands(commands, generalizeFirst=not generalize) + allCommands.append(commands) + + def storeBlendsToVarStore(arg): + if not isinstance(arg, list): + return + + if any(isinstance(subarg, list) for subarg in arg[:-1]): + raise NotImplementedError("Nested blend lists not supported (yet)") + + count = arg[-1] + assert (len(arg) - 1) % count == 0 + nRegions = (len(arg) - 1) // count - 1 + assert nRegions == getNumRegions(vsindex) + for i in range(count, len(arg) - 1, nRegions): + deltas = arg[i : i + nRegions] + assert len(deltas) == nRegions + varData = varStore.VarData[vsindex] + varData.Item.append(deltas) + varData.ItemCount += 1 + + def fetchBlendsFromVarStore(arg): + if not isinstance(arg, list): + return [arg] + + if any(isinstance(subarg, list) for subarg in arg[:-1]): + raise NotImplementedError("Nested blend lists not supported (yet)") + + count = arg[-1] + assert (len(arg) - 1) % count == 0 + numRegions = getNumRegions(vsindex) + newDefaults = [] + newDeltas = [] + for i in range(count): + defaultValue = arg[i] + + major = vsindex + minor = varDataCursor[major] + varDataCursor[major] += 1 + + varIdx = (major << 16) + minor + + defaultValue += round(defaultDeltas[varIdx]) + newDefaults.append(defaultValue) + + varData = varStore.VarData[major] + deltas = varData.Item[minor] + assert len(deltas) == numRegions + newDeltas.extend(deltas) + + if not numRegions: + return newDefaults # No deltas, just return the defaults + + return [newDefaults + newDeltas + [count]] + + # Check VarData's are empty + for varData in varStore.VarData: + assert varData.Item == [] + assert varData.ItemCount == 0 + + # Add charstring blend lists to VarStore so we can instantiate them + for commands in allCommands: + vsindex = 0 + for command in commands: + if command[0] == "vsindex": + vsindex = command[1][0] + continue + for arg in command[1]: + storeBlendsToVarStore(arg) + + # Add private blend lists to VarStore so we can instantiate values + vsindex = 0 + for opcode, name, arg_type, default, converter in privateDictOperators2: + if arg_type not in ("number", "delta", "array"): + continue + + vsindex = 0 + for private in privateDicts: + if not hasattr(private, name): + continue + values = getattr(private, name) + + if name == "vsindex": + vsindex = values[0] + continue + + if arg_type == "number": + values = [values] + + for value in values: + if not isinstance(value, list): + continue + + assert len(value) % (getNumRegions(vsindex) + 1) == 0 + count = len(value) // (getNumRegions(vsindex) + 1) + storeBlendsToVarStore(value + [count]) + + # Instantiate VarStore + defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits) + + # Read back new charstring blends from the instantiated VarStore + varDataCursor = [0] * len(varStore.VarData) + for commands in allCommands: + vsindex = 0 + for command in commands: + if command[0] == "vsindex": + vsindex = command[1][0] + continue + newArgs = [] + for arg in command[1]: + newArgs.extend(fetchBlendsFromVarStore(arg)) + command[1][:] = newArgs + + # Read back new private blends from the instantiated VarStore + for opcode, name, arg_type, default, converter in privateDictOperators2: + if arg_type not in ("number", "delta", "array"): + continue + + for private in privateDicts: + if not hasattr(private, name): + continue + values = getattr(private, name) + if arg_type == "number": + values = [values] + + newValues = [] + for value in values: + if not isinstance(value, list): + newValues.append(value) + continue + + value.append(1) + value = fetchBlendsFromVarStore(value) + newValues.extend(v[:-1] if isinstance(v, list) else v for v in value) + + if arg_type == "number": + newValues = newValues[0] + + setattr(private, name, newValues) + + # Empty out the VarStore + for i, varData in enumerate(varStore.VarData): + assert varDataCursor[i] == varData.ItemCount, ( + varDataCursor[i], + varData.ItemCount, + ) + varData.Item = [] + varData.ItemCount = 0 + + # Remove vsindex commands that are no longer needed, collect those that are. + usedVsindex = set() + for commands in allCommands: + if any(isinstance(arg, list) for command in commands for arg in command[1]): + vsindex = 0 + for command in commands: + if command[0] == "vsindex": + vsindex = command[1][0] + continue + if any(isinstance(arg, list) for arg in command[1]): + usedVsindex.add(vsindex) + else: + commands[:] = [command for command in commands if command[0] != "vsindex"] + + # Remove unused VarData and update vsindex values + vsindexMapping = {v: i for i, v in enumerate(sorted(usedVsindex))} + varStore.VarData = [ + varData for i, varData in enumerate(varStore.VarData) if i in usedVsindex + ] + varStore.VarDataCount = len(varStore.VarData) + for commands in allCommands: + for command in commands: + if command[0] == "vsindex": + command[1][0] = vsindexMapping[command[1][0]] + + # Remove initial vsindex commands that are implied + for commands in allCommands: + if commands and commands[0] == ("vsindex", [0]): + commands.pop(0) + + # Ship the charstrings! + for cs, commands in zip(charStrings, allCommands): + cs.program = commandsToProgram(commands) + + # Remove empty VarStore + if not varStore.VarData: + if "VarStore" in topDict.rawDict: + del topDict.rawDict["VarStore"] + del topDict.VarStore + del topDict.CharStrings.varStore + for private in privateDicts: + del private.vstore + + if downgrade: + from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF + + convertCFF2ToCFF(varfont) + + def _instantiateGvarGlyph( glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=True ): @@ -583,23 +880,6 @@ def _instantiateGvarGlyph( if defaultDeltas: coordinates += _g_l_y_f.GlyphCoordinates(defaultDeltas) - glyph = glyf[glyphname] - if glyph.isVarComposite(): - for component in glyph.components: - newLocation = {} - for tag, loc in component.location.items(): - if tag not in axisLimits: - newLocation[tag] = loc - continue - if component.flags & _g_l_y_f.VarComponentFlags.AXES_HAVE_VARIATION: - raise NotImplementedError( - "Instancing accross VarComposite axes with variation is not supported." - ) - limits = axisLimits[tag] - loc = limits.renormalizeValue(loc, extrapolate=False) - newLocation[tag] = loc - component.location = newLocation - # _setCoordinates also sets the hmtx/vmtx advance widths and sidebearings from # the four phantom points and glyph bounding boxes. # We call it unconditionally even if a glyph has no variations or no deltas are @@ -650,7 +930,7 @@ def instantiateGvar(varfont, axisLimits, optimize=True): key=lambda name: ( ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() or glyf[name].isVarComposite() + if glyf[name].isComposite() else 0 ), name, @@ -765,22 +1045,57 @@ def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder): # TODO(anthrotype) Add support for HVAR/VVAR in CFF2 -def _instantiateVHVAR(varfont, axisLimits, tableFields): +def _instantiateVHVAR(varfont, axisLimits, tableFields, *, round=round): location = axisLimits.pinnedLocation() tableTag = tableFields.tableTag fvarAxes = varfont["fvar"].axes - # Deltas from gvar table have already been applied to the hmtx/vmtx. For full - # instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return - if set(location).issuperset(axis.axisTag for axis in fvarAxes): - log.info("Dropping %s table", tableTag) - del varfont[tableTag] - return log.info("Instantiating %s table", tableTag) vhvar = varfont[tableTag].table varStore = vhvar.VarStore - # since deltas were already applied, the return value here is ignored - instantiateItemVariationStore(varStore, fvarAxes, axisLimits) + + if "glyf" in varfont: + # Deltas from gvar table have already been applied to the hmtx/vmtx. For full + # instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return + if set(location).issuperset(axis.axisTag for axis in fvarAxes): + log.info("Dropping %s table", tableTag) + del varfont[tableTag] + return + + defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits) + + if "glyf" not in varfont: + # CFF2 fonts need hmtx/vmtx updated here. For glyf fonts, the instantiateGvar + # function already updated the hmtx/vmtx from phantom points. Maybe remove + # that and do it here for both CFF2 and glyf fonts? + # + # Specially, if a font has glyf but not gvar, the hmtx/vmtx will not have been + # updated by instantiateGvar. Though one can call that a faulty font. + metricsTag = "vmtx" if tableTag == "VVAR" else "hmtx" + if metricsTag in varfont: + advMapping = getattr(vhvar, tableFields.advMapping) + metricsTable = varfont[metricsTag] + metrics = metricsTable.metrics + for glyphName, (advanceWidth, sb) in metrics.items(): + if advMapping: + varIdx = advMapping.mapping[glyphName] + else: + varIdx = varfont.getGlyphID(glyphName) + metrics[glyphName] = (advanceWidth + round(defaultDeltas[varIdx]), sb) + + if ( + tableTag == "VVAR" + and getattr(vhvar, tableFields.vOrigMapping) is not None + ): + log.warning( + "VORG table not yet updated to reflect changes in VVAR table" + ) + + # For full instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return + if set(location).issuperset(axis.axisTag for axis in fvarAxes): + log.info("Dropping %s table", tableTag) + del varfont[tableTag] + return if varStore.VarRegionList.Region: # Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap @@ -923,6 +1238,8 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits): newItemVarStore = tupleVarStore.asItemVarStore() itemVarStore.VarRegionList = newItemVarStore.VarRegionList + if not hasattr(itemVarStore, "VarDataCount"): # Happens fromXML + itemVarStore.VarDataCount = len(newItemVarStore.VarData) assert itemVarStore.VarDataCount == newItemVarStore.VarDataCount itemVarStore.VarData = newItemVarStore.VarData @@ -1019,7 +1336,11 @@ def _isValidAvarSegmentMap(axisTag, segmentMap): def instantiateAvar(varfont, axisLimits): # 'axisLimits' dict must contain user-space (non-normalized) coordinates. - segments = varfont["avar"].segments + avar = varfont["avar"] + if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore: + raise NotImplementedError("avar table with VarStore is not supported") + + segments = avar.segments # drop table if we instantiate all the axes pinnedAxes = set(axisLimits.pinnedLocation()) @@ -1080,7 +1401,7 @@ def instantiateAvar(varfont, axisLimits): newSegments[axisTag] = newMapping else: newSegments[axisTag] = mapping - varfont["avar"].segments = newSegments + avar.segments = newSegments def isInstanceWithinAxisRanges(location, axisRanges): @@ -1218,9 +1539,6 @@ def sanityCheckVariableTables(varfont): if "gvar" in varfont: if "glyf" not in varfont: raise ValueError("Can't have gvar without glyf") - # TODO(anthrotype) Remove once we do support partial instancing CFF2 - if "CFF2" in varfont: - raise NotImplementedError("Instancing CFF2 variable fonts is not supported yet") def instantiateVariableFont( @@ -1230,6 +1548,8 @@ def instantiateVariableFont( optimize=True, overlap=OverlapMode.KEEP_AND_SET_FLAGS, updateFontNames=False, + *, + downgradeCFF2=False, ): """Instantiate variable font, either fully or partially. @@ -1269,6 +1589,11 @@ def instantiateVariableFont( in the head and OS/2 table will be updated so they conform to the R/I/B/BI model. If the STAT table is missing or an Axis Value table is missing for a given axis coordinate, a ValueError will be raised. + downgradeCFF2 (bool): if True, downgrade the CFF2 table to CFF table when possible + ie. full instancing of all axes. This is useful for compatibility with older + software that does not support CFF2. Defaults to False. Note that this + operation also removes overlaps within glyph shapes, as CFF does not support + overlaps but CFF2 does. """ # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool overlap = OverlapMode(int(overlap)) @@ -1293,6 +1618,12 @@ def instantiateVariableFont( log.info("Updating name table") names.updateNameTable(varfont, axisLimits) + if "VARC" in varfont: + instantiateVARC(varfont, normalizedLimits) + + if "CFF2" in varfont: + instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2) + if "gvar" in varfont: instantiateGvar(varfont, normalizedLimits, optimize=optimize) @@ -1485,6 +1816,11 @@ def parseArgs(args): "a STAT table with Axis Value Tables", ) parser.add_argument( + "--downgrade-cff2", + action="store_true", + help="If all axes are pinned, downgrade CFF2 to CFF table format", + ) + parser.add_argument( "--no-recalc-timestamp", dest="recalc_timestamp", action="store_false", @@ -1545,7 +1881,7 @@ def main(args=None): ) isFullInstance = { - axisTag for axisTag, limit in axisLimits.items() if not isinstance(limit, tuple) + axisTag for axisTag, limit in axisLimits.items() if limit[0] == limit[2] }.issuperset(axis.axisTag for axis in varfont["fvar"].axes) instantiateVariableFont( @@ -1555,6 +1891,7 @@ def main(args=None): optimize=options.optimize, overlap=options.overlap, updateFontNames=options.update_name_table, + downgradeCFF2=options.downgrade_cff2, ) suffix = "-instance" if isFullInstance else "-partial" diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py index 5fc12e04c9..e212ecf8d1 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py @@ -749,22 +749,27 @@ def main(args=None): if "gvar" in font: # Is variable font - axisMapping = {} 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"] - for axisTag, segments in avar.segments.items(): - fvarMapping = axisMapping[axisTag].copy() - for location, value in segments.items(): - axisMapping[axisTag][value] = piecewiseLinearMap( - location, fvarMapping - ) + 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 + ) gvar = font["gvar"] glyf = font["glyf"] @@ -811,6 +816,8 @@ def main(args=None): ) + "'" ) + if normalized: + name += " (normalized)" names.append(name) fonts.append(glyphsets[locTuple]) locations.append(dict(locTuple)) diff --git a/contrib/python/fonttools/fontTools/varLib/models.py b/contrib/python/fonttools/fontTools/varLib/models.py index 59815316f8..819596991f 100644 --- a/contrib/python/fonttools/fontTools/varLib/models.py +++ b/contrib/python/fonttools/fontTools/varLib/models.py @@ -75,7 +75,7 @@ def normalizeValue(v, triple, extrapolate=False): return (v - default) / (upper - default) -def normalizeLocation(location, axes, extrapolate=False): +def normalizeLocation(location, axes, extrapolate=False, *, validate=False): """Normalizes location based on axis min/default/max values from axes. >>> axes = {"wght": (100, 400, 900)} @@ -114,6 +114,10 @@ def normalizeLocation(location, axes, extrapolate=False): >>> normalizeLocation({"wght": 1001}, axes) {'wght': 0.0} """ + if validate: + assert set(location.keys()) <= set(axes.keys()), set(location.keys()) - set( + axes.keys() + ) out = {} for tag, triple in axes.items(): v = location.get(tag, triple[1]) @@ -453,7 +457,10 @@ class VariationModel(object): self.deltaWeights.append(deltaWeight) def getDeltas(self, masterValues, *, round=noRound): - assert len(masterValues) == len(self.deltaWeights) + assert len(masterValues) == len(self.deltaWeights), ( + len(masterValues), + len(self.deltaWeights), + ) mapping = self.reverseMapping out = [] for i, weights in enumerate(self.deltaWeights): diff --git a/contrib/python/fonttools/fontTools/varLib/multiVarStore.py b/contrib/python/fonttools/fontTools/varLib/multiVarStore.py new file mode 100644 index 0000000000..f24a6e6f75 --- /dev/null +++ b/contrib/python/fonttools/fontTools/varLib/multiVarStore.py @@ -0,0 +1,253 @@ +from fontTools.misc.roundTools import noRound, otRound +from fontTools.misc.intTools import bit_count +from fontTools.misc.vector import Vector +from fontTools.ttLib.tables import otTables as ot +from fontTools.varLib.models import supportScalar +import fontTools.varLib.varStore # For monkey-patching +from fontTools.varLib.builder import ( + buildVarRegionList, + buildSparseVarRegionList, + buildSparseVarRegion, + buildMultiVarStore, + buildMultiVarData, +) +from fontTools.misc.iterTools import batched +from functools import partial +from collections import defaultdict +from heapq import heappush, heappop + + +NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX +ot.MultiVarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX + + +def _getLocationKey(loc): + return tuple(sorted(loc.items(), key=lambda kv: kv[0])) + + +class OnlineMultiVarStoreBuilder(object): + def __init__(self, axisTags): + self._axisTags = axisTags + self._regionMap = {} + self._regionList = buildSparseVarRegionList([], axisTags) + self._store = buildMultiVarStore(self._regionList, []) + self._data = None + self._model = None + self._supports = None + self._varDataIndices = {} + self._varDataCaches = {} + self._cache = None + + def setModel(self, model): + self.setSupports(model.supports) + self._model = model + + def setSupports(self, supports): + self._model = None + self._supports = list(supports) + if not self._supports[0]: + del self._supports[0] # Drop base master support + self._cache = None + self._data = None + + def finish(self, optimize=True): + self._regionList.RegionCount = len(self._regionList.Region) + self._store.MultiVarDataCount = len(self._store.MultiVarData) + return self._store + + def _add_MultiVarData(self): + regionMap = self._regionMap + regionList = self._regionList + + regions = self._supports + regionIndices = [] + for region in regions: + key = _getLocationKey(region) + idx = regionMap.get(key) + if idx is None: + varRegion = buildSparseVarRegion(region, self._axisTags) + idx = regionMap[key] = len(regionList.Region) + regionList.Region.append(varRegion) + regionIndices.append(idx) + + # Check if we have one already... + key = tuple(regionIndices) + varDataIdx = self._varDataIndices.get(key) + if varDataIdx is not None: + self._outer = varDataIdx + self._data = self._store.MultiVarData[varDataIdx] + self._cache = self._varDataCaches[key] + if len(self._data.Item) == 0xFFFF: + # This is full. Need new one. + varDataIdx = None + + if varDataIdx is None: + self._data = buildMultiVarData(regionIndices, []) + self._outer = len(self._store.MultiVarData) + self._store.MultiVarData.append(self._data) + self._varDataIndices[key] = self._outer + if key not in self._varDataCaches: + self._varDataCaches[key] = {} + self._cache = self._varDataCaches[key] + + def storeMasters(self, master_values, *, round=round): + deltas = self._model.getDeltas(master_values, round=round) + base = deltas.pop(0) + return base, self.storeDeltas(deltas, round=noRound) + + def storeDeltas(self, deltas, *, round=round): + deltas = tuple(round(d) for d in deltas) + + if not any(deltas): + return NO_VARIATION_INDEX + + deltas_tuple = tuple(tuple(d) for d in deltas) + + if not self._data: + self._add_MultiVarData() + + varIdx = self._cache.get(deltas_tuple) + if varIdx is not None: + return varIdx + + inner = len(self._data.Item) + if inner == 0xFFFF: + # Full array. Start new one. + self._add_MultiVarData() + return self.storeDeltas(deltas, round=noRound) + self._data.addItem(deltas, round=noRound) + + varIdx = (self._outer << 16) + inner + self._cache[deltas_tuple] = varIdx + return varIdx + + +def MultiVarData_addItem(self, deltas, *, round=round): + deltas = tuple(round(d) for d in deltas) + + assert len(deltas) == self.VarRegionCount + + values = [] + for d in deltas: + values.extend(d) + + self.Item.append(values) + self.ItemCount = len(self.Item) + + +ot.MultiVarData.addItem = MultiVarData_addItem + + +def SparseVarRegion_get_support(self, fvar_axes): + return { + fvar_axes[reg.AxisIndex].axisTag: (reg.StartCoord, reg.PeakCoord, reg.EndCoord) + for reg in self.SparseVarRegionAxis + } + + +ot.SparseVarRegion.get_support = SparseVarRegion_get_support + + +def MultiVarStore___bool__(self): + return bool(self.MultiVarData) + + +ot.MultiVarStore.__bool__ = MultiVarStore___bool__ + + +class MultiVarStoreInstancer(object): + def __init__(self, multivarstore, fvar_axes, location={}): + self.fvar_axes = fvar_axes + assert multivarstore is None or multivarstore.Format == 1 + self._varData = multivarstore.MultiVarData if multivarstore else [] + self._regions = ( + multivarstore.SparseVarRegionList.Region if multivarstore else [] + ) + self.setLocation(location) + + def setLocation(self, location): + self.location = dict(location) + self._clearCaches() + + def _clearCaches(self): + self._scalars = {} + + def _getScalar(self, regionIdx): + scalar = self._scalars.get(regionIdx) + if scalar is None: + support = self._regions[regionIdx].get_support(self.fvar_axes) + scalar = supportScalar(self.location, support) + self._scalars[regionIdx] = scalar + return scalar + + @staticmethod + def interpolateFromDeltasAndScalars(deltas, scalars): + if not deltas: + return Vector([]) + assert len(deltas) % len(scalars) == 0, (len(deltas), len(scalars)) + m = len(deltas) // len(scalars) + delta = Vector([0] * m) + for d, s in zip(batched(deltas, m), scalars): + if not s: + continue + delta += Vector(d) * s + return delta + + def __getitem__(self, varidx): + major, minor = varidx >> 16, varidx & 0xFFFF + if varidx == NO_VARIATION_INDEX: + return Vector([]) + varData = self._varData + scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex] + deltas = varData[major].Item[minor] + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + def interpolateFromDeltas(self, varDataIndex, deltas): + varData = self._varData + scalars = [self._getScalar(ri) for ri in varData[varDataIndex].VarRegionIndex] + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + +def MultiVarStore_subset_varidxes(self, varIdxes): + return ot.VarStore.subset_varidxes(self, varIdxes, VarData="MultiVarData") + + +def MultiVarStore_prune_regions(self): + return ot.VarStore.prune_regions( + self, VarData="MultiVarData", VarRegionList="SparseVarRegionList" + ) + + +ot.MultiVarStore.prune_regions = MultiVarStore_prune_regions +ot.MultiVarStore.subset_varidxes = MultiVarStore_subset_varidxes + + +def MultiVarStore_get_supports(self, major, fvarAxes): + supports = [] + varData = self.MultiVarData[major] + for regionIdx in varData.VarRegionIndex: + region = self.SparseVarRegionList.Region[regionIdx] + support = region.get_support(fvarAxes) + supports.append(support) + return supports + + +ot.MultiVarStore.get_supports = MultiVarStore_get_supports + + +def VARC_collect_varidxes(self, varidxes): + for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: + for component in glyph.components: + varidxes.add(component.axisValuesVarIndex) + varidxes.add(component.transformVarIndex) + + +def VARC_remap_varidxes(self, varidxes_map): + for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: + for component in glyph.components: + component.axisValuesVarIndex = varidxes_map[component.axisValuesVarIndex] + component.transformVarIndex = varidxes_map[component.transformVarIndex] + + +ot.VARC.collect_varidxes = VARC_collect_varidxes +ot.VARC.remap_varidxes = VARC_remap_varidxes diff --git a/contrib/python/fonttools/fontTools/varLib/mutator.py b/contrib/python/fonttools/fontTools/varLib/mutator.py index c7c37dabca..6c327f9454 100644 --- a/contrib/python/fonttools/fontTools/varLib/mutator.py +++ b/contrib/python/fonttools/fontTools/varLib/mutator.py @@ -201,7 +201,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True): key=lambda name: ( ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth - if glyf[name].isComposite() or glyf[name].isVarComposite() + if glyf[name].isComposite() else 0 ), name, diff --git a/contrib/python/fonttools/fontTools/varLib/varStore.py b/contrib/python/fonttools/fontTools/varLib/varStore.py index 7805769074..f54fad2db3 100644 --- a/contrib/python/fonttools/fontTools/varLib/varStore.py +++ b/contrib/python/fonttools/fontTools/varLib/varStore.py @@ -32,7 +32,7 @@ class OnlineVarStoreBuilder(object): self._supports = None self._varDataIndices = {} self._varDataCaches = {} - self._cache = {} + self._cache = None def setModel(self, model): self.setSupports(model.supports) @@ -43,7 +43,7 @@ class OnlineVarStoreBuilder(object): self._supports = list(supports) if not self._supports[0]: del self._supports[0] # Drop base master support - self._cache = {} + self._cache = None self._data = None def finish(self, optimize=True): @@ -54,7 +54,7 @@ class OnlineVarStoreBuilder(object): data.calculateNumShorts(optimize=optimize) return self._store - def _add_VarData(self): + def _add_VarData(self, num_items=1): regionMap = self._regionMap regionList = self._regionList @@ -76,7 +76,7 @@ class OnlineVarStoreBuilder(object): self._outer = varDataIdx self._data = self._store.VarData[varDataIdx] self._cache = self._varDataCaches[key] - if len(self._data.Item) == 0xFFFF: + if len(self._data.Item) + num_items > 0xFFFF: # This is full. Need new one. varDataIdx = None @@ -94,6 +94,14 @@ class OnlineVarStoreBuilder(object): base = deltas.pop(0) return base, self.storeDeltas(deltas, round=noRound) + def storeMastersMany(self, master_values_list, *, round=round): + deltas_list = [ + self._model.getDeltas(master_values, round=round) + for master_values in master_values_list + ] + base_list = [deltas.pop(0) for deltas in deltas_list] + return base_list, self.storeDeltasMany(deltas_list, round=noRound) + def storeDeltas(self, deltas, *, round=round): deltas = [round(d) for d in deltas] if len(deltas) == len(self._supports) + 1: @@ -102,23 +110,51 @@ class OnlineVarStoreBuilder(object): assert len(deltas) == len(self._supports) deltas = tuple(deltas) + if not self._data: + self._add_VarData() + varIdx = self._cache.get(deltas) if varIdx is not None: return varIdx - if not self._data: - self._add_VarData() inner = len(self._data.Item) if inner == 0xFFFF: # Full array. Start new one. self._add_VarData() - return self.storeDeltas(deltas) + return self.storeDeltas(deltas, round=noRound) self._data.addItem(deltas, round=noRound) varIdx = (self._outer << 16) + inner self._cache[deltas] = varIdx return varIdx + def storeDeltasMany(self, deltas_list, *, round=round): + deltas_list = [[round(d) for d in deltas] for deltas in deltas_list] + deltas_list = tuple(tuple(deltas) for deltas in deltas_list) + + if not self._data: + self._add_VarData(len(deltas_list)) + + varIdx = self._cache.get(deltas_list) + if varIdx is not None: + return varIdx + + inner = len(self._data.Item) + if inner + len(deltas_list) > 0xFFFF: + # Full array. Start new one. + self._add_VarData(len(deltas_list)) + return self.storeDeltasMany(deltas_list, round=noRound) + for i, deltas in enumerate(deltas_list): + self._data.addItem(deltas, round=noRound) + + varIdx = (self._outer << 16) + inner + i + self._cache[deltas] = varIdx + + varIdx = (self._outer << 16) + inner + self._cache[deltas_list] = varIdx + + return varIdx + def VarData_addItem(self, deltas, *, round=round): deltas = [round(d) for d in deltas] @@ -210,26 +246,29 @@ class VarStoreInstancer(object): def VarStore_subset_varidxes( - self, varIdxes, optimize=True, retainFirstMap=False, advIdxes=set() + self, + varIdxes, + optimize=True, + retainFirstMap=False, + advIdxes=set(), + *, + VarData="VarData", ): # Sort out used varIdxes by major/minor. - used = {} + used = defaultdict(set) for varIdx in varIdxes: if varIdx == NO_VARIATION_INDEX: continue major = varIdx >> 16 minor = varIdx & 0xFFFF - d = used.get(major) - if d is None: - d = used[major] = set() - d.add(minor) + used[major].add(minor) del varIdxes # # Subset VarData # - varData = self.VarData + varData = getattr(self, VarData) newVarData = [] varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX} for major, data in enumerate(varData): @@ -260,10 +299,11 @@ def VarStore_subset_varidxes( data.Item = newItems data.ItemCount = len(data.Item) - data.calculateNumShorts(optimize=optimize) + if VarData == "VarData": + data.calculateNumShorts(optimize=optimize) - self.VarData = newVarData - self.VarDataCount = len(self.VarData) + setattr(self, VarData, newVarData) + setattr(self, VarData + "Count", len(newVarData)) self.prune_regions() @@ -273,7 +313,7 @@ def VarStore_subset_varidxes( ot.VarStore.subset_varidxes = VarStore_subset_varidxes -def VarStore_prune_regions(self): +def VarStore_prune_regions(self, *, VarData="VarData", VarRegionList="VarRegionList"): """Remove unused VarRegions.""" # # Subset VarRegionList @@ -281,10 +321,10 @@ def VarStore_prune_regions(self): # Collect. usedRegions = set() - for data in self.VarData: + for data in getattr(self, VarData): usedRegions.update(data.VarRegionIndex) # Subset. - regionList = self.VarRegionList + regionList = getattr(self, VarRegionList) regions = regionList.Region newRegions = [] regionMap = {} @@ -294,7 +334,7 @@ def VarStore_prune_regions(self): regionList.Region = newRegions regionList.RegionCount = len(regionList.Region) # Map. - for data in self.VarData: + for data in getattr(self, VarData): data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex] diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index 91be3261df..51c9d08b3b 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.51.0) +VERSION(4.52.1) LICENSE(MIT) @@ -23,8 +23,11 @@ PY_SRCS( fontTools/__main__.py fontTools/afmLib.py fontTools/agl.py + fontTools/cffLib/CFF2ToCFF.py + fontTools/cffLib/CFFToCFF2.py fontTools/cffLib/__init__.py fontTools/cffLib/specializer.py + fontTools/cffLib/transforms.py fontTools/cffLib/width.py fontTools/colorLib/__init__.py fontTools/colorLib/builder.py @@ -84,6 +87,8 @@ PY_SRCS( fontTools/misc/filenames.py fontTools/misc/fixedTools.py fontTools/misc/intTools.py + fontTools/misc/iterTools.py + fontTools/misc/lazyTools.py fontTools/misc/loggingTools.py fontTools/misc/macCreatorType.py fontTools/misc/macRes.py @@ -215,6 +220,7 @@ PY_SRCS( fontTools/ttLib/tables/T_S_I__5.py fontTools/ttLib/tables/T_T_F_A_.py fontTools/ttLib/tables/TupleVariation.py + fontTools/ttLib/tables/V_A_R_C_.py fontTools/ttLib/tables/V_D_M_X_.py fontTools/ttLib/tables/V_O_R_G_.py fontTools/ttLib/tables/V_V_A_R_.py @@ -309,6 +315,7 @@ PY_SRCS( fontTools/varLib/iup.py fontTools/varLib/merger.py fontTools/varLib/models.py + fontTools/varLib/multiVarStore.py fontTools/varLib/mutator.py fontTools/varLib/mvar.py fontTools/varLib/plot.py diff --git a/contrib/python/zope.interface/py3/.dist-info/METADATA b/contrib/python/zope.interface/py3/.dist-info/METADATA index c903dd97fe..58a1b3d751 100644 --- a/contrib/python/zope.interface/py3/.dist-info/METADATA +++ b/contrib/python/zope.interface/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: zope.interface -Version: 6.4.post1 +Version: 6.4.post2 Summary: Interfaces for Python Home-page: https://github.com/zopefoundation/zope.interface Author: Zope Foundation and Contributors @@ -75,6 +75,13 @@ For detailed documentation, please see https://zopeinterface.readthedocs.io/en/l Changes ========= +6.4.post2 (unreleased) +====================== + +- Publish missing Windows wheels, second attempt. + (`#295 <https://github.com/zopefoundation/zope.interface/issues/295>`_) + + 6.4.post1 (2024-05-23) ====================== @@ -89,14 +96,14 @@ For detailed documentation, please see https://zopeinterface.readthedocs.io/en/l ``zope_interface-6.4.tar.gz`` instead of ``zope.interface-6.4-py2.tar.gz`` which cannot be installed by ``zc.buildout``. This release is a re-release of version 6.4 with the correct sdist name. - (`#298 <https://github.com/zopefoundation/zope.interface/issues/298>`) + (`#298 <https://github.com/zopefoundation/zope.interface/issues/298>`_) 6.4 (2024-05-15) ================ - Adjust for incompatible changes in Python 3.13b1. - (`#292 <https://github.com/zopefoundation/zope.interface/issues/292>`) + (`#292 <https://github.com/zopefoundation/zope.interface/issues/292>`_) - Build windows wheels on GHA. diff --git a/contrib/python/zope.interface/py3/ya.make b/contrib/python/zope.interface/py3/ya.make index e7b5d1332a..c2b5dcb393 100644 --- a/contrib/python/zope.interface/py3/ya.make +++ b/contrib/python/zope.interface/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.4.post1) +VERSION(6.4.post2) LICENSE(ZPL-2.1) |