You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			306 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			306 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
"""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 (
 | 
						|
            [topDict.Private.Subrs]
 | 
						|
            if hasattr(topDict, "Private") and hasattr(topDict.Private, "Subrs")
 | 
						|
            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
 | 
						|
 | 
						|
        thisLocalSubrs = (
 | 
						|
            localSubrs[fdIndex]
 | 
						|
            if fdIndex is not None
 | 
						|
            else (
 | 
						|
                getattr(topDict.Private, "Subrs", [])
 | 
						|
                if hasattr(topDict, "Private")
 | 
						|
                else []
 | 
						|
            )
 | 
						|
        )
 | 
						|
 | 
						|
        # Intentionally use custom type for nominalWidthX, such that any
 | 
						|
        # CharString that has an explicit width encoded will throw back to us.
 | 
						|
        extractor = T2WidthExtractor(
 | 
						|
            thisLocalSubrs,
 | 
						|
            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 len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]:
 | 
						|
                removeUnusedSubrs = True
 | 
						|
                subrNumber = program.pop(0)
 | 
						|
                assert isinstance(subrNumber, int), subrNumber
 | 
						|
                op = program.pop(0)
 | 
						|
                bias = extractor.localBias if op == "callsubr" else extractor.globalBias
 | 
						|
                subrNumber += bias
 | 
						|
                subrSet = thisLocalSubrs if op == "callsubr" else globalSubrs
 | 
						|
                subrProgram = subrSet[subrNumber].program
 | 
						|
                program[:0] = subrProgram
 | 
						|
            # Now pop the actual width
 | 
						|
            assert len(program) >= 1, program
 | 
						|
            program.pop(0)
 | 
						|
 | 
						|
        if program and program[-1] == "endchar":
 | 
						|
            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:]))
 |