Merge pull request #300 from brawer/fvar

[GX] Make it easier to construct ‘fvar’ tables from code
This commit is contained in:
Behdad Esfahbod 2015-06-26 01:48:03 -07:00
commit 389c445760
6 changed files with 395 additions and 49 deletions

View File

@ -29,8 +29,7 @@ class table__a_v_a_r(DefaultTable.DefaultTable):
self.segments = {} self.segments = {}
def compile(self, ttFont): def compile(self, ttFont):
fvarAxes = ttFont["fvar"].table.VariationAxis axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
axisTags = [axis.AxisTag for axis in fvarAxes]
header = {"version": 0x00010000, "axisCount": len(axisTags)} header = {"version": 0x00010000, "axisCount": len(axisTags)}
result = [sstruct.pack(AVAR_HEADER_FORMAT, header)] result = [sstruct.pack(AVAR_HEADER_FORMAT, header)]
for axis in axisTags: for axis in axisTags:
@ -43,8 +42,7 @@ class table__a_v_a_r(DefaultTable.DefaultTable):
return bytesjoin(result) return bytesjoin(result)
def decompile(self, data, ttFont): def decompile(self, data, ttFont):
fvarAxes = ttFont["fvar"].table.VariationAxis axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
axisTags = [axis.AxisTag for axis in fvarAxes]
header = {} header = {}
headerSize = sstruct.calcsize(AVAR_HEADER_FORMAT) headerSize = sstruct.calcsize(AVAR_HEADER_FORMAT)
header = sstruct.unpack(AVAR_HEADER_FORMAT, data[0:headerSize]) header = sstruct.unpack(AVAR_HEADER_FORMAT, data[0:headerSize])
@ -62,7 +60,7 @@ class table__a_v_a_r(DefaultTable.DefaultTable):
self.fixupSegments_(warn=warnings.warn) self.fixupSegments_(warn=warnings.warn)
def toXML(self, writer, ttFont, progress=None): def toXML(self, writer, ttFont, progress=None):
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis] axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
for axis in axisTags: for axis in axisTags:
writer.begintag("segment", axis=axis) writer.begintag("segment", axis=axis)
writer.newline() writer.newline()

View File

@ -4,6 +4,7 @@ from fontTools.misc.textTools import deHexStr
from fontTools.misc.xmlWriter import XMLWriter from fontTools.misc.xmlWriter import XMLWriter
from fontTools.ttLib import TTLibError from fontTools.ttLib import TTLibError
from fontTools.ttLib.tables._a_v_a_r import table__a_v_a_r from fontTools.ttLib.tables._a_v_a_r import table__a_v_a_r
from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis
import collections import collections
import unittest import unittest
@ -73,9 +74,11 @@ class AxisVariationTableTest(unittest.TestCase):
@staticmethod @staticmethod
def makeFont(axisTags): def makeFont(axisTags):
"""['opsz', 'wdth'] --> ttFont""" """['opsz', 'wdth'] --> ttFont"""
axes = [collections.namedtuple("A", "AxisTag")(axis) for axis in axisTags] fvar = table__f_v_a_r()
varaxis = collections.namedtuple("B", "VariationAxis")(axes) for tag in axisTags:
fvar = collections.namedtuple("C", "table")(varaxis) axis = Axis()
axis.axisTag = tag
fvar.axes.append(axis)
return {"fvar": fvar} return {"fvar": fvar}
@staticmethod @staticmethod

View File

