|
from collections import namedtuple |
|
from fontTools.cffLib import ( |
|
maxStackLimit, |
|
TopDictIndex, |
|
buildOrder, |
|
topDictOperators, |
|
topDictOperators2, |
|
privateDictOperators, |
|
privateDictOperators2, |
|
FDArrayIndex, |
|
FontDict, |
|
VarStoreData, |
|
) |
|
from io import BytesIO |
|
from fontTools.cffLib.specializer import specializeCommands, commandsToProgram |
|
from fontTools.ttLib import newTable |
|
from fontTools import varLib |
|
from fontTools.varLib.models import allEqual |
|
from fontTools.misc.loggingTools import deprecateFunction |
|
from fontTools.misc.roundTools import roundFunc |
|
from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor |
|
from fontTools.pens.t2CharStringPen import T2CharStringPen |
|
from functools import partial |
|
|
|
from .errors import ( |
|
VarLibCFFDictMergeError, |
|
VarLibCFFPointTypeMergeError, |
|
VarLibCFFHintTypeMergeError, |
|
VarLibMergeError, |
|
) |
|
|
|
|
|
|
|
MergeDictError = VarLibCFFDictMergeError |
|
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) |
|
|
|
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 |
|
|
|
|
|
@deprecateFunction("Use fontTools.cffLib.CFFToCFF2.convertCFFToCFF2 instead.") |
|
def convertCFFtoCFF2(varFont): |
|
from fontTools.cffLib.CFFToCFF2 import convertCFFToCFF2 |
|
|
|
return convertCFFToCFF2(varFont) |
|
|
|
|
|
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", |
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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 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. |
|
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]] |
|
""" |
|
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) |
|
|
|
|
|
deltas[0] = val_list[0] |
|
dataList.append(deltas) |
|
|
|
|
|
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] |
|
|
|
|
|
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"] |
|
|
|
|
|
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"): |
|
|
|
|
|
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"): |
|
|
|
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, filterEmpty=False): |
|
if glyphName not in charstrings: |
|
return None |
|
cs = charstrings[glyphName] |
|
|
|
if filterEmpty: |
|
cs.decompile() |
|
if cs.program == []: |
|
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)) |
|
): |
|
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, i != 0) for i, td in enumerate(top_dicts) |
|
] |
|
model, model_cs = masterModel.getSubModel(all_cs) |
|
|
|
|
|
default_charstring = model_cs[0] |
|
var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) |
|
|
|
|
|
|
|
default_charstring.outlineExtractor = MergeOutlineExtractor |
|
default_charstring.draw(var_pen) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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): |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
if vsindex != 0: |
|
new_cs.program[:0] = [vsindex, "vsindex"] |
|
|
|
|
|
|
|
|
|
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, |
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
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, |
|
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.01 |
|
): |
|
|
|
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): |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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: |
|
|
|
args = cmd[1] |
|
m_args = zip(*args) |
|
|
|
|
|
cmd[1] = list(m_args) |
|
lastOp = None |
|
for cmd in commands: |
|
op = cmd[0] |
|
|
|
|
|
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: |
|
|
|
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 |
|
|