aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/varLib/cff.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/varLib/cff.py')
-rw-r--r--Lib/fontTools/varLib/cff.py1239
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