from fontTools.misc.roundTools import noRound, otRound from fontTools.misc.intTools import bit_count from fontTools.misc.vector import Vector from fontTools.ttLib.tables import otTables as ot from fontTools.varLib.models import supportScalar import fontTools.varLib.varStore # For monkey-patching from fontTools.varLib.builder import ( buildVarRegionList, buildSparseVarRegionList, buildSparseVarRegion, buildMultiVarStore, buildMultiVarData, ) from fontTools.misc.iterTools import batched from functools import partial from collections import defaultdict from heapq import heappush, heappop NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX ot.MultiVarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX def _getLocationKey(loc): return tuple(sorted(loc.items(), key=lambda kv: kv[0])) class OnlineMultiVarStoreBuilder(object): def __init__(self, axisTags): self._axisTags = axisTags self._regionMap = {} self._regionList = buildSparseVarRegionList([], axisTags) self._store = buildMultiVarStore(self._regionList, []) self._data = None self._model = None self._supports = None self._varDataIndices = {} self._varDataCaches = {} self._cache = None def setModel(self, model): self.setSupports(model.supports) self._model = model def setSupports(self, supports): self._model = None self._supports = list(supports) if not self._supports[0]: del self._supports[0] # Drop base master support self._cache = None self._data = None def finish(self, optimize=True): self._regionList.RegionCount = len(self._regionList.Region) self._store.MultiVarDataCount = len(self._store.MultiVarData) return self._store def _add_MultiVarData(self): regionMap = self._regionMap regionList = self._regionList regions = self._supports regionIndices = [] for region in regions: key = _getLocationKey(region) idx = regionMap.get(key) if idx is None: varRegion = buildSparseVarRegion(region, self._axisTags) idx = regionMap[key] = len(regionList.Region) regionList.Region.append(varRegion) regionIndices.append(idx) # Check if we have one already... key = tuple(regionIndices) varDataIdx = self._varDataIndices.get(key) if varDataIdx is not None: self._outer = varDataIdx self._data = self._store.MultiVarData[varDataIdx] self._cache = self._varDataCaches[key] if len(self._data.Item) == 0xFFFF: # This is full. Need new one. varDataIdx = None if varDataIdx is None: self._data = buildMultiVarData(regionIndices, []) self._outer = len(self._store.MultiVarData) self._store.MultiVarData.append(self._data) self._varDataIndices[key] = self._outer if key not in self._varDataCaches: self._varDataCaches[key] = {} self._cache = self._varDataCaches[key] def storeMasters(self, master_values, *, round=round): deltas = self._model.getDeltas(master_values, round=round) base = deltas.pop(0) return base, self.storeDeltas(deltas, round=noRound) def storeDeltas(self, deltas, *, round=round): deltas = tuple(round(d) for d in deltas) if not any(deltas): return NO_VARIATION_INDEX deltas_tuple = tuple(tuple(d) for d in deltas) if not self._data: self._add_MultiVarData() varIdx = self._cache.get(deltas_tuple) if varIdx is not None: return varIdx inner = len(self._data.Item) if inner == 0xFFFF: # Full array. Start new one. self._add_MultiVarData() return self.storeDeltas(deltas, round=noRound) self._data.addItem(deltas, round=noRound) varIdx = (self._outer << 16) + inner self._cache[deltas_tuple] = varIdx return varIdx def MultiVarData_addItem(self, deltas, *, round=round): deltas = tuple(round(d) for d in deltas) assert len(deltas) == self.VarRegionCount values = [] for d in deltas: values.extend(d) self.Item.append(values) self.ItemCount = len(self.Item) ot.MultiVarData.addItem = MultiVarData_addItem def SparseVarRegion_get_support(self, fvar_axes): return { fvar_axes[reg.AxisIndex].axisTag: (reg.StartCoord, reg.PeakCoord, reg.EndCoord) for reg in self.SparseVarRegionAxis } ot.SparseVarRegion.get_support = SparseVarRegion_get_support def MultiVarStore___bool__(self): return bool(self.MultiVarData) ot.MultiVarStore.__bool__ = MultiVarStore___bool__ class MultiVarStoreInstancer(object): def __init__(self, multivarstore, fvar_axes, location={}): self.fvar_axes = fvar_axes assert multivarstore is None or multivarstore.Format == 1 self._varData = multivarstore.MultiVarData if multivarstore else [] self._regions = ( multivarstore.SparseVarRegionList.Region if multivarstore else [] ) self.setLocation(location) def setLocation(self, location): self.location = dict(location) self._clearCaches() def _clearCaches(self): self._scalars = {} def _getScalar(self, regionIdx): scalar = self._scalars.get(regionIdx) if scalar is None: support = self._regions[regionIdx].get_support(self.fvar_axes) scalar = supportScalar(self.location, support) self._scalars[regionIdx] = scalar return scalar @staticmethod def interpolateFromDeltasAndScalars(deltas, scalars): if not deltas: return Vector([]) assert len(deltas) % len(scalars) == 0, (len(deltas), len(scalars)) m = len(deltas) // len(scalars) delta = Vector([0] * m) for d, s in zip(batched(deltas, m), scalars): if not s: continue delta += Vector(d) * s return delta def __getitem__(self, varidx): major, minor = varidx >> 16, varidx & 0xFFFF if varidx == NO_VARIATION_INDEX: return Vector([]) varData = self._varData scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex] deltas = varData[major].Item[minor] return self.interpolateFromDeltasAndScalars(deltas, scalars) def interpolateFromDeltas(self, varDataIndex, deltas): varData = self._varData scalars = [self._getScalar(ri) for ri in varData[varDataIndex].VarRegionIndex] return self.interpolateFromDeltasAndScalars(deltas, scalars) def MultiVarStore_subset_varidxes(self, varIdxes): return ot.VarStore.subset_varidxes(self, varIdxes, VarData="MultiVarData") def MultiVarStore_prune_regions(self): return ot.VarStore.prune_regions( self, VarData="MultiVarData", VarRegionList="SparseVarRegionList" ) ot.MultiVarStore.prune_regions = MultiVarStore_prune_regions ot.MultiVarStore.subset_varidxes = MultiVarStore_subset_varidxes def MultiVarStore_get_supports(self, major, fvarAxes): supports = [] varData = self.MultiVarData[major] for regionIdx in varData.VarRegionIndex: region = self.SparseVarRegionList.Region[regionIdx] support = region.get_support(fvarAxes) supports.append(support) return supports ot.MultiVarStore.get_supports = MultiVarStore_get_supports def VARC_collect_varidxes(self, varidxes): for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: for component in glyph.components: varidxes.add(component.axisValuesVarIndex) varidxes.add(component.transformVarIndex) def VARC_remap_varidxes(self, varidxes_map): for glyph in self.VarCompositeGlyphs.VarCompositeGlyph: for component in glyph.components: component.axisValuesVarIndex = varidxes_map[component.axisValuesVarIndex] component.transformVarIndex = varidxes_map[component.transformVarIndex] ot.VARC.collect_varidxes = VARC_collect_varidxes ot.VARC.remap_varidxes = VARC_remap_varidxes