diff --git a/Lib/fontTools/cffLib/cff2_merge_funcs.py b/Lib/fontTools/cffLib/cff2_merge_funcs.py new file mode 100644 index 000000000..381a78a64 --- /dev/null +++ b/Lib/fontTools/cffLib/cff2_merge_funcs.py @@ -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 diff --git a/Lib/fontTools/cffLib/cff2mergePen.py b/Lib/fontTools/cffLib/cff2mergePen.py new file mode 100644 index 000000000..ac6476141 --- /dev/null +++ b/Lib/fontTools/cffLib/cff2mergePen.py @@ -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