@ -1,7 +1,192 @@
from __future__ import print_function, division, absolute_import from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import * from fontTools.misc.py23 import *
from .otBase import BaseTTXConverter from fontTools.misc import sstruct
from fontTools.misc.fixedTools import fixedToFloat, floatToFixed
from fontTools.misc.textTools import safeEval, num2binary, binary2num
from fontTools.ttLib import TTLibError
from . import DefaultTable
import struct
class table__f_v_a_r(BaseTTXConverter): # Apple's documentation of 'fvar':
pass # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html
FVAR_HEADER_FORMAT = """
> # big endian
version: L
offsetToData: H
countSizePairs: H
axisCount: H
axisSize: H
instanceCount: H
instanceSize: H
"""
FVAR_AXIS_FORMAT = """
> # big endian
axisTag: 4s
minValue: 16.16F
defaultValue: 16.16F
maxValue: 16.16F
flags: H
nameID: H
"""
FVAR_INSTANCE_FORMAT = """
> # big endian
nameID: H
flags: H
"""
class table__f_v_a_r(DefaultTable.DefaultTable):
dependencies = ["name"]
def __init__(self, tag="fvar"):
DefaultTable.DefaultTable.__init__(self, tag)
self.axes = []
self.instances = []
def compile(self, ttFont):
header = {
"version": 0x00010000,
"offsetToData": sstruct.calcsize(FVAR_HEADER_FORMAT),
"countSizePairs": 2,
"axisCount": len(self.axes),
"axisSize": sstruct.calcsize(FVAR_AXIS_FORMAT),
"instanceCount": len(self.instances),
"instanceSize": sstruct.calcsize(FVAR_INSTANCE_FORMAT) + len(self.axes) * 4
}
result = [sstruct.pack(FVAR_HEADER_FORMAT, header)]
result.extend([axis.compile() for axis in self.axes])
axisTags = [axis.axisTag for axis in self.axes]
result.extend([instance.compile(axisTags) for instance in self.instances])
return bytesjoin(result)
def decompile(self, data, ttFont):
header = {}
headerSize = sstruct.calcsize(FVAR_HEADER_FORMAT)
header = sstruct.unpack(FVAR_HEADER_FORMAT, data[0:headerSize])
if header["version"] != 0x00010000:
raise TTLibError("unsupported 'fvar' version %04x" % header["version"])
pos = header["offsetToData"]
axisSize = header["axisSize"]
for _ in range(header["axisCount"]):
axis = Axis()
axis.decompile(data[pos:pos+axisSize])
self.axes.append(axis)
pos += axisSize
instanceSize = header["instanceSize"]
axisTags = [axis.axisTag for axis in self.axes]
for _ in range(header["instanceCount"]):
instance = NamedInstance()
instance.decompile(data[pos:pos+instanceSize], axisTags)
self.instances.append(instance)
pos += instanceSize
def toXML(self, writer, ttFont, progress=None):
for axis in self.axes:
axis.toXML(writer, ttFont)
for instance in self.instances:
instance.toXML(writer, ttFont)
def fromXML(self, name, attrs, content, ttFont):
if name == "Axis":
axis = Axis()
axis.fromXML(name, attrs, content, ttFont)
self.axes.append(axis)
elif name == "NamedInstance":
instance = NamedInstance()
instance.fromXML(name, attrs, content, ttFont)
self.instances.append(instance)
class Axis(object):
def __init__(self):
self.axisTag = None
self.nameID = 0
self.flags = 0
self.minValue = -1.0
self.defaultValue = 0.0
self.maxValue = 1.0
def compile(self):
return sstruct.pack(FVAR_AXIS_FORMAT, self)
def decompile(self, data):
sstruct.unpack2(FVAR_AXIS_FORMAT, data, self)
def toXML(self, writer, ttFont):
name = ttFont["name"].getDebugName(self.nameID)
if name is not None:
writer.newline()
writer.comment(name)
writer.newline()
writer.begintag("Axis")
writer.newline()
for tag, value in [("AxisTag", self.axisTag),
("MinValue", str(self.minValue)),
("DefaultValue", str(self.defaultValue)),
("MaxValue", str(self.maxValue)),
("Flags", num2binary(self.flags, 16)),
("NameID", str(self.nameID))]:
writer.begintag(tag)
writer.write(value)
writer.endtag(tag)
writer.newline()
writer.endtag("Axis")
writer.newline()
def fromXML(self, name, _attrs, content, ttFont):
assert(name == "Axis")
for tag, _, value in filter(lambda t: type(t) is tuple, content):
value = ''.join(value)
if tag == "AxisTag":
self.axisTag = value
elif tag == "Flags":
self.flags = binary2num(value)
elif tag in ["MinValue", "DefaultValue", "MaxValue", "NameID"]:
setattr(self, tag[0].lower() + tag[1:], safeEval(value))
class NamedInstance(object):
def __init__(self):
self.nameID = 0
self.flags = 0
self.coordinates = {}
def compile(self, axisTags):
result = [sstruct.pack(FVAR_INSTANCE_FORMAT, self)]
for axis in axisTags:
fixedCoord = floatToFixed(self.coordinates[axis], 16)
result.append(struct.pack(">l", fixedCoord))
return bytesjoin(result)
def decompile(self, data, axisTags):
sstruct.unpack2(FVAR_INSTANCE_FORMAT, data, self)
pos = sstruct.calcsize(FVAR_INSTANCE_FORMAT)
for axis in axisTags:
value = struct.unpack(">l", data[pos : pos + 4])[0]
self.coordinates[axis] = fixedToFloat(value, 16)
pos += 4
def toXML(self, writer, ttFont):
name = ttFont["name"].getDebugName(self.nameID)
if name is not None:
writer.newline()
writer.comment(name)
writer.newline()
writer.begintag("NamedInstance", nameID=self.nameID,
flags=num2binary(self.flags, 16))
writer.newline()
for axis in ttFont["fvar"].axes:
writer.simpletag("coord", axis=axis.axisTag,
value=self.coordinates[axis.axisTag])
writer.newline()
writer.endtag("NamedInstance")
writer.newline()
def fromXML(self, name, attrs, content, ttFont):
assert(name == "NamedInstance")
self.flags = binary2num(attrs.get("flags", "0"))
self.nameID = safeEval(attrs["nameID"])
for tag, elementAttrs, _ in filter(lambda t: type(t) is tuple, content):
if tag == "coord":
self.coordinates[elementAttrs["axis"]] = safeEval(elementAttrs["value"])

