Merge pull request #3473 from fonttools/avar2-modules

[avar2] Implement avar2 support in ttGlyphSet.
This commit is contained in:
Behdad Esfahbod 2024-04-06 07:59:22 -05:00 committed by GitHub
commit 705acc994f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 103 additions and 23 deletions

View File

@ -6,6 +6,9 @@ from fontTools.misc.fixedTools import (
strToFixedToFloat as str2fl, strToFixedToFloat as str2fl,
) )
from fontTools.misc.textTools import bytesjoin, safeEval from fontTools.misc.textTools import bytesjoin, safeEval
from fontTools.misc.roundTools import otRound
from fontTools.varLib.models import piecewiseLinearMap
from fontTools.varLib.varStore import VarStoreInstancer, NO_VARIATION_INDEX
from fontTools.ttLib import TTLibError from fontTools.ttLib import TTLibError
from . import DefaultTable from . import DefaultTable
from . import otTables from . import otTables
@ -74,9 +77,10 @@ class table__a_v_a_r(BaseTTXConverter):
def decompile(self, data, ttFont): def decompile(self, data, ttFont):
super().decompile(data, ttFont) super().decompile(data, ttFont)
assert self.table.Version >= 0x00010000
self.majorVersion = self.table.Version >> 16 self.majorVersion = self.table.Version >> 16
self.minorVersion = self.table.Version & 0xFFFF self.minorVersion = self.table.Version & 0xFFFF
if self.majorVersion not in (1, 2):
raise NotImplementedError("Unknown avar table version")
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:
self.segments[axis] = {} self.segments[axis] = {}
@ -136,3 +140,48 @@ class table__a_v_a_r(BaseTTXConverter):
segment[fromValue] = toValue segment[fromValue] = toValue
else: else:
super().fromXML(name, attrs, content, ttFont) super().fromXML(name, attrs, content, ttFont)
def renormalizeLocation(self, location, font):
if self.majorVersion not in (1, 2):
raise NotImplementedError("Unknown avar table version")
avarSegments = self.segments
mappedLocation = {}
for axisTag, value in location.items():
avarMapping = avarSegments.get(axisTag, None)
if avarMapping is not None:
value = piecewiseLinearMap(value, avarMapping)
mappedLocation[axisTag] = value
if self.majorVersion < 2:
return mappedLocation
# Version 2
varIdxMap = self.table.VarIdxMap
varStore = self.table.VarStore
axes = font["fvar"].axes
if varStore is not None:
instancer = VarStoreInstancer(varStore, axes, mappedLocation)
coords = list(fl2fi(mappedLocation.get(axis.axisTag, 0), 14) for axis in axes)
out = []
for varIdx, v in enumerate(coords):
if varIdxMap is not None:
varIdx = varIdxMap[varIdx]
if varStore is not None:
delta = instancer[varIdx]
v += otRound(delta)
v = min(max(v, -(1 << 14)), +(1 << 14))
out.append(v)
mappedLocation = {
axis.axisTag: fi2fl(v, 14) for v, axis in zip(out, axes) if v != 0
}
return mappedLocation

View File

@ -110,6 +110,9 @@ class table__f_v_a_r(DefaultTable.DefaultTable):
instance.fromXML(name, attrs, content, ttFont) instance.fromXML(name, attrs, content, ttFont)
self.instances.append(instance) self.instances.append(instance)
def getAxes(self):
return {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in self.axes}
class Axis(object): class Axis(object):
def __init__(self): def __init__(self):

View File

@ -703,6 +703,9 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
assert inner <= 0xFFFF assert inner <= 0xFFFF
mapping.insert(index, (outer << 16) | inner) mapping.insert(index, (outer << 16) | inner)
def __getitem__(self, i):
return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
class VarIdxMap(BaseTable): class VarIdxMap(BaseTable):
def populateDefaults(self, propagator=None): def populateDefaults(self, propagator=None):
@ -755,6 +758,9 @@ class VarIdxMap(BaseTable):
assert inner <= 0xFFFF assert inner <= 0xFFFF
mapping[glyph] = (outer << 16) | inner mapping[glyph] = (outer << 16) | inner
def __getitem__(self, glyphName):
return self.mapping.get(glyphName, NO_VARIATION_INDEX)
class VarRegionList(BaseTable): class VarRegionList(BaseTable):
def preWrite(self, font): def preWrite(self, font):

View File

