Spaces:
Running
Running
"""CFF2 to CFF converter.""" | |
from fontTools.ttLib import TTFont, newTable | |
from fontTools.misc.cliTools import makeOutputFileName | |
from fontTools.cffLib import ( | |
TopDictIndex, | |
buildOrder, | |
buildDefaults, | |
topDictOperators, | |
privateDictOperators, | |
) | |
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 CFF 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, isCFF2=True) | |
for item in cff.topDictIndex: | |
# Iterate over, such that all are decompiled | |
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) | |
fdArray = topDict.FDArray | |
charStrings = topDict.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) | |
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) | |
def convertCFF2ToCFF(font, *, updatePostTable=True): | |
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 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="-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:])) | |