Spaces:
Sleeping
Sleeping
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 | |
from collections import Counter, defaultdict | |
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 | |
DELTAS_ARE_LONGS = 0xC0 | |
DELTAS_SIZE_MASK = 0xC0 | |
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 = list(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): | |
# Empty set means "all points used". | |
if None not in self.coordinates: | |
return frozenset() | |
used = frozenset([i for i, p in enumerate(self.coordinates) if p is not None]) | |
# Return None if no points used. | |
return used if used else None | |
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={}, pointData=None): | |
assert set(self.axes.keys()) <= set(axisTags), ( | |
"Unknown axis tag found.", | |
self.axes.keys(), | |
axisTags, | |
) | |
tupleData = [] | |
auxData = [] | |
if pointData is None: | |
usedPoints = self.getUsedPoints() | |
if usedPoints is None: # Nothing to encode | |
return b"", b"" | |
pointData = self.compilePoints(usedPoints) | |
coord = self.compileCoord(axisTags) | |
flags = sharedCoordIndices.get(coord) | |
if flags is None: | |
flags = EMBEDDED_PEAK_TUPLE | |
tupleData.append(coord) | |
intermediateCoord = self.compileIntermediateCoord(axisTags) | |
if intermediateCoord is not None: | |
flags |= INTERMEDIATE_REGION | |
tupleData.append(intermediateCoord) | |
# pointData of b'' implies "use shared points". | |
if pointData: | |
flags |= PRIVATE_POINT_NUMBERS | |
auxData.append(pointData) | |
auxData.append(self.compileDeltas()) | |
auxData = b"".join(auxData) | |
tupleData.insert(0, struct.pack(">HH", len(auxData), flags)) | |
return b"".join(tupleData), auxData | |
def compileCoord(self, axisTags): | |
result = [] | |
axes = self.axes | |
for axis in axisTags: | |
triple = axes.get(axis) | |
if triple is None: | |
result.append(b"\0\0") | |
else: | |
result.append(struct.pack(">h", fl2fi(triple[1], 14))) | |
return b"".join(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 b"".join(minCoords + maxCoords) | |
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 | |
def compilePoints(points): | |
# If the set consists of all points in the glyph, it gets encoded with | |
# a special encoding: a single zero byte. | |
# | |
# To use this optimization, points passed in must be empty set. | |
# The following two lines are not strictly necessary as the main code | |
# below would emit the same. But this is most common and faster. | |
if not points: | |
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) | |
result = bytearray() | |
# 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.append(numPoints) | |
else: | |
result.append((numPoints >> 8) | 0x80) | |
result.append(numPoints & 0xFF) | |
MAX_RUN_LENGTH = 127 | |
pos = 0 | |
lastValue = 0 | |
while pos < numPoints: | |
runLength = 0 | |
headerPos = len(result) | |
result.append(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: | |
result.append(delta) | |
else: | |
result.append(delta >> 8) | |
result.append(delta & 0xFF) | |
lastValue = curValue | |
pos += 1 | |
runLength += 1 | |
if useByteEncoding: | |
result[headerPos] = runLength - 1 | |
else: | |
result[headerPos] = (runLength - 1) | POINTS_ARE_WORDS | |
return result | |
def decompilePoints_(numPoints, data, offset, tableTag): | |
"""(numPoints, data, offset, tableTag) --> ([point1, point2, ...], newOffset)""" | |
assert tableTag in ("cvar", "gvar") | |
pos = offset | |
numPointsInData = data[pos] | |
pos += 1 | |
if (numPointsInData & POINTS_ARE_WORDS) != 0: | |
numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | data[pos] | |
pos += 1 | |
if numPointsInData == 0: | |
return (range(numPoints), pos) | |
result = [] | |
while len(result) < numPointsInData: | |
runHeader = 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): | |
deltaX = [] | |
deltaY = [] | |
if self.getCoordWidth() == 2: | |
for c in self.coordinates: | |
if c is None: | |
continue | |
deltaX.append(c[0]) | |
deltaY.append(c[1]) | |
else: | |
for c in self.coordinates: | |
if c is None: | |
continue | |
deltaX.append(c) | |
bytearr = bytearray() | |
self.compileDeltaValues_(deltaX, bytearr) | |
self.compileDeltaValues_(deltaY, bytearr) | |
return bytearr | |
def compileDeltaValues_(deltas, bytearr=None): | |
"""[value1, value2, value3, ...] --> bytearray | |
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. | |
if bytearr is None: | |
bytearr = bytearray() | |
pos = 0 | |
numDeltas = len(deltas) | |
while pos < numDeltas: | |
value = deltas[pos] | |
if value == 0: | |
pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) | |
elif -128 <= value <= 127: | |
pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) | |
elif -32768 <= value <= 32767: | |
pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) | |
else: | |
pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) | |
return bytearr | |
def encodeDeltaRunAsZeroes_(deltas, offset, bytearr): | |
pos = offset | |
numDeltas = len(deltas) | |
while pos < numDeltas and deltas[pos] == 0: | |
pos += 1 | |
runLength = pos - offset | |
while runLength >= 64: | |
bytearr.append(DELTAS_ARE_ZERO | 63) | |
runLength -= 64 | |
if runLength: | |
bytearr.append(DELTAS_ARE_ZERO | (runLength - 1)) | |
return pos | |
def encodeDeltaRunAsBytes_(deltas, offset, bytearr): | |
pos = offset | |
numDeltas = len(deltas) | |
while pos < numDeltas: | |
value = deltas[pos] | |
if not (-128 <= 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 = pos - offset | |
while runLength >= 64: | |
bytearr.append(63) | |
bytearr.extend(array.array("b", deltas[offset : offset + 64])) | |
offset += 64 | |
runLength -= 64 | |
if runLength: | |
bytearr.append(runLength - 1) | |
bytearr.extend(array.array("b", deltas[offset:pos])) | |
return pos | |
def encodeDeltaRunAsWords_(deltas, offset, bytearr): | |
pos = offset | |
numDeltas = len(deltas) | |
while pos < numDeltas: | |
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). | |
if ( | |
(-128 <= value <= 127) | |
and pos + 1 < numDeltas | |
and (-128 <= deltas[pos + 1] <= 127) | |
): | |
break | |
if not (-32768 <= value <= 32767): | |
break | |
pos += 1 | |
runLength = pos - offset | |
while runLength >= 64: | |
bytearr.append(DELTAS_ARE_WORDS | 63) | |
a = array.array("h", deltas[offset : offset + 64]) | |
if sys.byteorder != "big": | |
a.byteswap() | |
bytearr.extend(a) | |
offset += 64 | |
runLength -= 64 | |
if runLength: | |
bytearr.append(DELTAS_ARE_WORDS | (runLength - 1)) | |
a = array.array("h", deltas[offset:pos]) | |
if sys.byteorder != "big": | |
a.byteswap() | |
bytearr.extend(a) | |
return pos | |
def encodeDeltaRunAsLongs_(deltas, offset, bytearr): | |
pos = offset | |
numDeltas = len(deltas) | |
while pos < numDeltas: | |
value = deltas[pos] | |
if -32768 <= value <= 32767: | |
break | |
pos += 1 | |
runLength = pos - offset | |
while runLength >= 64: | |
bytearr.append(DELTAS_ARE_LONGS | 63) | |
a = array.array("i", deltas[offset : offset + 64]) | |
if sys.byteorder != "big": | |
a.byteswap() | |
bytearr.extend(a) | |
offset += 64 | |
runLength -= 64 | |
if runLength: | |
bytearr.append(DELTAS_ARE_LONGS | (runLength - 1)) | |
a = array.array("i", deltas[offset:pos]) | |
if sys.byteorder != "big": | |
a.byteswap() | |
bytearr.extend(a) | |
return pos | |
def decompileDeltas_(numDeltas, data, offset=0): | |
"""(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)""" | |
result = [] | |
pos = offset | |
while len(result) < numDeltas if numDeltas is not None else pos < len(data): | |
runHeader = data[pos] | |
pos += 1 | |
numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1 | |
if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_ZERO: | |
result.extend([0] * numDeltasInRun) | |
else: | |
if (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_LONGS: | |
deltas = array.array("i") | |
deltasSize = numDeltasInRun * 4 | |
elif (runHeader & DELTAS_SIZE_MASK) == DELTAS_ARE_WORDS: | |
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, (len(deltas), numDeltasInRun) | |
pos += deltasSize | |
result.extend(deltas) | |
assert numDeltas is None or len(result) == numDeltas | |
return (result, pos) | |
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) | |
unoptimizedLength = len(tupleData) + len(auxData) | |
tupleData, auxData = varOpt.compile(axisTags) | |
optimizedLength = len(tupleData) + len(auxData) | |
if optimizedLength < unoptimizedLength: | |
self.coordinates = varOpt.coordinates | |
def __imul__(self, scalar): | |
self.scaleDeltas(scalar) | |
return self | |
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, MAX_NUM_SHARED_COORDS=TUPLE_INDEX_MASK + 1 | |
): | |
coordCount = Counter() | |
for var in variations: | |
coord = var.compileCoord(axisTags) | |
coordCount[coord] += 1 | |
# In python < 3.7, most_common() ordering is non-deterministic | |
# so apply a sort to make sure the ordering is consistent. | |
sharedCoords = sorted( | |
coordCount.most_common(MAX_NUM_SHARED_COORDS), | |
key=lambda item: (-item[1], item[0]), | |
) | |
return [c[0] for c in sharedCoords if c[1] > 1] | |
def compileTupleVariationStore( | |
variations, pointCount, axisTags, sharedTupleIndices, useSharedPoints=True | |
): | |
# pointCount is actually unused. Keeping for API compat. | |
del pointCount | |
newVariations = [] | |
pointDatas = [] | |
# Compile all points and figure out sharing if desired | |
sharedPoints = None | |
# Collect, count, and compile point-sets for all variation sets | |
pointSetCount = defaultdict(int) | |
for v in variations: | |
points = v.getUsedPoints() | |
if points is None: # Empty variations | |
continue | |
pointSetCount[points] += 1 | |
newVariations.append(v) | |
pointDatas.append(points) | |
variations = newVariations | |
del newVariations | |
if not variations: | |
return (0, b"", b"") | |
n = len(variations[0].coordinates) | |
assert all( | |
len(v.coordinates) == n for v in variations | |
), "Variation sets have different sizes" | |
compiledPoints = { | |
pointSet: TupleVariation.compilePoints(pointSet) for pointSet in pointSetCount | |
} | |
tupleVariationCount = len(variations) | |
tuples = [] | |
data = [] | |
if useSharedPoints: | |
# Find point-set which saves most bytes. | |
def key(pn): | |
pointSet = pn[0] | |
count = pn[1] | |
return len(compiledPoints[pointSet]) * (count - 1) | |
sharedPoints = max(pointSetCount.items(), key=key)[0] | |
data.append(compiledPoints[sharedPoints]) | |
tupleVariationCount |= TUPLES_SHARE_POINT_NUMBERS | |
# b'' implies "use shared points" | |
pointDatas = [ | |
compiledPoints[points] if points != sharedPoints else b"" | |
for points in pointDatas | |
] | |
for v, p in zip(variations, pointDatas): | |
thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p) | |
tuples.append(thisTuple) | |
data.append(thisData) | |
tuples = b"".join(tuples) | |
data = b"".join(data) | |
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) | |