@ -781,26 +781,15 @@ class TTFont(object):
Raises ``TTLibError`` if the font is not a variable font. Raises ``TTLibError`` if the font is not a variable font.
""" """
from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap from fontTools.varLib.models import normalizeLocation
if "fvar" not in self: if "fvar" not in self:
raise TTLibError("Not a variable font") raise TTLibError("Not a variable font")
axes = { axes = self["fvar"].getAxes()
a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
for a in self["fvar"].axes
}
location = normalizeLocation(location, axes) location = normalizeLocation(location, axes)
if "avar" in self: if "avar" in self:
avar = self["avar"] location = self["avar"].renormalizeLocation(location, self)
avarSegments = avar.segments
mappedLocation = {}
for axisTag, value in location.items():
avarMapping = avarSegments.get(axisTag, None)
if avarMapping is not None:
value = piecewiseLinearMap(value, avarMapping)
mappedLocation[axisTag] = value
location = mappedLocation
return location return location
def getBestCmap( def getBestCmap(

View File

@ -749,22 +749,27 @@ def main(args=None):
if "gvar" in font: if "gvar" in font:
# Is variable font # Is variable font
axisMapping = {}
fvar = font["fvar"] fvar = font["fvar"]
axisMapping = {}
for axis in fvar.axes: for axis in fvar.axes:
axisMapping[axis.axisTag] = { axisMapping[axis.axisTag] = {
-1: axis.minValue, -1: axis.minValue,
0: axis.defaultValue, 0: axis.defaultValue,
1: axis.maxValue, 1: axis.maxValue,
} }
normalized = False
if "avar" in font: if "avar" in font:
avar = font["avar"] avar = font["avar"]
for axisTag, segments in avar.segments.items(): if getattr(avar.table, "VarStore", None):
fvarMapping = axisMapping[axisTag].copy() axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
for location, value in segments.items(): normalized = True
axisMapping[axisTag][value] = piecewiseLinearMap( else:
location, fvarMapping for axisTag, segments in avar.segments.items():
) fvarMapping = axisMapping[axisTag].copy()
for location, value in segments.items():
axisMapping[axisTag][value] = piecewiseLinearMap(
location, fvarMapping
)
gvar = font["gvar"] gvar = font["gvar"]
glyf = font["glyf"] glyf = font["glyf"]
@ -811,6 +816,8 @@ def main(args=None):
) )
+ "'" + "'"
) )
if normalized:
name += " (normalized)"
names.append(name) names.append(name)
fonts.append(glyphsets[locTuple]) fonts.append(glyphsets[locTuple])
locations.append(dict(locTuple)) locations.append(dict(locTuple))

View File

@ -2,6 +2,7 @@ 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.misc.fixedTools import floatToFixed as fl2fi from fontTools.misc.fixedTools import floatToFixed as fl2fi
from fontTools.pens.statisticsPen import StatisticsPen
from fontTools.ttLib import TTFont, TTLibError from fontTools.ttLib import TTFont, TTLibError
import fontTools.ttLib.tables.otTables as otTables 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
@ -9,9 +10,12 @@ from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis
import fontTools.varLib.models as models import fontTools.varLib.models as models
import fontTools.varLib.varStore as varStore import fontTools.varLib.varStore as varStore
from io import BytesIO from io import BytesIO
import os
import unittest import unittest
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
TEST_DATA = deHexStr( TEST_DATA = deHexStr(
"00 01 00 00 00 00 00 02 " "00 01 00 00 00 00 00 02 "
"00 04 C0 00 C0 00 00 00 00 00 13 33 33 33 40 00 40 00 " "00 04 C0 00 C0 00 00 00 00 00 13 33 33 33 40 00 40 00 "
@ -100,7 +104,7 @@ class AxisVariationTableTest(unittest.TestCase):
class Avar2Test(unittest.TestCase): class Avar2Test(unittest.TestCase):
def test(self): def test_roundtrip(self):
axisTags = ["wght", "wdth"] axisTags = ["wght", "wdth"]
fvar = table__f_v_a_r() fvar = table__f_v_a_r()
for tag in axisTags: for tag in axisTags:
@ -173,6 +177,28 @@ class Avar2Test(unittest.TestCase):
assert avar.table.VarStore.VarRegionList.RegionAxisCount == 2 assert avar.table.VarStore.VarRegionList.RegionAxisCount == 2
assert avar.table.VarStore.VarRegionList.RegionCount == 1 assert avar.table.VarStore.VarRegionList.RegionCount == 1
def test_ttGlyphSet(self):
ttf = os.path.join(DATA_DIR, "Amstelvar-avar2.subset.ttf")
font = TTFont(ttf)
regular = font.getGlyphSet()
black = font.getGlyphSet(location={"wght": 900})
expanded = font.getGlyphSet(location={"wdth": 125})
regularStats = StatisticsPen()
blackStats = StatisticsPen()
expandedStats = StatisticsPen()
for glyph in "Test":
regular[glyph].draw(regularStats)
black[glyph].draw(blackStats)
expanded[glyph].draw(expandedStats)
assert abs(regularStats.area) < abs(blackStats.area)
assert abs(expandedStats.area) < abs(blackStats.area)
assert regularStats.meanX < expandedStats.meanX
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys

Binary file not shown.