View File

@ -0,0 +1,195 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.textTools import deHexStr
from fontTools.misc.xmlWriter import XMLWriter
from fontTools.ttLib import TTLibError
from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e, NameRecord
import unittest
FVAR_DATA = deHexStr(
"00 01 00 00 00 10 00 02 00 02 00 14 00 02 00 0C "
"77 67 68 74 FF FF 00 00 00 00 00 00 00 01 00 00 00 00 01 01 "
"77 64 74 68 FF FF 00 00 00 00 00 00 00 01 00 00 00 00 01 02 "
"01 03 00 00 00 00 4C CD 00 01 00 00 "
"01 04 00 00 00 00 4C CD 00 00 33 33")
FVAR_AXIS_DATA = deHexStr(
"6F 70 73 7a ff ff 80 00 00 01 4c cd 00 01 80 00 98 76 01 59")
FVAR_INSTANCE_DATA = deHexStr("01 59 12 34 00 00 b3 33 00 00 80 00")
def xml_lines(writer):
content = writer.file.getvalue().decode("utf-8")
return [line.strip() for line in content.splitlines()][1:]
def AddName(font, name):
nameTable = font.get("name")
if nameTable is None:
nameTable = font["name"] = table__n_a_m_e()
nameTable.names = []
namerec = NameRecord()
namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
namerec.string = name.encode('mac_roman')
namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
nameTable.names.append(namerec)
return namerec
def MakeFont():
axes = [("wght", "Weight"), ("wdth", "Width")]
instances = [("Light", 0.3, 1.0), ("Light Condensed", 0.3, 0.2)]
fvarTable = table__f_v_a_r()
font = {"fvar": fvarTable}
for tag, name in axes:
axis = Axis()
axis.axisTag = tag
axis.nameID = AddName(font, name).nameID
fvarTable.axes.append(axis)
for name, weight, width in instances:
inst = NamedInstance()
inst.nameID = AddName(font, name).nameID
inst.coordinates = {"wght": weight, "wdth": width}
fvarTable.instances.append(inst)
return font
class FontVariationTableTest(unittest.TestCase):
def test_compile(self):
font = MakeFont()
h = font["fvar"].compile(font)
self.assertEqual(FVAR_DATA, font["fvar"].compile(font))
def test_decompile(self):
fvar = table__f_v_a_r()
fvar.decompile(FVAR_DATA, ttFont={"fvar": fvar})
self.assertEqual(["wght", "wdth"], [a.axisTag for a in fvar.axes])
self.assertEqual([259, 260], [i.nameID for i in fvar.instances])
def test_toXML(self):
font = MakeFont()
writer = XMLWriter(StringIO())
font["fvar"].toXML(writer, font)
xml = writer.file.getvalue().decode("utf-8")
self.assertEqual(2, xml.count("<Axis>"))
self.assertTrue("<AxisTag>wght</AxisTag>" in xml)
self.assertTrue("<AxisTag>wdth</AxisTag>" in xml)
self.assertEqual(2, xml.count("<NamedInstance "))
self.assertTrue("<!-- Light -->" in xml)
self.assertTrue("<!-- Light Condensed -->" in xml)
def test_fromXML(self):
fvar = table__f_v_a_r()
fvar.fromXML("Axis", {}, [("AxisTag", {}, ["opsz"])], ttFont=None)
fvar.fromXML("Axis", {}, [("AxisTag", {}, ["slnt"])], ttFont=None)
fvar.fromXML("NamedInstance", {"nameID": "765"}, [], ttFont=None)
fvar.fromXML("NamedInstance", {"nameID": "234"}, [], ttFont=None)
self.assertEqual(["opsz", "slnt"], [a.axisTag for a in fvar.axes])
self.assertEqual([765, 234], [i.nameID for i in fvar.instances])
class AxisTest(unittest.TestCase):
def test_compile(self):
axis = Axis()
axis.axisTag, axis.nameID, axis.flags = ('opsz', 345, 0x9876)
axis.minValue, axis.defaultValue, axis.maxValue = (-0.5, 1.3, 1.5)
self.assertEqual(FVAR_AXIS_DATA, axis.compile())
def test_decompile(self):
axis = Axis()
axis.decompile(FVAR_AXIS_DATA)
self.assertEqual("opsz", axis.axisTag)
self.assertEqual(345, axis.nameID)
self.assertEqual(0x9876, axis.flags)
self.assertEqual(-0.5, axis.minValue)
self.assertEqual(1.3, axis.defaultValue)
self.assertEqual(1.5, axis.maxValue)
def test_toXML(self):
font = MakeFont()
axis = Axis()
axis.decompile(FVAR_AXIS_DATA)
AddName(font, "Optical Size").nameID = 256
axis.nameID = 256
writer = XMLWriter(StringIO())
axis.toXML(writer, font)
self.assertEqual([
'',
'<!-- Optical Size -->',
'<Axis>',
'<AxisTag>opsz</AxisTag>',
'<MinValue>-0.5</MinValue>',
'<DefaultValue>1.3</DefaultValue>',
'<MaxValue>1.5</MaxValue>',
'<Flags>10011000 01110110</Flags>',
'<NameID>256</NameID>',
'</Axis>'
], xml_lines(writer))
def test_fromXML(self):
axis = Axis()
axis.fromXML("Axis", {}, [
("AxisTag", {}, ["opsz"]),
("MinValue", {}, ["-0.5"]),
("DefaultValue", {}, ["1.3"]),
("MaxValue", {}, ["1.5"]),
("Flags", {}, ["10011000 01110110"]),
("NameID", {}, ["256"])
], ttFont=None)
self.assertEqual("opsz", axis.axisTag)
self.assertEqual(-0.5, axis.minValue)
self.assertEqual(1.3, axis.defaultValue)
self.assertEqual(1.5, axis.maxValue)
self.assertEqual(0x9876, axis.flags)
self.assertEqual(256, axis.nameID)
class NamedInstanceTest(unittest.TestCase):
def test_compile(self):
inst = NamedInstance()
inst.nameID = 345
inst.flags = 0x1234
inst.coordinates = {"wght": 0.7, "wdth": 0.5}
self.assertEqual(FVAR_INSTANCE_DATA, inst.compile(["wght", "wdth"]))
def test_decompile(self):
inst = NamedInstance()
inst.decompile(FVAR_INSTANCE_DATA, ["wght", "wdth"])
self.assertEqual(345, inst.nameID)
self.assertEqual(0x1234, inst.flags)
self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates)
def test_toXML(self):
font = MakeFont()
inst = NamedInstance()
inst.nameID = AddName(font, "Light Condensed").nameID
inst.flags = 0x1234
inst.coordinates = {"wght": 0.7, "wdth": 0.5}
writer = XMLWriter(StringIO())
inst.toXML(writer, font)
self.assertEqual([
'',
'<!-- Light Condensed -->',
'<NamedInstance flags="00010010 00110100" nameID="%s">' % inst.nameID,
'<coord axis="wght" value="0.7"/>',
'<coord axis="wdth" value="0.5"/>',
'</NamedInstance>'
], xml_lines(writer))
def test_fromXML(self):
inst = NamedInstance()
attrs = {"flags": "00010010 00110100", "nameID": "345"}
inst.fromXML("NamedInstance", attrs, [
("coord", {"axis": "wght", "value": "0.7"}, []),
("coord", {"axis": "wdth", "value": "0.5"}, []),
], ttFont=MakeFont())
self.assertEqual(0x1234, inst.flags)
self.assertEqual(345, inst.nameID)
self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates)
if __name__ == "__main__":
unittest.main()

