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.
		
		
		
		
		
			
		
			
				
	
	
		
			234 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			234 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Python
		
	
"""CFF2 to CFF converter."""
 | 
						|
 | 
						|
from fontTools.ttLib import TTFont, newTable
 | 
						|
from fontTools.misc.cliTools import makeOutputFileName
 | 
						|
from fontTools.misc.psCharStrings import T2StackUseExtractor
 | 
						|
from fontTools.cffLib import (
 | 
						|
    TopDictIndex,
 | 
						|
    buildOrder,
 | 
						|
    buildDefaults,
 | 
						|
    topDictOperators,
 | 
						|
    privateDictOperators,
 | 
						|
    FDSelect,
 | 
						|
)
 | 
						|
from .transforms import desubroutinizeCharString
 | 
						|
from .specializer import specializeProgram
 | 
						|
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 CFF2 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)
 | 
						|
    for item in cff.topDictIndex:
 | 
						|
        # Iterate over, such that all are decompiled
 | 
						|
        item.cff2GetGlyphOrder = None
 | 
						|
        topDictData.append(item)
 | 
						|
    cff.topDictIndex = topDictData
 | 
						|
    topDict = topDictData[0]
 | 
						|
 | 
						|
    if hasattr(topDict, "VarStore"):
 | 
						|
        raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
 | 
						|
 | 
						|
    opOrder = buildOrder(topDictOperators)
 | 
						|
    topDict.order = opOrder
 | 
						|
    for key in topDict.rawDict.keys():
 | 
						|
        if key not in opOrder:
 | 
						|
            del topDict.rawDict[key]
 | 
						|
            if hasattr(topDict, key):
 | 
						|
                delattr(topDict, key)
 | 
						|
 | 
						|
    charStrings = topDict.CharStrings
 | 
						|
 | 
						|
    fdArray = topDict.FDArray
 | 
						|
    if not hasattr(topDict, "FDSelect"):
 | 
						|
        # FDSelect is optional in CFF2, but required in CFF.
 | 
						|
        fdSelect = topDict.FDSelect = FDSelect()
 | 
						|
        fdSelect.gidArray = [0] * len(charStrings.charStrings)
 | 
						|
 | 
						|
    defaults = buildDefaults(privateDictOperators)
 | 
						|
    order = buildOrder(privateDictOperators)
 | 
						|
    for fd in fdArray:
 | 
						|
        fd.setCFF2(False)
 | 
						|
        privateDict = fd.Private
 | 
						|
        privateDict.order = order
 | 
						|
        for key in order:
 | 
						|
            if key not in privateDict.rawDict and key in defaults:
 | 
						|
                privateDict.rawDict[key] = defaults[key]
 | 
						|
        for key in privateDict.rawDict.keys():
 | 
						|
            if key not in order:
 | 
						|
                del privateDict.rawDict[key]
 | 
						|
                if hasattr(privateDict, key):
 | 
						|
                    delattr(privateDict, key)
 | 
						|
 | 
						|
    # Add ending operators
 | 
						|
    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)
 | 
						|
 | 
						|
    # Handle stack use since stack-depth is lower in CFF than in CFF2.
 | 
						|
    for glyphName in charStrings.keys():
 | 
						|
        cs, fdIndex = charStrings.getItemAndSelector(glyphName)
 | 
						|
        if fdIndex is None:
 | 
						|
            fdIndex = 0
 | 
						|
        private = fdArray[fdIndex].Private
 | 
						|
        extractor = T2StackUseExtractor(
 | 
						|
            getattr(private, "Subrs", []), cff.GlobalSubrs, private=private
 | 
						|
        )
 | 
						|
        stackUse = extractor.execute(cs)
 | 
						|
        if stackUse > 48:  # CFF stack depth is 48
 | 
						|
            desubroutinizeCharString(cs)
 | 
						|
            cs.program = specializeProgram(cs.program)
 | 
						|
 | 
						|
    # Unused subroutines are still in CFF2 (ie. lacking 'return' operator)
 | 
						|
    # because they were not decompiled when we added the 'return'.
 | 
						|
    # Moreover, some used subroutines may have become unused after the
 | 
						|
    # stack-use fixup. So we remove all unused subroutines now.
 | 
						|
    cff.remove_unused_subroutines()
 | 
						|
 | 
						|
    mapping = {
 | 
						|
        name: ("cid" + str(n).zfill(5) if n else ".notdef")
 | 
						|
        for n, name in enumerate(topDict.charset)
 | 
						|
    }
 | 
						|
    topDict.charset = [
 | 
						|
        "cid" + str(n).zfill(5) if n else ".notdef" for n in range(len(topDict.charset))
 | 
						|
    ]
 | 
						|
    charStrings.charStrings = {
 | 
						|
        mapping[name]: v for name, v in charStrings.charStrings.items()
 | 
						|
    }
 | 
						|
 | 
						|
    topDict.ROS = ("Adobe", "Identity", 0)
 | 
						|
 | 
						|
 | 
						|
def convertCFF2ToCFF(font, *, updatePostTable=True):
 | 
						|
    if "CFF2" not in font:
 | 
						|
        raise ValueError("Input font does not contain a CFF2 table.")
 | 
						|
    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 CFF2 OTF font to CFF OTF font"""
 | 
						|
    if args is None:
 | 
						|
        import sys
 | 
						|
 | 
						|
        args = sys.argv[1:]
 | 
						|
 | 
						|
    import argparse
 | 
						|
 | 
						|
    parser = argparse.ArgumentParser(
 | 
						|
        "fonttools cffLib.CFF2ToCFF",
 | 
						|
        description="Convert a non-variable CFF2 font to CFF.",
 | 
						|
    )
 | 
						|
    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:]))
 |