[cffLib] Add CFFToCFF2 and CFF2ToCFF cmdline and module
The CFF2ToCFF module is rather solid, at least IMO. This takes convertCFFToCFF2 from cffLib. Apparently there's a more complete one in varLib.cff: https://github.com/fonttools/fonttools/issues/1835 Should merge the two and finish them.
This commit is contained in:
parent
60e30fe008
commit
39ec4e6c0c
132
Lib/fontTools/cffLib/CFF2ToCFF.py
Normal file
132
Lib/fontTools/cffLib/CFF2ToCFF.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""CFF to CFF2 converter."""
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.cffLib import TopDictIndex, buildOrder, topDictOperators
|
||||
from .width import optimizeWidths
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
__all__ = ["convertCFF2ToCFF", "main"]
|
||||
|
||||
|
||||
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`.)"""
|
||||
|
||||
cff.major = 1
|
||||
topDict = cff.topDictIndex[0]
|
||||
if hasattr(topDict, "VarStore"):
|
||||
raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
|
||||
|
||||
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, "Private"):
|
||||
privateDict = topDict.Private
|
||||
else:
|
||||
privateDict = None
|
||||
opOrder = buildOrder(topDictOperators)
|
||||
topDict.order = opOrder
|
||||
|
||||
fdArray = topDict.FDArray
|
||||
charStrings = topDict.CharStrings
|
||||
|
||||
for cs in charStrings.values():
|
||||
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 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.",
|
||||
)
|
||||
options = parser.parse_args(args)
|
||||
|
||||
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)
|
||||
cff = font["CFF2"].cff
|
||||
|
||||
cff.convertCFF2ToCFF(font)
|
||||
|
||||
del font["CFF2"]
|
||||
table = font["CFF "] = newTable("CFF ")
|
||||
table.cff = cff
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main(sys.argv[1:]))
|
166
Lib/fontTools/cffLib/CFFToCFF2.py
Normal file
166
Lib/fontTools/cffLib/CFFToCFF2.py
Normal file
@ -0,0 +1,166 @@
|
||||
"""CFF to CFF2 converter."""
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.cffLib import (
|
||||
TopDictIndex,
|
||||
FDArrayIndex,
|
||||
FontDict,
|
||||
buildOrder,
|
||||
topDictOperators,
|
||||
privateDictOperators,
|
||||
topDictOperators2,
|
||||
privateDictOperators2,
|
||||
)
|
||||
from io import BytesIO
|
||||
|
||||
__all__ = ["convertCFFToCFF2", "main"]
|
||||
|
||||
|
||||
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`.)"""
|
||||
|
||||
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
|
||||
for entry in topDictOperators:
|
||||
key = entry[1]
|
||||
if key not in opOrder:
|
||||
if key in topDict.rawDict:
|
||||
del topDict.rawDict[key]
|
||||
if hasattr(topDict, key):
|
||||
delattr(topDict, key)
|
||||
|
||||
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)
|
||||
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 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 privateDict.rawDict:
|
||||
# print "Removing private dict", key
|
||||
del privateDict.rawDict[key]
|
||||
if hasattr(privateDict, key):
|
||||
delattr(privateDict, key)
|
||||
# print "Removing privateDict attr", key
|
||||
|
||||
# TODO(behdad): What does the following comment even mean? Both CFF and CFF2
|
||||
# use the same T2Charstring class.
|
||||
# What I see missing is dropping the endchar and return operators...
|
||||
|
||||
# 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 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.",
|
||||
)
|
||||
options = parser.parse_args(args)
|
||||
|
||||
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)
|
||||
cff = font["CFF "].cff
|
||||
del font["CFF "]
|
||||
|
||||
cff.convertCFFToCFF2(font)
|
||||
|
||||
table = font["CFF2"] = newTable("CFF2")
|
||||
table.cff = cff
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main(sys.argv[1:]))
|
@ -386,125 +386,14 @@ class CFFFontSet(object):
|
||||
self.minor = int(attrs["value"])
|
||||
|
||||
def convertCFFToCFF2(self, otFont):
|
||||
"""Converts this object from CFF format to CFF2 format. This conversion
|
||||
is done 'in-place'. The conversion cannot be reversed.
|
||||
from .CFFToCFF2 import convertCFFToCFF2
|
||||
|
||||
This assumes a decompiled CFF table. (i.e. that the object has been
|
||||
filled via :meth:`decompile`.)"""
|
||||
self.major = 2
|
||||
cff2GetGlyphOrder = self.otFont.getGlyphOrder
|
||||
topDictData = TopDictIndex(None, cff2GetGlyphOrder)
|
||||
topDictData.items = self.topDictIndex.items
|
||||
self.topDictIndex = topDictData
|
||||
topDict = topDictData[0]
|
||||
if hasattr(topDict, "Private"):
|
||||
privateDict = topDict.Private
|
||||
else:
|
||||
privateDict = None
|
||||
opOrder = buildOrder(topDictOperators2)
|
||||
topDict.order = opOrder
|
||||
topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
|
||||
for entry in topDictOperators:
|
||||
key = entry[1]
|
||||
if key not in opOrder:
|
||||
if key in topDict.rawDict:
|
||||
del topDict.rawDict[key]
|
||||
if hasattr(topDict, key):
|
||||
delattr(topDict, key)
|
||||
|
||||
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)
|
||||
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 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 privateDict.rawDict:
|
||||
# print "Removing private dict", key
|
||||
del privateDict.rawDict[key]
|
||||
if hasattr(privateDict, key):
|
||||
delattr(privateDict, key)
|
||||
# print "Removing privateDict attr", key
|
||||
|
||||
# TODO(behdad): What does the following comment even mean? Both CFF and CFF2
|
||||
# use the same T2Charstring class.
|
||||
# What I see missing is dropping the endchar and return operators...
|
||||
|
||||
# At this point, the Subrs and Charstrings are all still T2Charstring class
|
||||
# easiest to fix this by compiling, then decompiling again
|
||||
file = BytesIO()
|
||||
self.compile(file, otFont, isCFF2=True)
|
||||
file.seek(0)
|
||||
self.decompile(file, otFont, isCFF2=True)
|
||||
convertCFFToCFF2(self, otFont)
|
||||
|
||||
def convertCFF2ToCFF(self, otFont):
|
||||
"""Converts this object from CFF2 format to CFF format. This conversion
|
||||
is done 'in-place'. The conversion cannot be reversed.
|
||||
from .CFF2ToCFF import convertCFF2ToCFF
|
||||
|
||||
The CFF2 font cannot be variable. This method will remove the VarStore,
|
||||
but will not process the CharStrings in any way (TODO instantiate to default).
|
||||
|
||||
This assumes a decompiled CFF table. (i.e. that the object has been
|
||||
filled via :meth:`decompile`.)"""
|
||||
self.major = 1
|
||||
topDictData = TopDictIndex(None)
|
||||
topDictData.items = self.topDictIndex.items
|
||||
self.topDictIndex = topDictData
|
||||
topDict = topDictData[0]
|
||||
if hasattr(topDict, "Private"):
|
||||
privateDict = topDict.Private
|
||||
else:
|
||||
privateDict = None
|
||||
opOrder = buildOrder(topDictOperators)
|
||||
topDict.order = opOrder
|
||||
|
||||
fdArray = topDict.FDArray
|
||||
charStrings = topDict.CharStrings
|
||||
|
||||
for cs in charStrings.values():
|
||||
cs.program.append("endchar")
|
||||
|
||||
# TODO Add "return" to subrs that don't end in endchar?
|
||||
|
||||
# At this point, the Subrs and Charstrings are all still T2Charstring class
|
||||
# easiest to fix this by compiling, then decompiling again
|
||||
# file = BytesIO()
|
||||
# self.compile(file, otFont, isCFF2=False)
|
||||
# file.seek(0)
|
||||
# self.decompile(file, otFont, isCFF2=False)
|
||||
convertCFF2ToCFF(self, otFont)
|
||||
|
||||
def desubroutinize(self):
|
||||
for fontName in self.fontNames:
|
||||
@ -801,8 +690,8 @@ class Index(object):
|
||||
compilerClass = IndexCompiler
|
||||
|
||||
def __init__(self, file=None, isCFF2=None):
|
||||
assert (isCFF2 is None) == (file is None)
|
||||
self.items = []
|
||||
self.offsets = offsets = []
|
||||
name = self.__class__.__name__
|
||||
if file is None:
|
||||
return
|
||||
@ -819,7 +708,6 @@ class Index(object):
|
||||
offSize = readCard8(file)
|
||||
log.log(DEBUG, " index count: %s offSize: %s", count, offSize)
|
||||
assert offSize <= 4, "offSize too large: %s" % offSize
|
||||
self.offsets = offsets = []
|
||||
pad = b"\0" * (4 - offSize)
|
||||
for index in range(count + 1):
|
||||
chunk = file.read(offSize)
|
||||
@ -997,7 +885,6 @@ class TopDictIndex(Index):
|
||||
compilerClass = TopDictIndexCompiler
|
||||
|
||||
def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, isCFF2=None):
|
||||
assert (isCFF2 is None) == (file is None)
|
||||
self.cff2GetGlyphOrder = cff2GetGlyphOrder
|
||||
if file is not None and isCFF2:
|
||||
self._isCFF2 = isCFF2
|
||||
|
@ -13,6 +13,9 @@ from operator import add
|
||||
from functools import reduce
|
||||
|
||||
|
||||
__all__ = ["optimizeWidths", "main"]
|
||||
|
||||
|
||||
class missingdict(dict):
|
||||
def __init__(self, missing_func):
|
||||
self.missing_func = missing_func
|
||||
|
@ -89,7 +89,7 @@ from fontTools.misc.fixedTools import (
|
||||
otRound,
|
||||
)
|
||||
from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
|
||||
from fontTools.ttLib import TTFont, getTableClass
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
||||
from fontTools.ttLib.tables import _g_l_y_f
|
||||
from fontTools import varLib
|
||||
@ -601,7 +601,7 @@ def instantiateCFF2(
|
||||
varStore = topDict.VarStore.otVarStore
|
||||
if not varStore:
|
||||
if downgrade:
|
||||
table = varfont["CFF "] = getTableClass("CFF ")()
|
||||
table = varfont["CFF "] = newTable("CFF ")
|
||||
table.cff = cff
|
||||
cff.convertCFF2ToCFF(varfont)
|
||||
del varfont["CFF2"]
|
||||
@ -821,7 +821,7 @@ def instantiateCFF2(
|
||||
del private.vstore
|
||||
|
||||
if downgrade:
|
||||
table = varfont["CFF "] = getTableClass("CFF ")()
|
||||
table = varfont["CFF "] = newTable("CFF ")
|
||||
table.cff = cff
|
||||
cff.convertCFF2ToCFF(varfont)
|
||||
del varfont["CFF2"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user