123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- """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:]))
|