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