You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
750 lines
25 KiB
750 lines
25 KiB
from fontTools.misc.py23 import bytechr, byteord, bytesjoin
|
|
from fontTools.misc.fixedTools import (
|
|
fixedToFloat as fi2fl,
|
|
floatToFixed as fl2fi,
|
|
floatToFixedToStr as fl2str,
|
|
strToFixedToFloat as str2fl,
|
|
otRound,
|
|
)
|
|
from fontTools.misc.textTools import safeEval
|
|
import array
|
|
import io
|
|
import logging
|
|
import struct
|
|
import sys
|
|
|
|
|
|
# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
|
|
|
|
EMBEDDED_PEAK_TUPLE = 0x8000
|
|
INTERMEDIATE_REGION = 0x4000
|
|
PRIVATE_POINT_NUMBERS = 0x2000
|
|
|
|
DELTAS_ARE_ZERO = 0x80
|
|
DELTAS_ARE_WORDS = 0x40
|
|
DELTA_RUN_COUNT_MASK = 0x3f
|
|
|
|
POINTS_ARE_WORDS = 0x80
|
|
POINT_RUN_COUNT_MASK = 0x7f
|
|
|
|
TUPLES_SHARE_POINT_NUMBERS = 0x8000
|
|
TUPLE_COUNT_MASK = 0x0fff
|
|
TUPLE_INDEX_MASK = 0x0fff
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class TupleVariation(object):
|
|
|
|
def __init__(self, axes, coordinates):
|
|
self.axes = axes.copy()
|
|
self.coordinates = coordinates[:]
|
|
|
|
def __repr__(self):
|
|
axes = ",".join(sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()]))
|
|
return "<TupleVariation %s %s>" % (axes, self.coordinates)
|
|
|
|
def __eq__(self, other):
|
|
return self.coordinates == other.coordinates and self.axes == other.axes
|
|
|
|
def getUsedPoints(self):
|
|
result = set()
|
|
for i, point in enumerate(self.coordinates):
|
|
if point is not None:
|
|
result.add(i)
|
|
return result
|
|
|
|
def hasImpact(self):
|
|
"""Returns True if this TupleVariation has any visible impact.
|
|
|
|
If the result is False, the TupleVariation can be omitted from the font
|
|
without making any visible difference.
|
|
"""
|
|
return any(c is not None for c in self.coordinates)
|
|
|
|
def toXML(self, writer, axisTags):
|
|
writer.begintag("tuple")
|
|
writer.newline()
|
|
for axis in axisTags:
|
|
value = self.axes.get(axis)
|
|
if value is not None:
|
|
minValue, value, maxValue = value
|
|
defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
|
|
defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
|
|
if minValue == defaultMinValue and maxValue == defaultMaxValue:
|
|
writer.simpletag("coord", axis=axis, value=fl2str(value, 14))
|
|
else:
|
|
attrs = [
|
|
("axis", axis),
|
|
("min", fl2str(minValue, 14)),
|
|
("value", fl2str(value, 14)),
|
|
("max", fl2str(maxValue, 14)),
|
|
]
|
|
writer.simpletag("coord", attrs)
|
|
writer.newline()
|
|
wrote_any_deltas = False
|
|
for i, delta in enumerate(self.coordinates):
|
|
if type(delta) == tuple and len(delta) == 2:
|
|
writer.simpletag("delta", pt=i, x=delta[0], y=delta[1])
|
|
writer.newline()
|
|
wrote_any_deltas = True
|
|
elif type(delta) == int:
|
|
writer.simpletag("delta", cvt=i, value=delta)
|
|
writer.newline()
|
|
wrote_any_deltas = True
|
|
elif delta is not None:
|
|
log.error("bad delta format")
|
|
writer.comment("bad delta #%d" % i)
|
|
writer.newline()
|
|
wrote_any_deltas = True
|
|
if not wrote_any_deltas:
|
|
writer.comment("no deltas")
|
|
writer.newline()
|
|
writer.endtag("tuple")
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, _content):
|
|
if name == "coord":
|
|
axis = attrs["axis"]
|
|
value = str2fl(attrs["value"], 14)
|
|
defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
|
|
defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
|
|
minValue = str2fl(attrs.get("min", defaultMinValue), 14)
|
|
maxValue = str2fl(attrs.get("max", defaultMaxValue), 14)
|
|
self.axes[axis] = (minValue, value, maxValue)
|
|
elif name == "delta":
|
|
if "pt" in attrs:
|
|
point = safeEval(attrs["pt"])
|
|
x = safeEval(attrs["x"])
|
|
y = safeEval(attrs["y"])
|
|
self.coordinates[point] = (x, y)
|
|
elif "cvt" in attrs:
|
|
cvt = safeEval(attrs["cvt"])
|
|
value = safeEval(attrs["value"])
|
|
self.coordinates[cvt] = value
|
|
else:
|
|
log.warning("bad delta format: %s" %
|
|
", ".join(sorted(attrs.keys())))
|
|
|
|
def compile(self, axisTags, sharedCoordIndices, sharedPoints):
|
|
tupleData = []
|
|
|
|
assert all(tag in axisTags for tag in self.axes.keys()), ("Unknown axis tag found.", self.axes.keys(), axisTags)
|
|
|
|
coord = self.compileCoord(axisTags)
|
|
if coord in sharedCoordIndices:
|
|
flags = sharedCoordIndices[coord]
|
|
else:
|
|
flags = EMBEDDED_PEAK_TUPLE
|
|
tupleData.append(coord)
|
|
|
|
intermediateCoord = self.compileIntermediateCoord(axisTags)
|
|
if intermediateCoord is not None:
|
|
flags |= INTERMEDIATE_REGION
|
|
tupleData.append(intermediateCoord)
|
|
|
|
points = self.getUsedPoints()
|
|
if sharedPoints == points:
|
|
# Only use the shared points if they are identical to the actually used points
|
|
auxData = self.compileDeltas(sharedPoints)
|
|
usesSharedPoints = True
|
|
else:
|
|
flags |= PRIVATE_POINT_NUMBERS
|
|
numPointsInGlyph = len(self.coordinates)
|
|
auxData = self.compilePoints(points, numPointsInGlyph) + self.compileDeltas(points)
|
|
usesSharedPoints = False
|
|
|
|
tupleData = struct.pack('>HH', len(auxData), flags) + bytesjoin(tupleData)
|
|
return (tupleData, auxData, usesSharedPoints)
|
|
|
|
def compileCoord(self, axisTags):
|
|
result = []
|
|
for axis in axisTags:
|
|
_minValue, value, _maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
|
|
result.append(struct.pack(">h", fl2fi(value, 14)))
|
|
return bytesjoin(result)
|
|
|
|
def compileIntermediateCoord(self, axisTags):
|
|
needed = False
|
|
for axis in axisTags:
|
|
minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
|
|
defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
|
|
defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
|
|
if (minValue != defaultMinValue) or (maxValue != defaultMaxValue):
|
|
needed = True
|
|
break
|
|
if not needed:
|
|
return None
|
|
minCoords = []
|
|
maxCoords = []
|
|
for axis in axisTags:
|
|
minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
|
|
minCoords.append(struct.pack(">h", fl2fi(minValue, 14)))
|
|
maxCoords.append(struct.pack(">h", fl2fi(maxValue, 14)))
|
|
return bytesjoin(minCoords + maxCoords)
|
|
|
|
@staticmethod
|
|
def decompileCoord_(axisTags, data, offset):
|
|
coord = {}
|
|
pos = offset
|
|
for axis in axisTags:
|
|
coord[axis] = fi2fl(struct.unpack(">h", data[pos:pos+2])[0], 14)
|
|
pos += 2
|
|
return coord, pos
|
|
|
|
@staticmethod
|
|
def compilePoints(points, numPointsInGlyph):
|
|
# If the set consists of all points in the glyph, it gets encoded with
|
|
# a special encoding: a single zero byte.
|
|
if len(points) == numPointsInGlyph:
|
|
return b"\0"
|
|
|
|
# In the 'gvar' table, the packing of point numbers is a little surprising.
|
|
# It consists of multiple runs, each being a delta-encoded list of integers.
|
|
# For example, the point set {17, 18, 19, 20, 21, 22, 23} gets encoded as
|
|
# [6, 17, 1, 1, 1, 1, 1, 1]. The first value (6) is the run length minus 1.
|
|
# There are two types of runs, with values being either 8 or 16 bit unsigned
|
|
# integers.
|
|
points = list(points)
|
|
points.sort()
|
|
numPoints = len(points)
|
|
|
|
# The binary representation starts with the total number of points in the set,
|
|
# encoded into one or two bytes depending on the value.
|
|
if numPoints < 0x80:
|
|
result = [bytechr(numPoints)]
|
|
else:
|
|
result = [bytechr((numPoints >> 8) | 0x80) + bytechr(numPoints & 0xff)]
|
|
|
|
MAX_RUN_LENGTH = 127
|
|
pos = 0
|
|
lastValue = 0
|
|
while pos < numPoints:
|
|
run = io.BytesIO()
|
|
runLength = 0
|
|
useByteEncoding = None
|
|
while pos < numPoints and runLength <= MAX_RUN_LENGTH:
|
|
curValue = points[pos]
|
|
delta = curValue - lastValue
|
|
if useByteEncoding is None:
|
|
useByteEncoding = 0 <= delta <= 0xff
|
|
if useByteEncoding and (delta > 0xff or delta < 0):
|
|
# we need to start a new run (which will not use byte encoding)
|
|
break
|
|
# TODO This never switches back to a byte-encoding from a short-encoding.
|
|
# That's suboptimal.
|
|
if useByteEncoding:
|
|
run.write(bytechr(delta))
|
|
else:
|
|
run.write(bytechr(delta >> 8))
|
|
run.write(bytechr(delta & 0xff))
|
|
lastValue = curValue
|
|
pos += 1
|
|
runLength += 1
|
|
if useByteEncoding:
|
|
runHeader = bytechr(runLength - 1)
|
|
else:
|
|
runHeader = bytechr((runLength - 1) | POINTS_ARE_WORDS)
|
|
result.append(runHeader)
|
|
result.append(run.getvalue())
|
|
|
|
return bytesjoin(result)
|
|
|
|
@staticmethod
|
|
def decompilePoints_(numPoints, data, offset, tableTag):
|
|
"""(numPoints, data, offset, tableTag) --> ([point1, point2, ...], newOffset)"""
|
|
assert tableTag in ('cvar', 'gvar')
|
|
pos = offset
|
|
numPointsInData = byteord(data[pos])
|
|
pos += 1
|
|
if (numPointsInData & POINTS_ARE_WORDS) != 0:
|
|
numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | byteord(data[pos])
|
|
pos += 1
|
|
if numPointsInData == 0:
|
|
return (range(numPoints), pos)
|
|
|
|
result = []
|
|
while len(result) < numPointsInData:
|
|
runHeader = byteord(data[pos])
|
|
pos += 1
|
|
numPointsInRun = (runHeader & POINT_RUN_COUNT_MASK) + 1
|
|
point = 0
|
|
if (runHeader & POINTS_ARE_WORDS) != 0:
|
|
points = array.array("H")
|
|
pointsSize = numPointsInRun * 2
|
|
else:
|
|
points = array.array("B")
|
|
pointsSize = numPointsInRun
|
|
points.frombytes(data[pos:pos+pointsSize])
|
|
if sys.byteorder != "big": points.byteswap()
|
|
|
|
assert len(points) == numPointsInRun
|
|
pos += pointsSize
|
|
|
|
result.extend(points)
|
|
|
|
# Convert relative to absolute
|
|
absolute = []
|
|
current = 0
|
|
for delta in result:
|
|
current += delta
|
|
absolute.append(current)
|
|
result = absolute
|
|
del absolute
|
|
|
|
badPoints = {str(p) for p in result if p < 0 or p >= numPoints}
|
|
if badPoints:
|
|
log.warning("point %s out of range in '%s' table" %
|
|
(",".join(sorted(badPoints)), tableTag))
|
|
return (result, pos)
|
|
|
|
def compileDeltas(self, points):
|
|
deltaX = []
|
|
deltaY = []
|
|
for p in sorted(list(points)):
|
|
c = self.coordinates[p]
|
|
if type(c) is tuple and len(c) == 2:
|
|
deltaX.append(c[0])
|
|
deltaY.append(c[1])
|
|
elif type(c) is int:
|
|
deltaX.append(c)
|
|
elif c is not None:
|
|
raise TypeError("invalid type of delta: %s" % type(c))
|
|
return self.compileDeltaValues_(deltaX) + self.compileDeltaValues_(deltaY)
|
|
|
|
@staticmethod
|
|
def compileDeltaValues_(deltas):
|
|
"""[value1, value2, value3, ...] --> bytestring
|
|
|
|
Emits a sequence of runs. Each run starts with a
|
|
byte-sized header whose 6 least significant bits
|
|
(header & 0x3F) indicate how many values are encoded
|
|
in this run. The stored length is the actual length
|
|
minus one; run lengths are thus in the range [1..64].
|
|
If the header byte has its most significant bit (0x80)
|
|
set, all values in this run are zero, and no data
|
|
follows. Otherwise, the header byte is followed by
|
|
((header & 0x3F) + 1) signed values. If (header &
|
|
0x40) is clear, the delta values are stored as signed
|
|
bytes; if (header & 0x40) is set, the delta values are
|
|
signed 16-bit integers.
|
|
""" # Explaining the format because the 'gvar' spec is hard to understand.
|
|
stream = io.BytesIO()
|
|
pos = 0
|
|
while pos < len(deltas):
|
|
value = deltas[pos]
|
|
if value == 0:
|
|
pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, stream)
|
|
elif value >= -128 and value <= 127:
|
|
pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, stream)
|
|
else:
|
|
pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, stream)
|
|
return stream.getvalue()
|
|
|
|
@staticmethod
|
|
def encodeDeltaRunAsZeroes_(deltas, offset, stream):
|
|
runLength = 0
|
|
pos = offset
|
|
numDeltas = len(deltas)
|
|
while pos < numDeltas and runLength < 64 and deltas[pos] == 0:
|
|
pos += 1
|
|
runLength += 1
|
|
assert runLength >= 1 and runLength <= 64
|
|
stream.write(bytechr(DELTAS_ARE_ZERO | (runLength - 1)))
|
|
return pos
|
|
|
|
@staticmethod
|
|
def encodeDeltaRunAsBytes_(deltas, offset, stream):
|
|
runLength = 0
|
|
pos = offset
|
|
numDeltas = len(deltas)
|
|
while pos < numDeltas and runLength < 64:
|
|
value = deltas[pos]
|
|
if value < -128 or value > 127:
|
|
break
|
|
# Within a byte-encoded run of deltas, a single zero
|
|
# is best stored literally as 0x00 value. However,
|
|
# if are two or more zeroes in a sequence, it is
|
|
# better to start a new run. For example, the sequence
|
|
# of deltas [15, 15, 0, 15, 15] becomes 6 bytes
|
|
# (04 0F 0F 00 0F 0F) when storing the zero value
|
|
# literally, but 7 bytes (01 0F 0F 80 01 0F 0F)
|
|
# when starting a new run.
|
|
if value == 0 and pos+1 < numDeltas and deltas[pos+1] == 0:
|
|
break
|
|
pos += 1
|
|
runLength += 1
|
|
assert runLength >= 1 and runLength <= 64
|
|
stream.write(bytechr(runLength - 1))
|
|
for i in range(offset, pos):
|
|
stream.write(struct.pack('b', otRound(deltas[i])))
|
|
return pos
|
|
|
|
@staticmethod
|
|
def encodeDeltaRunAsWords_(deltas, offset, stream):
|
|
runLength = 0
|
|
pos = offset
|
|
numDeltas = len(deltas)
|
|
while pos < numDeltas and runLength < 64:
|
|
value = deltas[pos]
|
|
# Within a word-encoded run of deltas, it is easiest
|
|
# to start a new run (with a different encoding)
|
|
# whenever we encounter a zero value. For example,
|
|
# the sequence [0x6666, 0, 0x7777] needs 7 bytes when
|
|
# storing the zero literally (42 66 66 00 00 77 77),
|
|
# and equally 7 bytes when starting a new run
|
|
# (40 66 66 80 40 77 77).
|
|
if value == 0:
|
|
break
|
|
|
|
# Within a word-encoded run of deltas, a single value
|
|
# in the range (-128..127) should be encoded literally
|
|
# because it is more compact. For example, the sequence
|
|
# [0x6666, 2, 0x7777] becomes 7 bytes when storing
|
|
# the value literally (42 66 66 00 02 77 77), but 8 bytes
|
|
# when starting a new run (40 66 66 00 02 40 77 77).
|
|
isByteEncodable = lambda value: value >= -128 and value <= 127
|
|
if isByteEncodable(value) and pos+1 < numDeltas and isByteEncodable(deltas[pos+1]):
|
|
break
|
|
pos += 1
|
|
runLength += 1
|
|
assert runLength >= 1 and runLength <= 64
|
|
stream.write(bytechr(DELTAS_ARE_WORDS | (runLength - 1)))
|
|
for i in range(offset, pos):
|
|
stream.write(struct.pack('>h', otRound(deltas[i])))
|
|
return pos
|
|
|
|
@staticmethod
|
|
def decompileDeltas_(numDeltas, data, offset):
|
|
"""(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)"""
|
|
result = []
|
|
pos = offset
|
|
while len(result) < numDeltas:
|
|
runHeader = byteord(data[pos])
|
|
pos += 1
|
|
numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1
|
|
if (runHeader & DELTAS_ARE_ZERO) != 0:
|
|
result.extend([0] * numDeltasInRun)
|
|
else:
|
|
if (runHeader & DELTAS_ARE_WORDS) != 0:
|
|
deltas = array.array("h")
|
|
deltasSize = numDeltasInRun * 2
|
|
else:
|
|
deltas = array.array("b")
|
|
deltasSize = numDeltasInRun
|
|
deltas.frombytes(data[pos:pos+deltasSize])
|
|
if sys.byteorder != "big": deltas.byteswap()
|
|
assert len(deltas) == numDeltasInRun
|
|
pos += deltasSize
|
|
result.extend(deltas)
|
|
assert len(result) == numDeltas
|
|
return (result, pos)
|
|
|
|
@staticmethod
|
|
def getTupleSize_(flags, axisCount):
|
|
size = 4
|
|
if (flags & EMBEDDED_PEAK_TUPLE) != 0:
|
|
size += axisCount * 2
|
|
if (flags & INTERMEDIATE_REGION) != 0:
|
|
size += axisCount * 4
|
|
return size
|
|
|
|
def getCoordWidth(self):
|
|
""" Return 2 if coordinates are (x, y) as in gvar, 1 if single values
|
|
as in cvar, or 0 if empty.
|
|
"""
|
|
firstDelta = next((c for c in self.coordinates if c is not None), None)
|
|
if firstDelta is None:
|
|
return 0 # empty or has no impact
|
|
if type(firstDelta) in (int, float):
|
|
return 1
|
|
if type(firstDelta) is tuple and len(firstDelta) == 2:
|
|
return 2
|
|
raise TypeError(
|
|
"invalid type of delta; expected (int or float) number, or "
|
|
"Tuple[number, number]: %r" % firstDelta
|
|
)
|
|
|
|
def scaleDeltas(self, scalar):
|
|
if scalar == 1.0:
|
|
return # no change
|
|
coordWidth = self.getCoordWidth()
|
|
self.coordinates = [
|
|
None
|
|
if d is None
|
|
else d * scalar
|
|
if coordWidth == 1
|
|
else (d[0] * scalar, d[1] * scalar)
|
|
for d in self.coordinates
|
|
]
|
|
|
|
def roundDeltas(self):
|
|
coordWidth = self.getCoordWidth()
|
|
self.coordinates = [
|
|
None
|
|
if d is None
|
|
else otRound(d)
|
|
if coordWidth == 1
|
|
else (otRound(d[0]), otRound(d[1]))
|
|
for d in self.coordinates
|
|
]
|
|
|
|
def calcInferredDeltas(self, origCoords, endPts):
|
|
from fontTools.varLib.iup import iup_delta
|
|
|
|
if self.getCoordWidth() == 1:
|
|
raise TypeError(
|
|
"Only 'gvar' TupleVariation can have inferred deltas"
|
|
)
|
|
if None in self.coordinates:
|
|
if len(self.coordinates) != len(origCoords):
|
|
raise ValueError(
|
|
"Expected len(origCoords) == %d; found %d"
|
|
% (len(self.coordinates), len(origCoords))
|
|
)
|
|
self.coordinates = iup_delta(self.coordinates, origCoords, endPts)
|
|
|
|
def optimize(self, origCoords, endPts, tolerance=0.5, isComposite=False):
|
|
from fontTools.varLib.iup import iup_delta_optimize
|
|
|
|
if None in self.coordinates:
|
|
return # already optimized
|
|
|
|
deltaOpt = iup_delta_optimize(
|
|
self.coordinates, origCoords, endPts, tolerance=tolerance
|
|
)
|
|
if None in deltaOpt:
|
|
if isComposite and all(d is None for d in deltaOpt):
|
|
# Fix for macOS composites
|
|
# https://github.com/fonttools/fonttools/issues/1381
|
|
deltaOpt = [(0, 0)] + [None] * (len(deltaOpt) - 1)
|
|
# Use "optimized" version only if smaller...
|
|
varOpt = TupleVariation(self.axes, deltaOpt)
|
|
|
|
# Shouldn't matter that this is different from fvar...?
|
|
axisTags = sorted(self.axes.keys())
|
|
tupleData, auxData, _ = self.compile(axisTags, [], None)
|
|
unoptimizedLength = len(tupleData) + len(auxData)
|
|
tupleData, auxData, _ = varOpt.compile(axisTags, [], None)
|
|
optimizedLength = len(tupleData) + len(auxData)
|
|
|
|
if optimizedLength < unoptimizedLength:
|
|
self.coordinates = varOpt.coordinates
|
|
|
|
def __iadd__(self, other):
|
|
if not isinstance(other, TupleVariation):
|
|
return NotImplemented
|
|
deltas1 = self.coordinates
|
|
length = len(deltas1)
|
|
deltas2 = other.coordinates
|
|
if len(deltas2) != length:
|
|
raise ValueError(
|
|
"cannot sum TupleVariation deltas with different lengths"
|
|
)
|
|
# 'None' values have different meanings in gvar vs cvar TupleVariations:
|
|
# within the gvar, when deltas are not provided explicitly for some points,
|
|
# they need to be inferred; whereas for the 'cvar' table, if deltas are not
|
|
# provided for some CVT values, then no adjustments are made (i.e. None == 0).
|
|
# Thus, we cannot sum deltas for gvar TupleVariations if they contain
|
|
# inferred inferred deltas (the latter need to be computed first using
|
|
# 'calcInferredDeltas' method), but we can treat 'None' values in cvar
|
|
# deltas as if they are zeros.
|
|
if self.getCoordWidth() == 2:
|
|
for i, d2 in zip(range(length), deltas2):
|
|
d1 = deltas1[i]
|
|
try:
|
|
deltas1[i] = (d1[0] + d2[0], d1[1] + d2[1])
|
|
except TypeError:
|
|
raise ValueError(
|
|
"cannot sum gvar deltas with inferred points"
|
|
)
|
|
else:
|
|
for i, d2 in zip(range(length), deltas2):
|
|
d1 = deltas1[i]
|
|
if d1 is not None and d2 is not None:
|
|
deltas1[i] = d1 + d2
|
|
elif d1 is None and d2 is not None:
|
|
deltas1[i] = d2
|
|
# elif d2 is None do nothing
|
|
return self
|
|
|
|
|
|
def decompileSharedTuples(axisTags, sharedTupleCount, data, offset):
|
|
result = []
|
|
for _ in range(sharedTupleCount):
|
|
t, offset = TupleVariation.decompileCoord_(axisTags, data, offset)
|
|
result.append(t)
|
|
return result
|
|
|
|
|
|
def compileSharedTuples(axisTags, variations):
|
|
coordCount = {}
|
|
for var in variations:
|
|
coord = var.compileCoord(axisTags)
|
|
coordCount[coord] = coordCount.get(coord, 0) + 1
|
|
sharedCoords = [(count, coord)
|
|
for (coord, count) in coordCount.items() if count > 1]
|
|
sharedCoords.sort(reverse=True)
|
|
MAX_NUM_SHARED_COORDS = TUPLE_INDEX_MASK + 1
|
|
sharedCoords = sharedCoords[:MAX_NUM_SHARED_COORDS]
|
|
return [c[1] for c in sharedCoords] # Strip off counts.
|
|
|
|
|
|
def compileTupleVariationStore(variations, pointCount,
|
|
axisTags, sharedTupleIndices,
|
|
useSharedPoints=True):
|
|
variations = [v for v in variations if v.hasImpact()]
|
|
if len(variations) == 0:
|
|
return (0, b"", b"")
|
|
|
|
# Each glyph variation tuples modifies a set of control points. To
|
|
# indicate which exact points are getting modified, a single tuple
|
|
# can either refer to a shared set of points, or the tuple can
|
|
# supply its private point numbers. Because the impact of sharing
|
|
# can be positive (no need for a private point list) or negative
|
|
# (need to supply 0,0 deltas for unused points), it is not obvious
|
|
# how to determine which tuples should take their points from the
|
|
# shared pool versus have their own. Perhaps we should resort to
|
|
# brute force, and try all combinations? However, if a glyph has n
|
|
# variation tuples, we would need to try 2^n combinations (because
|
|
# each tuple may or may not be part of the shared set). How many
|
|
# variations tuples do glyphs have?
|
|
#
|
|
# Skia.ttf: {3: 1, 5: 11, 6: 41, 7: 62, 8: 387, 13: 1, 14: 3}
|
|
# JamRegular.ttf: {3: 13, 4: 122, 5: 1, 7: 4, 8: 1, 9: 1, 10: 1}
|
|
# BuffaloGalRegular.ttf: {1: 16, 2: 13, 4: 2, 5: 4, 6: 19, 7: 1, 8: 3, 9: 8}
|
|
# (Reading example: In Skia.ttf, 41 glyphs have 6 variation tuples).
|
|
#
|
|
|
|
# Is this even worth optimizing? If we never use a shared point
|
|
# list, the private lists will consume 112K for Skia, 5K for
|
|
# BuffaloGalRegular, and 15K for JamRegular. If we always use a
|
|
# shared point list, the shared lists will consume 16K for Skia,
|
|
# 3K for BuffaloGalRegular, and 10K for JamRegular. However, in
|
|
# the latter case the delta arrays will become larger, but I
|
|
# haven't yet measured by how much. From gut feeling (which may be
|
|
# wrong), the optimum is to share some but not all points;
|
|
# however, then we would need to try all combinations.
|
|
#
|
|
# For the time being, we try two variants and then pick the better one:
|
|
# (a) each tuple supplies its own private set of points;
|
|
# (b) all tuples refer to a shared set of points, which consists of
|
|
# "every control point in the glyph that has explicit deltas".
|
|
usedPoints = set()
|
|
for v in variations:
|
|
usedPoints |= v.getUsedPoints()
|
|
tuples = []
|
|
data = []
|
|
someTuplesSharePoints = False
|
|
sharedPointVariation = None # To keep track of a variation that uses shared points
|
|
for v in variations:
|
|
privateTuple, privateData, _ = v.compile(
|
|
axisTags, sharedTupleIndices, sharedPoints=None)
|
|
sharedTuple, sharedData, usesSharedPoints = v.compile(
|
|
axisTags, sharedTupleIndices, sharedPoints=usedPoints)
|
|
if useSharedPoints and (len(sharedTuple) + len(sharedData)) < (len(privateTuple) + len(privateData)):
|
|
tuples.append(sharedTuple)
|
|
data.append(sharedData)
|
|
someTuplesSharePoints |= usesSharedPoints
|
|
sharedPointVariation = v
|
|
else:
|
|
tuples.append(privateTuple)
|
|
data.append(privateData)
|
|
if someTuplesSharePoints:
|
|
# Use the last of the variations that share points for compiling the packed point data
|
|
data = sharedPointVariation.compilePoints(usedPoints, len(sharedPointVariation.coordinates)) + bytesjoin(data)
|
|
tupleVariationCount = TUPLES_SHARE_POINT_NUMBERS | len(tuples)
|
|
else:
|
|
data = bytesjoin(data)
|
|
tupleVariationCount = len(tuples)
|
|
tuples = bytesjoin(tuples)
|
|
return tupleVariationCount, tuples, data
|
|
|
|
|
|
def decompileTupleVariationStore(tableTag, axisTags,
|
|
tupleVariationCount, pointCount, sharedTuples,
|
|
data, pos, dataPos):
|
|
numAxes = len(axisTags)
|
|
result = []
|
|
if (tupleVariationCount & TUPLES_SHARE_POINT_NUMBERS) != 0:
|
|
sharedPoints, dataPos = TupleVariation.decompilePoints_(
|
|
pointCount, data, dataPos, tableTag)
|
|
else:
|
|
sharedPoints = []
|
|
for _ in range(tupleVariationCount & TUPLE_COUNT_MASK):
|
|
dataSize, flags = struct.unpack(">HH", data[pos:pos+4])
|
|
tupleSize = TupleVariation.getTupleSize_(flags, numAxes)
|
|
tupleData = data[pos : pos + tupleSize]
|
|
pointDeltaData = data[dataPos : dataPos + dataSize]
|
|
result.append(decompileTupleVariation_(
|
|
pointCount, sharedTuples, sharedPoints,
|
|
tableTag, axisTags, tupleData, pointDeltaData))
|
|
pos += tupleSize
|
|
dataPos += dataSize
|
|
return result
|
|
|
|
|
|
def decompileTupleVariation_(pointCount, sharedTuples, sharedPoints,
|
|
tableTag, axisTags, data, tupleData):
|
|
assert tableTag in ("cvar", "gvar"), tableTag
|
|
flags = struct.unpack(">H", data[2:4])[0]
|
|
pos = 4
|
|
if (flags & EMBEDDED_PEAK_TUPLE) == 0:
|
|
peak = sharedTuples[flags & TUPLE_INDEX_MASK]
|
|
else:
|
|
peak, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
|
|
if (flags & INTERMEDIATE_REGION) != 0:
|
|
start, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
|
|
end, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
|
|
else:
|
|
start, end = inferRegion_(peak)
|
|
axes = {}
|
|
for axis in axisTags:
|
|
region = start[axis], peak[axis], end[axis]
|
|
if region != (0.0, 0.0, 0.0):
|
|
axes[axis] = region
|
|
pos = 0
|
|
if (flags & PRIVATE_POINT_NUMBERS) != 0:
|
|
points, pos = TupleVariation.decompilePoints_(
|
|
pointCount, tupleData, pos, tableTag)
|
|
else:
|
|
points = sharedPoints
|
|
|
|
deltas = [None] * pointCount
|
|
|
|
if tableTag == "cvar":
|
|
deltas_cvt, pos = TupleVariation.decompileDeltas_(
|
|
len(points), tupleData, pos)
|
|
for p, delta in zip(points, deltas_cvt):
|
|
if 0 <= p < pointCount:
|
|
deltas[p] = delta
|
|
|
|
elif tableTag == "gvar":
|
|
deltas_x, pos = TupleVariation.decompileDeltas_(
|
|
len(points), tupleData, pos)
|
|
deltas_y, pos = TupleVariation.decompileDeltas_(
|
|
len(points), tupleData, pos)
|
|
for p, x, y in zip(points, deltas_x, deltas_y):
|
|
if 0 <= p < pointCount:
|
|
deltas[p] = (x, y)
|
|
|
|
return TupleVariation(axes, deltas)
|
|
|
|
|
|
def inferRegion_(peak):
|
|
"""Infer start and end for a (non-intermediate) region
|
|
|
|
This helper function computes the applicability region for
|
|
variation tuples whose INTERMEDIATE_REGION flag is not set in the
|
|
TupleVariationHeader structure. Variation tuples apply only to
|
|
certain regions of the variation space; outside that region, the
|
|
tuple has no effect. To make the binary encoding more compact,
|
|
TupleVariationHeaders can omit the intermediateStartTuple and
|
|
intermediateEndTuple fields.
|
|
"""
|
|
start, end = {}, {}
|
|
for (axis, value) in peak.items():
|
|
start[axis] = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
|
|
end[axis] = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
|
|
return (start, end)
|