Merge pull request #2679 from fonttools/avar2

Avar2
This commit is contained in:
Behdad Esfahbod 2023-03-15 11:56:30 -06:00 committed by GitHub
commit 05872d6878
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 237 additions and 53 deletions

View File

@ -5,29 +5,20 @@ from fontTools.misc.fixedTools import (
floatToFixedToStr as fl2str, floatToFixedToStr as fl2str,
strToFixedToFloat as str2fl, strToFixedToFloat as str2fl,
) )
from fontTools.misc.textTools import bytesjoin from fontTools.misc.textTools import bytesjoin, safeEval
from fontTools.ttLib import TTLibError from fontTools.ttLib import TTLibError
from . import DefaultTable from . import DefaultTable
from . import otTables
import struct import struct
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Apple's documentation of 'avar': from .otBase import BaseTTXConverter
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6avar.html
AVAR_HEADER_FORMAT = """
> # big endian
majorVersion: H
minorVersion: H
reserved: H
axisCount: H
"""
assert sstruct.calcsize(AVAR_HEADER_FORMAT) == 8, sstruct.calcsize(AVAR_HEADER_FORMAT)
class table__a_v_a_r(DefaultTable.DefaultTable): class table__a_v_a_r(BaseTTXConverter):
"""Axis Variations Table """Axis Variations Table
This class represents the ``avar`` table of a variable font. The object has one This class represents the ``avar`` table of a variable font. The object has one
@ -54,46 +45,53 @@ class table__a_v_a_r(DefaultTable.DefaultTable):
dependencies = ["fvar"] dependencies = ["fvar"]
def __init__(self, tag=None): def __init__(self, tag=None):
DefaultTable.DefaultTable.__init__(self, tag) super().__init__(tag)
self.segments = {} self.segments = {}
def compile(self, ttFont): def compile(self, ttFont):
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
header = { if not hasattr(self, "table"):
"majorVersion": 1, self.table = otTables.avar()
"minorVersion": 0, if not hasattr(self.table, "Reserved"):
"reserved": 0, self.table.Reserved = 0
"axisCount": len(axisTags), self.table.Version = (getattr(self, "majorVersion", 1) << 16) | getattr(
} self, "minorVersion", 0
result = [sstruct.pack(AVAR_HEADER_FORMAT, header)] )
self.table.AxisCount = len(axisTags)
self.table.AxisSegmentMap = []
for axis in axisTags: for axis in axisTags:
mappings = sorted(self.segments[axis].items()) mappings = self.segments[axis]
result.append(struct.pack(">H", len(mappings))) segmentMap = otTables.AxisSegmentMap()
for key, value in mappings: segmentMap.PositionMapCount = len(mappings)
fixedKey = fl2fi(key, 14) segmentMap.AxisValueMap = []
fixedValue = fl2fi(value, 14) for key, value in sorted(mappings.items()):
result.append(struct.pack(">hh", fixedKey, fixedValue)) valueMap = otTables.AxisValueMap()
return bytesjoin(result) valueMap.FromCoordinate = key
valueMap.ToCoordinate = value
segmentMap.AxisValueMap.append(valueMap)
self.table.AxisSegmentMap.append(segmentMap)
return super().compile(ttFont)
def decompile(self, data, ttFont): def decompile(self, data, ttFont):
super().decompile(data, ttFont)
assert self.table.Version >= 0x00010000
self.majorVersion = self.table.Version >> 16
self.minorVersion = self.table.Version & 0xFFFF
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
header = {}
headerSize = sstruct.calcsize(AVAR_HEADER_FORMAT)
header = sstruct.unpack(AVAR_HEADER_FORMAT, data[0:headerSize])
majorVersion = header["majorVersion"]
if majorVersion != 1:
raise TTLibError("unsupported 'avar' version %d" % majorVersion)
pos = headerSize
for axis in axisTags: for axis in axisTags:
self.segments[axis] = {}
for axis, segmentMap in zip(axisTags, self.table.AxisSegmentMap):
segments = self.segments[axis] = {} segments = self.segments[axis] = {}
numPairs = struct.unpack(">H", data[pos : pos + 2])[0] for segment in segmentMap.AxisValueMap:
pos = pos + 2 segments[segment.FromCoordinate] = segment.ToCoordinate
for _ in range(numPairs):
fromValue, toValue = struct.unpack(">hh", data[pos : pos + 4])
segments[fi2fl(fromValue, 14)] = fi2fl(toValue, 14)
pos = pos + 4
def toXML(self, writer, ttFont): def toXML(self, writer, ttFont):
writer.simpletag(
"version",
major=getattr(self, "majorVersion", 1),
minor=getattr(self, "minorVersion", 0),
)
writer.newline()
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] 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)
@ -105,9 +103,24 @@ class table__a_v_a_r(DefaultTable.DefaultTable):
writer.newline() writer.newline()
writer.endtag("segment") writer.endtag("segment")
writer.newline() writer.newline()
if getattr(self, "majorVersion", 1) >= 2:
if self.table.VarIdxMap:
self.table.VarIdxMap.toXML(writer, ttFont, name="VarIdxMap")
if self.table.VarStore:
self.table.VarStore.toXML(writer, ttFont)
def fromXML(self, name, attrs, content, ttFont): def fromXML(self, name, attrs, content, ttFont):
if name == "segment": if not hasattr(self, "table"):
self.table = otTables.avar()
if not hasattr(self.table, "Reserved"):
self.table.Reserved = 0
if name == "version":
self.majorVersion = safeEval(attrs["major"])
self.minorVersion = safeEval(attrs["minor"])
self.table.Version = (getattr(self, "majorVersion", 1) << 16) | getattr(
self, "minorVersion", 0
)
elif name == "segment":
axis = attrs["axis"] axis = attrs["axis"]
segment = self.segments[axis] = {} segment = self.segments[axis] = {}
for element in content: for element in content:
@ -121,3 +134,5 @@ class table__a_v_a_r(DefaultTable.DefaultTable):
"duplicate entry for %s in axis '%s'", fromValue, axis "duplicate entry for %s in axis '%s'", fromValue, axis
) )
segment[fromValue] = toValue segment[fromValue] = toValue
else:
super().fromXML(name, attrs, content, ttFont)

