From 93633a85ef958e3636b8e53956f1e3611f8b54cc Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 4 Dec 2018 19:22:02 -0800 Subject: [PATCH] [CFF] Move variations-specific CFF code to varLib.cff module --- Lib/fontTools/cffLib/cff2mergePen.py | 247 ------------------ Lib/fontTools/varLib/__init__.py | 4 +- .../cff2_merge_funcs.py => varLib/cff.py} | 243 ++++++++++++++++- 3 files changed, 243 insertions(+), 251 deletions(-) delete mode 100644 Lib/fontTools/cffLib/cff2mergePen.py rename Lib/fontTools/{cffLib/cff2_merge_funcs.py => varLib/cff.py} (51%) diff --git a/Lib/fontTools/cffLib/cff2mergePen.py b/Lib/fontTools/cffLib/cff2mergePen.py deleted file mode 100644 index 2462d3e12..000000000 --- a/Lib/fontTools/cffLib/cff2mergePen.py +++ /dev/null @@ -1,247 +0,0 @@ -from __future__ import print_function, division, absolute_import -from fontTools.misc.psCharStrings import T2CharString -from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round -from fontTools.cffLib.specializer import (commandsToProgram, - specializeCommands) -from fontTools.cffLib import maxStackLimit -from fontTools.varLib.models import allEqual - - -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 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.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, commands): - """ - We first re-order the master coordinate values. - 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. - If the master values are all the same, we collapse the list to - as single value instead of a list. - """ - for cmd in 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 allEqual(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 - return commands - - @staticmethod - def mergeCommandsToProgram(commands, var_model, round_func): - """ - Takes a commands list as returned by programToCommands() and - converts it back to a T2CharString or CFF2Charstring program list. I - need to use this rather than specialize.commandsToProgram, as the - commands produced by CFF2CharStringMergePen initially contains a - list of coordinate values, one for each master, wherever a single - coordinate value is expected by the regular logic. The problem with - doing using the specialize.py functions is that a commands list is - expected to be a op name with its associated argument list. For the - commands list here, some of the arguments may need to be converted - to a new argument list and opcode. - This version will convert each list of master arguments to a blend - op and its arguments, and will also combine successive blend ops up - to the stack limit. - """ - program = [] - for op, args in commands: - num_args = len(args) - # some of the args may be blend lists, and some may be - # single coordinate values. - i = 0 - stack_use = 0 - while i < num_args: - arg = args[i] - if not isinstance(arg, list): - program.append(arg) - i += 1 - stack_use += 1 - else: - prev_stack_use = stack_use - """ The arg is a tuple of blend values. - These are each (master 0,master 1..master n) - Combine as many successive tuples as we can, - up to the max stack limit. - """ - num_masters = len(arg) - blendlist = [arg] - i += 1 - stack_use += 1 + num_masters # 1 for the num_blends arg - while (i < num_args) and isinstance(args[i], list): - blendlist.append(args[i]) - i += 1 - stack_use += num_masters - if stack_use + num_masters > maxStackLimit: - # if we are here, max stack is is the CFF2 max stack. - break - num_blends = len(blendlist) - # append the 'num_blends' default font values - for arg in blendlist: - if round_func: - arg[0] = round_func(arg[0]) - program.append(arg[0]) - for arg in blendlist: - # for each coordinate tuple, append the region deltas - if len(arg) != 3: - print(arg) - import pdb - pdb.set_trace() - deltas = var_model.getDeltas(arg) - if round_func: - deltas = [round_func(delta) for delta in deltas] - # First item in 'deltas' is the default master value; - # for CFF2 data, that has already been written. - program.extend(deltas[1:]) - program.append(num_blends) - program.append('blend') - stack_use = prev_stack_use + num_blends - if op: - program.append(op) - return program - - - def getCharString(self, private=None, globalSubrs=None, - var_model=None, optimize=True): - commands = self._commands - commands = self.reorder_blend_args(commands) - if optimize: - commands = specializeCommands(commands, generalizeFirst=False, - maxstack=maxStackLimit) - program = self.mergeCommandsToProgram(commands, var_model=var_model, - round_func=self.roundNumber) - charString = T2CharString(program=program, private=private, - globalSubrs=globalSubrs) - return charString diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 9e84e6bb7..733af420f 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -624,9 +624,7 @@ _DesignSpaceData = namedtuple( def _add_CFF2(varFont, model, master_fonts): - from fontTools.cffLib.cff2_merge_funcs import (convertCFFtoCFF2, - addCFFVarStore, - merge_region_fonts) + from .cff import (convertCFFtoCFF2, addCFFVarStore, merge_region_fonts) glyphOrder = varFont.getGlyphOrder() convertCFFtoCFF2(varFont) ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping) diff --git a/Lib/fontTools/cffLib/cff2_merge_funcs.py b/Lib/fontTools/varLib/cff.py similarity index 51% rename from Lib/fontTools/cffLib/cff2_merge_funcs.py rename to Lib/fontTools/varLib/cff.py index e155d7b00..cf06b30f4 100644 --- a/Lib/fontTools/cffLib/cff2_merge_funcs.py +++ b/Lib/fontTools/varLib/cff.py @@ -1,6 +1,9 @@ import os from fontTools.misc.py23 import BytesIO +from fontTools.misc.psCharStrings import T2CharString +from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round from fontTools.cffLib import ( + maxStackLimit, TopDictIndex, buildOrder, topDictOperators, @@ -11,7 +14,7 @@ from fontTools.cffLib import ( FontDict, VarStoreData ) -from fontTools.cffLib.cff2mergePen import CFF2CharStringMergePen +from fontTools.cffLib.specializer import (commandsToProgram, specializeCommands) from fontTools.ttLib import newTable from fontTools import varLib from fontTools.varLib.models import allEqual @@ -243,3 +246,241 @@ def merge_charstrings(default_charstrings, globalSubrs=default_charstring.globalSubrs, var_model=var_model, optimize=True) default_charstrings[gname] = new_charstring + + +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 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.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, commands): + """ + We first re-order the master coordinate values. + 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. + If the master values are all the same, we collapse the list to + as single value instead of a list. + """ + for cmd in 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 allEqual(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 + return commands + + @staticmethod + def mergeCommandsToProgram(commands, var_model, round_func): + """ + Takes a commands list as returned by programToCommands() and + converts it back to a T2CharString or CFF2Charstring program list. I + need to use this rather than specialize.commandsToProgram, as the + commands produced by CFF2CharStringMergePen initially contains a + list of coordinate values, one for each master, wherever a single + coordinate value is expected by the regular logic. The problem with + doing using the specialize.py functions is that a commands list is + expected to be a op name with its associated argument list. For the + commands list here, some of the arguments may need to be converted + to a new argument list and opcode. + This version will convert each list of master arguments to a blend + op and its arguments, and will also combine successive blend ops up + to the stack limit. + """ + program = [] + for op, args in commands: + num_args = len(args) + # some of the args may be blend lists, and some may be + # single coordinate values. + i = 0 + stack_use = 0 + while i < num_args: + arg = args[i] + if not isinstance(arg, list): + program.append(arg) + i += 1 + stack_use += 1 + else: + prev_stack_use = stack_use + """ The arg is a tuple of blend values. + These are each (master 0,master 1..master n) + Combine as many successive tuples as we can, + up to the max stack limit. + """ + num_masters = len(arg) + blendlist = [arg] + i += 1 + stack_use += 1 + num_masters # 1 for the num_blends arg + while (i < num_args) and isinstance(args[i], list): + blendlist.append(args[i]) + i += 1 + stack_use += num_masters + if stack_use + num_masters > maxStackLimit: + # if we are here, max stack is is the CFF2 max stack. + break + num_blends = len(blendlist) + # append the 'num_blends' default font values + for arg in blendlist: + if round_func: + arg[0] = round_func(arg[0]) + program.append(arg[0]) + for arg in blendlist: + # for each coordinate tuple, append the region deltas + if len(arg) != 3: + print(arg) + import pdb + pdb.set_trace() + deltas = var_model.getDeltas(arg) + if round_func: + deltas = [round_func(delta) for delta in deltas] + # First item in 'deltas' is the default master value; + # for CFF2 data, that has already been written. + program.extend(deltas[1:]) + program.append(num_blends) + program.append('blend') + stack_use = prev_stack_use + num_blends + if op: + program.append(op) + return program + + + def getCharString(self, private=None, globalSubrs=None, + var_model=None, optimize=True): + commands = self._commands + commands = self.reorder_blend_args(commands) + if optimize: + commands = specializeCommands(commands, generalizeFirst=False, + maxstack=maxStackLimit) + program = self.mergeCommandsToProgram(commands, var_model=var_model, + round_func=self.roundNumber) + charString = T2CharString(program=program, private=private, + globalSubrs=globalSubrs) + return charString