From 56a4d3f9e3373f720dc38d9a6dedf3d7898855ea Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Mon, 20 Apr 2015 21:29:23 +0200 Subject: [PATCH] [GX] Handle 'gvar' table with glyph variations --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 236 ++++++++++++++++++++ Lib/fontTools/ttLib/tables/_g_v_a_r_test.py | 115 ++++++++++ 2 files changed, 351 insertions(+) create mode 100644 Lib/fontTools/ttLib/tables/_g_v_a_r.py create mode 100644 Lib/fontTools/ttLib/tables/_g_v_a_r_test.py diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py new file mode 100644 index 000000000..cd09be821 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -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 '' % 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 diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r_test.py b/Lib/fontTools/ttLib/tables/_g_v_a_r_test.py new file mode 100644 index 000000000..a3b48202a --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r_test.py @@ -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()