Merge pull request #1580 from anthrotype/instantiate-gdef-gpos

[partial-instancer] support instantiating GDEF and GPOS
This commit is contained in:
Cosimo Lupo 2019-04-20 10:30:40 +01:00 committed by GitHub
commit fb03be3182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 421 additions and 31 deletions

View File

@ -597,9 +597,15 @@ def _merge_OTL(font, model, master_fonts, axisTags):
GDEF = font['GDEF'].table
assert GDEF.Version <= 0x00010002
except KeyError:
font['GDEF']= newTable('GDEF')
font['GDEF'] = newTable('GDEF')
GDEFTable = font["GDEF"] = newTable('GDEF')
GDEF = GDEFTable.table = ot.GDEF()
GDEF.GlyphClassDef = None
GDEF.AttachList = None
GDEF.LigCaretList = None
GDEF.MarkAttachClassDef = None
GDEF.MarkGlyphSetsDef = None
GDEF.Version = 0x00010003
GDEF.VarStore = store

View File

@ -19,6 +19,7 @@ from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.merger import MutatorMerger
import collections
from copy import deepcopy
import logging
@ -140,9 +141,7 @@ def instantiateCvar(varfont, location):
del varfont["cvar"]
def setMvarDeltas(varfont, deltaArray):
log.info("Setting MVAR deltas")
def setMvarDeltas(varfont, deltas):
mvar = varfont["MVAR"].table
records = mvar.ValueRecord
for rec in records:
@ -150,9 +149,7 @@ def setMvarDeltas(varfont, deltaArray):
if mvarTag not in MVAR_ENTRIES:
continue
tableTag, itemName = MVAR_ENTRIES[mvarTag]
varDataIndex = rec.VarIdx >> 16
itemIndex = rec.VarIdx & 0xFFFF
delta = deltaArray[varDataIndex][itemIndex]
delta = deltas[rec.VarIdx]
if delta != 0:
setattr(
varfont[tableTag],
@ -268,8 +265,8 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
May not specify coordinates for all the fvar axes.
Returns:
defaultDeltaArray: the deltas to be added to the default instance (list of list
of integers, indexed by outer/inner VarIdx)
defaultDeltas: to be added to the default instance, of type dict of ints keyed
by VariationIndex compound values: i.e. (outer << 16) + inner.
varIndexMapping: a mapping from old to new VarIdx after optimization (None if
varStore was fully instanced thus left empty).
"""
@ -287,7 +284,67 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
else:
varIndexMapping = None # VarStore is empty
return defaultDeltaArray, varIndexMapping
defaultDeltas = {
((major << 16) + minor): delta
for major, deltas in enumerate(defaultDeltaArray)
for minor, delta in enumerate(deltas)
}
return defaultDeltas, varIndexMapping
def instantiateOTL(varfont, location):
# TODO(anthrotype) Support partial instancing of JSTF and BASE tables
if "GDEF" not in varfont:
return
if "GPOS" in varfont:
msg = "Instantiating GDEF and GPOS tables"
else:
msg = "Instantiating GDEF table"
log.info(msg)
gdef = varfont["GDEF"].table
fvarAxes = varfont["fvar"].axes
defaultDeltas, varIndexMapping = instantiateItemVariationStore(
gdef.VarStore, fvarAxes, location
)
# When VF are built, big lookups may overflow and be broken into multiple
# subtables. MutatorMerger (which inherits from AligningMerger) reattaches
# them upon instancing, in case they can now fit a single subtable (if not,
# they will be split again upon compilation).
# This 'merger' also works as a 'visitor' that traverses the OTL tables and
# calls specific methods when instances of a given type are found.
# Specifically, it adds default deltas to GPOS Anchors/ValueRecords and GDEF
# LigatureCarets, and optionally deletes all VariationIndex tables if the
# VarStore is fully instanced.
merger = MutatorMerger(
varfont, defaultDeltas, deleteVariations=(varIndexMapping is None)
)
merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"])
if varIndexMapping:
gdef.remap_device_varidxes(varIndexMapping)
if "GPOS" in varfont:
varfont["GPOS"].table.remap_device_varidxes(varIndexMapping)
else:
# Downgrade GDEF.
del gdef.VarStore
gdef.Version = 0x00010002
if gdef.MarkGlyphSetsDef is None:
del gdef.MarkGlyphSetsDef
gdef.Version = 0x00010000
if not (
gdef.LigCaretList
or gdef.MarkAttachClassDef
or gdef.GlyphClassDef
or gdef.AttachList
or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)
):
del varfont["GDEF"]
def instantiateFeatureVariations(varfont, location):
@ -406,6 +463,8 @@ def instantiateVariableFont(varfont, axis_limits, inplace=False, optimize=True):
if "MVAR" in varfont:
instantiateMvar(varfont, axis_limits)
instantiateOTL(varfont, axis_limits)
instantiateFeatureVariations(varfont, axis_limits)
# TODO: actually process HVAR instead of dropping it

View File

@ -833,17 +833,10 @@ class MutatorMerger(AligningMerger):
the operation can benefit from many operations that the
aligning merger does."""
def __init__(self, font, location):
def __init__(self, font, instancer, deleteVariations=True):
Merger.__init__(self, font)
self.location = location
store = None
if 'GDEF' in font:
gdef = font['GDEF'].table
if gdef.Version >= 0x00010003:
store = gdef.VarStore
self.instancer = VarStoreInstancer(store, font['fvar'].axes, location)
self.instancer = instancer
self.deleteVariations = deleteVariations
@MutatorMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
@ -856,14 +849,16 @@ def merge(merger, self, lst):
instancer = merger.instancer
dev = self.DeviceTable
del self.DeviceTable
if merger.deleteVariations:
del self.DeviceTable
if dev:
assert dev.DeltaFormat == 0x8000
varidx = (dev.StartSize << 16) + dev.EndSize
delta = otRound(instancer[varidx])
self.Coordinate += delta
self.Coordinate += delta
self.Format = 1
if merger.deleteVariations:
self.Format = 1
@MutatorMerger.merger(ot.Anchor)
def merge(merger, self, lst):
@ -880,7 +875,8 @@ def merge(merger, self, lst):
if not hasattr(self, tableName):
continue
dev = getattr(self, tableName)
delattr(self, tableName)
if merger.deleteVariations:
delattr(self, tableName)
if dev is None:
continue
@ -891,7 +887,8 @@ def merge(merger, self, lst):
attr = v+'Coordinate'
setattr(self, attr, getattr(self, attr) + delta)
self.Format = 1
if merger.deleteVariations:
self.Format = 1
@MutatorMerger.merger(otBase.ValueRecord)
def merge(merger, self, lst):
@ -900,7 +897,6 @@ def merge(merger, self, lst):
self.__dict__ = lst[0].__dict__.copy()
instancer = merger.instancer
# TODO Handle differing valueformats
for name, tableName in [('XAdvance','XAdvDevice'),
('YAdvance','YAdvDevice'),
('XPlacement','XPlaDevice'),
@ -909,7 +905,8 @@ def merge(merger, self, lst):
if not hasattr(self, tableName):
continue
dev = getattr(self, tableName)
delattr(self, tableName)
if merger.deleteVariations:
delattr(self, tableName)
if dev is None:
continue

View File

@ -284,7 +284,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
gdef = varfont['GDEF'].table
instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
merger = MutatorMerger(varfont, loc)
merger = MutatorMerger(varfont, instancer)
merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])
# Downgrade GDEF.

