varLib. Add support for building CFF2 variable font with a call to varLib.build().

Add support functions for merging CFF tables into a CFF2 table.
This commit is contained in:
ReadRoberts 2018-10-23 10:19:43 -07:00
parent 08aef71458
commit 9626cfe15b
2 changed files with 571 additions and 0 deletions

View File

@ -0,0 +1,269 @@
import os
from fontTools.misc.py23 import BytesIO
from fontTools.cffLib import (TopDictIndex,
buildOrder,
topDictOperators,
topDictOperators2,
privateDictOperators,
privateDictOperators2,
FDArrayIndex,
FontDict,
VarStoreData,)
from fontTools.ttLib import newTable
from fontTools import varLib
from cff2mergePen import CFF2CharStringMergePen, MergeTypeError
def addCFFVarStore(varFont, varModel):
supports = varModel.supports[1:]
fvarTable = varFont['fvar']
axisKeys = [axis.axisTag for axis in fvarTable.axes]
varTupleList = varLib.builder.buildVarRegionList(supports, axisKeys)
varTupleIndexes = list(range(len(supports)))
varDeltasCFFV = varLib.builder.buildVarData(varTupleIndexes, None, False)
varStoreCFFV = varLib.builder.buildVarStore(varTupleList, [varDeltasCFFV])
topDict = varFont['CFF2'].cff.topDictIndex[0]
topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
def addNamesToPost(ttFont, fontGlyphList):
postTable = ttFont['post']
postTable.glyphOrder = ttFont.glyphOrder = fontGlyphList
postTable.formatType = 2.0
postTable.extraNames = []
postTable.mapping = {}
postTable.compile(ttFont)
def lib_convertCFFToCFF2(cff, otFont):
# This assumes a decompiled CFF table.
cff2GetGlyphOrder = cff.otFont.getGlyphOrder
topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
topDictData.items = cff.topDictIndex.items
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)
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
# Now delete up the decrecated topDict operators from CFF 1.0
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)
# At this point, the Subrs and Charstrings are all still T2Charstring class
# easiest to fix this by compiling, then decompiling again
cff.major = 2
file = BytesIO()
cff.compile(file, otFont, isCFF2=True)
file.seek(0)
cff.decompile(file, otFont, isCFF2=True)
def pointsDiffer(pointList):
p0 = max(pointList)
p1 = min(pointList)
result = False if p1 == p0 else True
return result
def convertCFFtoCFF2(varFont):
# Convert base font to a single master CFF2 font.
cffTable = varFont['CFF ']
lib_convertCFFToCFF2(cffTable.cff, varFont)
newCFF2 = newTable("CFF2")
newCFF2.cff = cffTable.cff
varFont['CFF2'] = newCFF2
del varFont['CFF ']
class MergeDictError(TypeError):
def __init__(self, key, value, values):
error_msg = ["For the Private Dict key ()".format(key)]
error_msg.append("the default font value list:")
error_msg.append("\t{}".format(value))
error_msg.append(
"had a different number of values than"
"a region font:")
for value in values:
error_msg.append("\t{}".format(value))
error_msg = os.linesep.join(error_msg)
def conv_to_int(num):
if round(num) == num:
return int(num)
else:
return num
def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model):
if hasattr(region_top_dicts[0], 'FDArray'):
regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
else:
regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
for fd_index, font_dict in enumerate(topDict.FDArray):
private_dict = font_dict.Private
pds = [private_dict] + [
regionFDArray[fd_index].Private for regionFDArray in regionFDArrays
]
for key, value in private_dict.rawDict.items():
if isinstance(value, list):
try:
values = [pd.rawDict[key] for pd in pds]
except KeyError:
del private_dict.rawDict[key]
print(
"Warning: {key} in default font Private dict is "
b"missing from another font, and was "
b"discarded.".format(key=key))
continue
try:
values = zip(*values)
except IndexError:
raise MergeDictError(key, value, values)
"""
Row 0 contains the first value from each master.
Convert each row from absolute values to relative
values from the previous row.
e.g for three masters, a list of values was:
master 0 OtherBlues = [-217,-205]
master 1 OtherBlues = [-234,-222]
master 1 OtherBlues = [-188,-176]
The call to zip() converts this to:
[(-217, -234, -188), (-205, -222, -176)]
and is converted finally to:
OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]]
"""
dataList = []
prev_val_list = [0] * num_masters
any_points_differ = False
for val_list in values:
rel_list = [(val - prev_val_list[i]) for (
i, val) in enumerate(val_list)]
if (not any_points_differ) and pointsDiffer(rel_list):
any_points_differ = True
prev_val_list = val_list
deltas = var_model.getDeltas(rel_list)
# Convert numbers with no decimal part to an int.
deltas = [conv_to_int(delta) for delta in deltas]
# For PrivateDict BlueValues, the default font
# values are absolute, not relative to the prior value.
deltas[0] = val_list[0]
dataList.append(deltas)
# If there are no blend values,then
# we can collapse the blend lists.
if not any_points_differ:
dataList = [data[0] for data in dataList]
else:
values = [pd.rawDict[key] for pd in pds]
if pointsDiffer(values):
dataList = var_model.getDeltas(values)
else:
dataList = values[0]
private_dict.rawDict[key] = dataList
class MergeCharError(TypeError):
def __init__(self, glyph_name, mergeError):
self.error_msg = "{mergeError} in glyph {glyph_name}".format(
mergeError=mergeError.error_msg,
glyph_name=glyph_name)
super(MergeCharError, self).__init__(self.error_msg)
def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
topDict = varFont['CFF2'].cff.topDictIndex[0]
default_charstrings = topDict.CharStrings
region_fonts = ordered_fonts_list[1:]
region_top_dicts = [
ttFont['CFF '].cff.topDictIndex[0] for ttFont in region_fonts
]
num_masters = len(model.mapping)
merge_PrivateDicts(topDict, region_top_dicts, num_masters, model)
merge_charstrings(default_charstrings,
glyphOrder,
num_masters,
region_top_dicts, model)
def merge_charstrings(default_charstrings,
glyphOrder,
num_masters,
region_top_dicts,
var_model):
for gname in glyphOrder:
default_charstring = default_charstrings[gname]
var_pen = CFF2CharStringMergePen([], num_masters, master_idx=0)
default_charstring.draw(var_pen)
for region_idx, region_td in enumerate(region_top_dicts):
region_idx += 1
region_charstrings = region_td.CharStrings
region_charstring = region_charstrings[gname]
var_pen.restart(region_idx)
try:
region_charstring.draw(var_pen)
except MergeTypeError as err:
err.gname = gname
err.region_idx = region_idx
raise MergeCharError(gname, err)
new_charstring = var_pen.getCharString(
private=default_charstring.private,
globalSubrs=default_charstring.globalSubrs,
var_model=var_model,
optimize=True)
default_charstrings[gname] = new_charstring

