|
"""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.)""" |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
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) :] = [] |
|
|
|
|
|
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 [] |
|
) |
|
) |
|
|
|
|
|
|
|
extractor = T2WidthExtractor( |
|
thisLocalSubrs, |
|
globalSubrs, |
|
nominalWidthXError, |
|
0, |
|
) |
|
try: |
|
extractor.execute(cs) |
|
except _NominalWidthUsedError: |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
program.pop(0) |
|
|
|
if program and program[-1] == "endchar": |
|
program.pop() |
|
|
|
if removeUnusedSubrs: |
|
cff.remove_unused_subroutines() |
|
|
|
|
|
|
|
cff.major = 2 |
|
cff2GetGlyphOrder = cff.otFont.getGlyphOrder |
|
topDictData = TopDictIndex(None, cff2GetGlyphOrder) |
|
for item in cff.topDictIndex: |
|
|
|
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: |
|
|
|
del privateDict.rawDict[key] |
|
if hasattr(privateDict, key): |
|
delattr(privateDict, key) |
|
|
|
else: |
|
|
|
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()): |
|
|
|
del privateDict.rawDict[key] |
|
if hasattr(privateDict, key): |
|
delattr(privateDict, key) |
|
|
|
|
|
|
|
for entry in topDictOperators: |
|
key = entry[1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:])) |
|
|