fonttools/Lib/fontTools/ttLib/tables/TupleVariation.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

809 lines
29 KiB
Python
Raw Normal View History

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
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)
2022-12-13 11:26:36 +00:00
def __repr__(self):
axes = ",".join(
sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()])
2022-12-13 11:26:36 +00:00
)
return "<TupleVariation %s %s>" % (axes, self.coordinates)
2022-12-13 11:26:36 +00:00
def __eq__(self, other):
return self.coordinates == other.coordinates and self.axes == other.axes
2022-12-13 11:26:36 +00:00
def getUsedPoints(self):
2021-04-08 19:33:57 -06:00
# 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
2022-12-13 11:26:36 +00:00
def hasImpact(self):
"""Returns True if this TupleVariation has any visible impact.
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
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()
2022-12-13 11:26:36 +00:00
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())))
2022-12-13 11:26:36 +00:00
def compile(self, axisTags, sharedCoordIndices={}, pointData=None):
assert set(self.axes.keys()) <= set(axisTags), (
"Unknown axis tag found.",
self.axes.keys(),
axisTags,
)
2022-12-13 11:26:36 +00:00
tupleData = []
auxData = []
2022-12-13 11:26:36 +00:00
if pointData is None:
usedPoints = self.getUsedPoints()
if usedPoints is None: # Nothing to encode
return b"", b""
pointData = self.compilePoints(usedPoints)
2022-12-13 11:26:36 +00:00
coord = self.compileCoord(axisTags)
flags = sharedCoordIndices.get(coord)
if flags is None:
flags = EMBEDDED_PEAK_TUPLE
tupleData.append(coord)
2022-12-13 11:26:36 +00:00
intermediateCoord = self.compileIntermediateCoord(axisTags)
if intermediateCoord is not None:
flags |= INTERMEDIATE_REGION
tupleData.append(intermediateCoord)
2022-12-13 11:26:36 +00:00
# pointData of b'' implies "use shared points".
if pointData:
flags |= PRIVATE_POINT_NUMBERS
auxData.append(pointData)
2022-12-13 11:26:36 +00:00
auxData.append(self.compileDeltas())
auxData = b"".join(auxData)
2022-12-13 11:26:36 +00:00
tupleData.insert(0, struct.pack(">HH", len(auxData), flags))
return b"".join(tupleData), auxData
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
@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
2022-12-13 11:26:36 +00:00
@staticmethod
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.
2021-04-08 19:33:57 -06:00
# 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"
2022-12-13 11:26:36 +00:00
# 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)
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
MAX_RUN_LENGTH = 127
pos = 0
lastValue = 0
while pos < numPoints:
runLength = 0
2022-12-13 11:26:36 +00:00
headerPos = len(result)
result.append(0)
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
return result
2022-12-13 11:26:36 +00:00
@staticmethod
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)
2022-12-13 11:26:36 +00:00
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()
2022-12-13 11:26:36 +00:00
assert len(points) == numPointsInRun
pos += pointsSize
2022-12-13 11:26:36 +00:00
result.extend(points)
2022-12-13 11:26:36 +00:00
# Convert relative to absolute
absolute = []
current = 0
for delta in result:
current += delta
absolute.append(current)
result = absolute
del absolute
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
)
return (result, pos)
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
@staticmethod
def compileDeltaValues_(deltas, bytearr=None):
"""[value1, value2, value3, ...] --> bytearray
2022-12-13 11:26:36 +00:00
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)
else:
pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
return bytearr
2022-12-13 11:26:36 +00:00
@staticmethod
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
2022-12-13 11:26:36 +00:00
@staticmethod
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
2022-12-13 11:26:36 +00:00
@staticmethod
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
2022-12-13 11:26:36 +00:00
# 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
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
2022-12-13 11:26:36 +00:00
@staticmethod
def decompileDeltas_(numDeltas, data, offset):
"""(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)"""
result = []
pos = offset
while len(result) < numDeltas:
runHeader = 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)
2022-12-13 11:26:36 +00:00
@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
2022-12-13 11:26:36 +00:00
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
)
2022-12-13 11:26:36 +00:00
def scaleDeltas(self, scalar):
if scalar == 1.0:
return # no change
coordWidth = self.getCoordWidth()
self.coordinates = [
2024-02-06 15:47:35 +02:00
(
None
if d is None
else d * scalar if coordWidth == 1 else (d[0] * scalar, d[1] * scalar)
)
for d in self.coordinates
]
2022-12-13 11:26:36 +00:00
def roundDeltas(self):
coordWidth = self.getCoordWidth()
self.coordinates = [
2024-02-06 15:47:35 +02:00
(
None
if d is None
else otRound(d) if coordWidth == 1 else (otRound(d[0]), otRound(d[1]))
)
for d in self.coordinates
]
2022-12-13 11:26:36 +00:00
def calcInferredDeltas(self, origCoords, endPts):
from fontTools.varLib.iup import iup_delta
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
def optimize(self, origCoords, endPts, tolerance=0.5, isComposite=False):
from fontTools.varLib.iup import iup_delta_optimize
2022-12-13 11:26:36 +00:00
if None in self.coordinates:
return # already optimized
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
# 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)
2022-12-13 11:26:36 +00:00
if optimizedLength < unoptimizedLength:
self.coordinates = varOpt.coordinates
2022-12-13 11:26:36 +00:00
2022-08-10 09:10:43 -06:00
def __imul__(self, scalar):
self.scaleDeltas(scalar)
return self
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
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]
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
# 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
2022-12-13 11:26:36 +00:00
if not variations:
return (0, b"", b"")
2022-12-13 11:26:36 +00:00
n = len(variations[0].coordinates)
assert all(
len(v.coordinates) == n for v in variations
), "Variation sets have different sizes"
2022-12-13 11:26:36 +00:00
compiledPoints = {
pointSet: TupleVariation.compilePoints(pointSet) for pointSet in pointSetCount
}
2022-12-13 11:26:36 +00:00
tupleVariationCount = len(variations)
tuples = []
data = []
2022-12-13 11:26:36 +00:00
if useSharedPoints:
# Find point-set which saves most bytes.
def key(pn):
pointSet = pn[0]
count = pn[1]
return len(compiledPoints[pointSet]) * (count - 1)
2022-12-13 11:26:36 +00:00
sharedPoints = max(pointSetCount.items(), key=key)[0]
2022-12-13 11:26:36 +00:00
data.append(compiledPoints[sharedPoints])
tupleVariationCount |= TUPLES_SHARE_POINT_NUMBERS
2022-12-13 11:26:36 +00:00
# b'' implies "use shared points"
pointDatas = [
compiledPoints[points] if points != sharedPoints else b""
for points in pointDatas
]
2022-12-13 11:26:36 +00:00
for v, p in zip(variations, pointDatas):
thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p)
2022-12-13 11:26:36 +00:00
tuples.append(thisTuple)
data.append(thisData)
2022-12-13 11:26:36 +00:00
tuples = b"".join(tuples)
data = b"".join(data)
2017-01-10 17:55:30 +01:00
return tupleVariationCount, tuples, data
2022-12-13 11:26:36 +00:00
2017-01-10 17:55:30 +01:00
def decompileTupleVariationStore(
tableTag,
axisTags,
tupleVariationCount,
pointCount,
sharedTuples,
data,
pos,
dataPos,
):
numAxes = len(axisTags)
result = []
2017-01-10 17:55:30 +01:00
if (tupleVariationCount & TUPLES_SHARE_POINT_NUMBERS) != 0:
sharedPoints, dataPos = TupleVariation.decompilePoints_(
pointCount, data, dataPos, tableTag
)
else:
sharedPoints = []
2017-01-10 17:55:30 +01:00
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,
)
2022-12-13 11:26:36 +00:00
)
pos += tupleSize
dataPos += dataSize
return result
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
deltas = [None] * pointCount
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
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)
2022-12-13 11:26:36 +00:00
return TupleVariation(axes, deltas)
def inferRegion_(peak):
"""Infer start and end for a (non-intermediate) region
2022-12-13 11:26:36 +00:00
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)