View File

@ -571,12 +571,10 @@ class Version(SimpleValue):
def read(self, reader, font, tableDict): def read(self, reader, font, tableDict):
value = reader.readLong() value = reader.readLong()
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
return value return value
def write(self, writer, font, tableDict, value, repeatIndex=None): def write(self, writer, font, tableDict, value, repeatIndex=None):
value = fi2ve(value) value = fi2ve(value)
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
writer.writeLong(value) writer.writeLong(value)
@staticmethod @staticmethod

76
Lib/fontTools/ttLib/tables/otData.py Executable file → Normal file
View File

@ -6157,4 +6157,80 @@ otData = [
), ),
], ],
), ),
#
# avar
#
(
"AxisValueMap",
[
(
"F2Dot14",
"FromCoordinate",
None,
None,
"A normalized coordinate value obtained using default normalization",
),
(
"F2Dot14",
"ToCoordinate",
None,
None,
"The modified, normalized coordinate value",
),
],
),
(
"AxisSegmentMap",
[
(
"uint16",
"PositionMapCount",
None,
None,
"The number of correspondence pairs for this axis",
),
(
"AxisValueMap",
"AxisValueMap",
"PositionMapCount",
0,
"The array of axis value map records for this axis",
),
],
),
(
"avar",
[
(
"Version",
"Version",
None,
None,
"Version of the avar table- 0x00010000 or 0x00020000",
),
("uint16", "Reserved", None, None, "Permanently reserved; set to zero"),
(
"uint16",
"AxisCount",
None,
None,
'The number of variation axes for this font. This must be the same number as axisCount in the "fvar" table',
),
(
"AxisSegmentMap",
"AxisSegmentMap",
"AxisCount",
0,
'The segment maps array — one segment map for each axis, in the order of axes specified in the "fvar" table',
),
(
"LOffsetTo(DeltaSetIndexMap)",
"VarIdxMap",
None,
"Version >= 0x00020000",
"",
),
("LOffset", "VarStore", None, "Version >= 0x00020000", ""),
],
),
] ]