View File

@ -0,0 +1,302 @@
# Copyright (c) 2009 Type Supply LLC
from __future__ import print_function, division, absolute_import
from fontTools.misc.fixedTools import otRound
from fontTools.misc.psCharStrings import CFF2Subr
from fontTools.pens.t2CharStringPen import T2CharStringPen
from fontTools.cffLib.specializer import (commandsToProgram,
specializeCommands,
)
class MergeTypeError(TypeError):
def __init__(self, point_type, pt_index, m_index, default_type):
self.error_msg = [
"'{point_type}' at point index {pt_index} in master "
"index {m_index} differs from the default font point "
"type '{default_type}'".format(
point_type=point_type,
pt_index=pt_index,
m_index=m_index,
default_type=default_type)
][0]
super(MergeTypeError, self).__init__(self.error_msg)
class CFF2CharStringMergePen(T2CharStringPen):
"""Pen to merge Type 2 CharStrings.
"""
def __init__(self, default_commands,
num_masters, master_idx, roundTolerance=0.5):
super(
CFF2CharStringMergePen,
self).__init__(width=None,
glyphSet=None, CFF2=True,
roundTolerance=roundTolerance)
self.pt_index = 0
self._commands = default_commands
self.m_index = master_idx
self.num_masters = num_masters
self.prev_move_idx = 0
self.roundTolerance = roundTolerance
def _round(self, number):
tolerance = self.roundTolerance
if tolerance == 0:
return number # no-op
rounded = otRound(number)
# return rounded integer if the tolerance >= 0.5, or if the absolute
# difference between the original float and the rounded integer is
# within the tolerance
if tolerance >= .5 or abs(rounded - number) <= tolerance:
return rounded
else:
# else return the value un-rounded
return number
def _p(self, pt):
""" Unlike T2CharstringPen, this class stores absolute values.
This is to allow the logic in check_and_fix_clospath() to work,
where the current or previous absolute point has to be compared to
the path start-point.
"""
self._p0 = pt
return list(self._p0)
def make_flat_curve(self, prev_coords, cur_coords):
# Convert line coords to curve coords.
dx = self._round((cur_coords[0] - prev_coords[0])/3.0)
dy = self._round((cur_coords[1] - prev_coords[1])/3.0)
new_coords = [prev_coords[0] + dx,
prev_coords[1] + dy,
prev_coords[0] + 2*dx,
prev_coords[1] + 2*dy
] + cur_coords
return new_coords
def make_curve_coords(self, coords, is_default):
# Convert line coords to curve coords.
prev_cmd = self._commands[self.pt_index-1]
if is_default:
new_coords = []
for i, cur_coords in enumerate(coords):
prev_coords = prev_cmd[1][i]
master_coords = self.make_flat_curve(
prev_coords[:2], cur_coords
)
new_coords.append(master_coords)
else:
cur_coords = coords
prev_coords = prev_cmd[1][-1]
new_coords = self.make_flat_curve(prev_coords[:2], cur_coords)
return new_coords
def check_and_fix_flat_curve(self, cmd, point_type, pt_coords):
if (point_type == 'rlineto') and (cmd[0] == 'rrcurveto'):
is_default = False
pt_coords = self.make_curve_coords(pt_coords, is_default)
success = True
elif (point_type == 'rrcurveto') and (cmd[0] == 'rlineto'):
is_default = True
expanded_coords = self.make_curve_coords(cmd[1], is_default)
cmd[1] = expanded_coords
cmd[0] = point_type
success = True
else:
success = False
return success, pt_coords
def check_and_fix_clospath(self, cmd, point_type, pt_coords):
""" Some workflows drop a lineto which closes a path.
Also, if the last segment is a curve in one master,
and a flat curve in another, the flat curve can get
converted to a closing lineto, and then dropped.
Test if:
1) one master op is a moveto,
2) the previous op for this master does not close the path
3) in the other master the current op is not a moveto
4) the current op in the otehr master closes the current path
If the default font is missing the closing lineto, insert it,
then proceed with merging the current op and pt_coords.
If the current region is missing the closing lineto
and therefore the current op is a moveto,
then add closing coordinates to self._commands,
and increment self.pt_index.
Note that if this may insert a point in the default font list,
so after using it, 'cmd' needs to be reset.
return True if we can fix this issue.
"""
if point_type == 'rmoveto':
# If this is the case, we know that cmd[0] != 'rmoveto'
# The previous op must not close the path for this region font.
prev_moveto_coords = self._commands[self.prev_move_idx][1][-1]
prv_coords = self._commands[self.pt_index-1][1][-1]
if prev_moveto_coords == prv_coords[-2:]:
return False
# The current op must close the path for the default font.
prev_moveto_coords2 = self._commands[self.prev_move_idx][1][0]
prv_coords = self._commands[self.pt_index][1][0]
if prev_moveto_coords2 != prv_coords[-2:]:
return False
# Add the closing line coords for this region
# so self._commands, then increment self.pt_index
# so that the current region op will get merged
# with the next default font moveto.
if cmd[0] == 'rrcurveto':
new_coords = self.make_curve_coords(prev_moveto_coords, False)
cmd[1].append(new_coords)
self.pt_index += 1
return True
if cmd[0] == 'rmoveto':
# The previous op must not close the path for the default font.
prev_moveto_coords = self._commands[self.prev_move_idx][1][0]
prv_coords = self._commands[self.pt_index-1][1][0]
if prev_moveto_coords == prv_coords[-2:]:
return False
# The current op must close the path for this region font.
prev_moveto_coords2 = self._commands[self.prev_move_idx][1][-1]
if prev_moveto_coords2 != pt_coords[-2:]:
return False
# Insert the close path segment in the default font.
# We omit the last coords from the previous moveto
# is it will be supplied by the current region point.
# after this function returns.
new_cmd = [point_type, None]
prev_move_coords = self._commands[self.prev_move_idx][1][:-1]
# Note that we omit the last region's coord from prev_move_coords,
# as that is from the current region, and we will add the
# current pts' coords from the current region in its place.
if point_type == 'rlineto':
new_cmd[1] = prev_move_coords
else:
# We omit the last set of coords from the
# previous moveto, as it will be supplied by the coords
# for the current region pt.
new_cmd[1] = self.make_curve_coords(prev_move_coords, True)
self._commands.insert(self.pt_index, new_cmd)
return True
return False
def add_point(self, point_type, pt_coords):
if self.m_index == 0:
self._commands.append([point_type, [pt_coords]])
else:
cmd = self._commands[self.pt_index]
if cmd[0] != point_type:
# Fix some issues that show up in some
# CFF workflows, even when fonts are
# topologically merge compatible.
success, pt_coords = self.check_and_fix_flat_curve(
cmd, point_type, pt_coords)
if not success:
success = self.check_and_fix_clospath(
cmd, point_type, pt_coords)
if success:
# We may have incremented self.pt_index
cmd = self._commands[self.pt_index]
if cmd[0] != point_type:
success = False
if not success:
raise MergeTypeError(
point_type,
self.pt_index,
len(cmd[1]),
cmd[0])
cmd[1].append(pt_coords)
self.pt_index += 1
def _moveTo(self, pt):
pt_coords = self._p(pt)
self.prev_move_abs_coords = self.roundPoint(self._p0)
self.add_point('rmoveto', pt_coords)
# I set prev_move_idx here because add_point()
# can change self.pt_index.
self.prev_move_idx = self.pt_index - 1
def _lineTo(self, pt):
pt_coords = self._p(pt)
self.add_point('rlineto', pt_coords)
def _curveToOne(self, pt1, pt2, pt3):
_p = self._p
pt_coords = _p(pt1)+_p(pt2)+_p(pt3)
self.add_point('rrcurveto', pt_coords)
def _closePath(self):
pass
def _endPath(self):
pass
def restart(self, region_idx):
self.pt_index = 0
self.m_index = region_idx
self._p0 = (0, 0)
def getCommands(self):
return self._commands
def reorder_blend_args(self):
"""
For a moveto to lineto, the args are now arranged as:
[ [master_0 x,y], [master_1 x,y], [master_2 x,y] ]
We re-arrange this to
[ [master_0 x, master_1 x, master_2 x],
[master_0 y, master_1 y, master_2 y]
]
We also make the value relative.
"""
for cmd in self._commands:
# arg[i] is the set of arguments for this operator from master i.
args = cmd[1]
m_args = zip(*args)
# m_args[n] is now all num_master args for the i'th argument
# for this operation.
cmd[1] = m_args
# Now convert from absolute to relative
x0 = y0 = [0]*self.num_masters
for cmd in self._commands:
is_x = True
coords = cmd[1]
rel_coords = []
for coord in coords:
prev_coord = x0 if is_x else y0
rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)]
if max(rel_coord) == min(rel_coord):
rel_coord = rel_coord[0]
rel_coords.append(rel_coord)
if is_x:
x0 = coord
else:
y0 = coord
is_x = not is_x
cmd[1] = rel_coords
def getCharString(
self, private=None, globalSubrs=None,
var_model=None, optimize=True
):
self.reorder_blend_args()
commands = self._commands
if optimize:
maxstack = 48 if not self._CFF2 else 513
commands = specializeCommands(commands,
generalizeFirst=False,
maxstack=maxstack)
program = commandsToProgram(commands, maxstack,
var_model, round_func=self._round)
charString = CFF2Subr(
program=program, private=private, globalSubrs=globalSubrs)
return charString