commit
b50b6f23c3
717
Lib/fontTools/ttLib/tables/_g_v_a_r.py
Normal file
717
Lib/fontTools/ttLib/tables/_g_v_a_r.py
Normal file
@ -0,0 +1,717 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools import ttLib
|
||||||
|
from fontTools.misc import sstruct
|
||||||
|
from fontTools.misc.fixedTools import fixedToFloat, floatToFixed
|
||||||
|
from fontTools.misc.textTools import safeEval
|
||||||
|
from fontTools.ttLib import TTLibError
|
||||||
|
from . import DefaultTable
|
||||||
|
import array
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# Apple's documentation of 'gvar':
|
||||||
|
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
|
||||||
|
#
|
||||||
|
# FreeType2 source code for parsing 'gvar':
|
||||||
|
# http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c
|
||||||
|
|
||||||
|
GVAR_HEADER_FORMAT = """
|
||||||
|
> # big endian
|
||||||
|
version: H
|
||||||
|
reserved: H
|
||||||
|
axisCount: H
|
||||||
|
sharedCoordCount: H
|
||||||
|
offsetToCoord: I
|
||||||
|
glyphCount: H
|
||||||
|
flags: H
|
||||||
|
offsetToData: I
|
||||||
|
"""
|
||||||
|
|
||||||
|
GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
|
||||||
|
|
||||||
|
TUPLES_SHARE_POINT_NUMBERS = 0x8000
|
||||||
|
TUPLE_COUNT_MASK = 0x0fff
|
||||||
|
|
||||||
|
EMBEDDED_TUPLE_COORD = 0x8000
|
||||||
|
INTERMEDIATE_TUPLE = 0x4000
|
||||||
|
PRIVATE_POINT_NUMBERS = 0x2000
|
||||||
|
TUPLE_INDEX_MASK = 0x0fff
|
||||||
|
|
||||||
|
DELTAS_ARE_ZERO = 0x80
|
||||||
|
DELTAS_ARE_WORDS = 0x40
|
||||||
|
DELTA_RUN_COUNT_MASK = 0x3f
|
||||||
|
|
||||||
|
POINTS_ARE_WORDS = 0x80
|
||||||
|
POINT_RUN_COUNT_MASK = 0x7f
|
||||||
|
|
||||||
|
|
||||||
|
class table__g_v_a_r(DefaultTable.DefaultTable):
|
||||||
|
|
||||||
|
dependencies = ["fvar", "glyf"]
|
||||||
|
|
||||||
|
def compile(self, ttFont):
|
||||||
|
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis]
|
||||||
|
|
||||||
|
sharedCoords = self.compileSharedCoords_(axisTags)
|
||||||
|
sharedCoordIndices = dict([(coord, i) for i, coord in enumerate(sharedCoords)])
|
||||||
|
sharedCoordSize = sum([len(c) for c in sharedCoords])
|
||||||
|
|
||||||
|
compiledGlyphs = self.compileGlyphs_(ttFont, axisTags, sharedCoordIndices)
|
||||||
|
offset = 0
|
||||||
|
offsets = []
|
||||||
|
for glyph in compiledGlyphs:
|
||||||
|
offsets.append(offset)
|
||||||
|
offset += len(glyph)
|
||||||
|
offsets.append(offset)
|
||||||
|
compiledOffsets, tableFormat = self.compileOffsets_(offsets)
|
||||||
|
|
||||||
|
header = {}
|
||||||
|
header["version"] = self.version
|
||||||
|
header["reserved"] = self.reserved
|
||||||
|
header["axisCount"] = len(axisTags)
|
||||||
|
header["sharedCoordCount"] = len(sharedCoords)
|
||||||
|
header["offsetToCoord"] = GVAR_HEADER_SIZE + len(compiledOffsets)
|
||||||
|
header["glyphCount"] = len(compiledGlyphs)
|
||||||
|
header["flags"] = tableFormat
|
||||||
|
header["offsetToData"] = header["offsetToCoord"] + sharedCoordSize
|
||||||
|
compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header)
|
||||||
|
|
||||||
|
result = [compiledHeader, compiledOffsets]
|
||||||
|
result.extend(sharedCoords)
|
||||||
|
result.extend(compiledGlyphs)
|
||||||
|
return bytesjoin(result)
|
||||||
|
|
||||||
|
def compileSharedCoords_(self, axisTags):
|
||||||
|
coordCount = {}
|
||||||
|
for variations in self.variations.values():
|
||||||
|
for gvar in variations:
|
||||||
|
coord = gvar.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 compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
|
||||||
|
result = []
|
||||||
|
for glyphName in ttFont.getGlyphOrder():
|
||||||
|
glyph = ttFont["glyf"][glyphName]
|
||||||
|
numPointsInGlyph = self.getNumPoints_(glyph)
|
||||||
|
result.append(self.compileGlyph_(glyphName, numPointsInGlyph, axisTags, sharedCoordIndices))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def compileGlyph_(self, glyphName, numPointsInGlyph, axisTags, sharedCoordIndices):
|
||||||
|
variations = self.variations.get(glyphName, [])
|
||||||
|
variations = [v for v in variations if v.hasImpact()]
|
||||||
|
if len(variations) == 0:
|
||||||
|
return 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: 18}
|
||||||
|
# (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".
|
||||||
|
allPoints = set(range(numPointsInGlyph))
|
||||||
|
tuples = []
|
||||||
|
data = []
|
||||||
|
someTuplesSharePoints = False
|
||||||
|
for gvar in variations:
|
||||||
|
privateTuple, privateData = gvar.compile(axisTags, sharedCoordIndices, sharedPoints=None)
|
||||||
|
sharedTuple, sharedData = gvar.compile(axisTags, sharedCoordIndices, sharedPoints=allPoints)
|
||||||
|
# TODO: If we use shared points, Apple MacOS X 10.9.5 cannot display our fonts.
|
||||||
|
# This is probably a problem with our code; find the problem and fix it.
|
||||||
|
#if (len(sharedTuple) + len(sharedData)) < (len(privateTuple) + len(privateData)):
|
||||||
|
if False:
|
||||||
|
tuples.append(sharedTuple)
|
||||||
|
data.append(sharedData)
|
||||||
|
someTuplesSharePoints = True
|
||||||
|
else:
|
||||||
|
tuples.append(privateTuple)
|
||||||
|
data.append(privateData)
|
||||||
|
if someTuplesSharePoints:
|
||||||
|
data = bytechr(0) + bytesjoin(data) # 0x00 = "all points in glyph"
|
||||||
|
tupleCount = TUPLES_SHARE_POINT_NUMBERS | len(tuples)
|
||||||
|
else:
|
||||||
|
data = bytesjoin(data)
|
||||||
|
tupleCount = len(tuples)
|
||||||
|
tuples = bytesjoin(tuples)
|
||||||
|
result = struct.pack(">HH", tupleCount, 4 + len(tuples)) + tuples + data
|
||||||
|
if len(result) % 2 != 0:
|
||||||
|
result = result + b"\0" # padding
|
||||||
|
return result
|
||||||
|
|
||||||
|
def decompile(self, data, ttFont):
|
||||||
|
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis]
|
||||||
|
glyphs = ttFont.getGlyphOrder()
|
||||||
|
sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
|
||||||
|
assert len(glyphs) == self.glyphCount
|
||||||
|
assert len(axisTags) == self.axisCount
|
||||||
|
offsets = self.decompileOffsets_(data[GVAR_HEADER_SIZE:], tableFormat=(self.flags & 1), glyphCount=self.glyphCount)
|
||||||
|
sharedCoords = self.decompileSharedCoords_(axisTags, data)
|
||||||
|
self.variations = {}
|
||||||
|
for i in range(self.glyphCount):
|
||||||
|
glyphName = glyphs[i]
|
||||||
|
glyph = ttFont["glyf"][glyphName]
|
||||||
|
numPointsInGlyph = self.getNumPoints_(glyph)
|
||||||
|
gvarData = data[self.offsetToData + offsets[i] : self.offsetToData + offsets[i + 1]]
|
||||||
|
self.variations[glyphName] = \
|
||||||
|
self.decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
|
||||||
|
|
||||||
|
def decompileSharedCoords_(self, axisTags, data):
|
||||||
|
result, _pos = GlyphVariation.decompileCoords_(axisTags, self.sharedCoordCount, data, self.offsetToCoord)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decompileOffsets_(data, tableFormat, glyphCount):
|
||||||
|
if tableFormat == 0:
|
||||||
|
# Short format: array of UInt16
|
||||||
|
offsets = array.array("H")
|
||||||
|
offsetsSize = (glyphCount + 1) * 2
|
||||||
|
else:
|
||||||
|
# Long format: array of UInt32
|
||||||
|
offsets = array.array("I")
|
||||||
|
offsetsSize = (glyphCount + 1) * 4
|
||||||
|
offsets.fromstring(data[0 : offsetsSize])
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
offsets.byteswap()
|
||||||
|
|
||||||
|
# In the short format, offsets need to be multiplied by 2.
|
||||||
|
# This is not documented in Apple's TrueType specification,
|
||||||
|
# but can be inferred from the FreeType implementation, and
|
||||||
|
# we could verify it with two sample GX fonts.
|
||||||
|
if tableFormat == 0:
|
||||||
|
offsets = [off * 2 for off in offsets]
|
||||||
|
|
||||||
|
return offsets
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compileOffsets_(offsets):
|
||||||
|
"""Packs a list of offsets into a 'gvar' offset table.
|
||||||
|
|
||||||
|
Returns a pair (bytestring, tableFormat). Bytestring is the
|
||||||
|
packed offset table. Format indicates whether the table
|
||||||
|
uses short (tableFormat=0) or long (tableFormat=1) integers.
|
||||||
|
The returned tableFormat should get packed into the flags field
|
||||||
|
of the 'gvar' header.
|
||||||
|
"""
|
||||||
|
assert len(offsets) >= 2
|
||||||
|
for i in range(1, len(offsets)):
|
||||||
|
assert offsets[i - 1] <= offsets[i]
|
||||||
|
if max(offsets) <= 0xffff * 2:
|
||||||
|
packed = array.array("H", [n >> 1 for n in offsets])
|
||||||
|
tableFormat = 0
|
||||||
|
else:
|
||||||
|
packed = array.array("I", offsets)
|
||||||
|
tableFormat = 1
|
||||||
|
if sys.byteorder != "big":
|
||||||
|
packed.byteswap()
|
||||||
|
return (packed.tostring(), tableFormat)
|
||||||
|
|
||||||
|
def decompileGlyph_(self, numPointsInGlyph, sharedCoords, axisTags, data):
|
||||||
|
if len(data) < 4:
|
||||||
|
return []
|
||||||
|
numAxes = len(axisTags)
|
||||||
|
tuples = []
|
||||||
|
flags, offsetToData = struct.unpack(">HH", data[:4])
|
||||||
|
pos = 4
|
||||||
|
dataPos = offsetToData
|
||||||
|
if (flags & TUPLES_SHARE_POINT_NUMBERS) != 0:
|
||||||
|
sharedPoints, dataPos = GlyphVariation.decompilePoints_(numPointsInGlyph, data, dataPos)
|
||||||
|
else:
|
||||||
|
sharedPoints = []
|
||||||
|
for _ in range(flags & TUPLE_COUNT_MASK):
|
||||||
|
dataSize, flags = struct.unpack(">HH", data[pos:pos+4])
|
||||||
|
tupleSize = GlyphVariation.getTupleSize_(flags, numAxes)
|
||||||
|
tupleData = data[pos : pos + tupleSize]
|
||||||
|
pointDeltaData = data[dataPos : dataPos + dataSize]
|
||||||
|
tuples.append(self.decompileTuple_(numPointsInGlyph, sharedCoords, sharedPoints, axisTags, tupleData, pointDeltaData))
|
||||||
|
pos += tupleSize
|
||||||
|
dataPos += dataSize
|
||||||
|
return tuples
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decompileTuple_(numPointsInGlyph, sharedCoords, sharedPoints, axisTags, data, tupleData):
|
||||||
|
flags = struct.unpack(">H", data[2:4])[0]
|
||||||
|
|
||||||
|
pos = 4
|
||||||
|
if (flags & EMBEDDED_TUPLE_COORD) == 0:
|
||||||
|
coord = sharedCoords[flags & TUPLE_INDEX_MASK]
|
||||||
|
else:
|
||||||
|
coord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
||||||
|
if (flags & INTERMEDIATE_TUPLE) != 0:
|
||||||
|
minCoord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
||||||
|
maxCoord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
||||||
|
else:
|
||||||
|
minCoord, maxCoord = table__g_v_a_r.computeMinMaxCoord_(coord)
|
||||||
|
axes = {}
|
||||||
|
for axis in axisTags:
|
||||||
|
coords = minCoord[axis], coord[axis], maxCoord[axis]
|
||||||
|
if coords != (0.0, 0.0, 0.0):
|
||||||
|
axes[axis] = coords
|
||||||
|
pos = 0
|
||||||
|
if (flags & PRIVATE_POINT_NUMBERS) != 0:
|
||||||
|
points, pos = GlyphVariation.decompilePoints_(numPointsInGlyph, tupleData, pos)
|
||||||
|
else:
|
||||||
|
points = sharedPoints
|
||||||
|
deltas_x, pos = GlyphVariation.decompileDeltas_(len(points), tupleData, pos)
|
||||||
|
deltas_y, pos = GlyphVariation.decompileDeltas_(len(points), tupleData, pos)
|
||||||
|
deltas = [None] * numPointsInGlyph
|
||||||
|
for p, x, y in zip(points, deltas_x, deltas_y):
|
||||||
|
deltas[p] = (x, y)
|
||||||
|
return GlyphVariation(axes, deltas)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def computeMinMaxCoord_(coord):
|
||||||
|
minCoord = {}
|
||||||
|
maxCoord = {}
|
||||||
|
for (axis, value) in coord.items():
|
||||||
|
minCoord[axis] = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
|
||||||
|
maxCoord[axis] = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
|
||||||
|
return (minCoord, maxCoord)
|
||||||
|
|
||||||
|
def toXML(self, writer, ttFont, progress=None):
|
||||||
|
writer.simpletag("version", value=self.version)
|
||||||
|
writer.newline()
|
||||||
|
writer.simpletag("reserved", value=self.reserved)
|
||||||
|
writer.newline()
|
||||||
|
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis]
|
||||||
|
for glyphName in ttFont.getGlyphOrder():
|
||||||
|
variations = self.variations.get(glyphName)
|
||||||
|
if not variations:
|
||||||
|
continue
|
||||||
|
writer.begintag("glyphVariations", glyph=glyphName)
|
||||||
|
writer.newline()
|
||||||
|
for gvar in variations:
|
||||||
|
gvar.toXML(writer, axisTags)
|
||||||
|
writer.endtag("glyphVariations")
|
||||||
|
writer.newline()
|
||||||
|
|
||||||
|
def fromXML(self, name, attrs, content, ttFont):
|
||||||
|
if name == "version":
|
||||||
|
self.version = safeEval(attrs["value"])
|
||||||
|
elif name == "reserved":
|
||||||
|
self.reserved = safeEval(attrs["value"])
|
||||||
|
elif name == "glyphVariations":
|
||||||
|
if not hasattr(self, "variations"):
|
||||||
|
self.variations = {}
|
||||||
|
glyphName = attrs["glyph"]
|
||||||
|
glyph = ttFont["glyf"][glyphName]
|
||||||
|
numPointsInGlyph = self.getNumPoints_(glyph)
|
||||||
|
glyphVariations = []
|
||||||
|
for element in content:
|
||||||
|
if isinstance(element, tuple):
|
||||||
|
name, attrs, content = element
|
||||||
|
if name == "tuple":
|
||||||
|
gvar = GlyphVariation({}, [None] * numPointsInGlyph)
|
||||||
|
glyphVariations.append(gvar)
|
||||||
|
for tupleElement in content:
|
||||||
|
if isinstance(tupleElement, tuple):
|
||||||
|
tupleName, tupleAttrs, tupleContent = tupleElement
|
||||||
|
gvar.fromXML(tupleName, tupleAttrs, tupleContent)
|
||||||
|
self.variations[glyphName] = glyphVariations
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getNumPoints_(glyph):
|
||||||
|
NUM_PHANTOM_POINTS = 4
|
||||||
|
if glyph.isComposite():
|
||||||
|
return len(glyph.components) + NUM_PHANTOM_POINTS
|
||||||
|
else:
|
||||||
|
# Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
|
||||||
|
return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS
|
||||||
|
|
||||||
|
|
||||||
|
class GlyphVariation(object):
|
||||||
|
def __init__(self, axes, coordinates):
|
||||||
|
self.axes = axes
|
||||||
|
self.coordinates = coordinates
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
axes = ",".join(sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()]))
|
||||||
|
return "<GlyphVariation %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 GlyphVariation has any visible impact.
|
||||||
|
|
||||||
|
If the result is False, the GlyphVariation 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 = 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:
|
||||||
|
writer.simpletag("coord", axis=axis, value=value, min=minValue, max=maxValue)
|
||||||
|
writer.newline()
|
||||||
|
wrote_any_points = False
|
||||||
|
for i, point in enumerate(self.coordinates):
|
||||||
|
if point is not None:
|
||||||
|
writer.simpletag("delta", pt=i, x=point[0], y=point[1])
|
||||||
|
writer.newline()
|
||||||
|
wrote_any_points = True
|
||||||
|
if not wrote_any_points:
|
||||||
|
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":
|
||||||
|
point = safeEval(attrs["pt"])
|
||||||
|
x = safeEval(attrs["x"])
|
||||||
|
y = safeEval(attrs["y"])
|
||||||
|
self.coordinates[point] = (x, y)
|
||||||
|
|
||||||
|
def compile(self, axisTags, sharedCoordIndices, sharedPoints):
|
||||||
|
tupleData = []
|
||||||
|
|
||||||
|
coord = self.compileCoord(axisTags)
|
||||||
|
if coord in sharedCoordIndices:
|
||||||
|
flags = sharedCoordIndices[coord]
|
||||||
|
else:
|
||||||
|
flags = EMBEDDED_TUPLE_COORD
|
||||||
|
tupleData.append(coord)
|
||||||
|
|
||||||
|
intermediateCoord = self.compileIntermediateCoord(axisTags)
|
||||||
|
if intermediateCoord is not None:
|
||||||
|
flags |= INTERMEDIATE_TUPLE
|
||||||
|
tupleData.append(intermediateCoord)
|
||||||
|
|
||||||
|
if sharedPoints is not None:
|
||||||
|
auxData = self.compileDeltas(sharedPoints)
|
||||||
|
else:
|
||||||
|
flags |= PRIVATE_POINT_NUMBERS
|
||||||
|
points = self.getUsedPoints()
|
||||||
|
numPointsInGlyph = len(self.coordinates)
|
||||||
|
auxData = self.compilePoints(points, numPointsInGlyph) + self.compileDeltas(points)
|
||||||
|
|
||||||
|
tupleData = struct.pack('>HH', len(auxData), flags) + bytesjoin(tupleData)
|
||||||
|
return (tupleData, auxData)
|
||||||
|
|
||||||
|
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 decompileCoords_(axisTags, numCoords, data, offset):
|
||||||
|
result = []
|
||||||
|
pos = offset
|
||||||
|
for _ in range(numCoords):
|
||||||
|
coord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
||||||
|
result.append(coord)
|
||||||
|
return result, 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
|
||||||
|
while pos < numPoints:
|
||||||
|
run = io.BytesIO()
|
||||||
|
runLength = 0
|
||||||
|
lastValue = 0
|
||||||
|
useByteEncoding = (points[pos] <= 0xff)
|
||||||
|
while pos < numPoints and runLength <= MAX_RUN_LENGTH:
|
||||||
|
curValue = points[pos]
|
||||||
|
delta = curValue - lastValue
|
||||||
|
if useByteEncoding and delta > 0xff:
|
||||||
|
# we need to start a new run (which will not use byte encoding)
|
||||||
|
break
|
||||||
|
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_(numPointsInGlyph, data, offset):
|
||||||
|
"""(numPointsInGlyph, data, offset) --> ([point1, point2, ...], newOffset)"""
|
||||||
|
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(numPointsInGlyph), 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:
|
||||||
|
for _ in range(numPointsInRun):
|
||||||
|
point += byteord(data[pos])
|
||||||
|
pos += 1
|
||||||
|
result.append(point)
|
||||||
|
else:
|
||||||
|
for _ in range(numPointsInRun):
|
||||||
|
point += struct.unpack(">H", data[pos:pos+2])[0]
|
||||||
|
pos += 2
|
||||||
|
result.append(point)
|
||||||
|
if max(result) >= numPointsInGlyph:
|
||||||
|
raise TTLibError("malformed 'gvar' table")
|
||||||
|
return (result, pos)
|
||||||
|
|
||||||
|
def compileDeltas(self, points):
|
||||||
|
deltaX = []
|
||||||
|
deltaY = []
|
||||||
|
for p in sorted(list(points)):
|
||||||
|
c = self.coordinates[p]
|
||||||
|
if c is not None:
|
||||||
|
deltaX.append(c[0])
|
||||||
|
deltaY.append(c[1])
|
||||||
|
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 = GlyphVariation.encodeDeltaRunAsZeroes_(deltas, pos, stream)
|
||||||
|
elif value >= -128 and value <= 127:
|
||||||
|
pos = GlyphVariation.encodeDeltaRunAsBytes_(deltas, pos, stream)
|
||||||
|
else:
|
||||||
|
pos = GlyphVariation.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', 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', 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)
|
||||||
|
elif (runHeader & DELTAS_ARE_WORDS) != 0:
|
||||||
|
for _ in range(numDeltasInRun):
|
||||||
|
result.append(struct.unpack(">h", data[pos:pos+2])[0])
|
||||||
|
pos += 2
|
||||||
|
else:
|
||||||
|
for _ in range(numDeltasInRun):
|
||||||
|
result.append(struct.unpack(">b", data[pos:pos+1])[0])
|
||||||
|
pos += 1
|
||||||
|
assert len(result) == numDeltas
|
||||||
|
return (result, pos)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getTupleSize_(flags, axisCount):
|
||||||
|
size = 4
|
||||||
|
if (flags & EMBEDDED_TUPLE_COORD) != 0:
|
||||||
|
size += axisCount * 2
|
||||||
|
if (flags & INTERMEDIATE_TUPLE) != 0:
|
||||||
|
size += axisCount * 4
|
||||||
|
return size
|
551
Lib/fontTools/ttLib/tables/_g_v_a_r_test.py
Normal file
551
Lib/fontTools/ttLib/tables/_g_v_a_r_test.py
Normal file
@ -0,0 +1,551 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import, unicode_literals
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools.misc.textTools import deHexStr, hexStr
|
||||||
|
from fontTools.misc.xmlWriter import XMLWriter
|
||||||
|
from fontTools.ttLib import TTLibError
|
||||||
|
from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, GlyphVariation
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
def hexencode(s):
|
||||||
|
h = hexStr(s).upper()
|
||||||
|
return ' '.join([h[i:i+2] for i in range(0, len(h), 2)])
|
||||||
|
|
||||||
|
# Glyph variation table of uppercase I in the Skia font, as printed in Apple's
|
||||||
|
# TrueType spec. The actual Skia font uses a different table for uppercase I.
|
||||||
|
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
|
||||||
|
SKIA_GVAR_I = deHexStr(
|
||||||
|
"00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 "
|
||||||
|
"00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 "
|
||||||
|
"00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E "
|
||||||
|
"FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 "
|
||||||
|
"04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 "
|
||||||
|
"04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 "
|
||||||
|
"F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A "
|
||||||
|
"09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 "
|
||||||
|
"81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 "
|
||||||
|
"99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 "
|
||||||
|
"10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 "
|
||||||
|
"D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 "
|
||||||
|
"00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 "
|
||||||
|
"80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 "
|
||||||
|
"08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 "
|
||||||
|
"01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE "
|
||||||
|
"FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00")
|
||||||
|
|
||||||
|
# Shared coordinates in the Skia font, as printed in Apple's TrueType spec.
|
||||||
|
SKIA_SHARED_COORDS = deHexStr(
|
||||||
|
"40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 "
|
||||||
|
"C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00")
|
||||||
|
|
||||||
|
|
||||||
|
class GlyphVariationTableTest(unittest.TestCase):
|
||||||
|
def test_compileOffsets_shortFormat(self):
|
||||||
|
self.assertEqual((deHexStr("00 00 00 02 FF C0"), 0),
|
||||||
|
table__g_v_a_r.compileOffsets_([0, 4, 0x1ff80]))
|
||||||
|
|
||||||
|
def test_compileOffsets_longFormat(self):
|
||||||
|
self.assertEqual((deHexStr("00 00 00 00 00 00 00 04 CA FE BE EF"), 1),
|
||||||
|
table__g_v_a_r.compileOffsets_([0, 4, 0xCAFEBEEF]))
|
||||||
|
|
||||||
|
def test_decompileOffsets_shortFormat(self):
|
||||||
|
decompileOffsets = table__g_v_a_r.decompileOffsets_
|
||||||
|
data = deHexStr("00 11 22 33 44 55 66 77 88 99 aa bb")
|
||||||
|
self.assertEqual([2*0x0011, 2*0x2233, 2*0x4455, 2*0x6677, 2*0x8899, 2*0xaabb],
|
||||||
|
list(decompileOffsets(data, tableFormat=0, glyphCount=5)))
|
||||||
|
|
||||||
|
def test_decompileOffsets_longFormat(self):
|
||||||
|
decompileOffsets = table__g_v_a_r.decompileOffsets_
|
||||||
|
data = deHexStr("00 11 22 33 44 55 66 77 88 99 aa bb")
|
||||||
|
self.assertEqual([0x00112233, 0x44556677, 0x8899aabb],
|
||||||
|
list(decompileOffsets(data, tableFormat=1, glyphCount=2)))
|
||||||
|
|
||||||
|
def test_compileGlyph_noVariations(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.variations = {}
|
||||||
|
self.assertEqual(b"", table.compileGlyph_("glyphname", 8, ["wght", "opsz"], {}))
|
||||||
|
|
||||||
|
def test_compileGlyph_emptyVariations(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.variations = {"glyphname": []}
|
||||||
|
self.assertEqual(b"", table.compileGlyph_("glyphname", 8, ["wght", "opsz"], {}))
|
||||||
|
|
||||||
|
def test_compileGlyph_onlyRedundantVariations(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)}
|
||||||
|
table.variations = {"glyphname": [
|
||||||
|
GlyphVariation(axes, [None] * 4),
|
||||||
|
GlyphVariation(axes, [None] * 4),
|
||||||
|
GlyphVariation(axes, [None] * 4)
|
||||||
|
]}
|
||||||
|
self.assertEqual(b"", table.compileGlyph_("glyphname", 8, ["wght", "opsz"], {}))
|
||||||
|
|
||||||
|
def test_compileGlyph_roundTrip(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
numPointsInGlyph = 4
|
||||||
|
glyphCoords = [(1,1), (2,2), (3,3), (4,4)]
|
||||||
|
gvar1 = GlyphVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, glyphCoords)
|
||||||
|
gvar2 = GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, glyphCoords)
|
||||||
|
table.variations = {"oslash": [gvar1, gvar2]}
|
||||||
|
data = table.compileGlyph_("oslash", numPointsInGlyph, axisTags, {})
|
||||||
|
self.assertEqual([gvar1, gvar2], table.decompileGlyph_(numPointsInGlyph, {}, axisTags, data))
|
||||||
|
|
||||||
|
def test_compileSharedCoords(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.variations = {}
|
||||||
|
deltas = [None] * 4
|
||||||
|
table.variations["A"] = [
|
||||||
|
GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.5, 0.7, 1.0)}, deltas)
|
||||||
|
]
|
||||||
|
table.variations["B"] = [
|
||||||
|
GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.2, 0.7, 1.0)}, deltas),
|
||||||
|
GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.2, 0.8, 1.0)}, deltas)
|
||||||
|
]
|
||||||
|
table.variations["C"] = [
|
||||||
|
GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.7, 1.0)}, deltas),
|
||||||
|
GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.8, 1.0)}, deltas),
|
||||||
|
GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.9, 1.0)}, deltas)
|
||||||
|
]
|
||||||
|
# {"wght":1.0, "wdth":0.7} is shared 3 times; {"wght":1.0, "wdth":0.8} is shared twice.
|
||||||
|
# Min and max values are not part of the shared coordinate pool and should get ignored.
|
||||||
|
result = table.compileSharedCoords_(["wght", "wdth"])
|
||||||
|
self.assertEqual(["40 00 2C CD", "40 00 33 33"], [hexencode(c) for c in result])
|
||||||
|
|
||||||
|
def test_decompileSharedCoords_Skia(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.offsetToCoord = 0
|
||||||
|
table.sharedCoordCount = 8
|
||||||
|
sharedCoords = table.decompileSharedCoords_(["wght", "wdth"], SKIA_SHARED_COORDS)
|
||||||
|
self.assertEqual([
|
||||||
|
{"wght": 1.0, "wdth": 0.0},
|
||||||
|
{"wght": -1.0, "wdth": 0.0},
|
||||||
|
{"wght": 0.0, "wdth": 1.0},
|
||||||
|
{"wght": 0.0, "wdth": -1.0},
|
||||||
|
{"wght": -1.0, "wdth": -1.0},
|
||||||
|
{"wght": 1.0, "wdth": -1.0},
|
||||||
|
{"wght": 1.0, "wdth": 1.0},
|
||||||
|
{"wght": -1.0, "wdth": 1.0}
|
||||||
|
], sharedCoords)
|
||||||
|
|
||||||
|
def test_decompileSharedCoords_empty(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.offsetToCoord = 0
|
||||||
|
table.sharedCoordCount = 0
|
||||||
|
self.assertEqual([], table.decompileSharedCoords_(["wght"], b""))
|
||||||
|
|
||||||
|
def test_decompileGlyph_Skia_I(self):
|
||||||
|
axes = ["wght", "wdth"]
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.offsetToCoord = 0
|
||||||
|
table.sharedCoordCount = 8
|
||||||
|
table.axisCount = len(axes)
|
||||||
|
sharedCoords = table.decompileSharedCoords_(axes, SKIA_SHARED_COORDS)
|
||||||
|
tuples = table.decompileGlyph_(18, sharedCoords, axes, SKIA_GVAR_I)
|
||||||
|
self.assertEqual(8, len(tuples))
|
||||||
|
self.assertEqual({"wght": (0.0, 1.0, 1.0)}, tuples[0].axes)
|
||||||
|
self.assertEqual("257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 257,0 "
|
||||||
|
"259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0",
|
||||||
|
" ".join(["%d,%d" % c for c in tuples[0].coordinates]))
|
||||||
|
|
||||||
|
def test_decompileGlyph_empty(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
self.assertEqual([], table.decompileGlyph_(numPointsInGlyph=5, sharedCoords=[], axisTags=[], data=b""))
|
||||||
|
|
||||||
|
def test_computeMinMaxCord(self):
|
||||||
|
coord = {"wght": -0.3, "wdth": 0.7}
|
||||||
|
minCoord, maxCoord = table__g_v_a_r.computeMinMaxCoord_(coord)
|
||||||
|
self.assertEqual({"wght": -0.3, "wdth": 0.0}, minCoord)
|
||||||
|
self.assertEqual({"wght": 0.0, "wdth": 0.7}, maxCoord)
|
||||||
|
|
||||||
|
class GlyphVariationTest(unittest.TestCase):
|
||||||
|
def __init__(self, methodName):
|
||||||
|
unittest.TestCase.__init__(self, methodName)
|
||||||
|
if not hasattr(self, "assertSetEqual"): # only in Python 2.7 and later
|
||||||
|
self.assertSetEqual = self.assertEqual
|
||||||
|
|
||||||
|
def test_equal(self):
|
||||||
|
gvar1 = GlyphVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
|
||||||
|
gvar2 = GlyphVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
|
||||||
|
self.assertEqual(gvar1, gvar2)
|
||||||
|
|
||||||
|
def test_equal_differentAxes(self):
|
||||||
|
gvar1 = GlyphVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
|
||||||
|
gvar2 = GlyphVariation({"wght":(0.7, 0.8, 0.9)}, [(0,0), (9,8), (7,6)])
|
||||||
|
self.assertNotEqual(gvar1, gvar2)
|
||||||
|
|
||||||
|
def test_equal_differentCoordinates(self):
|
||||||
|
gvar1 = GlyphVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
|
||||||
|
gvar2 = GlyphVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8)])
|
||||||
|
self.assertNotEqual(gvar1, gvar2)
|
||||||
|
|
||||||
|
def test_hasImpact_someDeltasNotZero(self):
|
||||||
|
axes = {"wght":(0.0, 1.0, 1.0)}
|
||||||
|
gvar = GlyphVariation(axes, [(0,0), (9,8), (7,6)])
|
||||||
|
self.assertTrue(gvar.hasImpact())
|
||||||
|
|
||||||
|
def test_hasImpact_allDeltasZero(self):
|
||||||
|
axes = {"wght":(0.0, 1.0, 1.0)}
|
||||||
|
gvar = GlyphVariation(axes, [(0,0), (0,0), (0,0)])
|
||||||
|
self.assertTrue(gvar.hasImpact())
|
||||||
|
|
||||||
|
def test_hasImpact_allDeltasNone(self):
|
||||||
|
axes = {"wght":(0.0, 1.0, 1.0)}
|
||||||
|
gvar = GlyphVariation(axes, [None, None, None])
|
||||||
|
self.assertFalse(gvar.hasImpact())
|
||||||
|
|
||||||
|
def test_toXML(self):
|
||||||
|
writer = XMLWriter(StringIO())
|
||||||
|
axes = {"wdth":(0.3, 0.4, 0.5), "wght":(0.0, 1.0, 1.0), "opsz":(-0.7, -0.7, 0.0)}
|
||||||
|
g = GlyphVariation(axes, [(9,8), None, (7,6), (0,0), (-1,-2), None])
|
||||||
|
g.toXML(writer, ["wdth", "wght", "opsz"])
|
||||||
|
self.assertEqual([
|
||||||
|
'<tuple>',
|
||||||
|
'<coord axis="wdth" max="0.5" min="0.3" value="0.4"/>',
|
||||||
|
'<coord axis="wght" value="1.0"/>',
|
||||||
|
'<coord axis="opsz" value="-0.7"/>',
|
||||||
|
'<delta pt="0" x="9" y="8"/>',
|
||||||
|
'<delta pt="2" x="7" y="6"/>',
|
||||||
|
'<delta pt="3" x="0" y="0"/>',
|
||||||
|
'<delta pt="4" x="-1" y="-2"/>',
|
||||||
|
'</tuple>'
|
||||||
|
], GlyphVariationTest.xml_lines(writer))
|
||||||
|
|
||||||
|
def test_toXML_allDeltasNone(self):
|
||||||
|
writer = XMLWriter(StringIO())
|
||||||
|
axes = {"wght":(0.0, 1.0, 1.0)}
|
||||||
|
g = GlyphVariation(axes, [None] * 5)
|
||||||
|
g.toXML(writer, ["wght", "wdth"])
|
||||||
|
self.assertEqual([
|
||||||
|
'<tuple>',
|
||||||
|
'<coord axis="wght" value="1.0"/>',
|
||||||
|
'<!-- no deltas -->',
|
||||||
|
'</tuple>'
|
||||||
|
], GlyphVariationTest.xml_lines(writer))
|
||||||
|
|
||||||
|
def test_fromXML(self):
|
||||||
|
g = GlyphVariation({}, [None] * 4)
|
||||||
|
g.fromXML("coord", {"axis":"wdth", "min":"0.3", "value":"0.4", "max":"0.5"}, [])
|
||||||
|
g.fromXML("coord", {"axis":"wght", "value":"1.0"}, [])
|
||||||
|
g.fromXML("coord", {"axis":"opsz", "value":"-0.5"}, [])
|
||||||
|
g.fromXML("delta", {"pt":"1", "x":"33", "y":"44"}, [])
|
||||||
|
g.fromXML("delta", {"pt":"2", "x":"-2", "y":"170"}, [])
|
||||||
|
self.assertEqual({
|
||||||
|
"wdth":( 0.3, 0.4, 0.5),
|
||||||
|
"wght":( 0.0, 1.0, 1.0),
|
||||||
|
"opsz":(-0.5, -0.5, 0.0)
|
||||||
|
}, g.axes)
|
||||||
|
self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates)
|
||||||
|
|
||||||
|
def test_compile_sharedCoords_nonIntermediate_sharedPoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
sharedCoordIndices = { gvar.compileCoord(axisTags): 0x77 }
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices, sharedPoints=set([0,1,2]))
|
||||||
|
# len(data)=8; flags=None; tupleIndex=0x77
|
||||||
|
# embeddedCoord=[]; intermediateCoord=[]
|
||||||
|
self.assertEqual("00 08 00 77", hexencode(tuple))
|
||||||
|
self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_sharedCoords_intermediate_sharedPoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
sharedCoordIndices = { gvar.compileCoord(axisTags): 0x77 }
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices, sharedPoints=set([0,1,2]))
|
||||||
|
# len(data)=8; flags=INTERMEDIATE_TUPLE; tupleIndex=0x77
|
||||||
|
# embeddedCoord=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)]
|
||||||
|
self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tuple))
|
||||||
|
self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_sharedCoords_nonIntermediate_privatePoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
sharedCoordIndices = { gvar.compileCoord(axisTags): 0x77 }
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices, sharedPoints=None)
|
||||||
|
# len(data)=13; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
|
||||||
|
# embeddedCoord=[]; intermediateCoord=[]
|
||||||
|
self.assertEqual("00 09 20 77", hexencode(tuple))
|
||||||
|
self.assertEqual("00 " # all points in glyph
|
||||||
|
"02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_sharedCoords_intermediate_privatePoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
sharedCoordIndices = { gvar.compileCoord(axisTags): 0x77 }
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices, sharedPoints=None)
|
||||||
|
# len(data)=13; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
|
||||||
|
# embeddedCoord=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)]
|
||||||
|
self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00", hexencode(tuple))
|
||||||
|
self.assertEqual("00 " # all points in glyph
|
||||||
|
"02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_embeddedCoords_nonIntermediate_sharedPoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices={}, sharedPoints=set([0,1,2]))
|
||||||
|
# len(data)=8; flags=EMBEDDED_TUPLE_COORD
|
||||||
|
# embeddedCoord=[(0.5, 0.8)]; intermediateCoord=[]
|
||||||
|
self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tuple))
|
||||||
|
self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_embeddedCoords_intermediate_sharedPoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices={}, sharedPoints=set([0,1,2]))
|
||||||
|
# len(data)=8; flags=EMBEDDED_TUPLE_COORD
|
||||||
|
# embeddedCoord=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)]
|
||||||
|
self.assertEqual("00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33", hexencode(tuple))
|
||||||
|
self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_embeddedCoords_nonIntermediate_privatePoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices={}, sharedPoints=None)
|
||||||
|
# len(data)=13; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_TUPLE_COORD
|
||||||
|
# embeddedCoord=[(0.5, 0.8)]; intermediateCoord=[]
|
||||||
|
self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tuple))
|
||||||
|
self.assertEqual("00 " # all points in glyph
|
||||||
|
"02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compile_embeddedCoords_intermediate_privatePoints(self):
|
||||||
|
gvar = GlyphVariation({"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)},
|
||||||
|
[(7,4), (8,5), (9,6)])
|
||||||
|
axisTags = ["wght", "wdth"]
|
||||||
|
tuple, data = gvar.compile(axisTags, sharedCoordIndices={}, sharedPoints=None)
|
||||||
|
# len(data)=13; flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_TUPLE|EMBEDDED_TUPLE_COORD
|
||||||
|
# embeddedCoord=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
|
||||||
|
self.assertEqual("00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", hexencode(tuple))
|
||||||
|
self.assertEqual("00 " # all points in glyph
|
||||||
|
"02 07 08 09 " # deltaX: [7, 8, 9]
|
||||||
|
"02 04 05 06", # deltaY: [4, 5, 6]
|
||||||
|
hexencode(data))
|
||||||
|
|
||||||
|
def test_compileCoord(self):
|
||||||
|
gvar = GlyphVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4)
|
||||||
|
self.assertEqual("C0 00 20 00", hexencode(gvar.compileCoord(["wght", "wdth"])))
|
||||||
|
self.assertEqual("20 00 C0 00", hexencode(gvar.compileCoord(["wdth", "wght"])))
|
||||||
|
self.assertEqual("C0 00", hexencode(gvar.compileCoord(["wght"])))
|
||||||
|
|
||||||
|
def test_compileIntermediateCoord(self):
|
||||||
|
gvar = GlyphVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4)
|
||||||
|
self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(gvar.compileIntermediateCoord(["wght", "wdth"])))
|
||||||
|
self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(gvar.compileIntermediateCoord(["wdth", "wght"])))
|
||||||
|
self.assertEqual(None, gvar.compileIntermediateCoord(["wght"]))
|
||||||
|
self.assertEqual("19 9A 26 66", hexencode(gvar.compileIntermediateCoord(["wdth"])))
|
||||||
|
|
||||||
|
def test_decompileCoord(self):
|
||||||
|
decompileCoord = GlyphVariation.decompileCoord_
|
||||||
|
data = deHexStr("DE AD C0 00 20 00 DE AD")
|
||||||
|
self.assertEqual(({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2))
|
||||||
|
|
||||||
|
def test_decompileCoord_roundTrip(self):
|
||||||
|
# Make sure we are not affected by https://github.com/behdad/fonttools/issues/286
|
||||||
|
data = deHexStr("7F B9 80 35")
|
||||||
|
values, _ = GlyphVariation.decompileCoord_(["wght", "wdth"], data, 0)
|
||||||
|
axisValues = dict([(axis, (val, val, val)) for axis, val in values.items()])
|
||||||
|
gvar = GlyphVariation(axisValues, [None] * 4)
|
||||||
|
self.assertEqual("7F B9 80 35", hexencode(gvar.compileCoord(["wght", "wdth"])))
|
||||||
|
|
||||||
|
def test_decompileCoords(self):
|
||||||
|
decompileCoords = GlyphVariation.decompileCoords_
|
||||||
|
axes = ["wght", "wdth", "opsz"]
|
||||||
|
coords = [
|
||||||
|
{"wght": 1.0, "wdth": 0.0, "opsz": 0.5},
|
||||||
|
{"wght": -1.0, "wdth": 0.0, "opsz": 0.25},
|
||||||
|
{"wght": 0.0, "wdth": -1.0, "opsz": 1.0}
|
||||||
|
]
|
||||||
|
data = deHexStr("DE AD 40 00 00 00 20 00 C0 00 00 00 10 00 00 00 C0 00 40 00")
|
||||||
|
self.assertEqual((coords, 20), decompileCoords(axes, numCoords=3, data=data, offset=2))
|
||||||
|
|
||||||
|
def test_compilePoints(self):
|
||||||
|
compilePoints = lambda p: GlyphVariation.compilePoints(set(p), numPointsInGlyph=999)
|
||||||
|
self.assertEqual("00", hexencode(compilePoints(range(999)))) # all points in glyph
|
||||||
|
self.assertEqual("01 00 07", hexencode(compilePoints([7])))
|
||||||
|
self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535])))
|
||||||
|
self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15])))
|
||||||
|
self.assertEqual("06 05 07 01 F7 02 01 F2", hexencode(compilePoints([7, 8, 255, 257, 258, 500])))
|
||||||
|
self.assertEqual("03 01 07 01 80 01 F4", hexencode(compilePoints([7, 8, 500])))
|
||||||
|
self.assertEqual("04 01 07 01 81 BE EF 0C 0F", hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE])))
|
||||||
|
self.assertEqual("81 2C" + # 300 points (0x12c) in total
|
||||||
|
" 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127]
|
||||||
|
" 7F 80" + (127 * " 01") + # second run, contains 128 points: [128 .. 255]
|
||||||
|
" AB 01 00" + (43 * " 00 01"), # third run, contains 44 points: [256 .. 299]
|
||||||
|
hexencode(compilePoints(range(300))))
|
||||||
|
self.assertEqual("81 8F" + # 399 points (0x18f) in total
|
||||||
|
" 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127]
|
||||||
|
" 7F 80" + (127 * " 01") + # second run, contains 128 points: [128 .. 255]
|
||||||
|
" FF 01 00" + (127 * " 00 01") + # third run, contains 128 points: [256 .. 383]
|
||||||
|
" 8E 01 80" + (14 * " 00 01"), # fourth run, contains 15 points: [384 .. 398]
|
||||||
|
hexencode(compilePoints(range(399))))
|
||||||
|
|
||||||
|
def test_decompilePoints(self):
|
||||||
|
numPointsInGlyph = 65536
|
||||||
|
allPoints = list(range(numPointsInGlyph))
|
||||||
|
def decompilePoints(data, offset):
|
||||||
|
points, offset = GlyphVariation.decompilePoints_(numPointsInGlyph, deHexStr(data), offset)
|
||||||
|
# Conversion to list only needed for Python 3.2.
|
||||||
|
return (list(points), offset)
|
||||||
|
# all points in glyph
|
||||||
|
self.assertEqual((allPoints, 1), decompilePoints("00", 0))
|
||||||
|
# all points in glyph (in overly verbose encoding, not explicitly prohibited by spec)
|
||||||
|
self.assertEqual((allPoints, 2), decompilePoints("80 00", 0))
|
||||||
|
# 2 points; first run: [9, 9+6]
|
||||||
|
self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0))
|
||||||
|
# 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF)
|
||||||
|
self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0))
|
||||||
|
# 1 point; first run: [7]
|
||||||
|
self.assertEqual(([7], 3), decompilePoints("01 00 07", 0))
|
||||||
|
# 1 point; first run: [7] in overly verbose encoding
|
||||||
|
self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0))
|
||||||
|
# 1 point; first run: [65535]; requires words to be treated as unsigned numbers
|
||||||
|
self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0))
|
||||||
|
# 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2).
|
||||||
|
self.assertEqual(([7, 8, 255, 257], 7), decompilePoints("04 01 07 01 01 FF 02", 0))
|
||||||
|
# combination of all encodings, preceded and followed by 4 bytes of unused data
|
||||||
|
data = "DE AD DE AD 04 01 07 01 81 BE EF 0C 0F DE AD DE AD"
|
||||||
|
self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4))
|
||||||
|
self.assertSetEqual(set(range(300)), set(decompilePoints(
|
||||||
|
"81 2C" + # 300 points (0x12c) in total
|
||||||
|
" 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127]
|
||||||
|
" 7F 80" + (127 * " 01") + # second run, contains 128 points: [128 .. 255]
|
||||||
|
" AB 01 00" + (43 * " 00 01"), # third run, contains 44 points: [256 .. 299]
|
||||||
|
0)[0]))
|
||||||
|
self.assertSetEqual(set(range(399)), set(decompilePoints(
|
||||||
|
"81 8F" + # 399 points (0x18f) in total
|
||||||
|
" 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127]
|
||||||
|
" 7F 80" + (127 * " 01") + # second run, contains 128 points: [128 .. 255]
|
||||||
|
" FF 01 00" + (127 * " 00 01") + # third run, contains 128 points: [256 .. 383]
|
||||||
|
" 8E 01 80" + (14 * " 00 01"), # fourth run, contains 15 points: [384 .. 398]
|
||||||
|
0)[0]))
|
||||||
|
|
||||||
|
def test_decompilePoints_shouldGuardAgainstBadPointNumbers(self):
|
||||||
|
decompilePoints = GlyphVariation.decompilePoints_
|
||||||
|
# 2 points; first run: [3, 9].
|
||||||
|
numPointsInGlyph = 8
|
||||||
|
self.assertRaises(TTLibError, decompilePoints, numPointsInGlyph, deHexStr("02 01 03 06"), 0)
|
||||||
|
|
||||||
|
def test_decompilePoints_roundTrip(self):
|
||||||
|
numPointsInGlyph = 500 # greater than 255, so we also exercise code path for 16-bit encoding
|
||||||
|
compile = lambda points: GlyphVariation.compilePoints(points, numPointsInGlyph)
|
||||||
|
decompile = lambda data: set(GlyphVariation.decompilePoints_(numPointsInGlyph, data, 0)[0])
|
||||||
|
for i in range(50):
|
||||||
|
points = set(random.sample(range(numPointsInGlyph), 30))
|
||||||
|
self.assertSetEqual(points, decompile(compile(points)),
|
||||||
|
"failed round-trip decompile/compilePoints; points=%s" % points)
|
||||||
|
allPoints = set(range(numPointsInGlyph))
|
||||||
|
self.assertSetEqual(allPoints, decompile(compile(allPoints)))
|
||||||
|
|
||||||
|
def test_compileDeltas(self):
|
||||||
|
gvar = GlyphVariation({}, [(0,0), (1, 0), (2, 0), (3, 3)])
|
||||||
|
points = set([1, 2])
|
||||||
|
# deltaX for points: [1, 2]; deltaY for points: [0, 0]
|
||||||
|
self.assertEqual("01 01 02 81", hexencode(gvar.compileDeltas(points)))
|
||||||
|
|
||||||
|
def test_compileDeltaValues(self):
|
||||||
|
compileDeltaValues = lambda values: hexencode(GlyphVariation.compileDeltaValues_(values))
|
||||||
|
# zeroes
|
||||||
|
self.assertEqual("80", compileDeltaValues([0]))
|
||||||
|
self.assertEqual("BF", compileDeltaValues([0] * 64))
|
||||||
|
self.assertEqual("BF 80", compileDeltaValues([0] * 65))
|
||||||
|
self.assertEqual("BF A3", compileDeltaValues([0] * 100))
|
||||||
|
self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256))
|
||||||
|
# bytes
|
||||||
|
self.assertEqual("00 01", compileDeltaValues([1]))
|
||||||
|
self.assertEqual("06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2]))
|
||||||
|
self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64))
|
||||||
|
self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65))
|
||||||
|
# words
|
||||||
|
self.assertEqual("40 66 66", compileDeltaValues([0x6666]))
|
||||||
|
self.assertEqual("43 66 66 7F FF FF FF 80 00", compileDeltaValues([0x6666, 32767, -1, -32768]))
|
||||||
|
self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64))
|
||||||
|
self.assertEqual("7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65))
|
||||||
|
# bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run
|
||||||
|
self.assertEqual("04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127]))
|
||||||
|
self.assertEqual("01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127]))
|
||||||
|
self.assertEqual("01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127]))
|
||||||
|
self.assertEqual("01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127]))
|
||||||
|
# bytes, zeroes
|
||||||
|
self.assertEqual("01 01 00", compileDeltaValues([1, 0]))
|
||||||
|
self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0]))
|
||||||
|
# words, bytes, words: a single byte is more compact when encoded as part of the words run
|
||||||
|
self.assertEqual("42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777]))
|
||||||
|
self.assertEqual("40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777]))
|
||||||
|
# words, zeroes, words
|
||||||
|
self.assertEqual("40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777]))
|
||||||
|
self.assertEqual("40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777]))
|
||||||
|
self.assertEqual("40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777]))
|
||||||
|
# words, zeroes, bytes
|
||||||
|
self.assertEqual("40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3]))
|
||||||
|
self.assertEqual("40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3]))
|
||||||
|
self.assertEqual("40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3]))
|
||||||
|
# words, zeroes
|
||||||
|
self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0]))
|
||||||
|
self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0]))
|
||||||
|
|
||||||
|
def test_decompileDeltas(self):
|
||||||
|
decompileDeltas = GlyphVariation.decompileDeltas_
|
||||||
|
# 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F)
|
||||||
|
self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0))
|
||||||
|
# 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F)
|
||||||
|
self.assertEqual(([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0))
|
||||||
|
# 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F)
|
||||||
|
self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0))
|
||||||
|
# combination of all three encodings, preceded and followed by 4 bytes of unused data
|
||||||
|
data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF")
|
||||||
|
self.assertEqual(([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4))
|
||||||
|
|
||||||
|
def test_decompileDeltas_roundTrip(self):
|
||||||
|
numDeltas = 30
|
||||||
|
compile = GlyphVariation.compileDeltaValues_
|
||||||
|
decompile = lambda data: GlyphVariation.decompileDeltas_(numDeltas, data, 0)[0]
|
||||||
|
for i in range(50):
|
||||||
|
deltas = random.sample(range(-128, 127), 10)
|
||||||
|
deltas.extend(random.sample(range(-32768, 32767), 10))
|
||||||
|
deltas.extend([0] * 10)
|
||||||
|
random.shuffle(deltas)
|
||||||
|
if hasattr(self, "assertListEqual"):
|
||||||
|
# Python 2.7 and later
|
||||||
|
self.assertListEqual(deltas, decompile(compile(deltas)),
|
||||||
|
"failed round-trip decompile/compileDeltas; deltas=%s" % deltas)
|
||||||
|
else:
|
||||||
|
# Python 2.6
|
||||||
|
self.assertEqual(deltas, decompile(compile(deltas)),
|
||||||
|
"failed round-trip decompile/compileDeltas; deltas=%s" % deltas)
|
||||||
|
|
||||||
|
def test_getTupleSize(self):
|
||||||
|
getTupleSize = GlyphVariation.getTupleSize_
|
||||||
|
numAxes = 3
|
||||||
|
self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes))
|
||||||
|
self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes))
|
||||||
|
self.assertEqual(4, getTupleSize(0x2077, numAxes))
|
||||||
|
self.assertEqual(4, getTupleSize(11, numAxes))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xml_lines(writer):
|
||||||
|
content = writer.file.getvalue().decode("utf-8")
|
||||||
|
return [line.strip() for line in content.splitlines()][1:]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user