diff options
Diffstat (limited to 'Lib/fontTools/varLib/cff.py')
-rw-r--r-- | Lib/fontTools/varLib/cff.py | 1239 |
1 files changed, 645 insertions, 594 deletions
diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py index 727efa70..52e6a884 100644 --- a/Lib/fontTools/varLib/cff.py +++ b/Lib/fontTools/varLib/cff.py @@ -1,19 +1,18 @@ from collections import namedtuple from fontTools.cffLib import ( - maxStackLimit, - TopDictIndex, - buildOrder, - topDictOperators, - topDictOperators2, - privateDictOperators, - privateDictOperators2, - FDArrayIndex, - FontDict, - VarStoreData + maxStackLimit, + TopDictIndex, + buildOrder, + topDictOperators, + topDictOperators2, + privateDictOperators, + privateDictOperators2, + FDArrayIndex, + FontDict, + VarStoreData, ) from io import BytesIO -from fontTools.cffLib.specializer import ( - specializeCommands, commandsToProgram) +from fontTools.cffLib.specializer import specializeCommands, commandsToProgram from fontTools.ttLib import newTable from fontTools import varLib from fontTools.varLib.models import allEqual @@ -23,8 +22,11 @@ from fontTools.pens.t2CharStringPen import T2CharStringPen from functools import partial from .errors import ( - VarLibCFFDictMergeError, VarLibCFFPointTypeMergeError, - VarLibCFFHintTypeMergeError,VarLibMergeError) + VarLibCFFDictMergeError, + VarLibCFFPointTypeMergeError, + VarLibCFFHintTypeMergeError, + VarLibMergeError, +) # Backwards compatibility @@ -33,196 +35,206 @@ MergeTypeError = VarLibCFFPointTypeMergeError def addCFFVarStore(varFont, varModel, varDataList, masterSupports): - fvarTable = varFont['fvar'] - axisKeys = [axis.axisTag for axis in fvarTable.axes] - varTupleList = varLib.builder.buildVarRegionList(masterSupports, axisKeys) - varStoreCFFV = varLib.builder.buildVarStore(varTupleList, varDataList) + fvarTable = varFont["fvar"] + axisKeys = [axis.axisTag for axis in fvarTable.axes] + varTupleList = varLib.builder.buildVarRegionList(masterSupports, axisKeys) + varStoreCFFV = varLib.builder.buildVarStore(varTupleList, varDataList) - topDict = varFont['CFF2'].cff.topDictIndex[0] - topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV) - if topDict.FDArray[0].vstore is None: - fdArray = topDict.FDArray - for fontDict in fdArray: - if hasattr(fontDict, "Private"): - fontDict.Private.vstore = topDict.VarStore + topDict = varFont["CFF2"].cff.topDictIndex[0] + topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV) + if topDict.FDArray[0].vstore is None: + fdArray = topDict.FDArray + for fontDict in fdArray: + if hasattr(fontDict, "Private"): + fontDict.Private.vstore = topDict.VarStore 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) - if privateDict is not None: - 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 list(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) + # 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) + if privateDict is not None: + 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 list(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 deprecated 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 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 '] + # 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 "] def conv_to_int(num): - if isinstance(num, float) and num.is_integer(): - return int(num) - return num - - -pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues", - "FamilyOtherBlues", "BlueScale", "BlueShift", - "BlueFuzz", "StdHW", "StdVW", "StemSnapH", - "StemSnapV") + if isinstance(num, float) and num.is_integer(): + return int(num) + return num + + +pd_blend_fields = ( + "BlueValues", + "OtherBlues", + "FamilyBlues", + "FamilyOtherBlues", + "BlueScale", + "BlueShift", + "BlueFuzz", + "StdHW", + "StdVW", + "StemSnapH", + "StemSnapV", +) def get_private(regionFDArrays, fd_index, ri, fd_map): - region_fdArray = regionFDArrays[ri] - region_fd_map = fd_map[fd_index] - if ri in region_fd_map: - region_fdIndex = region_fd_map[ri] - private = region_fdArray[region_fdIndex].Private - else: - private = None - return private + region_fdArray = regionFDArrays[ri] + region_fd_map = fd_map[fd_index] + if ri in region_fd_map: + region_fdIndex = region_fd_map[ri] + private = region_fdArray[region_fdIndex].Private + else: + private = None + return private def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map): - """ - I step through the FontDicts in the FDArray of the varfont TopDict. - For each varfont FontDict: - - * step through each key in FontDict.Private. - * For each key, step through each relevant source font Private dict, and - build a list of values to blend. - - The 'relevant' source fonts are selected by first getting the right - submodel using ``vsindex_dict[vsindex]``. The indices of the - ``subModel.locations`` are mapped to source font list indices by - assuming the latter order is the same as the order of the - ``var_model.locations``. I can then get the index of each subModel - location in the list of ``var_model.locations``. - """ - - topDict = top_dicts[0] - region_top_dicts = top_dicts[1:] - 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 - vsindex = getattr(private_dict, 'vsindex', 0) - # At the moment, no PrivateDict has a vsindex key, but let's support - # how it should work. See comment at end of - # merge_charstrings() - still need to optimize use of vsindex. - sub_model, _ = vsindex_dict[vsindex] - master_indices = [] - for loc in sub_model.locations[1:]: - i = var_model.locations.index(loc) - 1 - master_indices.append(i) - pds = [private_dict] - last_pd = private_dict - for ri in master_indices: - pd = get_private(regionFDArrays, fd_index, ri, fd_map) - # If the region font doesn't have this FontDict, just reference - # the last one used. - if pd is None: - pd = last_pd - else: - last_pd = pd - pds.append(pd) - num_masters = len(pds) - for key, value in private_dict.rawDict.items(): - dataList = [] - if key not in pd_blend_fields: - continue - if isinstance(value, list): - try: - values = [pd.rawDict[key] for pd in pds] - except KeyError: - print( - "Warning: {key} in default font Private dict is " - "missing from another font, and was " - "discarded.".format(key=key)) - continue - try: - values = zip(*values) - except IndexError: - raise VarLibCFFDictMergeError(key, value, values) - """ + """ + I step through the FontDicts in the FDArray of the varfont TopDict. + For each varfont FontDict: + + * step through each key in FontDict.Private. + * For each key, step through each relevant source font Private dict, and + build a list of values to blend. + + The 'relevant' source fonts are selected by first getting the right + submodel using ``vsindex_dict[vsindex]``. The indices of the + ``subModel.locations`` are mapped to source font list indices by + assuming the latter order is the same as the order of the + ``var_model.locations``. I can then get the index of each subModel + location in the list of ``var_model.locations``. + """ + + topDict = top_dicts[0] + region_top_dicts = top_dicts[1:] + 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 + vsindex = getattr(private_dict, "vsindex", 0) + # At the moment, no PrivateDict has a vsindex key, but let's support + # how it should work. See comment at end of + # merge_charstrings() - still need to optimize use of vsindex. + sub_model, _ = vsindex_dict[vsindex] + master_indices = [] + for loc in sub_model.locations[1:]: + i = var_model.locations.index(loc) - 1 + master_indices.append(i) + pds = [private_dict] + last_pd = private_dict + for ri in master_indices: + pd = get_private(regionFDArrays, fd_index, ri, fd_map) + # If the region font doesn't have this FontDict, just reference + # the last one used. + if pd is None: + pd = last_pd + else: + last_pd = pd + pds.append(pd) + num_masters = len(pds) + for key, value in private_dict.rawDict.items(): + dataList = [] + if key not in pd_blend_fields: + continue + if isinstance(value, list): + try: + values = [pd.rawDict[key] for pd in pds] + except KeyError: + print( + "Warning: {key} in default font Private dict is " + "missing from another font, and was " + "discarded.".format(key=key) + ) + continue + try: + values = zip(*values) + except IndexError: + raise VarLibCFFDictMergeError(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. @@ -235,427 +247,466 @@ def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map): and is converted finally to: OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] """ - 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 not allEqual(rel_list): - any_points_differ = True - prev_val_list = val_list - deltas = sub_model.getDeltas(rel_list) - # 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 not allEqual(values): - dataList = sub_model.getDeltas(values) - else: - dataList = values[0] - - # Convert numbers with no decimal part to an int - if isinstance(dataList, list): - for i, item in enumerate(dataList): - if isinstance(item, list): - for j, jtem in enumerate(item): - dataList[i][j] = conv_to_int(jtem) - else: - dataList[i] = conv_to_int(item) - else: - dataList = conv_to_int(dataList) - - private_dict.rawDict[key] = 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 not allEqual(rel_list): + any_points_differ = True + prev_val_list = val_list + deltas = sub_model.getDeltas(rel_list) + # 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 not allEqual(values): + dataList = sub_model.getDeltas(values) + else: + dataList = values[0] + + # Convert numbers with no decimal part to an int + if isinstance(dataList, list): + for i, item in enumerate(dataList): + if isinstance(item, list): + for j, jtem in enumerate(item): + dataList[i][j] = conv_to_int(jtem) + else: + dataList[i] = conv_to_int(item) + else: + dataList = conv_to_int(dataList) + + private_dict.rawDict[key] = dataList def _cff_or_cff2(font): - if "CFF " in font: - return font["CFF "] - return font["CFF2"] + if "CFF " in font: + return font["CFF "] + return font["CFF2"] def getfd_map(varFont, fonts_list): - """ Since a subset source font may have fewer FontDicts in their - FDArray than the default font, we have to match up the FontDicts in - the different fonts . We do this with the FDSelect array, and by - assuming that the same glyph will reference matching FontDicts in - each source font. We return a mapping from fdIndex in the default - font to a dictionary which maps each master list index of each - region font to the equivalent fdIndex in the region font.""" - fd_map = {} - default_font = fonts_list[0] - region_fonts = fonts_list[1:] - num_regions = len(region_fonts) - topDict = _cff_or_cff2(default_font).cff.topDictIndex[0] - if not hasattr(topDict, 'FDSelect'): - # All glyphs reference only one FontDict. - # Map the FD index for regions to index 0. - fd_map[0] = {ri:0 for ri in range(num_regions)} - return fd_map - - gname_mapping = {} - default_fdSelect = topDict.FDSelect - glyphOrder = default_font.getGlyphOrder() - for gid, fdIndex in enumerate(default_fdSelect): - gname_mapping[glyphOrder[gid]] = fdIndex - if fdIndex not in fd_map: - fd_map[fdIndex] = {} - for ri, region_font in enumerate(region_fonts): - region_glyphOrder = region_font.getGlyphOrder() - region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0] - if not hasattr(region_topDict, 'FDSelect'): - # All the glyphs share the same FontDict. Pick any glyph. - default_fdIndex = gname_mapping[region_glyphOrder[0]] - fd_map[default_fdIndex][ri] = 0 - else: - region_fdSelect = region_topDict.FDSelect - for gid, fdIndex in enumerate(region_fdSelect): - default_fdIndex = gname_mapping[region_glyphOrder[gid]] - region_map = fd_map[default_fdIndex] - if ri not in region_map: - region_map[ri] = fdIndex - return fd_map - - -CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict') + """Since a subset source font may have fewer FontDicts in their + FDArray than the default font, we have to match up the FontDicts in + the different fonts . We do this with the FDSelect array, and by + assuming that the same glyph will reference matching FontDicts in + each source font. We return a mapping from fdIndex in the default + font to a dictionary which maps each master list index of each + region font to the equivalent fdIndex in the region font.""" + fd_map = {} + default_font = fonts_list[0] + region_fonts = fonts_list[1:] + num_regions = len(region_fonts) + topDict = _cff_or_cff2(default_font).cff.topDictIndex[0] + if not hasattr(topDict, "FDSelect"): + # All glyphs reference only one FontDict. + # Map the FD index for regions to index 0. + fd_map[0] = {ri: 0 for ri in range(num_regions)} + return fd_map + + gname_mapping = {} + default_fdSelect = topDict.FDSelect + glyphOrder = default_font.getGlyphOrder() + for gid, fdIndex in enumerate(default_fdSelect): + gname_mapping[glyphOrder[gid]] = fdIndex + if fdIndex not in fd_map: + fd_map[fdIndex] = {} + for ri, region_font in enumerate(region_fonts): + region_glyphOrder = region_font.getGlyphOrder() + region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0] + if not hasattr(region_topDict, "FDSelect"): + # All the glyphs share the same FontDict. Pick any glyph. + default_fdIndex = gname_mapping[region_glyphOrder[0]] + fd_map[default_fdIndex][ri] = 0 + else: + region_fdSelect = region_topDict.FDSelect + for gid, fdIndex in enumerate(region_fdSelect): + default_fdIndex = gname_mapping[region_glyphOrder[gid]] + region_map = fd_map[default_fdIndex] + if ri not in region_map: + region_map[ri] = fdIndex + return fd_map + + +CVarData = namedtuple("CVarData", "varDataList masterSupports vsindex_dict") + + def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder): - topDict = varFont['CFF2'].cff.topDictIndex[0] - top_dicts = [topDict] + [ - _cff_or_cff2(ttFont).cff.topDictIndex[0] - for ttFont in ordered_fonts_list[1:] - ] - num_masters = len(model.mapping) - cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model) - fd_map = getfd_map(varFont, ordered_fonts_list) - merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map) - addCFFVarStore(varFont, model, cvData.varDataList, - cvData.masterSupports) - - -def _get_cs(charstrings, glyphName): - if glyphName not in charstrings: - return None - return charstrings[glyphName] - -def _add_new_vsindex(model, key, masterSupports, vsindex_dict, - vsindex_by_key, varDataList): - varTupleIndexes = [] - for support in model.supports[1:]: - if support not in masterSupports: - masterSupports.append(support) - varTupleIndexes.append(masterSupports.index(support)) - var_data = varLib.builder.buildVarData(varTupleIndexes, None, False) - vsindex = len(vsindex_dict) - vsindex_by_key[key] = vsindex - vsindex_dict[vsindex] = (model, [key]) - varDataList.append(var_data) - return vsindex + topDict = varFont["CFF2"].cff.topDictIndex[0] + top_dicts = [topDict] + [ + _cff_or_cff2(ttFont).cff.topDictIndex[0] for ttFont in ordered_fonts_list[1:] + ] + num_masters = len(model.mapping) + cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model) + fd_map = getfd_map(varFont, ordered_fonts_list) + merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map) + addCFFVarStore(varFont, model, cvData.varDataList, cvData.masterSupports) + + +def _get_cs(charstrings, glyphName, filterEmpty=False): + if glyphName not in charstrings: + return None + cs = charstrings[glyphName] + + if filterEmpty: + cs.decompile() + if cs.program == []: # CFF2 empty charstring + return None + elif ( + len(cs.program) <= 2 + and cs.program[-1] == "endchar" + and (len(cs.program) == 1 or type(cs.program[0]) in (int, float)) + ): # CFF1 empty charstring + return None + + return cs + + +def _add_new_vsindex( + model, key, masterSupports, vsindex_dict, vsindex_by_key, varDataList +): + varTupleIndexes = [] + for support in model.supports[1:]: + if support not in masterSupports: + masterSupports.append(support) + varTupleIndexes.append(masterSupports.index(support)) + var_data = varLib.builder.buildVarData(varTupleIndexes, None, False) + vsindex = len(vsindex_dict) + vsindex_by_key[key] = vsindex + vsindex_dict[vsindex] = (model, [key]) + varDataList.append(var_data) + return vsindex -def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel): - vsindex_dict = {} - vsindex_by_key = {} - varDataList = [] - masterSupports = [] - default_charstrings = top_dicts[0].CharStrings - for gid, gname in enumerate(glyphOrder): - all_cs = [ - _get_cs(td.CharStrings, gname) - for td in top_dicts] - if len([gs for gs in all_cs if gs is not None]) == 1: - continue - model, model_cs = masterModel.getSubModel(all_cs) - # create the first pass CFF2 charstring, from - # the default charstring. - default_charstring = model_cs[0] - var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) - # We need to override outlineExtractor because these - # charstrings do have widths in the 'program'; we need to drop these - # values rather than post assertion error for them. - default_charstring.outlineExtractor = MergeOutlineExtractor - default_charstring.draw(var_pen) - - # Add the coordinates from all the other regions to the - # blend lists in the CFF2 charstring. - region_cs = model_cs[1:] - for region_idx, region_charstring in enumerate(region_cs, start=1): - var_pen.restart(region_idx) - region_charstring.outlineExtractor = MergeOutlineExtractor - region_charstring.draw(var_pen) - - # Collapse each coordinate list to a blend operator and its args. - new_cs = var_pen.getCharString( - private=default_charstring.private, - globalSubrs=default_charstring.globalSubrs, - var_model=model, optimize=True) - default_charstrings[gname] = new_cs - - if (not var_pen.seen_moveto) or ('blend' not in new_cs.program): - # If this is not a marking glyph, or if there are no blend - # arguments, then we can use vsindex 0. No need to - # check if we need a new vsindex. - continue - - # If the charstring required a new model, create - # a VarData table to go with, and set vsindex. - key = tuple(v is not None for v in all_cs) - try: - vsindex = vsindex_by_key[key] - except KeyError: - vsindex = _add_new_vsindex(model, key, masterSupports, vsindex_dict, - vsindex_by_key, varDataList) - # We do not need to check for an existing new_cs.private.vsindex, - # as we know it doesn't exist yet. - if vsindex != 0: - new_cs.program[:0] = [vsindex, 'vsindex'] - - # If there is no variation in any of the charstrings, then vsindex_dict - # never gets built. This could still be needed if there is variation - # in the PrivatDict, so we will build the default data for vsindex = 0. - if not vsindex_dict: - key = (True,) * num_masters - _add_new_vsindex(masterModel, key, masterSupports, vsindex_dict, - vsindex_by_key, varDataList) - cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports, - vsindex_dict=vsindex_dict) - # XXX To do: optimize use of vsindex between the PrivateDicts and - # charstrings - return cvData +def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel): + vsindex_dict = {} + vsindex_by_key = {} + varDataList = [] + masterSupports = [] + default_charstrings = top_dicts[0].CharStrings + for gid, gname in enumerate(glyphOrder): + # interpret empty non-default masters as missing glyphs from a sparse master + all_cs = [ + _get_cs(td.CharStrings, gname, i != 0) for i, td in enumerate(top_dicts) + ] + model, model_cs = masterModel.getSubModel(all_cs) + # create the first pass CFF2 charstring, from + # the default charstring. + default_charstring = model_cs[0] + var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) + # We need to override outlineExtractor because these + # charstrings do have widths in the 'program'; we need to drop these + # values rather than post assertion error for them. + default_charstring.outlineExtractor = MergeOutlineExtractor + default_charstring.draw(var_pen) + + # Add the coordinates from all the other regions to the + # blend lists in the CFF2 charstring. + region_cs = model_cs[1:] + for region_idx, region_charstring in enumerate(region_cs, start=1): + var_pen.restart(region_idx) + region_charstring.outlineExtractor = MergeOutlineExtractor + region_charstring.draw(var_pen) + + # Collapse each coordinate list to a blend operator and its args. + new_cs = var_pen.getCharString( + private=default_charstring.private, + globalSubrs=default_charstring.globalSubrs, + var_model=model, + optimize=True, + ) + default_charstrings[gname] = new_cs + + if not region_cs: + continue + + if (not var_pen.seen_moveto) or ("blend" not in new_cs.program): + # If this is not a marking glyph, or if there are no blend + # arguments, then we can use vsindex 0. No need to + # check if we need a new vsindex. + continue + + # If the charstring required a new model, create + # a VarData table to go with, and set vsindex. + key = tuple(v is not None for v in all_cs) + try: + vsindex = vsindex_by_key[key] + except KeyError: + vsindex = _add_new_vsindex( + model, key, masterSupports, vsindex_dict, vsindex_by_key, varDataList + ) + # We do not need to check for an existing new_cs.private.vsindex, + # as we know it doesn't exist yet. + if vsindex != 0: + new_cs.program[:0] = [vsindex, "vsindex"] + + # If there is no variation in any of the charstrings, then vsindex_dict + # never gets built. This could still be needed if there is variation + # in the PrivatDict, so we will build the default data for vsindex = 0. + if not vsindex_dict: + key = (True,) * num_masters + _add_new_vsindex( + masterModel, key, masterSupports, vsindex_dict, vsindex_by_key, varDataList + ) + cvData = CVarData( + varDataList=varDataList, + masterSupports=masterSupports, + vsindex_dict=vsindex_dict, + ) + # XXX To do: optimize use of vsindex between the PrivateDicts and + # charstrings + return cvData class CFFToCFF2OutlineExtractor(T2OutlineExtractor): - """ This class is used to remove the initial width from the CFF - charstring without trying to add the width to self.nominalWidthX, - which is None. """ - def popallWidth(self, evenOdd=0): - args = self.popall() - if not self.gotWidth: - if evenOdd ^ (len(args) % 2): - args = args[1:] - self.width = self.defaultWidthX - self.gotWidth = 1 - return args + """This class is used to remove the initial width from the CFF + charstring without trying to add the width to self.nominalWidthX, + which is None.""" + + def popallWidth(self, evenOdd=0): + args = self.popall() + if not self.gotWidth: + if evenOdd ^ (len(args) % 2): + args = args[1:] + self.width = self.defaultWidthX + self.gotWidth = 1 + return args class MergeOutlineExtractor(CFFToCFF2OutlineExtractor): - """ Used to extract the charstring commands - including hints - from a - CFF charstring in order to merge it as another set of region data - into a CFF2 variable font charstring.""" - - def __init__(self, pen, localSubrs, globalSubrs, - nominalWidthX, defaultWidthX, private=None): - super().__init__(pen, localSubrs, - globalSubrs, nominalWidthX, defaultWidthX, private) - - def countHints(self): - args = self.popallWidth() - self.hintCount = self.hintCount + len(args) // 2 - return args - - def _hint_op(self, type, args): - self.pen.add_hint(type, args) - - def op_hstem(self, index): - args = self.countHints() - self._hint_op('hstem', args) - - def op_vstem(self, index): - args = self.countHints() - self._hint_op('vstem', args) - - def op_hstemhm(self, index): - args = self.countHints() - self._hint_op('hstemhm', args) - - def op_vstemhm(self, index): - args = self.countHints() - self._hint_op('vstemhm', args) - - def _get_hintmask(self, index): - if not self.hintMaskBytes: - args = self.countHints() - if args: - self._hint_op('vstemhm', args) - self.hintMaskBytes = (self.hintCount + 7) // 8 - hintMaskBytes, index = self.callingStack[-1].getBytes(index, - self.hintMaskBytes) - return index, hintMaskBytes - - def op_hintmask(self, index): - index, hintMaskBytes = self._get_hintmask(index) - self.pen.add_hintmask('hintmask', [hintMaskBytes]) - return hintMaskBytes, index - - def op_cntrmask(self, index): - index, hintMaskBytes = self._get_hintmask(index) - self.pen.add_hintmask('cntrmask', [hintMaskBytes]) - return hintMaskBytes, index + """Used to extract the charstring commands - including hints - from a + CFF charstring in order to merge it as another set of region data + into a CFF2 variable font charstring.""" + + def __init__( + self, + pen, + localSubrs, + globalSubrs, + nominalWidthX, + defaultWidthX, + private=None, + blender=None, + ): + super().__init__( + pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private, blender + ) + + def countHints(self): + args = self.popallWidth() + self.hintCount = self.hintCount + len(args) // 2 + return args + + def _hint_op(self, type, args): + self.pen.add_hint(type, args) + + def op_hstem(self, index): + args = self.countHints() + self._hint_op("hstem", args) + + def op_vstem(self, index): + args = self.countHints() + self._hint_op("vstem", args) + + def op_hstemhm(self, index): + args = self.countHints() + self._hint_op("hstemhm", args) + + def op_vstemhm(self, index): + args = self.countHints() + self._hint_op("vstemhm", args) + + def _get_hintmask(self, index): + if not self.hintMaskBytes: + args = self.countHints() + if args: + self._hint_op("vstemhm", args) + self.hintMaskBytes = (self.hintCount + 7) // 8 + hintMaskBytes, index = self.callingStack[-1].getBytes(index, self.hintMaskBytes) + return index, hintMaskBytes + + def op_hintmask(self, index): + index, hintMaskBytes = self._get_hintmask(index) + self.pen.add_hintmask("hintmask", [hintMaskBytes]) + return hintMaskBytes, index + + def op_cntrmask(self, index): + index, hintMaskBytes = self._get_hintmask(index) + self.pen.add_hintmask("cntrmask", [hintMaskBytes]) + return hintMaskBytes, index class CFF2CharStringMergePen(T2CharStringPen): - """Pen to merge Type 2 CharStrings. - """ - def __init__( - self, default_commands, glyphName, num_masters, master_idx, - roundTolerance=0.5): - super().__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.seen_moveto = False - self.glyphName = glyphName - self.round = roundFunc(roundTolerance, round=round) - - 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: - raise VarLibCFFPointTypeMergeError( - point_type, - self.pt_index, len(cmd[1]), - cmd[0], self.glyphName) - cmd[1].append(pt_coords) - self.pt_index += 1 - - def add_hint(self, hint_type, args): - if self.m_index == 0: - self._commands.append([hint_type, [args]]) - else: - cmd = self._commands[self.pt_index] - if cmd[0] != hint_type: - raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]), - cmd[0], self.glyphName) - cmd[1].append(args) - self.pt_index += 1 - - def add_hintmask(self, hint_type, abs_args): - # For hintmask, fonttools.cffLib.specializer.py expects - # each of these to be represented by two sequential commands: - # first holding only the operator name, with an empty arg list, - # second with an empty string as the op name, and the mask arg list. - if self.m_index == 0: - self._commands.append([hint_type, []]) - self._commands.append(["", [abs_args]]) - else: - cmd = self._commands[self.pt_index] - if cmd[0] != hint_type: - raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]), - cmd[0], self.glyphName) - self.pt_index += 1 - cmd = self._commands[self.pt_index] - cmd[1].append(abs_args) - self.pt_index += 1 - - def _moveTo(self, pt): - if not self.seen_moveto: - self.seen_moveto = True - 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, get_delta_func): - """ - 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] - ] - - If the master values are all the same, we collapse the list to - as single value instead of a list. - - We then convert this to:: - - [ [master_0 x] + [x delta tuple] + [numBlends=1] - [master_0 y] + [y delta tuple] + [numBlends=1] - ] - """ - 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] = list(m_args) - lastOp = None - for cmd in commands: - op = cmd[0] - # masks are represented by two cmd's: first has only op names, - # second has only args. - if lastOp in ['hintmask', 'cntrmask']: - coord = list(cmd[1]) - if not allEqual(coord): - raise VarLibMergeError("Hintmask values cannot differ between source fonts.") - cmd[1] = [coord[0][0]] - else: - coords = cmd[1] - new_coords = [] - for coord in coords: - if allEqual(coord): - new_coords.append(coord[0]) - else: - # convert to deltas - deltas = get_delta_func(coord)[1:] - coord = [coord[0]] + deltas - coord.append(1) - new_coords.append(coord) - cmd[1] = new_coords - lastOp = op - return commands - - def getCharString( - self, private=None, globalSubrs=None, - var_model=None, optimize=True): - commands = self._commands - commands = self.reorder_blend_args(commands, partial (var_model.getDeltas, round=self.round)) - if optimize: - commands = specializeCommands( - commands, generalizeFirst=False, - maxstack=maxStackLimit) - program = commandsToProgram(commands) - charString = T2CharString( - program=program, private=private, - globalSubrs=globalSubrs) - return charString + """Pen to merge Type 2 CharStrings.""" + + def __init__( + self, default_commands, glyphName, num_masters, master_idx, roundTolerance=0.01 + ): + # For roundTolerance see https://github.com/fonttools/fonttools/issues/2838 + super().__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.seen_moveto = False + self.glyphName = glyphName + self.round = roundFunc(roundTolerance, round=round) + + 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: + raise VarLibCFFPointTypeMergeError( + point_type, self.pt_index, len(cmd[1]), cmd[0], self.glyphName + ) + cmd[1].append(pt_coords) + self.pt_index += 1 + + def add_hint(self, hint_type, args): + if self.m_index == 0: + self._commands.append([hint_type, [args]]) + else: + cmd = self._commands[self.pt_index] + if cmd[0] != hint_type: + raise VarLibCFFHintTypeMergeError( + hint_type, self.pt_index, len(cmd[1]), cmd[0], self.glyphName + ) + cmd[1].append(args) + self.pt_index += 1 + + def add_hintmask(self, hint_type, abs_args): + # For hintmask, fonttools.cffLib.specializer.py expects + # each of these to be represented by two sequential commands: + # first holding only the operator name, with an empty arg list, + # second with an empty string as the op name, and the mask arg list. + if self.m_index == 0: + self._commands.append([hint_type, []]) + self._commands.append(["", [abs_args]]) + else: + cmd = self._commands[self.pt_index] + if cmd[0] != hint_type: + raise VarLibCFFHintTypeMergeError( + hint_type, self.pt_index, len(cmd[1]), cmd[0], self.glyphName + ) + self.pt_index += 1 + cmd = self._commands[self.pt_index] + cmd[1].append(abs_args) + self.pt_index += 1 + + def _moveTo(self, pt): + if not self.seen_moveto: + self.seen_moveto = True + 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, get_delta_func): + """ + 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] + ] + + If the master values are all the same, we collapse the list to + as single value instead of a list. + + We then convert this to:: + + [ [master_0 x] + [x delta tuple] + [numBlends=1] + [master_0 y] + [y delta tuple] + [numBlends=1] + ] + """ + 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] = list(m_args) + lastOp = None + for cmd in commands: + op = cmd[0] + # masks are represented by two cmd's: first has only op names, + # second has only args. + if lastOp in ["hintmask", "cntrmask"]: + coord = list(cmd[1]) + if not allEqual(coord): + raise VarLibMergeError( + "Hintmask values cannot differ between source fonts." + ) + cmd[1] = [coord[0][0]] + else: + coords = cmd[1] + new_coords = [] + for coord in coords: + if allEqual(coord): + new_coords.append(coord[0]) + else: + # convert to deltas + deltas = get_delta_func(coord)[1:] + coord = [coord[0]] + deltas + coord.append(1) + new_coords.append(coord) + cmd[1] = new_coords + lastOp = op + return commands + + def getCharString( + self, private=None, globalSubrs=None, var_model=None, optimize=True + ): + commands = self._commands + commands = self.reorder_blend_args( + commands, partial(var_model.getDeltas, round=self.round) + ) + if optimize: + commands = specializeCommands( + commands, generalizeFirst=False, maxstack=maxStackLimit + ) + program = commandsToProgram(commands) + charString = T2CharString( + program=program, private=private, globalSubrs=globalSubrs + ) + return charString |