[GX] Handle 'gvar' table with glyph variations
This commit is contained in:
parent
a09d96f6a2
commit
56a4d3f9e3
236
Lib/fontTools/ttLib/tables/_g_v_a_r.py
Normal file
236
Lib/fontTools/ttLib/tables/_g_v_a_r.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools.misc import sstruct
|
||||||
|
from fontTools.misc.fixedTools import fixedToFloat
|
||||||
|
from fontTools.misc.textTools import safeEval
|
||||||
|
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
|
||||||
|
#
|
||||||
|
# TrueType source code for parsing 'gvar':
|
||||||
|
# http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c
|
||||||
|
|
||||||
|
gvarHeaderFormat = b"""
|
||||||
|
> # big endian
|
||||||
|
version: H
|
||||||
|
reserved: H
|
||||||
|
axisCount: H
|
||||||
|
sharedCoordCount: H
|
||||||
|
offsetToCoord: I
|
||||||
|
glyphCount: H
|
||||||
|
flags: H
|
||||||
|
offsetToData: I
|
||||||
|
"""
|
||||||
|
|
||||||
|
gvarItemFormat = b"""
|
||||||
|
> # big endian
|
||||||
|
tupleCount: H
|
||||||
|
offsetToData: H
|
||||||
|
"""
|
||||||
|
|
||||||
|
GVAR_HEADER_SIZE = sstruct.calcsize(gvarHeaderFormat)
|
||||||
|
gvarItemSize = sstruct.calcsize(gvarItemFormat)
|
||||||
|
|
||||||
|
TUPLES_SHARE_POINT_NUMBERS = 0x8000
|
||||||
|
TUPLE_COUNT_MASK = 0x0fff
|
||||||
|
|
||||||
|
EMBEDDED_TUPLE_COORD = 0x8000
|
||||||
|
INTERMEDIATE_TUPLE = 0x4000
|
||||||
|
PRIVATE_POINT_NUMBERS = 0x2000
|
||||||
|
TUPLE_INDEX_MASK = 0x0fff
|
||||||
|
|
||||||
|
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(gvarHeaderFormat, data[0:GVAR_HEADER_SIZE], self)
|
||||||
|
assert len(glyphs) == self.glyphCount
|
||||||
|
assert len(axisTags) == self.axisCount
|
||||||
|
offsets = self.decompileOffsets_(data)
|
||||||
|
sharedCoords = self.decompileSharedCoords_(axisTags, data)
|
||||||
|
self.variations = {}
|
||||||
|
for i in range(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.decompileVariations_(numPoints, sharedCoords, axisTags, gvarData)
|
||||||
|
|
||||||
|
def decompileSharedCoords_(self, axisTags, data):
|
||||||
|
result = []
|
||||||
|
pos = self.offsetToCoord
|
||||||
|
stride = len(axisTags) * 2
|
||||||
|
for i in range(self.sharedCoordCount):
|
||||||
|
coord = self.decompileCoord_(axisTags, data[pos:pos+stride])
|
||||||
|
result.append(coord)
|
||||||
|
pos += stride
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decompileCoord_(axisTags, data):
|
||||||
|
coord = {}
|
||||||
|
pos = 0
|
||||||
|
for axis in axisTags:
|
||||||
|
coord[axis] = fixedToFloat(struct.unpack(b">h", data[pos:pos+2])[0], 14)
|
||||||
|
pos += 2
|
||||||
|
return coord
|
||||||
|
|
||||||
|
def decompileOffsets_(self, data):
|
||||||
|
if (self.flags & 1) == 0:
|
||||||
|
# Short format: array of UInt16
|
||||||
|
offsets = array.array("H")
|
||||||
|
offsetsSize = (self.glyphCount + 1) * 2
|
||||||
|
else:
|
||||||
|
# Long format: array of UInt32
|
||||||
|
offsets = array.array("I")
|
||||||
|
offsetsSize = (self.glyphCount + 1) * 4
|
||||||
|
offsetsData = data[GVAR_HEADER_SIZE : GVAR_HEADER_SIZE + offsetsSize]
|
||||||
|
offsets.fromstring(offsetsData)
|
||||||
|
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 (self.flags & 1) == 0:
|
||||||
|
offsets = [off * 2 for off in offsets]
|
||||||
|
|
||||||
|
return offsets
|
||||||
|
|
||||||
|
def decompileVariations_(self, numPoints, sharedCoords, axisTags, data):
|
||||||
|
if len(data) < 4:
|
||||||
|
return []
|
||||||
|
tupleCount, offsetToData = struct.unpack(b">HH", data[:4])
|
||||||
|
tuplesSharePointNumbers = (tupleCount & TUPLES_SHARE_POINT_NUMBERS) != 0
|
||||||
|
tupleCount = tupleCount & TUPLE_COUNT_MASK
|
||||||
|
tuplePos = 4
|
||||||
|
dataPos = offsetToData
|
||||||
|
for i in range(tupleCount):
|
||||||
|
tupleSize, tupleIndex = struct.unpack(b">HH", data[tuplePos:tuplePos+4])
|
||||||
|
if (tupleIndex & EMBEDDED_TUPLE_COORD) != 0:
|
||||||
|
print('****** %d ' % (tupleIndex & TUPLE_INDEX_MASK))
|
||||||
|
print(' '.join(x.encode('hex') for x in data))
|
||||||
|
coord = self.decompileCoord_(axisTags, data[tuplePos+4:])
|
||||||
|
else:
|
||||||
|
pass #coord = sharedCoords[tupleIndex & TUPLE_INDEX_MASK].copy()
|
||||||
|
tuplePos += self.getTupleSize(tupleIndex)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- from here comes junk ---------------------------------------------------
|
||||||
|
# TODO: Remove glyphName argument, it is just for debugging.
|
||||||
|
def OBSOLETE_decompileGlyphVariation(self, glyphName, sharedCoords, data):
|
||||||
|
tracing = False or (glyphName == 'I')
|
||||||
|
if tracing: print(' '.join(x.encode('hex') for x in data))
|
||||||
|
if len(data) == 0:
|
||||||
|
return []
|
||||||
|
result = []
|
||||||
|
tupleCount, offsetToData = struct.unpack(b">HH", data[:4])
|
||||||
|
tuplesSharePointNumbers = (tupleCount & 0x8000) != 0
|
||||||
|
tupleCount = tupleCount & 0xfff
|
||||||
|
pos = 4
|
||||||
|
dataPos = offsetToData
|
||||||
|
if tracing: print('tuplesSharePointNumbers=%s' % tuplesSharePointNumbers)
|
||||||
|
for i in range(tupleCount):
|
||||||
|
tupleSize, tupleIndex = struct.unpack(b">HH", data[pos:pos+4])
|
||||||
|
if (tupleIndex & kEmbeddedTupleCoord) != 0:
|
||||||
|
coord = None # TODO: Implement
|
||||||
|
else:
|
||||||
|
coord = sharedCoords[tupleIndex & kTupleIndexMask].copy()
|
||||||
|
if tracing:
|
||||||
|
print('Tuple %d: pos=%d, tupleSize=%04x, byteSize=%d, index=%04x' % (i, pos, tupleSize, self.getTupleSize(tupleSize), tupleIndex))
|
||||||
|
tupleData = buffer(data, dataPos, tupleSize)
|
||||||
|
tuple = self.decompileTupleData(tuplesSharePointNumbers, coord, tupleData)
|
||||||
|
result.append(tuple)
|
||||||
|
pos += self.getTupleSize(tupleIndex)
|
||||||
|
dataPos += tupleSize
|
||||||
|
print(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def OBSOLETE_decompileTupleData(self, tuplesSharePointNumbers, coord, data):
|
||||||
|
#print(" tuplesSharePointNumbers: %s" % tuplesSharePointNumbers)
|
||||||
|
#print(" tupleData: %s" % ' '.join([c.encode('hex') for c in data]))
|
||||||
|
tuple = GlyphVariation(coord)
|
||||||
|
#t.decompilePackedPoints(data)
|
||||||
|
#pos = 0
|
||||||
|
#numPoints = ord(data[pos])
|
||||||
|
#if numPoints >= 0x80:
|
||||||
|
# pos += 1
|
||||||
|
# numPoints = (numPoints & 0x7f) << 8 + ord(data[pos])
|
||||||
|
#pos += 1
|
||||||
|
#
|
||||||
|
# 0 means "all points in glyph"; TODO: how to find out this number?
|
||||||
|
#if numPoints != 0:
|
||||||
|
# # TODO
|
||||||
|
# pass
|
||||||
|
|
||||||
|
#print(" numPoints: %d" % numPoints)
|
||||||
|
#assert not tuplesSharePointNumbers # TODO: implement shared point numbers
|
||||||
|
# TODO: decode deltas
|
||||||
|
return tuple
|
||||||
|
|
||||||
|
def getTupleSize(self, tupleIndex):
|
||||||
|
"""Returns the byte size of a tuple given the value of its tupleIndex field."""
|
||||||
|
size = 4
|
||||||
|
if (tupleIndex & EMBEDDED_TUPLE_COORD) != 0:
|
||||||
|
size += self.axisCount * 2
|
||||||
|
if (tupleIndex & INTERMEDIATE_TUPLE) != 0:
|
||||||
|
size += self.axisCount * 4
|
||||||
|
return size
|
||||||
|
|
||||||
|
def toXML(self, writer, ttFont):
|
||||||
|
writer.simpletag("Version", value=self.version)
|
||||||
|
writer.newline()
|
||||||
|
writer.simpletag("Reserved", value=self.reserved)
|
||||||
|
writer.newline()
|
||||||
|
|
||||||
|
|
||||||
|
POINTS_ARE_WORDS = 0x80
|
||||||
|
POINT_RUN_COUNT_MASK = 0x7F
|
||||||
|
|
||||||
|
class GlyphVariation:
|
||||||
|
def __init__(self, axes):
|
||||||
|
self.axes = axes
|
||||||
|
self.coordinates = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
axes = ','.join(sorted(['%s=%s' % (name, value) for (name, value) in self.axes.items()]))
|
||||||
|
return '<GlyphVariation %s>' % axes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decompilePoints(data):
|
||||||
|
pos = 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
#def decompilePackedPoints(self, data):
|
||||||
|
# pos = 0
|
||||||
|
# numPoints = ord(data[pos])
|
||||||
|
# if numPoints >= 0x80:
|
||||||
|
# pos += 1
|
||||||
|
# numPoints = (numPoints & 0x7f) << 8 | ord(data[pos])
|
||||||
|
# pos += 1
|
||||||
|
# points = []
|
||||||
|
# if numPoints == 0:
|
||||||
|
# return (points, data[pos:])
|
||||||
|
# i = 0
|
||||||
|
# while i < numPoints:
|
||||||
|
# controlByte = ord(data[pos])
|
||||||
|
# if (controlByte & 0x80) != 0:
|
||||||
|
#
|
||||||
|
# pos += 1
|
||||||
|
# numPointsInRun = (numPointsInRun & 0x7f) << 8 | ord(data[pos])
|
||||||
|
# print('********************** numPointsInRun: %d' % numPointsInRun)
|
||||||
|
# break
|
115
Lib/fontTools/ttLib/tables/_g_v_a_r_test.py
Normal file
115
Lib/fontTools/ttLib/tables/_g_v_a_r_test.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import, unicode_literals
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
import unittest
|
||||||
|
from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, GVAR_HEADER_SIZE, GlyphVariation
|
||||||
|
|
||||||
|
|
||||||
|
def hexdecode(s):
|
||||||
|
return bytesjoin([c.decode("hex") for c in s.split()])
|
||||||
|
|
||||||
|
# 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 = hexdecode(
|
||||||
|
"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 = hexdecode(
|
||||||
|
"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")
|
||||||
|
|
||||||
|
class GlyphVariationTableTest(unittest.TestCase):
|
||||||
|
def test_decompileOffsets_shortFormat(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.flags = 0
|
||||||
|
table.glyphCount = 5
|
||||||
|
data = b'X' * GVAR_HEADER_SIZE + hexdecode("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],
|
||||||
|
table.decompileOffsets_(data))
|
||||||
|
|
||||||
|
def test_decompileOffsets_longFormat(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.flags = 1
|
||||||
|
table.glyphCount = 2
|
||||||
|
data = b'X' * GVAR_HEADER_SIZE + hexdecode("00 11 22 33 44 55 66 77 88 99 aa bb")
|
||||||
|
self.assertEqual([0x00112233, 0x44556677, 0x8899aabb],
|
||||||
|
list(table.decompileOffsets_(data)))
|
||||||
|
|
||||||
|
def test_decompileSharedCoords(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.offsetToCoord = 4
|
||||||
|
table.sharedCoordCount = 3
|
||||||
|
data = b"XXXX" + hexdecode(
|
||||||
|
"40 00 00 00 20 00 "
|
||||||
|
"C0 00 00 00 10 00 "
|
||||||
|
"00 00 C0 00 40 00")
|
||||||
|
self.assertEqual([
|
||||||
|
{"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}
|
||||||
|
], table.decompileSharedCoords_(["wght", "wdth", "opsz"], data))
|
||||||
|
|
||||||
|
def test_decompileSharedCoords_Skia(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.offsetToCoord = 0
|
||||||
|
table.sharedCoordCount = 0
|
||||||
|
sharedCoords = table.decompileSharedCoords_(["wght", "wdth"], SKIA_SHARED_COORDS)
|
||||||
|
self.assertEqual([], 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_decompileVariations_Skia_I(self):
|
||||||
|
axes = ["wght", "wdth"]
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.offsetToCoord = 0
|
||||||
|
table.sharedCoordCount = 0
|
||||||
|
table.axisCount = len(axes)
|
||||||
|
sharedCoords = table.decompileSharedCoords_(axes, SKIA_SHARED_COORDS)
|
||||||
|
self.assertEqual([], table.decompileVariations_(99, sharedCoords, axes, SKIA_GVAR_I))
|
||||||
|
|
||||||
|
def test_decompileVariations_empty(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
self.assertEqual([], table.decompileVariations_(numPoints=5, sharedCoords=[], axisTags=[], data=b""))
|
||||||
|
|
||||||
|
def test_getTupleSize(self):
|
||||||
|
table = table__g_v_a_r()
|
||||||
|
table.axisCount = 3
|
||||||
|
self.assertEqual(4 + table.axisCount * 2, table.getTupleSize(0x8042))
|
||||||
|
self.assertEqual(4 + table.axisCount * 4, table.getTupleSize(0x4077))
|
||||||
|
self.assertEqual(4, table.getTupleSize(0x2077))
|
||||||
|
self.assertEqual(4, table.getTupleSize(11))
|
||||||
|
|
||||||
|
|
||||||
|
class GlyphVariationTest(unittest.TestCase):
|
||||||
|
def test_decompilePackedPoints(self):
|
||||||
|
pass
|
||||||
|
# 02 01 00 02 80 40 03 eb 81
|
||||||
|
# 01 00 00 80 80
|
||||||
|
# t = hexdecode("00 82 02 ff ff ff 83 02 01 01 01 84 91")
|
||||||
|
#gvar = GlyphVariation({})
|
||||||
|
#data = hexdecode("01 00 00 80 80")
|
||||||
|
#print('******* %s' % gvar.decompilePackedPoints(data))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user