View File

@ -637,6 +637,7 @@
</STAT> </STAT>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -1809,6 +1809,7 @@
</VORG> </VORG>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -378,6 +378,7 @@
</GSUB> </GSUB>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -406,6 +406,7 @@
</VVAR> </VVAR>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -129,6 +129,7 @@
</VVAR> </VVAR>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -140,6 +140,7 @@
</VVAR> </VVAR>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -9,6 +9,7 @@
</GlyphOrder> </GlyphOrder>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -8,6 +8,7 @@
</GlyphOrder> </GlyphOrder>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -2668,6 +2668,7 @@
</STAT> </STAT>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -2,6 +2,7 @@
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.36"> <ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.36">
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.5" to="-0.75"/> <mapping from="-0.5" to="-0.75"/>

View File

@ -417,6 +417,7 @@
</post> </post>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="0000"> <segment axis="0000">
</segment> </segment>
<segment axis="0001"> <segment axis="0001">

View File

@ -1,9 +1,13 @@
from fontTools.misc.testTools import parseXML from fontTools.misc.testTools import parseXML
from fontTools.misc.textTools import deHexStr 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.misc.fixedTools import floatToFixed as fl2fi
from fontTools.ttLib import TTFont, TTLibError
import fontTools.ttLib.tables.otTables as otTables
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 from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis
import fontTools.varLib.models as models
import fontTools.varLib.varStore as varStore
from io import BytesIO from io import BytesIO
import unittest import unittest
@ -44,13 +48,6 @@ class AxisVariationTableTest(unittest.TestCase):
avar.segments, avar.segments,
) )
def test_decompile_unsupportedVersion(self):
avar = table__a_v_a_r()
font = self.makeFont(["wdth", "wght"])
self.assertRaises(
TTLibError, avar.decompile, deHexStr("02 01 03 06 00 00 00 00"), font
)
def test_toXML(self): def test_toXML(self):
avar = table__a_v_a_r() avar = table__a_v_a_r()
avar.segments["opsz"] = {-1.0: -1.0, 0.0: 0.0, 0.2999878: 0.7999878, 1.0: 1.0} avar.segments["opsz"] = {-1.0: -1.0, 0.0: 0.0, 0.2999878: 0.7999878, 1.0: 1.0}
@ -58,6 +55,7 @@ class AxisVariationTableTest(unittest.TestCase):
avar.toXML(writer, self.makeFont(["opsz"])) avar.toXML(writer, self.makeFont(["opsz"]))
self.assertEqual( self.assertEqual(
[ [
'<version major="1" minor="0"/>',
'<segment axis="opsz">', '<segment axis="opsz">',
'<mapping from="-1.0" to="-1.0"/>', '<mapping from="-1.0" to="-1.0"/>',
'<mapping from="0.0" to="0.0"/>', '<mapping from="0.0" to="0.0"/>',
@ -91,7 +89,9 @@ class AxisVariationTableTest(unittest.TestCase):
axis = Axis() axis = Axis()
axis.axisTag = tag axis.axisTag = tag
fvar.axes.append(axis) fvar.axes.append(axis)
return {"fvar": fvar} font = TTFont()
font["fvar"] = fvar
return font
@staticmethod @staticmethod
def xml_lines(writer): def xml_lines(writer):
@ -99,6 +99,82 @@ class AxisVariationTableTest(unittest.TestCase):
return [line.strip() for line in content.splitlines()][1:] return [line.strip() for line in content.splitlines()][1:]
class Avar2Test(unittest.TestCase):
def test(self):
axisTags = ["wght", "wdth"]
fvar = table__f_v_a_r()
for tag in axisTags:
axis = Axis()
axis.axisTag = tag
fvar.axes.append(axis)
master_locations_normalized = [
{},
{"wght": 1, "wdth": -1},
]
data = [
{},
{"wdth": -0.8},
]
model = models.VariationModel(master_locations_normalized, axisTags)
store_builder = varStore.OnlineVarStoreBuilder(axisTags)
store_builder.setModel(model)
varIdxes = {}
for axis in axisTags:
masters = [fl2fi(m.get(axis, 0), 14) for m in data]
varIdxes[axis] = store_builder.storeMasters(masters)[1]
store = store_builder.finish()
mapping = store.optimize()
varIdxes = {axis: mapping[value] for axis, value in varIdxes.items()}
del model, store_builder, mapping
varIdxMap = otTables.DeltaSetIndexMap()
varIdxMap.Format = 1
varIdxMap.mapping = []
for tag in axisTags:
varIdxMap.mapping.append(varIdxes[tag])
avar = table__a_v_a_r()
avar.segments["wght"] = {}
avar.segments["wdth"] = {-1.0: -1.0, 0.0: 0.0, 0.4: 0.5, 1.0: 1.0}
avar.majorVersion = 2
avar.table = otTables.avar()
avar.table.VarIdxMap = varIdxMap
avar.table.VarStore = store
font = TTFont()
font["fvar"] = fvar
font["avar"] = avar
b = BytesIO()
font.save(b)
b.seek(0)
font2 = TTFont(b)
assert font2["avar"].table.VarStore.VarRegionList.RegionAxisCount == 2
assert font2["avar"].table.VarStore.VarRegionList.RegionCount == 1
xml1 = BytesIO()
writer = XMLWriter(xml1)
font["avar"].toXML(writer, font)
xml2 = BytesIO()
writer = XMLWriter(xml2)
font2["avar"].toXML(writer, font2)
assert xml1.getvalue() == xml2.getvalue(), (xml1.getvalue(), xml2.getvalue())
avar = table__a_v_a_r()
xml = b"".join(xml2.getvalue().splitlines()[1:])
for name, attrs, content in parseXML(xml):
avar.fromXML(name, attrs, content, ttFont=TTFont())
assert avar.table.VarStore.VarRegionList.RegionAxisCount == 2
assert avar.table.VarStore.VarRegionList.RegionCount == 1
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys

View File

@ -761,6 +761,7 @@
</STAT> </STAT>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.5" to="-0.7283"/> <mapping from="-0.5" to="-0.7283"/>

View File

@ -2,6 +2,7 @@
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14"> <ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14">
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/> <mapping from="0.0" to="0.0"/>

View File

@ -2,6 +2,7 @@
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14"> <ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14">
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.6667" to="-0.7969"/> <mapping from="-0.6667" to="-0.7969"/>

View File

@ -2,6 +2,7 @@
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14"> <ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14">
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.6667" to="-0.7969"/> <mapping from="-0.6667" to="-0.7969"/>

View File

@ -787,6 +787,7 @@
</STAT> </STAT>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.6667" to="-0.7969"/> <mapping from="-0.6667" to="-0.7969"/>

View File

@ -1139,6 +1139,7 @@
</STAT> </STAT>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.6667" to="-0.7969"/> <mapping from="-0.6667" to="-0.7969"/>

View File

@ -1336,6 +1336,7 @@
</STAT> </STAT>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="wght"> <segment axis="wght">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.6667" to="-0.74194"/> <mapping from="-0.6667" to="-0.74194"/>

View File

@ -213,6 +213,7 @@
</GPOS> </GPOS>
<avar> <avar>
<version major="1" minor="0"/>
<segment axis="opsz"> <segment axis="opsz">
<mapping from="-1.0" to="-1.0"/> <mapping from="-1.0" to="-1.0"/>
<mapping from="-0.01" to="-0.9"/> <mapping from="-0.01" to="-0.9"/>