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