def reorder_blend_args(self, commands, get_delta_func, round_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]) assert allEqual(coord), ( "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:] if round_func: deltas = [round_func(delta) for delta in deltas] coord = [coord[0]] + deltas new_coords.append(coord) cmd[1] = new_coords lastOp = op return commands
def _PairPosFormat2_merge(self, lst, merger): assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." merger.mergeObjects(self, lst, exclude=('Coverage', 'ClassDef1', 'Class1Count', 'ClassDef2', 'Class2Count', 'Class1Record', 'ValueFormat1', 'ValueFormat2')) # Align coverages glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst]) self.Coverage.glyphs = glyphs # Currently, if the coverage of PairPosFormat2 subtables are different, # we do NOT bother walking down the subtable list when filling in new # rows for alignment. As such, this is only correct if current subtable # is the last subtable in the lookup. Ensure that. # # Note that our canonicalization process merges trailing PairPosFormat2's, # so in reality this is rare. for l,subtables in zip(lst,merger.lookup_subtables): if l.Coverage.glyphs != glyphs: assert l == subtables[-1] matrices = _PairPosFormat2_align_matrices(self, lst, merger.font) self.Class1Record = list(matrices[0]) # TODO move merger to be selfless merger.mergeLists(self.Class1Record, matrices)
def _Lookup_PairPosFormat2_subtables_flatten(lst, font): assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." self = ot.PairPos() self.Format = 2 self.Coverage = ot.Coverage() self.Coverage.Format = 1 self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) # Align them glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst]) self.Coverage.glyphs = glyphs matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True) matrix = self.Class1Record = [] for rows in zip(*matrices): row = ot.Class1Record() matrix.append(row) row.Class2Record = [] row = row.Class2Record for cols in zip(*list(r.Class2Record for r in rows)): col = next(iter(c for c in cols if c is not None)) row.append(col) return self
def _PairPosFormat1_merge(self, lst, merger): assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." # Merge everything else; makes sure Format is the same. merger.mergeObjects(self, lst, exclude=('Coverage', 'PairSet', 'PairSetCount', 'ValueFormat1', 'ValueFormat2')) empty = ot.PairSet() empty.PairValueRecord = [] empty.PairValueCount = 0 # Align them glyphs, padded = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst], default=empty) self.Coverage.glyphs = glyphs self.PairSet = [ot.PairSet() for g in glyphs] self.PairSetCount = len(self.PairSet) for glyph, ps in zip(glyphs, self.PairSet): ps._firstGlyph = glyph merger.mergeLists(self.PairSet, padded)
def merge(merger, self, lst): subtables = merger.lookup_subtables = [l.SubTable for l in lst] # Remove Extension subtables for l,sts in list(zip(lst,subtables))+[(self,self.SubTable)]: if not sts: continue if sts[0].__class__.__name__.startswith('Extension'): assert allEqual([st.__class__ for st in sts]) assert allEqual([st.ExtensionLookupType for st in sts]) l.LookupType = sts[0].ExtensionLookupType new_sts = [st.ExtSubTable for st in sts] del sts[:] sts.extend(new_sts) isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) if isPairPos: # AFDKO and feaLib sometimes generate two Format1 subtables instead of one. # Merge those before continuing. # https://github.com/fonttools/fonttools/issues/719 self.SubTable = _Lookup_PairPos_subtables_canonicalize(self.SubTable, merger.font) subtables = merger.lookup_subtables = [_Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables] merger.mergeLists(self.SubTable, subtables) self.SubTableCount = len(self.SubTable) if isPairPos: # If format-1 subtable created during canonicalization is empty, remove it. assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 if not self.SubTable[0].Coverage.glyphs: self.SubTable.pop(0) self.SubTableCount -= 1 # If format-2 subtable created during canonicalization is empty, remove it. assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 if not self.SubTable[-1].Coverage.glyphs: self.SubTable.pop(-1) self.SubTableCount -= 1 merger.mergeObjects(self, lst, exclude=['SubTable', 'SubTableCount']) del merger.lookup_subtables
def merge(merger, self, lst): if self is None: assert allNone(lst), (lst) return self.classDefs = {} # We only care about the .classDefs self = self.classDefs lst = [l.classDefs for l in lst] allKeys = set() allKeys.update(*[l.keys() for l in lst]) for k in allKeys: allValues = nonNone(l.get(k) for l in lst) assert allEqual(allValues), allValues if not allValues: self[k] = None else: self[k] = allValues[0]
def reorder_blend_args(self, commands): """ We first re-order the master coordinate values. For a moveto to lineto, the args are now arranged as: [ [master_0 x,y], [master_1 x,y], [master_2 x,y] ] We re-arrange this to [ [master_0 x, master_1 x, master_2 x], [master_0 y, master_1 y, master_2 y] ] We also make the value relative. If the master values are all the same, we collapse the list to as single value instead of a list. """ for cmd in commands: # arg[i] is the set of arguments for this operator from master i. args = cmd[1] m_args = zip(*args) # m_args[n] is now all num_master args for the i'th argument # for this operation. cmd[1] = m_args # Now convert from absolute to relative x0 = [0]*self.num_masters y0 = [0]*self.num_masters for cmd in self._commands: is_x = True coords = cmd[1] rel_coords = [] for coord in coords: prev_coord = x0 if is_x else y0 rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)] if allEqual(rel_coord): rel_coord = rel_coord[0] rel_coords.append(rel_coord) if is_x: x0 = coord else: y0 = coord is_x = not is_x cmd[1] = rel_coords return commands
def _Lookup_PairPosFormat1_subtables_flatten(lst, font): assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." self = ot.PairPos() self.Format = 1 self.Coverage = ot.Coverage() self.Coverage.Format = 1 self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) # Align them glyphs, padded = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst]) self.Coverage.glyphs = glyphs self.PairSet = [_PairSet_flatten([v for v in values if v is not None], font) for values in zip(*padded)] self.PairSetCount = len(self.PairSet) return self
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True): assert tolerance >= 0 log.info("Generating gvar") assert "gvar" not in font gvar = font["gvar"] = newTable('gvar') gvar.version = 1 gvar.reserved = 0 gvar.variations = {} for glyph in font.getGlyphOrder(): allData = [_GetCoordinates(m, glyph) for m in master_ttfs] model, allData = masterModel.getSubModel(allData) allCoords = [d[0] for d in allData] allControls = [d[1] for d in allData] control = allControls[0] if not models.allEqual(allControls): log.warning("glyph %s has incompatible masters; skipping" % glyph) continue del allControls # Update gvar gvar.variations[glyph] = [] deltas = model.getDeltas(allCoords) supports = model.supports assert len(deltas) == len(supports) # Prepare for IUP optimization origCoords = deltas[0] endPts = control[1] if control[0] >= 1 else list(range(len( control[1]))) for i, (delta, support) in enumerate(zip(deltas[1:], supports[1:])): if all(abs(v) <= tolerance for v in delta.array): continue var = TupleVariation(support, delta) if optimize: delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance) if None in delta_opt: # Use "optimized" version only if smaller... var_opt = TupleVariation(support, delta_opt) axis_tags = sorted(support.keys( )) # Shouldn't matter that this is different from fvar...? tupleData, auxData, _ = var.compile(axis_tags, [], None) unoptimized_len = len(tupleData) + len(auxData) tupleData, auxData, _ = var_opt.compile( axis_tags, [], None) optimized_len = len(tupleData) + len(auxData) if optimized_len < unoptimized_len: var = var_opt gvar.variations[glyph].append(var)
def _add_HVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating HVAR") glyphOrder = font.getGlyphOrder() hAdvanceDeltasAndSupports = {} metricses = [m["hmtx"].metrics for m in master_ttfs] for glyph in glyphOrder: hAdvances = [ metrics[glyph][0] if glyph in metrics else None for metrics in metricses ] hAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports( hAdvances) singleModel = models.allEqual( id(v[1]) for v in hAdvanceDeltasAndSupports.values()) directStore = None if singleModel: # Build direct mapping supports = next(iter(hAdvanceDeltasAndSupports.values()))[1][1:] varTupleList = builder.buildVarRegionList(supports, axisTags) varTupleIndexes = list(range(len(supports))) varData = builder.buildVarData(varTupleIndexes, [], optimize=False) for glyphName in glyphOrder: varData.addItem(hAdvanceDeltasAndSupports[glyphName][0]) varData.optimize() directStore = builder.buildVarStore(varTupleList, [varData]) # Build optimized indirect mapping storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) mapping = {} for glyphName in glyphOrder: deltas, supports = hAdvanceDeltasAndSupports[glyphName] storeBuilder.setSupports(supports) mapping[glyphName] = storeBuilder.storeDeltas(deltas) indirectStore = storeBuilder.finish() mapping2 = indirectStore.optimize() mapping = [mapping2[mapping[g]] for g in glyphOrder] advanceMapping = builder.buildVarIdxMap(mapping, glyphOrder) use_direct = False if directStore: # Compile both, see which is more compact writer = OTTableWriter() directStore.compile(writer, font) directSize = len(writer.getAllData()) writer = OTTableWriter() indirectStore.compile(writer, font) advanceMapping.compile(writer, font) indirectSize = len(writer.getAllData()) use_direct = directSize < indirectSize # Done; put it all together. assert "HVAR" not in font HVAR = font["HVAR"] = newTable('HVAR') hvar = HVAR.table = ot.HVAR() hvar.Version = 0x00010000 hvar.LsbMap = hvar.RsbMap = None if use_direct: hvar.VarStore = directStore hvar.AdvWidthMap = None else: hvar.VarStore = indirectStore hvar.AdvWidthMap = advanceMapping
def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'): self.ClassCount = max(l.ClassCount for l in lst) MarkCoverageGlyphs, MarkRecords = \ _merge_GlyphOrders(merger.font, [getattr(l, Mark+'Coverage').glyphs for l in lst], [getattr(l, Mark+'Array').MarkRecord for l in lst]) getattr(self, Mark+'Coverage').glyphs = MarkCoverageGlyphs BaseCoverageGlyphs, BaseRecords = \ _merge_GlyphOrders(merger.font, [getattr(l, Base+'Coverage').glyphs for l in lst], [getattr(getattr(l, Base+'Array'), Base+'Record') for l in lst]) getattr(self, Base+'Coverage').glyphs = BaseCoverageGlyphs # MarkArray records = [] for g,glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): allClasses = [r.Class for r in glyphRecords if r is not None] # TODO Right now we require that all marks have same class in # all masters that cover them. This is not required. # # We can relax that by just requiring that all marks that have # the same class in a master, have the same class in every other # master. Indeed, if, say, a sparse master only covers one mark, # that mark probably will get class 0, which would possibly be # different from its class in other masters. # # We can even go further and reclassify marks to support any # input. But, since, it's unlikely that two marks being both, # say, "top" in one master, and one being "top" and other being # "top-right" in another master, we shouldn't do that, as any # failures in that case will probably signify mistakes in the # input masters. assert allEqual(allClasses), allClasses if not allClasses: rec = None else: rec = ot.MarkRecord() rec.Class = allClasses[0] allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords] if allNone(allAnchors): anchor = None else: anchor = ot.Anchor() anchor.Format = 1 merger.mergeThings(anchor, allAnchors) rec.MarkAnchor = anchor records.append(rec) array = ot.MarkArray() array.MarkRecord = records array.MarkCount = len(records) setattr(self, Mark+"Array", array) # BaseArray records = [] for g,glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): if allNone(glyphRecords): rec = None else: rec = getattr(ot, Base+'Record')() anchors = [] setattr(rec, Base+'Anchor', anchors) glyphAnchors = [[] if r is None else getattr(r, Base+'Anchor') for r in glyphRecords] for l in glyphAnchors: l.extend([None] * (self.ClassCount - len(l))) for allAnchors in zip(*glyphAnchors): if allNone(allAnchors): anchor = None else: anchor = ot.Anchor() anchor.Format = 1 merger.mergeThings(anchor, allAnchors) anchors.append(anchor) records.append(rec) array = getattr(ot, Base+'Array')() setattr(array, Base+'Record', records) setattr(array, Base+'Count', len(records)) setattr(self, Base+'Array', array)
def buildVarDevTable(store_builder, master_values): if allEqual(master_values): return master_values[0], None base, varIdx = store_builder.storeMasters(master_values) return base, builder.buildVarDevTable(varIdx)
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True): assert tolerance >= 0 log.info("Generating gvar") assert "gvar" not in font gvar = font["gvar"] = newTable('gvar') gvar.version = 1 gvar.reserved = 0 gvar.variations = {} glyf = font['glyf'] for glyph in font.getGlyphOrder(): isComposite = glyf[glyph].isComposite() allData = [_GetCoordinates(m, glyph) for m in master_ttfs] model, allData = masterModel.getSubModel(allData) allCoords = [d[0] for d in allData] allControls = [d[1] for d in allData] control = allControls[0] if not models.allEqual(allControls): log.warning("glyph %s has incompatible masters; skipping" % glyph) continue del allControls # Update gvar gvar.variations[glyph] = [] deltas = model.getDeltas(allCoords) supports = model.supports assert len(deltas) == len(supports) # Prepare for IUP optimization origCoords = deltas[0] endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): if all(abs(v) <= tolerance for v in delta.array) and not isComposite: continue var = TupleVariation(support, delta) if optimize: delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance) if None in delta_opt: """In composite glyphs, there should be one 0 entry to make sure the gvar entry is written to the font. This is to work around an issue with macOS 10.14 and can be removed once the behaviour of macOS is changed. https://github.com/fonttools/fonttools/issues/1381 """ if all(d is None for d in delta_opt): delta_opt = [(0, 0)] + [None] * (len(delta_opt) - 1) # Use "optimized" version only if smaller... var_opt = TupleVariation(support, delta_opt) axis_tags = sorted(support.keys()) # Shouldn't matter that this is different from fvar...? tupleData, auxData, _ = var.compile(axis_tags, [], None) unoptimized_len = len(tupleData) + len(auxData) tupleData, auxData, _ = var_opt.compile(axis_tags, [], None) optimized_len = len(tupleData) + len(auxData) if optimized_len < unoptimized_len: var = var_opt gvar.variations[glyph].append(var)
def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model): if hasattr(region_top_dicts[0], 'FDArray'): regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts] else: regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts] for fd_index, font_dict in enumerate(topDict.FDArray): private_dict = font_dict.Private pds = [private_dict] + [ regionFDArray[fd_index].Private for regionFDArray in regionFDArrays ] for key, value in private_dict.rawDict.items(): if key not in pd_blend_fields: continue if isinstance(value, list): try: values = [pd.rawDict[key] for pd in pds] except KeyError: del private_dict.rawDict[key] print( b"Warning: {key} in default font Private dict is " b"missing from another font, and was " b"discarded.".format(key=key)) continue try: values = zip(*values) except IndexError: raise MergeDictError(key, value, values) """ Row 0 contains the first value from each master. Convert each row from absolute values to relative values from the previous row. e.g for three masters, a list of values was: master 0 OtherBlues = [-217,-205] master 1 OtherBlues = [-234,-222] master 1 OtherBlues = [-188,-176] The call to zip() converts this to: [(-217, -234, -188), (-205, -222, -176)] and is converted finally to: OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] """ dataList = [] prev_val_list = [0] * num_masters any_points_differ = False for val_list in values: rel_list = [(val - prev_val_list[i]) for ( i, val) in enumerate(val_list)] if (not any_points_differ) and not allEqual(rel_list): any_points_differ = True prev_val_list = val_list deltas = var_model.getDeltas(rel_list) # Convert numbers with no decimal part to an int. deltas = [conv_to_int(delta) for delta in deltas] # For PrivateDict BlueValues, the default font # values are absolute, not relative to the prior value. deltas[0] = val_list[0] dataList.append(deltas) # If there are no blend values,then # we can collapse the blend lists. if not any_points_differ: dataList = [data[0] for data in dataList] else: values = [pd.rawDict[key] for pd in pds] if not allEqual(values): dataList = var_model.getDeltas(values) else: dataList = values[0] private_dict.rawDict[key] = dataList
def _add_HVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating HVAR") glyphOrder = font.getGlyphOrder() hAdvanceDeltasAndSupports = {} metricses = [m["hmtx"].metrics for m in master_ttfs] for glyph in glyphOrder: hAdvances = [metrics[glyph][0] if glyph in metrics else None for metrics in metricses] hAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(hAdvances) singleModel = models.allEqual(id(v[1]) for v in hAdvanceDeltasAndSupports.values()) directStore = None if singleModel: # Build direct mapping supports = next(iter(hAdvanceDeltasAndSupports.values()))[1][1:] varTupleList = builder.buildVarRegionList(supports, axisTags) varTupleIndexes = list(range(len(supports))) varData = builder.buildVarData(varTupleIndexes, [], optimize=False) for glyphName in glyphOrder: varData.addItem(hAdvanceDeltasAndSupports[glyphName][0]) varData.optimize() directStore = builder.buildVarStore(varTupleList, [varData]) # Build optimized indirect mapping storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) mapping = {} for glyphName in glyphOrder: deltas,supports = hAdvanceDeltasAndSupports[glyphName] storeBuilder.setSupports(supports) mapping[glyphName] = storeBuilder.storeDeltas(deltas) indirectStore = storeBuilder.finish() mapping2 = indirectStore.optimize() mapping = [mapping2[mapping[g]] for g in glyphOrder] advanceMapping = builder.buildVarIdxMap(mapping, glyphOrder) use_direct = False if directStore: # Compile both, see which is more compact writer = OTTableWriter() directStore.compile(writer, font) directSize = len(writer.getAllData()) writer = OTTableWriter() indirectStore.compile(writer, font) advanceMapping.compile(writer, font) indirectSize = len(writer.getAllData()) use_direct = directSize < indirectSize # Done; put it all together. assert "HVAR" not in font HVAR = font["HVAR"] = newTable('HVAR') hvar = HVAR.table = ot.HVAR() hvar.Version = 0x00010000 hvar.LsbMap = hvar.RsbMap = None if use_direct: hvar.VarStore = directStore hvar.AdvWidthMap = None else: hvar.VarStore = indirectStore hvar.AdvWidthMap = advanceMapping
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 model_keys[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, model_keys = 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(): if key not in pd_blend_fields: continue if isinstance(value, list): try: values = [pd.rawDict[key] for pd in pds] except KeyError: del private_dict.rawDict[key] print( b"Warning: {key} in default font Private dict is " b"missing from another font, and was " b"discarded.".format(key=key)) continue try: values = zip(*values) except IndexError: raise MergeDictError(key, value, values) """ Row 0 contains the first value from each master. Convert each row from absolute values to relative values from the previous row. e.g for three masters, a list of values was: master 0 OtherBlues = [-217,-205] master 1 OtherBlues = [-234,-222] master 1 OtherBlues = [-188,-176] The call to zip() converts this to: [(-217, -234, -188), (-205, -222, -176)] and is converted finally to: OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] """ dataList = [] prev_val_list = [0] * num_masters any_points_differ = False for val_list in values: rel_list = [(val - prev_val_list[i]) for ( i, val) in enumerate(val_list)] if (not any_points_differ) and not allEqual(rel_list): any_points_differ = True prev_val_list = val_list deltas = sub_model.getDeltas(rel_list) # Convert numbers with no decimal part to an int. deltas = [conv_to_int(delta) for delta in deltas] # For PrivateDict BlueValues, the default font # values are absolute, not relative to the prior value. deltas[0] = val_list[0] dataList.append(deltas) # If there are no blend values,then # we can collapse the blend lists. if not any_points_differ: dataList = [data[0] for data in dataList] else: values = [pd.rawDict[key] for pd in pds] if not allEqual(values): dataList = sub_model.getDeltas(values) else: dataList = values[0] private_dict.rawDict[key] = dataList
def _add_MVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating MVAR") store_builder = varStore.OnlineVarStoreBuilder(axisTags) records = [] lastTableTag = None fontTable = None tables = None # HACK: we need to special-case post.underlineThickness and .underlinePosition # and unilaterally/arbitrarily define a sentinel value to distinguish the case # when a post table is present in a given master simply because that's where # the glyph names in TrueType must be stored, but the underline values are not # meant to be used for building MVAR's deltas. The value of -0x8000 (-36768) # the minimum FWord (int16) value, was chosen for its unlikelyhood to appear # in real-world underline position/thickness values. specialTags = {"unds": -0x8000, "undo": -0x8000} for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]): # For each tag, fetch the associated table from all fonts (or not when we are # still looking at a tag from the same tables) and set up the variation model # for them. if tableTag != lastTableTag: tables = fontTable = None if tableTag in font: fontTable = font[tableTag] tables = [] for master in master_ttfs: if tableTag not in master or ( tag in specialTags and getattr(master[tableTag], itemName) == specialTags[tag] ): tables.append(None) else: tables.append(master[tableTag]) model, tables = masterModel.getSubModel(tables) store_builder.setModel(model) lastTableTag = tableTag if tables is None: # Tag not applicable to the master font. continue # TODO support gasp entries master_values = [getattr(table, itemName) for table in tables] if models.allEqual(master_values): base, varIdx = master_values[0], None else: base, varIdx = store_builder.storeMasters(master_values) setattr(fontTable, itemName, base) if varIdx is None: continue log.info(' %s: %s.%s %s', tag, tableTag, itemName, master_values) rec = ot.MetricsValueRecord() rec.ValueTag = tag rec.VarIdx = varIdx records.append(rec) assert "MVAR" not in font if records: store = store_builder.finish() # Optimize mapping = store.optimize() for rec in records: rec.VarIdx = mapping[rec.VarIdx] MVAR = font["MVAR"] = newTable('MVAR') mvar = MVAR.table = ot.MVAR() mvar.Version = 0x00010000 mvar.Reserved = 0 mvar.VarStore = store # XXX these should not be hard-coded but computed automatically mvar.ValueRecordSize = 8 mvar.ValueRecordCount = len(records) mvar.ValueRecord = sorted(records, key=lambda r: r.ValueTag)
def _get_advance_metrics(font, masterModel, master_ttfs, axisTags, glyphOrder, advMetricses, vOrigMetricses=None): vhAdvanceDeltasAndSupports = {} vOrigDeltasAndSupports = {} for glyph in glyphOrder: vhAdvances = [metrics[glyph][0] if glyph in metrics else None for metrics in advMetricses] vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(vhAdvances) singleModel = models.allEqual(id(v[1]) for v in vhAdvanceDeltasAndSupports.values()) if vOrigMetricses: singleModel = False for glyph in glyphOrder: # We need to supply a vOrigs tuple with non-None default values # for each glyph. vOrigMetricses contains values only for those # glyphs which have a non-default vOrig. vOrigs = [metrics[glyph] if glyph in metrics else defaultVOrig for metrics, defaultVOrig in vOrigMetricses] vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(vOrigs) directStore = None if singleModel: # Build direct mapping supports = next(iter(vhAdvanceDeltasAndSupports.values()))[1][1:] varTupleList = builder.buildVarRegionList(supports, axisTags) varTupleIndexes = list(range(len(supports))) varData = builder.buildVarData(varTupleIndexes, [], optimize=False) for glyphName in glyphOrder: varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0]) varData.optimize() directStore = builder.buildVarStore(varTupleList, [varData]) # Build optimized indirect mapping storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) advMapping = {} for glyphName in glyphOrder: deltas, supports = vhAdvanceDeltasAndSupports[glyphName] storeBuilder.setSupports(supports) advMapping[glyphName] = storeBuilder.storeDeltas(deltas) if vOrigMetricses: vOrigMap = {} for glyphName in glyphOrder: deltas, supports = vOrigDeltasAndSupports[glyphName] storeBuilder.setSupports(supports) vOrigMap[glyphName] = storeBuilder.storeDeltas(deltas) indirectStore = storeBuilder.finish() mapping2 = indirectStore.optimize() advMapping = [mapping2[advMapping[g]] for g in glyphOrder] advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder) if vOrigMetricses: vOrigMap = [mapping2[vOrigMap[g]] for g in glyphOrder] useDirect = False vOrigMapping = None if directStore: # Compile both, see which is more compact writer = OTTableWriter() directStore.compile(writer, font) directSize = len(writer.getAllData()) writer = OTTableWriter() indirectStore.compile(writer, font) advanceMapping.compile(writer, font) indirectSize = len(writer.getAllData()) useDirect = directSize < indirectSize if useDirect: metricsStore = directStore advanceMapping = None else: metricsStore = indirectStore if vOrigMetricses: vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder) return metricsStore, advanceMapping, vOrigMapping
def _merge_TTHinting(font, masterModel, master_ttfs, tolerance=0.5): log.info("Merging TT hinting") assert "cvar" not in font # Check that the existing hinting is compatible # fpgm and prep table for tag in ("fpgm", "prep"): all_pgms = [m[tag].program for m in master_ttfs if tag in m] if len(all_pgms) == 0: continue if tag in font: font_pgm = font[tag].program else: font_pgm = Program() if any(pgm != font_pgm for pgm in all_pgms): log.warning("Masters have incompatible %s tables, hinting is discarded." % tag) _remove_TTHinting(font) return # glyf table for name, glyph in font["glyf"].glyphs.items(): all_pgms = [ m["glyf"][name].program for m in master_ttfs if name in m['glyf'] and hasattr(m["glyf"][name], "program") ] if not any(all_pgms): continue glyph.expand(font["glyf"]) if hasattr(glyph, "program"): font_pgm = glyph.program else: font_pgm = Program() if any(pgm != font_pgm for pgm in all_pgms if pgm): log.warning("Masters have incompatible glyph programs in glyph '%s', hinting is discarded." % name) # TODO Only drop hinting from this glyph. _remove_TTHinting(font) return # cvt table all_cvs = [Vector(m["cvt "].values) if 'cvt ' in m else None for m in master_ttfs] nonNone_cvs = models.nonNone(all_cvs) if not nonNone_cvs: # There is no cvt table to make a cvar table from, we're done here. return if not models.allEqual(len(c) for c in nonNone_cvs): log.warning("Masters have incompatible cvt tables, hinting is discarded.") _remove_TTHinting(font) return # We can build the cvar table now. cvar = font["cvar"] = newTable('cvar') cvar.version = 1 cvar.variations = [] deltas, supports = masterModel.getDeltasAndSupports(all_cvs) for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): delta = [otRound(d) for d in delta] if all(abs(v) <= tolerance for v in delta): continue var = TupleVariation(support, delta) cvar.variations.append(var)
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True): assert tolerance >= 0 log.info("Generating gvar") assert "gvar" not in font gvar = font["gvar"] = newTable('gvar') gvar.version = 1 gvar.reserved = 0 gvar.variations = {} glyf = font['glyf'] # use hhea.ascent of base master as default vertical origin when vmtx is missing baseAscent = font['hhea'].ascent for glyph in font.getGlyphOrder(): isComposite = glyf[glyph].isComposite() allData = [ m["glyf"].getCoordinatesAndControls(glyph, m, defaultVerticalOrigin=baseAscent) for m in master_ttfs ] model, allData = masterModel.getSubModel(allData) allCoords = [d[0] for d in allData] allControls = [d[1] for d in allData] control = allControls[0] if not models.allEqual(allControls): log.warning("glyph %s has incompatible masters; skipping" % glyph) continue del allControls # Update gvar gvar.variations[glyph] = [] deltas = model.getDeltas(allCoords) supports = model.supports assert len(deltas) == len(supports) # Prepare for IUP optimization origCoords = deltas[0] endPts = control.endPts for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): if all(abs(v) <= tolerance for v in delta.array) and not isComposite: continue var = TupleVariation(support, delta) if optimize: delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance) if None in delta_opt: """In composite glyphs, there should be one 0 entry to make sure the gvar entry is written to the font. This is to work around an issue with macOS 10.14 and can be removed once the behaviour of macOS is changed. https://github.com/fonttools/fonttools/issues/1381 """ if all(d is None for d in delta_opt): delta_opt = [(0, 0)] + [None] * (len(delta_opt) - 1) # Use "optimized" version only if smaller... var_opt = TupleVariation(support, delta_opt) axis_tags = sorted(support.keys()) # Shouldn't matter that this is different from fvar...? tupleData, auxData, _ = var.compile(axis_tags, [], None) unoptimized_len = len(tupleData) + len(auxData) tupleData, auxData, _ = var_opt.compile(axis_tags, [], None) optimized_len = len(tupleData) + len(auxData) if optimized_len < unoptimized_len: var = var_opt gvar.variations[glyph].append(var)
def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'): self.ClassCount = max(l.ClassCount for l in lst) MarkCoverageGlyphs, MarkRecords = \ _merge_GlyphOrders(merger.font, [getattr(l, Mark+'Coverage').glyphs for l in lst], [getattr(l, Mark+'Array').MarkRecord for l in lst]) getattr(self, Mark + 'Coverage').glyphs = MarkCoverageGlyphs BaseCoverageGlyphs, BaseRecords = \ _merge_GlyphOrders(merger.font, [getattr(l, Base+'Coverage').glyphs for l in lst], [getattr(getattr(l, Base+'Array'), Base+'Record') for l in lst]) getattr(self, Base + 'Coverage').glyphs = BaseCoverageGlyphs # MarkArray records = [] for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): allClasses = [r.Class for r in glyphRecords if r is not None] # TODO Right now we require that all marks have same class in # all masters that cover them. This is not required. # # We can relax that by just requiring that all marks that have # the same class in a master, have the same class in every other # master. Indeed, if, say, a sparse master only covers one mark, # that mark probably will get class 0, which would possibly be # different from its class in other masters. # # We can even go further and reclassify marks to support any # input. But, since, it's unlikely that two marks being both, # say, "top" in one master, and one being "top" and other being # "top-right" in another master, we shouldn't do that, as any # failures in that case will probably signify mistakes in the # input masters. assert allEqual(allClasses), allClasses if not allClasses: rec = None else: rec = ot.MarkRecord() rec.Class = allClasses[0] allAnchors = [ None if r is None else r.MarkAnchor for r in glyphRecords ] if allNone(allAnchors): anchor = None else: anchor = ot.Anchor() anchor.Format = 1 merger.mergeThings(anchor, allAnchors) rec.MarkAnchor = anchor records.append(rec) array = ot.MarkArray() array.MarkRecord = records array.MarkCount = len(records) setattr(self, Mark + "Array", array) # BaseArray records = [] for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): if allNone(glyphRecords): rec = None else: rec = getattr(ot, Base + 'Record')() anchors = [] setattr(rec, Base + 'Anchor', anchors) glyphAnchors = [[] if r is None else getattr(r, Base + 'Anchor') for r in glyphRecords] for l in glyphAnchors: l.extend([None] * (self.ClassCount - len(l))) for allAnchors in zip(*glyphAnchors): if allNone(allAnchors): anchor = None else: anchor = ot.Anchor() anchor.Format = 1 merger.mergeThings(anchor, allAnchors) anchors.append(anchor) records.append(rec) array = getattr(ot, Base + 'Array')() setattr(array, Base + 'Record', records) setattr(array, Base + 'Count', len(records)) setattr(self, Base + 'Array', array)
def buildVarDevTable(store_builder, master_values): if allEqual(master_values): return master_values[0], None base, varIdx = store_builder.storeMasters(master_values) return base, builder.buildVarDevTable(varIdx)
def _get_advance_metrics(font, masterModel, master_ttfs, axisTags, glyphOrder, advMetricses, vOrigMetricses=None): vhAdvanceDeltasAndSupports = {} vOrigDeltasAndSupports = {} for glyph in glyphOrder: vhAdvances = [metrics[glyph][0] if glyph in metrics else None for metrics in advMetricses] vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(vhAdvances) singleModel = models.allEqual(id(v[1]) for v in vhAdvanceDeltasAndSupports.values()) if vOrigMetricses: singleModel = False for glyph in glyphOrder: # We need to supply a vOrigs tuple with non-None default values # for each glyph. vOrigMetricses contains values only for those # glyphs which have a non-default vOrig. vOrigs = [metrics[glyph] if glyph in metrics else defaultVOrig for metrics, defaultVOrig in vOrigMetricses] vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(vOrigs) directStore = None if singleModel: # Build direct mapping supports = next(iter(vhAdvanceDeltasAndSupports.values()))[1][1:] varTupleList = builder.buildVarRegionList(supports, axisTags) varTupleIndexes = list(range(len(supports))) varData = builder.buildVarData(varTupleIndexes, [], optimize=False) for glyphName in glyphOrder: varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0]) varData.optimize() directStore = builder.buildVarStore(varTupleList, [varData]) # Build optimized indirect mapping storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) advMapping = {} for glyphName in glyphOrder: deltas, supports = vhAdvanceDeltasAndSupports[glyphName] storeBuilder.setSupports(supports) advMapping[glyphName] = storeBuilder.storeDeltas(deltas) if vOrigMetricses: vOrigMap = {} for glyphName in glyphOrder: deltas, supports = vOrigDeltasAndSupports[glyphName] storeBuilder.setSupports(supports) vOrigMap[glyphName] = storeBuilder.storeDeltas(deltas) indirectStore = storeBuilder.finish() mapping2 = indirectStore.optimize() advMapping = [mapping2[advMapping[g]] for g in glyphOrder] advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder) if vOrigMetricses: vOrigMap = [mapping2[vOrigMap[g]] for g in glyphOrder] useDirect = False vOrigMapping = None if directStore: # Compile both, see which is more compact writer = OTTableWriter() directStore.compile(writer, font) directSize = len(writer.getAllData()) writer = OTTableWriter() indirectStore.compile(writer, font) advanceMapping.compile(writer, font) indirectSize = len(writer.getAllData()) useDirect = directSize < indirectSize if useDirect: metricsStore = directStore advanceMapping = None else: metricsStore = indirectStore if vOrigMetricses: vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder) return metricsStore, advanceMapping, vOrigMapping
def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model): if hasattr(region_top_dicts[0], 'FDArray'): regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts] else: regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts] for fd_index, font_dict in enumerate(topDict.FDArray): private_dict = font_dict.Private pds = [private_dict] + [ regionFDArray[fd_index].Private for regionFDArray in regionFDArrays ] for key, value in private_dict.rawDict.items(): if key not in pd_blend_fields: continue if isinstance(value, list): try: values = [pd.rawDict[key] for pd in pds] except KeyError: del private_dict.rawDict[key] print( b"Warning: {key} in default font Private dict is " b"missing from another font, and was " b"discarded.".format(key=key)) continue try: values = zip(*values) except IndexError: raise MergeDictError(key, value, values) """ Row 0 contains the first value from each master. Convert each row from absolute values to relative values from the previous row. e.g for three masters, a list of values was: master 0 OtherBlues = [-217,-205] master 1 OtherBlues = [-234,-222] master 1 OtherBlues = [-188,-176] The call to zip() converts this to: [(-217, -234, -188), (-205, -222, -176)] and is converted finally to: OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] """ dataList = [] prev_val_list = [0] * num_masters any_points_differ = False for val_list in values: rel_list = [(val - prev_val_list[i]) for ( i, val) in enumerate(val_list)] if (not any_points_differ) and not allEqual(rel_list): any_points_differ = True prev_val_list = val_list deltas = var_model.getDeltas(rel_list) # Convert numbers with no decimal part to an int. deltas = [conv_to_int(delta) for delta in deltas] # For PrivateDict BlueValues, the default font # values are absolute, not relative to the prior value. deltas[0] = val_list[0] dataList.append(deltas) # If there are no blend values,then # we can collapse the blend lists. if not any_points_differ: dataList = [data[0] for data in dataList] else: values = [pd.rawDict[key] for pd in pds] if not allEqual(values): dataList = var_model.getDeltas(values) else: dataList = values[0] private_dict.rawDict[key] = dataList
def prepareVariableComponentData(vcFont, axisTags, globalAxisNames, neutralOnly=False): storeBuilder = OnlineVarStoreBuilder(axisTags) vcData = {} for glyphName in sorted(vcFont.keys()): glyph = vcFont[glyphName] axisTags = { axisTag for v in glyph.variations for axisTag in v.location } if neutralOnly and not axisTags - globalAxisNames: masters = [glyph] else: masters = [glyph] + glyph.variations if not glyph.outline.isEmpty() and glyph.components: # This glyph mixes outlines and classic components, it will have been # flattened upon TTF compilation continue locations = [m.location for m in masters] storeBuilder.setModel(VariationModel(locations)) components = [] for i in range(len(glyph.components)): assert allEqual([m.components[i].name for m in masters]) baseName = masters[0].components[i].name coords = [dict(m.components[i].coord) for m in masters] sanitizeCoords(coords, vcFont[baseName]) transforms = [m.components[i].transform for m in masters] for t in transforms[1:]: assert t.keys() == transforms[0].keys() coordMasterValues = { k: [coord[k] for coord in coords] for k in coords[0].keys() } transformMasterValues = { k: [transform[k] for transform in transforms] for k in transforms[0].keys() } coord = compileMasterValuesDict(storeBuilder, coordMasterValues, 14) # 2.14 transform = compileMasterValuesDict(storeBuilder, transformMasterValues, 16) # 16.16 components.append((baseName, coord, transform)) if components: vcData[glyphName] = components varStore = storeBuilder.finish() mapping = varStore.optimize() assert 0xFFFFFFFF not in mapping mapping[0xFFFFFFFF] = 0xFFFFFFFF for glyphName, components in vcData.items(): for baseName, coord, transform in components: remapValuesDict(coord, mapping) remapValuesDict(transform, mapping) return vcData, varStore
def _add_MVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating MVAR") store_builder = varStore.OnlineVarStoreBuilder(axisTags) records = [] lastTableTag = None fontTable = None tables = None for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]): if tableTag != lastTableTag: tables = fontTable = None if tableTag in font: fontTable = font[tableTag] tables = [ master[tableTag] if tableTag in master else None for master in master_ttfs ] lastTableTag = tableTag if tables is None: continue # TODO support gasp entries model, tables = masterModel.getSubModel(tables) store_builder.setModel(model) master_values = [getattr(table, itemName) for table in tables] if models.allEqual(master_values): base, varIdx = master_values[0], None else: base, varIdx = store_builder.storeMasters(master_values) setattr(fontTable, itemName, base) if varIdx is None: continue log.info(' %s: %s.%s %s', tag, tableTag, itemName, master_values) rec = ot.MetricsValueRecord() rec.ValueTag = tag rec.VarIdx = varIdx records.append(rec) assert "MVAR" not in font if records: store = store_builder.finish() # Optimize mapping = store.optimize() for rec in records: rec.VarIdx = mapping[rec.VarIdx] MVAR = font["MVAR"] = newTable('MVAR') mvar = MVAR.table = ot.MVAR() mvar.Version = 0x00010000 mvar.Reserved = 0 mvar.VarStore = store # XXX these should not be hard-coded but computed automatically mvar.ValueRecordSize = 8 mvar.ValueRecordCount = len(records) mvar.ValueRecord = sorted(records, key=lambda r: r.ValueTag)
def _merge_TTHinting(font, masterModel, master_ttfs, tolerance=0.5): log.info("Merging TT hinting") assert "cvar" not in font # Check that the existing hinting is compatible # fpgm and prep table for tag in ("fpgm", "prep"): all_pgms = [m[tag].program for m in master_ttfs if tag in m] if len(all_pgms) == 0: continue if tag in font: font_pgm = font[tag].program else: font_pgm = Program() if any(pgm != font_pgm for pgm in all_pgms): log.warning("Masters have incompatible %s tables, hinting is discarded." % tag) _remove_TTHinting(font) return # glyf table for name, glyph in font["glyf"].glyphs.items(): all_pgms = [ m["glyf"][name].program for m in master_ttfs if name in m['glyf'] and hasattr(m["glyf"][name], "program") ] if not any(all_pgms): continue glyph.expand(font["glyf"]) if hasattr(glyph, "program"): font_pgm = glyph.program else: font_pgm = Program() if any(pgm != font_pgm for pgm in all_pgms if pgm): log.warning("Masters have incompatible glyph programs in glyph '%s', hinting is discarded." % name) # TODO Only drop hinting from this glyph. _remove_TTHinting(font) return # cvt table all_cvs = [Vector(m["cvt "].values) if 'cvt ' in m else None for m in master_ttfs] nonNone_cvs = models.nonNone(all_cvs) if not nonNone_cvs: # There is no cvt table to make a cvar table from, we're done here. return if not models.allEqual(len(c) for c in nonNone_cvs): log.warning("Masters have incompatible cvt tables, hinting is discarded.") _remove_TTHinting(font) return # We can build the cvar table now. cvar = font["cvar"] = newTable('cvar') cvar.version = 1 cvar.variations = [] deltas, supports = masterModel.getDeltasAndSupports(all_cvs) for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): delta = [otRound(d) for d in delta] if all(abs(v) <= tolerance for v in delta): continue var = TupleVariation(support, delta) cvar.variations.append(var)
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) """ 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) # 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 _add_MVAR(font, masterModel, master_ttfs, axisTags): log.info("Generating MVAR") store_builder = varStore.OnlineVarStoreBuilder(axisTags) records = [] lastTableTag = None fontTable = None tables = None # HACK: we need to special-case post.underlineThickness and .underlinePosition # and unilaterally/arbitrarily define a sentinel value to distinguish the case # when a post table is present in a given master simply because that's where # the glyph names in TrueType must be stored, but the underline values are not # meant to be used for building MVAR's deltas. The value of -0x8000 (-36768) # the minimum FWord (int16) value, was chosen for its unlikelyhood to appear # in real-world underline position/thickness values. specialTags = {"unds": -0x8000, "undo": -0x8000} for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]): # For each tag, fetch the associated table from all fonts (or not when we are # still looking at a tag from the same tables) and set up the variation model # for them. if tableTag != lastTableTag: tables = fontTable = None if tableTag in font: fontTable = font[tableTag] tables = [] for master in master_ttfs: if tableTag not in master or ( tag in specialTags and getattr(master[tableTag], itemName) == specialTags[tag] ): tables.append(None) else: tables.append(master[tableTag]) model, tables = masterModel.getSubModel(tables) store_builder.setModel(model) lastTableTag = tableTag if tables is None: # Tag not applicable to the master font. continue # TODO support gasp entries master_values = [getattr(table, itemName) for table in tables] if models.allEqual(master_values): base, varIdx = master_values[0], None else: base, varIdx = store_builder.storeMasters(master_values) setattr(fontTable, itemName, base) if varIdx is None: continue log.info(' %s: %s.%s %s', tag, tableTag, itemName, master_values) rec = ot.MetricsValueRecord() rec.ValueTag = tag rec.VarIdx = varIdx records.append(rec) assert "MVAR" not in font if records: store = store_builder.finish() # Optimize mapping = store.optimize() for rec in records: rec.VarIdx = mapping[rec.VarIdx] MVAR = font["MVAR"] = newTable('MVAR') mvar = MVAR.table = ot.MVAR() mvar.Version = 0x00010000 mvar.Reserved = 0 mvar.VarStore = store # XXX these should not be hard-coded but computed automatically mvar.ValueRecordSize = 8 mvar.ValueRecordCount = len(records) mvar.ValueRecord = sorted(records, key=lambda r: r.ValueTag)
def merge(merger, self, lst): subtables = merger.lookup_subtables = [l.SubTable for l in lst] # Remove Extension subtables for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]: if not sts: continue if sts[0].__class__.__name__.startswith('Extension'): if not allEqual([st.__class__ for st in sts]): raise InconsistentExtensions( merger, expected="Extension", got=[st.__class__.__name__ for st in sts]) if not allEqual([st.ExtensionLookupType for st in sts]): raise InconsistentExtensions(merger) l.LookupType = sts[0].ExtensionLookupType new_sts = [st.ExtSubTable for st in sts] del sts[:] sts.extend(new_sts) isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) if isPairPos: # AFDKO and feaLib sometimes generate two Format1 subtables instead of one. # Merge those before continuing. # https://github.com/fonttools/fonttools/issues/719 self.SubTable = _Lookup_PairPos_subtables_canonicalize( self.SubTable, merger.font) subtables = merger.lookup_subtables = [ _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables ] else: isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos) if isSinglePos: numSubtables = [len(st) for st in subtables] if not all([nums == numSubtables[0] for nums in numSubtables]): # Flatten list of SinglePos subtables to single Format 2 subtable, # with all value records set to the rec format type. # We use buildSinglePos() to optimize the lookup after merging. valueFormatList = [ t.ValueFormat for st in subtables for t in st ] # Find the minimum value record that can accomodate all the singlePos subtables. mirf = reduce(ior, valueFormatList) self.SubTable = _Lookup_SinglePos_subtables_flatten( self.SubTable, merger.font, mirf) subtables = merger.lookup_subtables = [ _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf) for st in subtables ] flattened = True else: flattened = False merger.mergeLists(self.SubTable, subtables) self.SubTableCount = len(self.SubTable) if isPairPos: # If format-1 subtable created during canonicalization is empty, remove it. assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 if not self.SubTable[0].Coverage.glyphs: self.SubTable.pop(0) self.SubTableCount -= 1 # If format-2 subtable created during canonicalization is empty, remove it. assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 if not self.SubTable[-1].Coverage.glyphs: self.SubTable.pop(-1) self.SubTableCount -= 1 elif isSinglePos and flattened: singlePosTable = self.SubTable[0] glyphs = singlePosTable.Coverage.glyphs # We know that singlePosTable is Format 2, as this is set # in _Lookup_SinglePos_subtables_flatten. singlePosMapping = { gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value) } self.SubTable = buildSinglePos(singlePosMapping, merger.font.getReverseGlyphMap()) merger.mergeObjects(self, lst, exclude=['SubTable', 'SubTableCount']) del merger.lookup_subtables