| from __future__ import print_function, division, absolute_import |
| from fontTools.misc.py23 import * |
| from fontTools.misc.fixedTools import fixedToFloat, floatToFixed, 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. |
| """ |
| for c in self.coordinates: |
| if c is not None: |
| return True |
| return False |
| |
| 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 = (float(v) for v in 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=value) |
| else: |
| attrs = [ |
| ("axis", axis), |
| ("min", minValue), |
| ("value", value), |
| ("max", maxValue), |
| ] |
| 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 = float(attrs["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 |
| minValue = float(attrs.get("min", defaultMinValue)) |
| maxValue = float(attrs.get("max", defaultMaxValue)) |
| 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", floatToFixed(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", floatToFixed(minValue, 14))) |
| maxCoords.append(struct.pack(">h", floatToFixed(maxValue, 14))) |
| return bytesjoin(minCoords + maxCoords) |
| |
| @staticmethod |
| def decompileCoord_(axisTags, data, offset): |
| coord = {} |
| pos = offset |
| for axis in axisTags: |
| coord[axis] = fixedToFloat(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.fromstring(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 ValueError("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.fromstring(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 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) |