Home | History | Annotate | Download | only in varLib
      1 from __future__ import print_function, division, absolute_import
      2 from fontTools.misc.py23 import *
      3 from fontTools.misc.fixedTools import otRound
      4 from fontTools.ttLib.tables import otTables as ot
      5 from fontTools.varLib.models import supportScalar
      6 from fontTools.varLib.builder import (buildVarRegionList, buildVarStore,
      7 				      buildVarRegion, buildVarData)
      8 from functools import partial
      9 from collections import defaultdict
     10 from array import array
     11 
     12 
     13 def _getLocationKey(loc):
     14 	return tuple(sorted(loc.items(), key=lambda kv: kv[0]))
     15 
     16 
     17 class OnlineVarStoreBuilder(object):
     18 
     19 	def __init__(self, axisTags):
     20 		self._axisTags = axisTags
     21 		self._regionMap = {}
     22 		self._regionList = buildVarRegionList([], axisTags)
     23 		self._store = buildVarStore(self._regionList, [])
     24 		self._data = None
     25 		self._model = None
     26 		self._supports = None
     27 		self._varDataIndices = {}
     28 		self._varDataCaches = {}
     29 		self._cache = {}
     30 
     31 	def setModel(self, model):
     32 		self.setSupports(model.supports)
     33 		self._model = model
     34 
     35 	def setSupports(self, supports):
     36 		self._model = None
     37 		self._supports = list(supports)
     38 		if not self._supports[0]:
     39 			del self._supports[0] # Drop base master support
     40 		self._cache = {}
     41 		self._data = None
     42 
     43 	def finish(self, optimize=True):
     44 		self._regionList.RegionCount = len(self._regionList.Region)
     45 		self._store.VarDataCount = len(self._store.VarData)
     46 		for data in self._store.VarData:
     47 			data.ItemCount = len(data.Item)
     48 			data.calculateNumShorts(optimize=optimize)
     49 		return self._store
     50 
     51 	def _add_VarData(self):
     52 		regionMap = self._regionMap
     53 		regionList = self._regionList
     54 
     55 		regions = self._supports
     56 		regionIndices = []
     57 		for region in regions:
     58 			key = _getLocationKey(region)
     59 			idx = regionMap.get(key)
     60 			if idx is None:
     61 				varRegion = buildVarRegion(region, self._axisTags)
     62 				idx = regionMap[key] = len(regionList.Region)
     63 				regionList.Region.append(varRegion)
     64 			regionIndices.append(idx)
     65 
     66 		# Check if we have one already...
     67 		key = tuple(regionIndices)
     68 		varDataIdx = self._varDataIndices.get(key)
     69 		if varDataIdx is not None:
     70 			self._outer = varDataIdx
     71 			self._data = self._store.VarData[varDataIdx]
     72 			self._cache = self._varDataCaches[key]
     73 			if len(self._data.Item) == 0xFFF:
     74 				# This is full.  Need new one.
     75 				varDataIdx = None
     76 
     77 		if varDataIdx is None:
     78 			self._data = buildVarData(regionIndices, [], optimize=False)
     79 			self._outer = len(self._store.VarData)
     80 			self._store.VarData.append(self._data)
     81 			self._varDataIndices[key] = self._outer
     82 			if key not in self._varDataCaches:
     83 				self._varDataCaches[key] = {}
     84 			self._cache = self._varDataCaches[key]
     85 
     86 
     87 	def storeMasters(self, master_values):
     88 		deltas = self._model.getDeltas(master_values)
     89 		base = otRound(deltas.pop(0))
     90 		return base, self.storeDeltas(deltas)
     91 
     92 	def storeDeltas(self, deltas):
     93 		# Pity that this exists here, since VarData_addItem
     94 		# does the same.  But to look into our cache, it's
     95 		# good to adjust deltas here as well...
     96 		deltas = [otRound(d) for d in deltas]
     97 		if len(deltas) == len(self._supports) + 1:
     98 			deltas = tuple(deltas[1:])
     99 		else:
    100 			assert len(deltas) == len(self._supports)
    101 			deltas = tuple(deltas)
    102 
    103 		varIdx = self._cache.get(deltas)
    104 		if varIdx is not None:
    105 			return varIdx
    106 
    107 		if not self._data:
    108 			self._add_VarData()
    109 		inner = len(self._data.Item)
    110 		if inner == 0xFFFF:
    111 			# Full array. Start new one.
    112 			self._add_VarData()
    113 			return self.storeDeltas(deltas)
    114 		self._data.addItem(deltas)
    115 
    116 		varIdx = (self._outer << 16) + inner
    117 		self._cache[deltas] = varIdx
    118 		return varIdx
    119 
    120 def VarData_addItem(self, deltas):
    121 	deltas = [otRound(d) for d in deltas]
    122 
    123 	countUs = self.VarRegionCount
    124 	countThem = len(deltas)
    125 	if countUs + 1 == countThem:
    126 		deltas = tuple(deltas[1:])
    127 	else:
    128 		assert countUs == countThem, (countUs, countThem)
    129 		deltas = tuple(deltas)
    130 	self.Item.append(list(deltas))
    131 	self.ItemCount = len(self.Item)
    132 
    133 ot.VarData.addItem = VarData_addItem
    134 
    135 def VarRegion_get_support(self, fvar_axes):
    136 	return {fvar_axes[i].axisTag: (reg.StartCoord,reg.PeakCoord,reg.EndCoord)
    137 		for i,reg in enumerate(self.VarRegionAxis)}
    138 
    139 ot.VarRegion.get_support = VarRegion_get_support
    140 
    141 class VarStoreInstancer(object):
    142 
    143 	def __init__(self, varstore, fvar_axes, location={}):
    144 		self.fvar_axes = fvar_axes
    145 		assert varstore is None or varstore.Format == 1
    146 		self._varData = varstore.VarData if varstore else []
    147 		self._regions = varstore.VarRegionList.Region if varstore else []
    148 		self.setLocation(location)
    149 
    150 	def setLocation(self, location):
    151 		self.location = dict(location)
    152 		self._clearCaches()
    153 
    154 	def _clearCaches(self):
    155 		self._scalars = {}
    156 
    157 	def _getScalar(self, regionIdx):
    158 		scalar = self._scalars.get(regionIdx)
    159 		if scalar is None:
    160 			support = self._regions[regionIdx].get_support(self.fvar_axes)
    161 			scalar = supportScalar(self.location, support)
    162 			self._scalars[regionIdx] = scalar
    163 		return scalar
    164 
    165 	@staticmethod
    166 	def interpolateFromDeltasAndScalars(deltas, scalars):
    167 		delta = 0.
    168 		for d,s in zip(deltas, scalars):
    169 			if not s: continue
    170 			delta += d * s
    171 		return delta
    172 
    173 	def __getitem__(self, varidx):
    174 		major, minor = varidx >> 16, varidx & 0xFFFF
    175 		varData = self._varData
    176 		scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
    177 		deltas = varData[major].Item[minor]
    178 		return self.interpolateFromDeltasAndScalars(deltas, scalars)
    179 
    180 	def interpolateFromDeltas(self, varDataIndex, deltas):
    181 		varData = self._varData
    182 		scalars = [self._getScalar(ri) for ri in
    183 					varData[varDataIndex].VarRegionIndex]
    184 		return self.interpolateFromDeltasAndScalars(deltas, scalars)
    185 
    186 
    187 #
    188 # Optimizations
    189 #
    190 
    191 def VarStore_subset_varidxes(self, varIdxes, optimize=True):
    192 
    193 	# Sort out used varIdxes by major/minor.
    194 	used = {}
    195 	for varIdx in varIdxes:
    196 		major = varIdx >> 16
    197 		minor = varIdx & 0xFFFF
    198 		d = used.get(major)
    199 		if d is None:
    200 			d = used[major] = set()
    201 		d.add(minor)
    202 	del varIdxes
    203 
    204 	#
    205 	# Subset VarData
    206 	#
    207 
    208 	varData = self.VarData
    209 	newVarData = []
    210 	varDataMap = {}
    211 	for major,data in enumerate(varData):
    212 		usedMinors = used.get(major)
    213 		if usedMinors is None:
    214 			continue
    215 		newMajor = len(newVarData)
    216 		newVarData.append(data)
    217 
    218 		items = data.Item
    219 		newItems = []
    220 		for minor in sorted(usedMinors):
    221 			newMinor = len(newItems)
    222 			newItems.append(items[minor])
    223 			varDataMap[(major<<16)+minor] = (newMajor<<16)+newMinor
    224 
    225 		data.Item = newItems
    226 		data.ItemCount = len(data.Item)
    227 
    228 		data.calculateNumShorts(optimize=optimize)
    229 
    230 	self.VarData = newVarData
    231 	self.VarDataCount = len(self.VarData)
    232 
    233 	self.prune_regions()
    234 
    235 	return varDataMap
    236 
    237 ot.VarStore.subset_varidxes = VarStore_subset_varidxes
    238 
    239 def VarStore_prune_regions(self):
    240 	"""Remove unused VarRegions."""
    241 	#
    242 	# Subset VarRegionList
    243 	#
    244 
    245 	# Collect.
    246 	usedRegions = set()
    247 	for data in self.VarData:
    248 		usedRegions.update(data.VarRegionIndex)
    249 	# Subset.
    250 	regionList = self.VarRegionList
    251 	regions = regionList.Region
    252 	newRegions = []
    253 	regionMap = {}
    254 	for i in sorted(usedRegions):
    255 		regionMap[i] = len(newRegions)
    256 		newRegions.append(regions[i])
    257 	regionList.Region = newRegions
    258 	regionList.RegionCount = len(regionList.Region)
    259 	# Map.
    260 	for data in self.VarData:
    261 		data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex]
    262 
    263 ot.VarStore.prune_regions = VarStore_prune_regions
    264 
    265 
    266 def _visit(self, func):
    267 	"""Recurse down from self, if type of an object is ot.Device,
    268 	call func() on it.  Works on otData-style classes."""
    269 
    270 	if type(self) == ot.Device:
    271 		func(self)
    272 
    273 	elif isinstance(self, list):
    274 		for that in self:
    275 			_visit(that, func)
    276 
    277 	elif hasattr(self, 'getConverters') and not hasattr(self, 'postRead'):
    278 		for conv in self.getConverters():
    279 			that = getattr(self, conv.name, None)
    280 			if that is not None:
    281 				_visit(that, func)
    282 
    283 	elif isinstance(self, ot.ValueRecord):
    284 		for that in self.__dict__.values():
    285 			_visit(that, func)
    286 
    287 def _Device_recordVarIdx(self, s):
    288 	"""Add VarIdx in this Device table (if any) to the set s."""
    289 	if self.DeltaFormat == 0x8000:
    290 		s.add((self.StartSize<<16)+self.EndSize)
    291 
    292 def Object_collect_device_varidxes(self, varidxes):
    293 	adder = partial(_Device_recordVarIdx, s=varidxes)
    294 	_visit(self, adder)
    295 
    296 ot.GDEF.collect_device_varidxes = Object_collect_device_varidxes
    297 ot.GPOS.collect_device_varidxes = Object_collect_device_varidxes
    298 
    299 def _Device_mapVarIdx(self, mapping, done):
    300 	"""Map VarIdx in this Device table (if any) through mapping."""
    301 	if id(self) in done:
    302 		return
    303 	done.add(id(self))
    304 	if self.DeltaFormat == 0x8000:
    305 		varIdx = mapping[(self.StartSize<<16)+self.EndSize]
    306 		self.StartSize = varIdx >> 16
    307 		self.EndSize = varIdx & 0xFFFF
    308 
    309 def Object_remap_device_varidxes(self, varidxes_map):
    310 	mapper = partial(_Device_mapVarIdx, mapping=varidxes_map, done=set())
    311 	_visit(self, mapper)
    312 
    313 ot.GDEF.remap_device_varidxes = Object_remap_device_varidxes
    314 ot.GPOS.remap_device_varidxes = Object_remap_device_varidxes
    315 
    316 
    317 class _Encoding(object):
    318 
    319 	def __init__(self, chars):
    320 		self.chars = chars
    321 		self.width = self._popcount(chars)
    322 		self.overhead = self._characteristic_overhead(chars)
    323 		self.items = set()
    324 
    325 	def append(self, row):
    326 		self.items.add(row)
    327 
    328 	def extend(self, lst):
    329 		self.items.update(lst)
    330 
    331 	def get_room(self):
    332 		"""Maximum number of bytes that can be added to characteristic
    333 		while still being beneficial to merge it into another one."""
    334 		count = len(self.items)
    335 		return max(0, (self.overhead - 1) // count - self.width)
    336 	room = property(get_room)
    337 
    338 	@property
    339 	def gain(self):
    340 		"""Maximum possible byte gain from merging this into another
    341 		characteristic."""
    342 		count = len(self.items)
    343 		return max(0, self.overhead - count * (self.width + 1))
    344 
    345 	def sort_key(self):
    346 		return self.width, self.chars
    347 
    348 	def __len__(self):
    349 		return len(self.items)
    350 
    351 	def can_encode(self, chars):
    352 		return not (chars & ~self.chars)
    353 
    354 	def __sub__(self, other):
    355 		return self._popcount(self.chars & ~other.chars)
    356 
    357 	@staticmethod
    358 	def _popcount(n):
    359 		# Apparently this is the fastest native way to do it...
    360 		# https://stackoverflow.com/a/9831671
    361 		return bin(n).count('1')
    362 
    363 	@staticmethod
    364 	def _characteristic_overhead(chars):
    365 		"""Returns overhead in bytes of encoding this characteristic
    366 		as a VarData."""
    367 		c = 6
    368 		while chars:
    369 			if chars & 3:
    370 				c += 2
    371 			chars >>= 2
    372 		return c
    373 
    374 
    375 	def _find_yourself_best_new_encoding(self, done_by_width):
    376 		self.best_new_encoding = None
    377 		for new_width in range(self.width+1, self.width+self.room+1):
    378 			for new_encoding in done_by_width[new_width]:
    379 				if new_encoding.can_encode(self.chars):
    380 					break
    381 			else:
    382 				new_encoding = None
    383 			self.best_new_encoding = new_encoding
    384 
    385 
    386 class _EncodingDict(dict):
    387 
    388 	def __missing__(self, chars):
    389 		r = self[chars] = _Encoding(chars)
    390 		return r
    391 
    392 	def add_row(self, row):
    393 		chars = self._row_characteristics(row)
    394 		self[chars].append(row)
    395 
    396 	@staticmethod
    397 	def _row_characteristics(row):
    398 		"""Returns encoding characteristics for a row."""
    399 		chars = 0
    400 		i = 1
    401 		for v in row:
    402 			if v:
    403 				chars += i
    404 			if not (-128 <= v <= 127):
    405 				chars += i * 2
    406 			i <<= 2
    407 		return chars
    408 
    409 
    410 def VarStore_optimize(self):
    411 	"""Optimize storage. Returns mapping from old VarIdxes to new ones."""
    412 
    413 	# TODO
    414 	# Check that no two VarRegions are the same; if they are, fold them.
    415 
    416 	n = len(self.VarRegionList.Region) # Number of columns
    417 	zeroes = array('h', [0]*n)
    418 
    419 	front_mapping = {} # Map from old VarIdxes to full row tuples
    420 
    421 	encodings = _EncodingDict()
    422 
    423 	# Collect all items into a set of full rows (with lots of zeroes.)
    424 	for major,data in enumerate(self.VarData):
    425 		regionIndices = data.VarRegionIndex
    426 
    427 		for minor,item in enumerate(data.Item):
    428 
    429 			row = array('h', zeroes)
    430 			for regionIdx,v in zip(regionIndices, item):
    431 				row[regionIdx] += v
    432 			row = tuple(row)
    433 
    434 			encodings.add_row(row)
    435 			front_mapping[(major<<16)+minor] = row
    436 
    437 	# Separate encodings that have no gain (are decided) and those having
    438 	# possible gain (possibly to be merged into others.)
    439 	encodings = sorted(encodings.values(), key=_Encoding.__len__, reverse=True)
    440 	done_by_width = defaultdict(list)
    441 	todo = []
    442 	for encoding in encodings:
    443 		if not encoding.gain:
    444 			done_by_width[encoding.width].append(encoding)
    445 		else:
    446 			todo.append(encoding)
    447 
    448 	# For each encoding that is possibly to be merged, find the best match
    449 	# in the decided encodings, and record that.
    450 	todo.sort(key=_Encoding.get_room)
    451 	for encoding in todo:
    452 		encoding._find_yourself_best_new_encoding(done_by_width)
    453 
    454 	# Walk through todo encodings, for each, see if merging it with
    455 	# another todo encoding gains more than each of them merging with
    456 	# their best decided encoding. If yes, merge them and add resulting
    457 	# encoding back to todo queue.  If not, move the enconding to decided
    458 	# list.  Repeat till done.
    459 	while todo:
    460 		encoding = todo.pop()
    461 		best_idx = None
    462 		best_gain = 0
    463 		for i,other_encoding in enumerate(todo):
    464 			combined_chars = other_encoding.chars | encoding.chars
    465 			combined_width = _Encoding._popcount(combined_chars)
    466 			combined_overhead = _Encoding._characteristic_overhead(combined_chars)
    467 			combined_gain = (
    468 					+ encoding.overhead
    469 					+ other_encoding.overhead
    470 					- combined_overhead
    471 					- (combined_width - encoding.width) * len(encoding)
    472 					- (combined_width - other_encoding.width) * len(other_encoding)
    473 					)
    474 			this_gain = 0 if encoding.best_new_encoding is None else (
    475 						+ encoding.overhead
    476 						- (encoding.best_new_encoding.width - encoding.width) * len(encoding)
    477 					)
    478 			other_gain = 0 if other_encoding.best_new_encoding is None else (
    479 						+ other_encoding.overhead
    480 						- (other_encoding.best_new_encoding.width - other_encoding.width) * len(other_encoding)
    481 					)
    482 			separate_gain = this_gain + other_gain
    483 
    484 			if combined_gain > separate_gain:
    485 				best_idx = i
    486 				best_gain = combined_gain - separate_gain
    487 
    488 		if best_idx is None:
    489 			# Encoding is decided as is
    490 			done_by_width[encoding.width].append(encoding)
    491 		else:
    492 			other_encoding = todo[best_idx]
    493 			combined_chars = other_encoding.chars | encoding.chars
    494 			combined_encoding = _Encoding(combined_chars)
    495 			combined_encoding.extend(encoding.items)
    496 			combined_encoding.extend(other_encoding.items)
    497 			combined_encoding._find_yourself_best_new_encoding(done_by_width)
    498 			del todo[best_idx]
    499 			todo.append(combined_encoding)
    500 
    501 	# Assemble final store.
    502 	back_mapping = {} # Mapping from full rows to new VarIdxes
    503 	encodings = sum(done_by_width.values(), [])
    504 	encodings.sort(key=_Encoding.sort_key)
    505 	self.VarData = []
    506 	for major,encoding in enumerate(encodings):
    507 		data = ot.VarData()
    508 		self.VarData.append(data)
    509 		data.VarRegionIndex = range(n)
    510 		data.VarRegionCount = len(data.VarRegionIndex)
    511 		data.Item = sorted(encoding.items)
    512 		for minor,item in enumerate(data.Item):
    513 			back_mapping[item] = (major<<16)+minor
    514 
    515 	# Compile final mapping.
    516 	varidx_map = {}
    517 	for k,v in front_mapping.items():
    518 		varidx_map[k] = back_mapping[v]
    519 
    520 	# Remove unused regions.
    521 	self.prune_regions()
    522 
    523 	# Recalculate things and go home.
    524 	self.VarRegionList.RegionCount = len(self.VarRegionList.Region)
    525 	self.VarDataCount = len(self.VarData)
    526 	for data in self.VarData:
    527 		data.ItemCount = len(data.Item)
    528 		data.optimize()
    529 
    530 	return varidx_map
    531 
    532 ot.VarStore.optimize = VarStore_optimize
    533 
    534 
    535 def main(args=None):
    536 	from argparse import ArgumentParser
    537 	from fontTools import configLogger
    538 	from fontTools.ttLib import TTFont
    539 	from fontTools.ttLib.tables.otBase import OTTableWriter
    540 
    541 	parser = ArgumentParser(prog='varLib.varStore')
    542 	parser.add_argument('fontfile')
    543 	parser.add_argument('outfile', nargs='?')
    544 	options = parser.parse_args(args)
    545 
    546 	# TODO: allow user to configure logging via command-line options
    547 	configLogger(level="INFO")
    548 
    549 	fontfile = options.fontfile
    550 	outfile = options.outfile
    551 
    552 	font = TTFont(fontfile)
    553 	gdef = font['GDEF']
    554 	store = gdef.table.VarStore
    555 
    556 	writer = OTTableWriter()
    557 	store.compile(writer, font)
    558 	size = len(writer.getAllData())
    559 	print("Before: %7d bytes" % size)
    560 
    561 	varidx_map = store.optimize()
    562 
    563 	gdef.table.remap_device_varidxes(varidx_map)
    564 	if 'GPOS' in font:
    565 		font['GPOS'].table.remap_device_varidxes(varidx_map)
    566 
    567 	writer = OTTableWriter()
    568 	store.compile(writer, font)
    569 	size = len(writer.getAllData())
    570 	print("After:  %7d bytes" % size)
    571 
    572 	if outfile is not None:
    573 		font.save(outfile)
    574 
    575 
    576 if __name__ == "__main__":
    577 	import sys
    578 	if len(sys.argv) > 1:
    579 		sys.exit(main())
    580 	import doctest
    581 	sys.exit(doctest.testmod().failed)
    582