Fix syntax error reported by build system: can't mix string string types when doing literal concatenation Fix local import reference - doesn't work in Python3. Addressed issues raised by @msousa for PR 1345 yesterday. Will change cff2_merge_funcs.py and cff2mergePen.py from tab to space indentations after the current comments are resolved. Add various improvements from comments: - do not edit the post table under varLib.build(). Setting post table format 2 or 3 is now expected to be managed by whatever calls varLib.build(). - In the t2CharStringPen module, rename closure _round() nested in makeRoundFunc to an exportable function, and use it in cff2mergePen. - remove TypeSupply copyright from cff2mergePen. - use modulo function to convert float to int when it is meant to be 0 in cff2mergePen. cff2_merge_funcs.py:merge_PrivateDicts() should only be blending the hint related fields in the PrivateDict. This oversight that was surfaced by @madig reporting an error building his Cantrell font. The bug appeared when the font was subroutinized, as the pen draw method then has to interpret the Subr field in order to access T2Charstring subroutines. Fix expected ttx output file. When I removed the logic to add glyph names to the post table, glyph names in the ttx file changed. Miguel prefers a simple list for readability in cff2_merge_funs.py:138.
293 lines
9.4 KiB
Python
293 lines
9.4 KiB
Python
from __future__ import print_function, division, absolute_import
|
|
from fontTools.misc.psCharStrings import CFF2Subr
|
|
from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
|
|
from fontTools.cffLib.specializer import (commandsToProgram,
|
|
specializeCommands)
|
|
from fontTools.cffLib import maxStackLimit
|
|
|
|
|
|
class MergeTypeError(TypeError):
|
|
def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
|
|
self.error_msg = [
|
|
"In glyph '{gname}' "
|
|
"'{point_type}' at point index {pt_index} in master "
|
|
"index {m_index} differs from the default font point "
|
|
"type '{default_type}'"
|
|
"".format(gname=glyphName,
|
|
point_type=point_type, pt_index=pt_index,
|
|
m_index=m_index, default_type=default_type)
|
|
][0]
|
|
super(MergeTypeError, self).__init__(self.error_msg)
|
|
|
|
|
|
|
|
|
|
def makeRoundNumberFunc(tolerance):
|
|
if tolerance < 0:
|
|
raise ValueError("Rounding tolerance must be positive")
|
|
|
|
def roundNumber(val):
|
|
return t2c_round(val, tolerance)
|
|
|
|
return roundNumber
|
|
|
|
|
|
class CFF2CharStringMergePen(T2CharStringPen):
|
|
"""Pen to merge Type 2 CharStrings.
|
|
"""
|
|
def __init__(self, default_commands,
|
|
glyphName, 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.glyphName = glyphName
|
|
self.roundNumber = makeRoundNumberFunc(roundTolerance)
|
|
|
|
def _p(self, pt):
|
|
""" Unlike T2CharstringPen, this class stores absolute values.
|
|
This is to allow the logic in check_and_fix_closepath() 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.roundNumber((cur_coords[0] - prev_coords[0])/3.0)
|
|
dy = self.roundNumber((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_closepath(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_closepath(
|
|
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], self.glyphName)
|
|
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 = [0]*self.num_masters
|
|
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:
|
|
commands = specializeCommands(commands, generalizeFirst=False,
|
|
maxstack=maxStackLimit)
|
|
program = commandsToProgram(commands, var_model=var_model,
|
|
round_func=self.roundNumber)
|
|
charString = CFF2Subr(program=program, private=private,
|
|
globalSubrs=globalSubrs)
|
|
return charString
|