View File

@ -52,7 +52,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
dependencies = ["fvar", "glyf"] dependencies = ["fvar", "glyf"]
def compile(self, ttFont): def compile(self, ttFont):
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis] axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
sharedCoords = self.compileSharedCoords_(axisTags) sharedCoords = self.compileSharedCoords_(axisTags)
sharedCoordIndices = {coord:i for i, coord in enumerate(sharedCoords)} sharedCoordIndices = {coord:i for i, coord in enumerate(sharedCoords)}
@ -168,7 +168,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
return result return result
def decompile(self, data, ttFont): def decompile(self, data, ttFont):
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis] axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
glyphs = ttFont.getGlyphOrder() glyphs = ttFont.getGlyphOrder()
sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
assert len(glyphs) == self.glyphCount assert len(glyphs) == self.glyphCount
@ -301,7 +301,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
writer.newline() writer.newline()
writer.simpletag("reserved", value=self.reserved) writer.simpletag("reserved", value=self.reserved)
writer.newline() writer.newline()
axisTags = [axis.AxisTag for axis in ttFont["fvar"].table.VariationAxis] axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
for glyphName in ttFont.getGlyphOrder(): for glyphName in ttFont.getGlyphOrder():
variations = self.variations.get(glyphName) variations = self.variations.get(glyphName)
if not variations: if not variations:

View File

@ -1020,39 +1020,4 @@ otData = [
('uint16', 'SettingNameID', None, None, 'The name table index for the setting name.'), ('uint16', 'SettingNameID', None, None, 'The name table index for the setting name.'),
]), ]),
##
## Apple TrueType GX tables
##
#
# fvar
#
('fvar', [
('Version', 'Version', None, None, 'Version of the fvar table-initially set to 0x00010000.'),
('uint16', 'OffsetToData', None, None, 'Set to 16.'),
('uint16', 'CountSizePairs', None, None, 'Set to 2.'),
('uint16', 'AxisCount', None, None, 'Number of style axes in this font.'),
('uint16', 'AxisSize', None, None, 'Set to 20.'),
('uint16', 'InstanceCount', None, None, 'Number of named instances in this font.'),
('uint16', 'InstanceSize', None, None, 'Number of bytes in each instance.'),
('VariationAxis', 'VariationAxis', 'AxisCount', 0, 'The variation axes array.'),
('NamedInstance', 'NamedInstance', 'InstanceCount', 0, 'The named instances array.'),
]),
('VariationAxis', [
('Tag', 'AxisTag', None, None, '4-byte AxisTag identifier'),
('Fixed', 'MinValue', None, None, 'The minimum style coordinate for the axis.'),
('Fixed', 'DefaultValue', None, None, 'The default style coordinate for the axis.'),
('Fixed', 'MaxValue', None, None, 'The maximum style coordinate for the axis.'),
('uint16', 'Flags', None, None, 'Set to zero.'),
('uint16', 'NameID', None, None, 'The name table index for the setting name.'),
]),
('NamedInstance', [
('uint16', 'NameID', None, None, 'The name table index for the instance name.'),
('uint16', 'Flags', None, None, 'Set to zero.'),
('Fixed', 'Coords', 'AxisCount', 0, 'The maximum style coordinate for the axis.'),
]),
] ]