"""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 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 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 = thisLocalSubrs 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 . 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:]))