320 lines
9.9 KiB
Python
320 lines
9.9 KiB
Python
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.tables._g_l_y_f import GlyphCoordinates
|
|
from . import DefaultTable
|
|
import array
|
|
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 = b"""
|
|
> # 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 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:],
|
|
format=(self.flags & 1), glyphCount=self.glyphCount)
|
|
sharedCoords = self.decompileSharedCoords_(axisTags, data)
|
|
self.variations = {}
|
|
for i in xrange(self.glyphCount):
|
|
glyphName = glyphs[i]
|
|
glyph = ttFont["glyf"][glyphName]
|
|
if glyph.isComposite():
|
|
numPoints = len(glyph.components) + 4
|
|
else:
|
|
# Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
|
|
numPoints = len(getattr(glyph, "coordinates", [])) + 4
|
|
gvarData = data[self.offsetToData + offsets[i] : self.offsetToData + offsets[i + 1]]
|
|
self.variations[glyphName] = self.decompileTuples_(numPoints, sharedCoords, axisTags, gvarData)
|
|
|
|
def decompileSharedCoords_(self, axisTags, data):
|
|
result, pos = GlyphVariation.decompileCoords_(axisTags, self.sharedCoordCount, data, self.offsetToCoord)
|
|
return result
|
|
|
|
@staticmethod
|
|
def decompileOffsets_(data, format, glyphCount):
|
|
if format == 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 format == 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, format). Bytestring is the
|
|
packed offset table. Format indicates whether the table
|
|
uses short (format=0) or long (format=1) integers.
|
|
The returned format should get packed into the flags field
|
|
of the 'gvar' header.
|
|
"""
|
|
assert len(offsets) >= 2
|
|
for i in xrange(1, len(offsets)):
|
|
assert offsets[i - 1] <= offsets[i]
|
|
if max(offsets) <= 0xffff * 2:
|
|
packed = array.array(b"H", [n >> 1 for n in offsets])
|
|
format = 0
|
|
else:
|
|
packed = array.array(b"I", offsets)
|
|
format = 1
|
|
if sys.byteorder != "big":
|
|
packed.byteswap()
|
|
return (packed.tostring(), format)
|
|
|
|
def decompileTuples_(self, numPoints, sharedCoords, axisTags, data):
|
|
if len(data) < 4:
|
|
return []
|
|
tuples = []
|
|
flags, offsetToData = struct.unpack(b">HH", data[:4])
|
|
pos = 4
|
|
dataPos = offsetToData
|
|
if (flags & TUPLES_SHARE_POINT_NUMBERS) != 0:
|
|
sharedPoints, dataPos = GlyphVariation.decompilePoints_(numPoints, data, dataPos)
|
|
else:
|
|
sharedPoints = []
|
|
for i in xrange(flags & TUPLE_COUNT_MASK):
|
|
dataSize, flags = struct.unpack(b">HH", data[pos:pos+4])
|
|
tupleSize = GlyphVariation.getTupleSize_(flags, self.axisCount)
|
|
tuple = data[pos : pos + tupleSize]
|
|
tupleData = data[dataPos : dataPos + dataSize]
|
|
tuples.append(self.decompileTuple_(numPoints, sharedCoords, sharedPoints, axisTags, tuple, tupleData))
|
|
pos += tupleSize
|
|
dataPos += dataSize
|
|
return tuples
|
|
|
|
@staticmethod
|
|
def decompileTuple_(numPoints, sharedCoords, sharedPoints, axisTags, data, tupleData):
|
|
flags = struct.unpack(b">H", data[2:4])[0]
|
|
|
|
pos = 4
|
|
coordSize = len(axisTags) * 2
|
|
if (flags & EMBEDDED_TUPLE_COORD) == 0:
|
|
coord = sharedCoords[flags & TUPLE_INDEX_MASK]
|
|
else:
|
|
coord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
|
minCoord = maxCoord = coord
|
|
if (flags & INTERMEDIATE_TUPLE) != 0:
|
|
minCoord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
|
maxCoord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
|
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_(numPoints, tupleData, pos)
|
|
else:
|
|
points = sharedPoints
|
|
deltas_x, pos = GlyphVariation.decompileDeltas_(len(points), tupleData, pos)
|
|
deltas_y, pos = GlyphVariation.decompileDeltas_(len(points), tupleData, pos)
|
|
deltas = GlyphCoordinates.zeros(numPoints)
|
|
for p, x, y in zip(points, deltas_x, deltas_y):
|
|
deltas[p] = (x, y)
|
|
return GlyphVariation(axes, deltas)
|
|
|
|
def toXML(self, writer, ttFont):
|
|
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():
|
|
tuples = self.variations.get(glyphName)
|
|
if not tuples:
|
|
continue
|
|
writer.begintag("glyphVariations", glyph=glyphName)
|
|
writer.newline()
|
|
for tuple in tuples:
|
|
tuple.toXML(writer, axisTags)
|
|
writer.endtag("glyphVariations")
|
|
writer.newline()
|
|
|
|
|
|
class GlyphVariation:
|
|
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 toXML(self, writer, axisTags):
|
|
writer.begintag("tuple")
|
|
writer.newline()
|
|
for axis in axisTags:
|
|
value = self.axes.get(axis)
|
|
if value != None:
|
|
minValue, value, maxValue = value
|
|
if minValue == value and maxValue == value:
|
|
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 in xrange(len(self.coordinates)):
|
|
x, y = self.coordinates[i]
|
|
if x != 0 or y != 0:
|
|
writer.simpletag("delta", pt=i, x=x, y=y)
|
|
writer.newline()
|
|
wrote_any_points = True
|
|
if not wrote_any_points:
|
|
writer.comment("all deltas are (0,0)")
|
|
writer.newline()
|
|
writer.endtag("tuple")
|
|
writer.newline()
|
|
|
|
@staticmethod
|
|
def decompileCoord_(axisTags, data, offset):
|
|
coord = {}
|
|
pos = offset
|
|
for axis in axisTags:
|
|
coord[axis] = fixedToFloat(struct.unpack(b">h", data[pos:pos+2])[0], 14)
|
|
pos += 2
|
|
return coord, pos
|
|
|
|
@staticmethod
|
|
def compileCoord_(axisTags, coord):
|
|
result = []
|
|
for axis in axisTags:
|
|
value = floatToFixed(coord.get(axis, 0.0), 14)
|
|
result.append(struct.pack(b">h", value))
|
|
return bytesjoin(result)
|
|
|
|
@staticmethod
|
|
def decompileCoords_(axisTags, numCoords, data, offset):
|
|
result = []
|
|
pos = offset
|
|
for i in xrange(numCoords):
|
|
coord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos)
|
|
result.append(coord)
|
|
return result, pos
|
|
|
|
@staticmethod
|
|
def compileCoords_(axisTags, coords):
|
|
return bytesjoin([GlyphVariation.compileCoord_(axisTags, c) for c in coords])
|
|
|
|
@staticmethod
|
|
def decompilePoints_(numPoints, data, offset):
|
|
"""(numPoints, data, offset) --> ([point1, point2, ...], newOffset)"""
|
|
pos = offset
|
|
numPointsInData = ord(data[pos])
|
|
pos += 1
|
|
if (numPointsInData & POINTS_ARE_WORDS) != 0:
|
|
numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | ord(data[pos])
|
|
pos += 1
|
|
if numPointsInData == 0:
|
|
return (range(numPoints), pos)
|
|
result = []
|
|
while len(result) < numPointsInData:
|
|
runHeader = ord(data[pos])
|
|
pos += 1
|
|
numPointsInRun = (runHeader & POINT_RUN_COUNT_MASK) + 1
|
|
point = 0
|
|
if (runHeader & POINTS_ARE_WORDS) == 0:
|
|
for i in xrange(numPointsInRun):
|
|
point += ord(data[pos])
|
|
pos += 1
|
|
result.append(point)
|
|
else:
|
|
for i in xrange(numPointsInRun):
|
|
point += struct.unpack(">H", data[pos:pos+2])[0]
|
|
pos += 2
|
|
result.append(point)
|
|
if max(result) >= numPoints:
|
|
raise ttLib.TTLibError("malformed 'gvar' table")
|
|
return (result, pos)
|
|
|
|
@staticmethod
|
|
def decompileDeltas_(numDeltas, data, offset):
|
|
"""(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)"""
|
|
result = []
|
|
pos = offset
|
|
while len(result) < numDeltas:
|
|
runHeader = ord(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 i in xrange(numDeltasInRun):
|
|
result.append(struct.unpack(">h", data[pos:pos+2])[0])
|
|
pos += 2
|
|
else:
|
|
for i in xrange(numDeltasInRun):
|
|
result.append(struct.unpack(">b", data[pos])[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
|