robot-piglet 9 месяцев назад
Родитель
Сommit
c3763910c6

+ 28 - 1
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)
 ----------------------------
 

+ 1 - 1
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"]

+ 165 - 0
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:]))

+ 287 - 0
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:]))

+ 25 - 200
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):

+ 4 - 6
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.")

+ 482 - 0
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

+ 3 - 0
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

+ 1 - 1
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__":

+ 0 - 1
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")
 

Некоторые файлы не были показаны из-за большого количества измененных файлов