Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/TupleVariation.py: 12%
522 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1from fontTools.misc.fixedTools import (
2 fixedToFloat as fi2fl,
3 floatToFixed as fl2fi,
4 floatToFixedToStr as fl2str,
5 strToFixedToFloat as str2fl,
6 otRound,
7)
8from fontTools.misc.textTools import safeEval
9import array
10from collections import Counter, defaultdict
11import io
12import logging
13import struct
14import sys
17# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
19EMBEDDED_PEAK_TUPLE = 0x8000
20INTERMEDIATE_REGION = 0x4000
21PRIVATE_POINT_NUMBERS = 0x2000
23DELTAS_ARE_ZERO = 0x80
24DELTAS_ARE_WORDS = 0x40
25DELTA_RUN_COUNT_MASK = 0x3F
27POINTS_ARE_WORDS = 0x80
28POINT_RUN_COUNT_MASK = 0x7F
30TUPLES_SHARE_POINT_NUMBERS = 0x8000
31TUPLE_COUNT_MASK = 0x0FFF
32TUPLE_INDEX_MASK = 0x0FFF
34log = logging.getLogger(__name__)
37class TupleVariation(object):
38 def __init__(self, axes, coordinates):
39 self.axes = axes.copy()
40 self.coordinates = list(coordinates)
42 def __repr__(self):
43 axes = ",".join(
44 sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()])
45 )
46 return "<TupleVariation %s %s>" % (axes, self.coordinates)
48 def __eq__(self, other):
49 return self.coordinates == other.coordinates and self.axes == other.axes
51 def getUsedPoints(self):
52 # Empty set means "all points used".
53 if None not in self.coordinates:
54 return frozenset()
55 used = frozenset([i for i, p in enumerate(self.coordinates) if p is not None])
56 # Return None if no points used.
57 return used if used else None
59 def hasImpact(self):
60 """Returns True if this TupleVariation has any visible impact.
62 If the result is False, the TupleVariation can be omitted from the font
63 without making any visible difference.
64 """
65 return any(c is not None for c in self.coordinates)
67 def toXML(self, writer, axisTags):
68 writer.begintag("tuple")
69 writer.newline()
70 for axis in axisTags:
71 value = self.axes.get(axis)
72 if value is not None:
73 minValue, value, maxValue = value
74 defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
75 defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
76 if minValue == defaultMinValue and maxValue == defaultMaxValue:
77 writer.simpletag("coord", axis=axis, value=fl2str(value, 14))
78 else:
79 attrs = [
80 ("axis", axis),
81 ("min", fl2str(minValue, 14)),
82 ("value", fl2str(value, 14)),
83 ("max", fl2str(maxValue, 14)),
84 ]
85 writer.simpletag("coord", attrs)
86 writer.newline()
87 wrote_any_deltas = False
88 for i, delta in enumerate(self.coordinates):
89 if type(delta) == tuple and len(delta) == 2:
90 writer.simpletag("delta", pt=i, x=delta[0], y=delta[1])
91 writer.newline()
92 wrote_any_deltas = True
93 elif type(delta) == int:
94 writer.simpletag("delta", cvt=i, value=delta)
95 writer.newline()
96 wrote_any_deltas = True
97 elif delta is not None:
98 log.error("bad delta format")
99 writer.comment("bad delta #%d" % i)
100 writer.newline()
101 wrote_any_deltas = True
102 if not wrote_any_deltas:
103 writer.comment("no deltas")
104 writer.newline()
105 writer.endtag("tuple")
106 writer.newline()
108 def fromXML(self, name, attrs, _content):
109 if name == "coord":
110 axis = attrs["axis"]
111 value = str2fl(attrs["value"], 14)
112 defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
113 defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
114 minValue = str2fl(attrs.get("min", defaultMinValue), 14)
115 maxValue = str2fl(attrs.get("max", defaultMaxValue), 14)
116 self.axes[axis] = (minValue, value, maxValue)
117 elif name == "delta":
118 if "pt" in attrs:
119 point = safeEval(attrs["pt"])
120 x = safeEval(attrs["x"])
121 y = safeEval(attrs["y"])
122 self.coordinates[point] = (x, y)
123 elif "cvt" in attrs:
124 cvt = safeEval(attrs["cvt"])
125 value = safeEval(attrs["value"])
126 self.coordinates[cvt] = value
127 else:
128 log.warning("bad delta format: %s" % ", ".join(sorted(attrs.keys())))
130 def compile(self, axisTags, sharedCoordIndices={}, pointData=None):
131 assert set(self.axes.keys()) <= set(axisTags), (
132 "Unknown axis tag found.",
133 self.axes.keys(),
134 axisTags,
135 )
137 tupleData = []
138 auxData = []
140 if pointData is None:
141 usedPoints = self.getUsedPoints()
142 if usedPoints is None: # Nothing to encode
143 return b"", b""
144 pointData = self.compilePoints(usedPoints)
146 coord = self.compileCoord(axisTags)
147 flags = sharedCoordIndices.get(coord)
148 if flags is None:
149 flags = EMBEDDED_PEAK_TUPLE
150 tupleData.append(coord)
152 intermediateCoord = self.compileIntermediateCoord(axisTags)
153 if intermediateCoord is not None:
154 flags |= INTERMEDIATE_REGION
155 tupleData.append(intermediateCoord)
157 # pointData of b'' implies "use shared points".
158 if pointData:
159 flags |= PRIVATE_POINT_NUMBERS
160 auxData.append(pointData)
162 auxData.append(self.compileDeltas())
163 auxData = b"".join(auxData)
165 tupleData.insert(0, struct.pack(">HH", len(auxData), flags))
166 return b"".join(tupleData), auxData
168 def compileCoord(self, axisTags):
169 result = []
170 axes = self.axes
171 for axis in axisTags:
172 triple = axes.get(axis)
173 if triple is None:
174 result.append(b"\0\0")
175 else:
176 result.append(struct.pack(">h", fl2fi(triple[1], 14)))
177 return b"".join(result)
179 def compileIntermediateCoord(self, axisTags):
180 needed = False
181 for axis in axisTags:
182 minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
183 defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
184 defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
185 if (minValue != defaultMinValue) or (maxValue != defaultMaxValue):
186 needed = True
187 break
188 if not needed:
189 return None
190 minCoords = []
191 maxCoords = []
192 for axis in axisTags:
193 minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
194 minCoords.append(struct.pack(">h", fl2fi(minValue, 14)))
195 maxCoords.append(struct.pack(">h", fl2fi(maxValue, 14)))
196 return b"".join(minCoords + maxCoords)
198 @staticmethod
199 def decompileCoord_(axisTags, data, offset):
200 coord = {}
201 pos = offset
202 for axis in axisTags:
203 coord[axis] = fi2fl(struct.unpack(">h", data[pos : pos + 2])[0], 14)
204 pos += 2
205 return coord, pos
207 @staticmethod
208 def compilePoints(points):
209 # If the set consists of all points in the glyph, it gets encoded with
210 # a special encoding: a single zero byte.
211 #
212 # To use this optimization, points passed in must be empty set.
213 # The following two lines are not strictly necessary as the main code
214 # below would emit the same. But this is most common and faster.
215 if not points:
216 return b"\0"
218 # In the 'gvar' table, the packing of point numbers is a little surprising.
219 # It consists of multiple runs, each being a delta-encoded list of integers.
220 # For example, the point set {17, 18, 19, 20, 21, 22, 23} gets encoded as
221 # [6, 17, 1, 1, 1, 1, 1, 1]. The first value (6) is the run length minus 1.
222 # There are two types of runs, with values being either 8 or 16 bit unsigned
223 # integers.
224 points = list(points)
225 points.sort()
226 numPoints = len(points)
228 result = bytearray()
229 # The binary representation starts with the total number of points in the set,
230 # encoded into one or two bytes depending on the value.
231 if numPoints < 0x80:
232 result.append(numPoints)
233 else:
234 result.append((numPoints >> 8) | 0x80)
235 result.append(numPoints & 0xFF)
237 MAX_RUN_LENGTH = 127
238 pos = 0
239 lastValue = 0
240 while pos < numPoints:
241 runLength = 0
243 headerPos = len(result)
244 result.append(0)
246 useByteEncoding = None
247 while pos < numPoints and runLength <= MAX_RUN_LENGTH:
248 curValue = points[pos]
249 delta = curValue - lastValue
250 if useByteEncoding is None:
251 useByteEncoding = 0 <= delta <= 0xFF
252 if useByteEncoding and (delta > 0xFF or delta < 0):
253 # we need to start a new run (which will not use byte encoding)
254 break
255 # TODO This never switches back to a byte-encoding from a short-encoding.
256 # That's suboptimal.
257 if useByteEncoding:
258 result.append(delta)
259 else:
260 result.append(delta >> 8)
261 result.append(delta & 0xFF)
262 lastValue = curValue
263 pos += 1
264 runLength += 1
265 if useByteEncoding:
266 result[headerPos] = runLength - 1
267 else:
268 result[headerPos] = (runLength - 1) | POINTS_ARE_WORDS
270 return result
272 @staticmethod
273 def decompilePoints_(numPoints, data, offset, tableTag):
274 """(numPoints, data, offset, tableTag) --> ([point1, point2, ...], newOffset)"""
275 assert tableTag in ("cvar", "gvar")
276 pos = offset
277 numPointsInData = data[pos]
278 pos += 1
279 if (numPointsInData & POINTS_ARE_WORDS) != 0:
280 numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | data[pos]
281 pos += 1
282 if numPointsInData == 0:
283 return (range(numPoints), pos)
285 result = []
286 while len(result) < numPointsInData:
287 runHeader = data[pos]
288 pos += 1
289 numPointsInRun = (runHeader & POINT_RUN_COUNT_MASK) + 1
290 point = 0
291 if (runHeader & POINTS_ARE_WORDS) != 0:
292 points = array.array("H")
293 pointsSize = numPointsInRun * 2
294 else:
295 points = array.array("B")
296 pointsSize = numPointsInRun
297 points.frombytes(data[pos : pos + pointsSize])
298 if sys.byteorder != "big":
299 points.byteswap()
301 assert len(points) == numPointsInRun
302 pos += pointsSize
304 result.extend(points)
306 # Convert relative to absolute
307 absolute = []
308 current = 0
309 for delta in result:
310 current += delta
311 absolute.append(current)
312 result = absolute
313 del absolute
315 badPoints = {str(p) for p in result if p < 0 or p >= numPoints}
316 if badPoints:
317 log.warning(
318 "point %s out of range in '%s' table"
319 % (",".join(sorted(badPoints)), tableTag)
320 )
321 return (result, pos)
323 def compileDeltas(self):
324 deltaX = []
325 deltaY = []
326 if self.getCoordWidth() == 2:
327 for c in self.coordinates:
328 if c is None:
329 continue
330 deltaX.append(c[0])
331 deltaY.append(c[1])
332 else:
333 for c in self.coordinates:
334 if c is None:
335 continue
336 deltaX.append(c)
337 bytearr = bytearray()
338 self.compileDeltaValues_(deltaX, bytearr)
339 self.compileDeltaValues_(deltaY, bytearr)
340 return bytearr
342 @staticmethod
343 def compileDeltaValues_(deltas, bytearr=None):
344 """[value1, value2, value3, ...] --> bytearray
346 Emits a sequence of runs. Each run starts with a
347 byte-sized header whose 6 least significant bits
348 (header & 0x3F) indicate how many values are encoded
349 in this run. The stored length is the actual length
350 minus one; run lengths are thus in the range [1..64].
351 If the header byte has its most significant bit (0x80)
352 set, all values in this run are zero, and no data
353 follows. Otherwise, the header byte is followed by
354 ((header & 0x3F) + 1) signed values. If (header &
355 0x40) is clear, the delta values are stored as signed
356 bytes; if (header & 0x40) is set, the delta values are
357 signed 16-bit integers.
358 """ # Explaining the format because the 'gvar' spec is hard to understand.
359 if bytearr is None:
360 bytearr = bytearray()
361 pos = 0
362 numDeltas = len(deltas)
363 while pos < numDeltas:
364 value = deltas[pos]
365 if value == 0:
366 pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr)
367 elif -128 <= value <= 127:
368 pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr)
369 else:
370 pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
371 return bytearr
373 @staticmethod
374 def encodeDeltaRunAsZeroes_(deltas, offset, bytearr):
375 pos = offset
376 numDeltas = len(deltas)
377 while pos < numDeltas and deltas[pos] == 0:
378 pos += 1
379 runLength = pos - offset
380 while runLength >= 64:
381 bytearr.append(DELTAS_ARE_ZERO | 63)
382 runLength -= 64
383 if runLength:
384 bytearr.append(DELTAS_ARE_ZERO | (runLength - 1))
385 return pos
387 @staticmethod
388 def encodeDeltaRunAsBytes_(deltas, offset, bytearr):
389 pos = offset
390 numDeltas = len(deltas)
391 while pos < numDeltas:
392 value = deltas[pos]
393 if not (-128 <= value <= 127):
394 break
395 # Within a byte-encoded run of deltas, a single zero
396 # is best stored literally as 0x00 value. However,
397 # if are two or more zeroes in a sequence, it is
398 # better to start a new run. For example, the sequence
399 # of deltas [15, 15, 0, 15, 15] becomes 6 bytes
400 # (04 0F 0F 00 0F 0F) when storing the zero value
401 # literally, but 7 bytes (01 0F 0F 80 01 0F 0F)
402 # when starting a new run.
403 if value == 0 and pos + 1 < numDeltas and deltas[pos + 1] == 0:
404 break
405 pos += 1
406 runLength = pos - offset
407 while runLength >= 64:
408 bytearr.append(63)
409 bytearr.extend(array.array("b", deltas[offset : offset + 64]))
410 offset += 64
411 runLength -= 64
412 if runLength:
413 bytearr.append(runLength - 1)
414 bytearr.extend(array.array("b", deltas[offset:pos]))
415 return pos
417 @staticmethod
418 def encodeDeltaRunAsWords_(deltas, offset, bytearr):
419 pos = offset
420 numDeltas = len(deltas)
421 while pos < numDeltas:
422 value = deltas[pos]
423 # Within a word-encoded run of deltas, it is easiest
424 # to start a new run (with a different encoding)
425 # whenever we encounter a zero value. For example,
426 # the sequence [0x6666, 0, 0x7777] needs 7 bytes when
427 # storing the zero literally (42 66 66 00 00 77 77),
428 # and equally 7 bytes when starting a new run
429 # (40 66 66 80 40 77 77).
430 if value == 0:
431 break
433 # Within a word-encoded run of deltas, a single value
434 # in the range (-128..127) should be encoded literally
435 # because it is more compact. For example, the sequence
436 # [0x6666, 2, 0x7777] becomes 7 bytes when storing
437 # the value literally (42 66 66 00 02 77 77), but 8 bytes
438 # when starting a new run (40 66 66 00 02 40 77 77).
439 if (
440 (-128 <= value <= 127)
441 and pos + 1 < numDeltas
442 and (-128 <= deltas[pos + 1] <= 127)
443 ):
444 break
445 pos += 1
446 runLength = pos - offset
447 while runLength >= 64:
448 bytearr.append(DELTAS_ARE_WORDS | 63)
449 a = array.array("h", deltas[offset : offset + 64])
450 if sys.byteorder != "big":
451 a.byteswap()
452 bytearr.extend(a)
453 offset += 64
454 runLength -= 64
455 if runLength:
456 bytearr.append(DELTAS_ARE_WORDS | (runLength - 1))
457 a = array.array("h", deltas[offset:pos])
458 if sys.byteorder != "big":
459 a.byteswap()
460 bytearr.extend(a)
461 return pos
463 @staticmethod
464 def decompileDeltas_(numDeltas, data, offset):
465 """(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)"""
466 result = []
467 pos = offset
468 while len(result) < numDeltas:
469 runHeader = data[pos]
470 pos += 1
471 numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1
472 if (runHeader & DELTAS_ARE_ZERO) != 0:
473 result.extend([0] * numDeltasInRun)
474 else:
475 if (runHeader & DELTAS_ARE_WORDS) != 0:
476 deltas = array.array("h")
477 deltasSize = numDeltasInRun * 2
478 else:
479 deltas = array.array("b")
480 deltasSize = numDeltasInRun
481 deltas.frombytes(data[pos : pos + deltasSize])
482 if sys.byteorder != "big":
483 deltas.byteswap()
484 assert len(deltas) == numDeltasInRun
485 pos += deltasSize
486 result.extend(deltas)
487 assert len(result) == numDeltas
488 return (result, pos)
490 @staticmethod
491 def getTupleSize_(flags, axisCount):
492 size = 4
493 if (flags & EMBEDDED_PEAK_TUPLE) != 0:
494 size += axisCount * 2
495 if (flags & INTERMEDIATE_REGION) != 0:
496 size += axisCount * 4
497 return size
499 def getCoordWidth(self):
500 """Return 2 if coordinates are (x, y) as in gvar, 1 if single values
501 as in cvar, or 0 if empty.
502 """
503 firstDelta = next((c for c in self.coordinates if c is not None), None)
504 if firstDelta is None:
505 return 0 # empty or has no impact
506 if type(firstDelta) in (int, float):
507 return 1
508 if type(firstDelta) is tuple and len(firstDelta) == 2:
509 return 2
510 raise TypeError(
511 "invalid type of delta; expected (int or float) number, or "
512 "Tuple[number, number]: %r" % firstDelta
513 )
515 def scaleDeltas(self, scalar):
516 if scalar == 1.0:
517 return # no change
518 coordWidth = self.getCoordWidth()
519 self.coordinates = [
520 None
521 if d is None
522 else d * scalar
523 if coordWidth == 1
524 else (d[0] * scalar, d[1] * scalar)
525 for d in self.coordinates
526 ]
528 def roundDeltas(self):
529 coordWidth = self.getCoordWidth()
530 self.coordinates = [
531 None
532 if d is None
533 else otRound(d)
534 if coordWidth == 1
535 else (otRound(d[0]), otRound(d[1]))
536 for d in self.coordinates
537 ]
539 def calcInferredDeltas(self, origCoords, endPts):
540 from fontTools.varLib.iup import iup_delta
542 if self.getCoordWidth() == 1:
543 raise TypeError("Only 'gvar' TupleVariation can have inferred deltas")
544 if None in self.coordinates:
545 if len(self.coordinates) != len(origCoords):
546 raise ValueError(
547 "Expected len(origCoords) == %d; found %d"
548 % (len(self.coordinates), len(origCoords))
549 )
550 self.coordinates = iup_delta(self.coordinates, origCoords, endPts)
552 def optimize(self, origCoords, endPts, tolerance=0.5, isComposite=False):
553 from fontTools.varLib.iup import iup_delta_optimize
555 if None in self.coordinates:
556 return # already optimized
558 deltaOpt = iup_delta_optimize(
559 self.coordinates, origCoords, endPts, tolerance=tolerance
560 )
561 if None in deltaOpt:
562 if isComposite and all(d is None for d in deltaOpt):
563 # Fix for macOS composites
564 # https://github.com/fonttools/fonttools/issues/1381
565 deltaOpt = [(0, 0)] + [None] * (len(deltaOpt) - 1)
566 # Use "optimized" version only if smaller...
567 varOpt = TupleVariation(self.axes, deltaOpt)
569 # Shouldn't matter that this is different from fvar...?
570 axisTags = sorted(self.axes.keys())
571 tupleData, auxData = self.compile(axisTags)
572 unoptimizedLength = len(tupleData) + len(auxData)
573 tupleData, auxData = varOpt.compile(axisTags)
574 optimizedLength = len(tupleData) + len(auxData)
576 if optimizedLength < unoptimizedLength:
577 self.coordinates = varOpt.coordinates
579 def __imul__(self, scalar):
580 self.scaleDeltas(scalar)
581 return self
583 def __iadd__(self, other):
584 if not isinstance(other, TupleVariation):
585 return NotImplemented
586 deltas1 = self.coordinates
587 length = len(deltas1)
588 deltas2 = other.coordinates
589 if len(deltas2) != length:
590 raise ValueError("cannot sum TupleVariation deltas with different lengths")
591 # 'None' values have different meanings in gvar vs cvar TupleVariations:
592 # within the gvar, when deltas are not provided explicitly for some points,
593 # they need to be inferred; whereas for the 'cvar' table, if deltas are not
594 # provided for some CVT values, then no adjustments are made (i.e. None == 0).
595 # Thus, we cannot sum deltas for gvar TupleVariations if they contain
596 # inferred inferred deltas (the latter need to be computed first using
597 # 'calcInferredDeltas' method), but we can treat 'None' values in cvar
598 # deltas as if they are zeros.
599 if self.getCoordWidth() == 2:
600 for i, d2 in zip(range(length), deltas2):
601 d1 = deltas1[i]
602 try:
603 deltas1[i] = (d1[0] + d2[0], d1[1] + d2[1])
604 except TypeError:
605 raise ValueError("cannot sum gvar deltas with inferred points")
606 else:
607 for i, d2 in zip(range(length), deltas2):
608 d1 = deltas1[i]
609 if d1 is not None and d2 is not None:
610 deltas1[i] = d1 + d2
611 elif d1 is None and d2 is not None:
612 deltas1[i] = d2
613 # elif d2 is None do nothing
614 return self
617def decompileSharedTuples(axisTags, sharedTupleCount, data, offset):
618 result = []
619 for _ in range(sharedTupleCount):
620 t, offset = TupleVariation.decompileCoord_(axisTags, data, offset)
621 result.append(t)
622 return result
625def compileSharedTuples(
626 axisTags, variations, MAX_NUM_SHARED_COORDS=TUPLE_INDEX_MASK + 1
627):
628 coordCount = Counter()
629 for var in variations:
630 coord = var.compileCoord(axisTags)
631 coordCount[coord] += 1
632 # In python < 3.7, most_common() ordering is non-deterministic
633 # so apply a sort to make sure the ordering is consistent.
634 sharedCoords = sorted(
635 coordCount.most_common(MAX_NUM_SHARED_COORDS),
636 key=lambda item: (-item[1], item[0]),
637 )
638 return [c[0] for c in sharedCoords if c[1] > 1]
641def compileTupleVariationStore(
642 variations, pointCount, axisTags, sharedTupleIndices, useSharedPoints=True
643):
644 # pointCount is actually unused. Keeping for API compat.
645 del pointCount
646 newVariations = []
647 pointDatas = []
648 # Compile all points and figure out sharing if desired
649 sharedPoints = None
651 # Collect, count, and compile point-sets for all variation sets
652 pointSetCount = defaultdict(int)
653 for v in variations:
654 points = v.getUsedPoints()
655 if points is None: # Empty variations
656 continue
657 pointSetCount[points] += 1
658 newVariations.append(v)
659 pointDatas.append(points)
660 variations = newVariations
661 del newVariations
663 if not variations:
664 return (0, b"", b"")
666 n = len(variations[0].coordinates)
667 assert all(
668 len(v.coordinates) == n for v in variations
669 ), "Variation sets have different sizes"
671 compiledPoints = {
672 pointSet: TupleVariation.compilePoints(pointSet) for pointSet in pointSetCount
673 }
675 tupleVariationCount = len(variations)
676 tuples = []
677 data = []
679 if useSharedPoints:
680 # Find point-set which saves most bytes.
681 def key(pn):
682 pointSet = pn[0]
683 count = pn[1]
684 return len(compiledPoints[pointSet]) * (count - 1)
686 sharedPoints = max(pointSetCount.items(), key=key)[0]
688 data.append(compiledPoints[sharedPoints])
689 tupleVariationCount |= TUPLES_SHARE_POINT_NUMBERS
691 # b'' implies "use shared points"
692 pointDatas = [
693 compiledPoints[points] if points != sharedPoints else b""
694 for points in pointDatas
695 ]
697 for v, p in zip(variations, pointDatas):
698 thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p)
700 tuples.append(thisTuple)
701 data.append(thisData)
703 tuples = b"".join(tuples)
704 data = b"".join(data)
705 return tupleVariationCount, tuples, data
708def decompileTupleVariationStore(
709 tableTag,
710 axisTags,
711 tupleVariationCount,
712 pointCount,
713 sharedTuples,
714 data,
715 pos,
716 dataPos,
717):
718 numAxes = len(axisTags)
719 result = []
720 if (tupleVariationCount & TUPLES_SHARE_POINT_NUMBERS) != 0:
721 sharedPoints, dataPos = TupleVariation.decompilePoints_(
722 pointCount, data, dataPos, tableTag
723 )
724 else:
725 sharedPoints = []
726 for _ in range(tupleVariationCount & TUPLE_COUNT_MASK):
727 dataSize, flags = struct.unpack(">HH", data[pos : pos + 4])
728 tupleSize = TupleVariation.getTupleSize_(flags, numAxes)
729 tupleData = data[pos : pos + tupleSize]
730 pointDeltaData = data[dataPos : dataPos + dataSize]
731 result.append(
732 decompileTupleVariation_(
733 pointCount,
734 sharedTuples,
735 sharedPoints,
736 tableTag,
737 axisTags,
738 tupleData,
739 pointDeltaData,
740 )
741 )
742 pos += tupleSize
743 dataPos += dataSize
744 return result
747def decompileTupleVariation_(
748 pointCount, sharedTuples, sharedPoints, tableTag, axisTags, data, tupleData
749):
750 assert tableTag in ("cvar", "gvar"), tableTag
751 flags = struct.unpack(">H", data[2:4])[0]
752 pos = 4
753 if (flags & EMBEDDED_PEAK_TUPLE) == 0:
754 peak = sharedTuples[flags & TUPLE_INDEX_MASK]
755 else:
756 peak, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
757 if (flags & INTERMEDIATE_REGION) != 0:
758 start, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
759 end, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
760 else:
761 start, end = inferRegion_(peak)
762 axes = {}
763 for axis in axisTags:
764 region = start[axis], peak[axis], end[axis]
765 if region != (0.0, 0.0, 0.0):
766 axes[axis] = region
767 pos = 0
768 if (flags & PRIVATE_POINT_NUMBERS) != 0:
769 points, pos = TupleVariation.decompilePoints_(
770 pointCount, tupleData, pos, tableTag
771 )
772 else:
773 points = sharedPoints
775 deltas = [None] * pointCount
777 if tableTag == "cvar":
778 deltas_cvt, pos = TupleVariation.decompileDeltas_(len(points), tupleData, pos)
779 for p, delta in zip(points, deltas_cvt):
780 if 0 <= p < pointCount:
781 deltas[p] = delta
783 elif tableTag == "gvar":
784 deltas_x, pos = TupleVariation.decompileDeltas_(len(points), tupleData, pos)
785 deltas_y, pos = TupleVariation.decompileDeltas_(len(points), tupleData, pos)
786 for p, x, y in zip(points, deltas_x, deltas_y):
787 if 0 <= p < pointCount:
788 deltas[p] = (x, y)
790 return TupleVariation(axes, deltas)
793def inferRegion_(peak):
794 """Infer start and end for a (non-intermediate) region
796 This helper function computes the applicability region for
797 variation tuples whose INTERMEDIATE_REGION flag is not set in the
798 TupleVariationHeader structure. Variation tuples apply only to
799 certain regions of the variation space; outside that region, the
800 tuple has no effect. To make the binary encoding more compact,
801 TupleVariationHeaders can omit the intermediateStartTuple and
802 intermediateEndTuple fields.
803 """
804 start, end = {}, {}
805 for axis, value in peak.items():
806 start[axis] = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
807 end[axis] = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
808 return (start, end)