View File

@ -1,11 +1,17 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools import ttLib
from fontTools import designspaceLib
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
from fontTools.ttLib.tables import _f_v_a_r
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools import varLib
from fontTools.varLib import instancer
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib import builder
from fontTools.varLib import models
import collections
from copy import deepcopy
import os
import pytest
@ -326,7 +332,15 @@ class InstantiateItemVariationStoreTest(object):
varStore, fvarAxes, location
)
assert defaultDeltas == expected_deltas
defaultDeltaArray = []
for varidx, delta in sorted(defaultDeltas.items()):
major, minor = varidx >> 16, varidx & 0xFFFF
if major == len(defaultDeltaArray):
defaultDeltaArray.append([])
assert len(defaultDeltaArray[major]) == minor
defaultDeltaArray[major].append(delta)
assert defaultDeltaArray == expected_deltas
assert varStore.VarRegionList.RegionCount == num_regions
@ -465,8 +479,7 @@ class TupleVarStoreAdapterTest(object):
itemVarStore2 = adapter.asItemVarStore()
assert [
reg.get_support(fvarAxes)
for reg in itemVarStore2.VarRegionList.Region
reg.get_support(fvarAxes) for reg in itemVarStore2.VarRegionList.Region
] == regions
assert itemVarStore2.VarDataCount == 2
@ -477,3 +490,318 @@ class TupleVarStoreAdapterTest(object):
]
assert itemVarStore2.VarData[1].VarRegionIndex == [3, 4, 5, 6]
assert itemVarStore2.VarData[1].Item == [[5, -15, 25, -35], [45, -55, 65, -75]]
def makeTTFont(glyphOrder, features):
font = ttLib.TTFont()
font.setGlyphOrder(glyphOrder)
addOpenTypeFeaturesFromString(font, features)
font["name"] = ttLib.newTable("name")
return font
def _makeDSAxesDict(axes):
dsAxes = collections.OrderedDict()
for axisTag, axisValues in axes:
axis = designspaceLib.AxisDescriptor()
axis.name = axis.tag = axis.labelNames["en"] = axisTag
axis.minimum, axis.default, axis.maximum = axisValues
dsAxes[axis.tag] = axis
return dsAxes
def makeVariableFont(masters, baseIndex, axes, masterLocations):
vf = deepcopy(masters[baseIndex])
dsAxes = _makeDSAxesDict(axes)
fvar = varLib._add_fvar(vf, dsAxes, instances=())
axisTags = [axis.axisTag for axis in fvar.axes]
normalizedLocs = [models.normalizeLocation(m, dict(axes)) for m in masterLocations]
model = models.VariationModel(normalizedLocs, axisOrder=axisTags)
varLib._merge_OTL(vf, model, masters, axisTags)
return vf
def makeParametrizedVF(glyphOrder, features, values, increments):
# Create a test VF with given glyphs and parametrized OTL features.
# The VF is built from 9 masters (3 x 3 along wght and wdth), with
# locations hard-coded and base master at wght=400 and wdth=100.
# 'values' is a list of initial values that are interpolated in the
# 'features' string, and incremented for each subsequent master by the
# given 'increments' (list of 2-tuple) along the two axes.
assert values and len(values) == len(increments)
assert all(len(i) == 2 for i in increments)
masterLocations = [
{"wght": 100, "wdth": 50},
{"wght": 100, "wdth": 100},
{"wght": 100, "wdth": 150},
{"wght": 400, "wdth": 50},
{"wght": 400, "wdth": 100}, # base master
{"wght": 400, "wdth": 150},
{"wght": 700, "wdth": 50},
{"wght": 700, "wdth": 100},
{"wght": 700, "wdth": 150},
]
n = len(values)
values = list(values)
masters = []
for _ in range(3):
for _ in range(3):
master = makeTTFont(glyphOrder, features=features % tuple(values))
masters.append(master)
for i in range(n):
values[i] += increments[i][1]
for i in range(n):
values[i] += increments[i][0]
baseIndex = 4
axes = [("wght", (100, 400, 700)), ("wdth", (50, 100, 150))]
vf = makeVariableFont(masters, baseIndex, axes, masterLocations)
return vf
@pytest.fixture
def varfontGDEF():
glyphOrder = [".notdef", "f", "i", "f_i"]
features = (
"feature liga { sub f i by f_i;} liga;"
"table GDEF { LigatureCaretByPos f_i %d; } GDEF;"
)
values = [100]
increments = [(+30, +10)]
return makeParametrizedVF(glyphOrder, features, values, increments)
@pytest.fixture
def varfontGPOS():
glyphOrder = [".notdef", "V", "A"]
features = "feature kern { pos V A %d; } kern;"
values = [-80]
increments = [(-10, -5)]
return makeParametrizedVF(glyphOrder, features, values, increments)
@pytest.fixture
def varfontGPOS2():
glyphOrder = [".notdef", "V", "A", "acutecomb"]
features = (
"markClass [acutecomb] <anchor 150 -10> @TOP_MARKS;"
"feature mark {"
" pos base A <anchor %d 450> mark @TOP_MARKS;"
"} mark;"
"feature kern {"
" pos V A %d;"
"} kern;"
)
values = [200, -80]
increments = [(+30, +10), (-10, -5)]
return makeParametrizedVF(glyphOrder, features, values, increments)
class InstantiateOTLTest(object):
@pytest.mark.parametrize(
"location, expected",
[
({"wght": -1.0}, 110), # -60
({"wght": 0}, 170),
({"wght": 0.5}, 200), # +30
({"wght": 1.0}, 230), # +60
({"wdth": -1.0}, 160), # -10
({"wdth": -0.3}, 167), # -3
({"wdth": 0}, 170),
({"wdth": 1.0}, 180), # +10
],
)
def test_pin_and_drop_axis_GDEF(self, varfontGDEF, location, expected):
vf = varfontGDEF
assert "GDEF" in vf
instancer.instantiateOTL(vf, location)
assert "GDEF" in vf
gdef = vf["GDEF"].table
assert gdef.Version == 0x00010003
assert gdef.VarStore
assert gdef.LigCaretList
caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0]
assert caretValue.Format == 3
assert hasattr(caretValue, "DeviceTable")
assert caretValue.DeviceTable.DeltaFormat == 0x8000
assert caretValue.Coordinate == expected
@pytest.mark.parametrize(
"location, expected",
[
({"wght": -1.0, "wdth": -1.0}, 100), # -60 - 10
({"wght": -1.0, "wdth": 0.0}, 110), # -60
({"wght": -1.0, "wdth": 1.0}, 120), # -60 + 10
({"wght": 0.0, "wdth": -1.0}, 160), # -10
({"wght": 0.0, "wdth": 0.0}, 170),
({"wght": 0.0, "wdth": 1.0}, 180), # +10
({"wght": 1.0, "wdth": -1.0}, 220), # +60 - 10
({"wght": 1.0, "wdth": 0.0}, 230), # +60
({"wght": 1.0, "wdth": 1.0}, 240), # +60 + 10
],
)
def test_full_instance_GDEF(self, varfontGDEF, location, expected):
vf = varfontGDEF
assert "GDEF" in vf
instancer.instantiateOTL(vf, location)
assert "GDEF" in vf
gdef = vf["GDEF"].table
assert gdef.Version == 0x00010000
assert not hasattr(gdef, "VarStore")
assert gdef.LigCaretList
caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0]
assert caretValue.Format == 1
assert not hasattr(caretValue, "DeviceTable")
assert caretValue.Coordinate == expected
@pytest.mark.parametrize(
"location, expected",
[
({"wght": -1.0}, -85), # +25
({"wght": 0}, -110),
({"wght": 1.0}, -135), # -25
({"wdth": -1.0}, -105), # +5
({"wdth": 0}, -110),
({"wdth": 1.0}, -115), # -5
],
)
def test_pin_and_drop_axis_GPOS_kern(self, varfontGPOS, location, expected):
vf = varfontGPOS
assert "GDEF" in vf
assert "GPOS" in vf
instancer.instantiateOTL(vf, location)
gdef = vf["GDEF"].table
gpos = vf["GPOS"].table
assert gdef.Version == 0x00010003
assert gdef.VarStore
assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos
pairPos = gpos.LookupList.Lookup[0].SubTable[0]
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
assert valueRec1.XAdvDevice
assert valueRec1.XAdvDevice.DeltaFormat == 0x8000
assert valueRec1.XAdvance == expected
@pytest.mark.parametrize(
"location, expected",
[
({"wght": -1.0, "wdth": -1.0}, -80), # +25 + 5
({"wght": -1.0, "wdth": 0.0}, -85), # +25
({"wght": -1.0, "wdth": 1.0}, -90), # +25 - 5
({"wght": 0.0, "wdth": -1.0}, -105), # +5
({"wght": 0.0, "wdth": 0.0}, -110),
({"wght": 0.0, "wdth": 1.0}, -115), # -5
({"wght": 1.0, "wdth": -1.0}, -130), # -25 + 5
({"wght": 1.0, "wdth": 0.0}, -135), # -25
({"wght": 1.0, "wdth": 1.0}, -140), # -25 - 5
],
)
def test_full_instance_GPOS_kern(self, varfontGPOS, location, expected):
vf = varfontGPOS
assert "GDEF" in vf
assert "GPOS" in vf
instancer.instantiateOTL(vf, location)
assert "GDEF" not in vf
gpos = vf["GPOS"].table
assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos
pairPos = gpos.LookupList.Lookup[0].SubTable[0]
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
assert not hasattr(valueRec1, "XAdvDevice")
assert valueRec1.XAdvance == expected
@pytest.mark.parametrize(
"location, expected",
[
({"wght": -1.0}, (210, -85)), # -60, +25
({"wght": 0}, (270, -110)),
({"wght": 0.5}, (300, -122)), # +30, -12
({"wght": 1.0}, (330, -135)), # +60, -25
({"wdth": -1.0}, (260, -105)), # -10, +5
({"wdth": -0.3}, (267, -108)), # -3, +2
({"wdth": 0}, (270, -110)),
({"wdth": 1.0}, (280, -115)), # +10, -5
],
)
def test_pin_and_drop_axis_GPOS_mark_and_kern(
self, varfontGPOS2, location, expected
):
vf = varfontGPOS2
assert "GDEF" in vf
assert "GPOS" in vf
instancer.instantiateOTL(vf, location)
v1, v2 = expected
gdef = vf["GDEF"].table
gpos = vf["GPOS"].table
assert gdef.Version == 0x00010003
assert gdef.VarStore
assert gdef.GlyphClassDef
assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos
markBasePos = gpos.LookupList.Lookup[0].SubTable[0]
baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0]
assert baseAnchor.Format == 3
assert baseAnchor.XDeviceTable
assert baseAnchor.XDeviceTable.DeltaFormat == 0x8000
assert not baseAnchor.YDeviceTable
assert baseAnchor.XCoordinate == v1
assert baseAnchor.YCoordinate == 450
assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos
pairPos = gpos.LookupList.Lookup[1].SubTable[0]
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
assert valueRec1.XAdvDevice
assert valueRec1.XAdvDevice.DeltaFormat == 0x8000
assert valueRec1.XAdvance == v2
@pytest.mark.parametrize(
"location, expected",
[
({"wght": -1.0, "wdth": -1.0}, (200, -80)), # -60 - 10, +25 + 5
({"wght": -1.0, "wdth": 0.0}, (210, -85)), # -60, +25
({"wght": -1.0, "wdth": 1.0}, (220, -90)), # -60 + 10, +25 - 5
({"wght": 0.0, "wdth": -1.0}, (260, -105)), # -10, +5
({"wght": 0.0, "wdth": 0.0}, (270, -110)),
({"wght": 0.0, "wdth": 1.0}, (280, -115)), # +10, -5
({"wght": 1.0, "wdth": -1.0}, (320, -130)), # +60 - 10, -25 + 5
({"wght": 1.0, "wdth": 0.0}, (330, -135)), # +60, -25
({"wght": 1.0, "wdth": 1.0}, (340, -140)), # +60 + 10, -25 - 5
],
)
def test_full_instance_GPOS_mark_and_kern(self, varfontGPOS2, location, expected):
vf = varfontGPOS2
assert "GDEF" in vf
assert "GPOS" in vf
instancer.instantiateOTL(vf, location)
v1, v2 = expected
gdef = vf["GDEF"].table
gpos = vf["GPOS"].table
assert gdef.Version == 0x00010000
assert not hasattr(gdef, "VarStore")
assert gdef.GlyphClassDef
assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos
markBasePos = gpos.LookupList.Lookup[0].SubTable[0]
baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0]
assert baseAnchor.Format == 1
assert not hasattr(baseAnchor, "XDeviceTable")
assert not hasattr(baseAnchor, "YDeviceTable")
assert baseAnchor.XCoordinate == v1
assert baseAnchor.YCoordinate == 450
assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos
pairPos = gpos.LookupList.Lookup[1].SubTable[0]
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
assert not hasattr(valueRec1, "XAdvDevice")
assert valueRec1.XAdvance == v2