Merge pull request #1583 from anthrotype/partial-instantiate-HVAR

[instancer] support instantiating HVAR and VVAR  (TTF only for now)
This commit is contained in:
Cosimo Lupo 2019-05-01 12:50:45 +02:00 committed by GitHub
commit 691547b00b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 180 additions and 55 deletions

View File

@ -362,9 +362,9 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
"coord" is an array of GlyphCoordinates which must include the four
"phantom points".
Only the horizontal advance and sidebearings in "hmtx" table are updated
from the first two phantom points. The last two phantom points for
vertical typesetting are currently ignored.
Both the horizontal/vertical advances and left/top sidebearings in "hmtx"
and "vmtx" tables (if any) are updated from four phantom points and
the glyph's bounding boxes.
"""
# TODO: Create new glyph if not already present
assert glyphName in self.glyphs
@ -372,8 +372,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
# Handle phantom points for (left, right, top, bottom) positions.
assert len(coord) >= 4
if not hasattr(glyph, 'xMin'):
glyph.recalcBounds(self)
leftSideX = coord[-4][0]
rightSideX = coord[-3][0]
topSideY = coord[-2][1]
@ -400,9 +398,15 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
# https://github.com/fonttools/fonttools/pull/1198
horizontalAdvanceWidth = 0
leftSideBearing = otRound(glyph.xMin - leftSideX)
# TODO Handle vertical metrics?
ttFont["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
if "vmtx" in ttFont:
verticalAdvanceWidth = otRound(topSideY - bottomSideY)
if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal
verticalAdvanceWidth = 0
topSideBearing = otRound(topSideY - glyph.yMax)
ttFont["vmtx"].metrics[glyphName] = verticalAdvanceWidth, topSideBearing
_GlyphControls = namedtuple(
"_GlyphControls", "numberOfContours endPts flags components"

View File

@ -367,20 +367,20 @@ def _merge_TTHinting(font, masterModel, master_ttfs, tolerance=0.5):
var = TupleVariation(support, delta)
cvar.variations.append(var)
MetricsFields = namedtuple('MetricsFields',
_MetricsFields = namedtuple('_MetricsFields',
['tableTag', 'metricsTag', 'sb1', 'sb2', 'advMapping', 'vOrigMapping'])
hvarFields = MetricsFields(tableTag='HVAR', metricsTag='hmtx', sb1='LsbMap',
HVAR_FIELDS = _MetricsFields(tableTag='HVAR', metricsTag='hmtx', sb1='LsbMap',
sb2='RsbMap', advMapping='AdvWidthMap', vOrigMapping=None)
vvarFields = MetricsFields(tableTag='VVAR', metricsTag='vmtx', sb1='TsbMap',
VVAR_FIELDS = _MetricsFields(tableTag='VVAR', metricsTag='vmtx', sb1='TsbMap',
sb2='BsbMap', advMapping='AdvHeightMap', vOrigMapping='VOrgMap')
def _add_HVAR(font, masterModel, master_ttfs, axisTags):
_add_VHVAR(font, masterModel, master_ttfs, axisTags, hvarFields)
_add_VHVAR(font, masterModel, master_ttfs, axisTags, HVAR_FIELDS)
def _add_VVAR(font, masterModel, master_ttfs, axisTags):
_add_VHVAR(font, masterModel, master_ttfs, axisTags, vvarFields)
_add_VHVAR(font, masterModel, master_ttfs, axisTags, VVAR_FIELDS)
def _add_VHVAR(font, masterModel, master_ttfs, axisTags, tableFields):

View File

@ -17,6 +17,7 @@ from fontTools.varLib.models import supportScalar, normalizeValue, piecewiseLine
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools import varLib
from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.merger import MutatorMerger
@ -82,8 +83,8 @@ def instantiateGvarGlyph(varfont, glyphname, location, optimize=True):
if defaultDeltas:
coordinates += GlyphCoordinates(defaultDeltas)
# this will also set the hmtx advance widths and sidebearings from
# the fourth-last and third-last phantom points (and glyph.xMin)
# this will also set the hmtx/vmtx advance widths and sidebearings from
# the four phantom points and glyph bounding boxes
glyf.setCoordinates(glyphname, coordinates, varfont)
if not tupleVarStore:
@ -158,24 +159,74 @@ def setMvarDeltas(varfont, deltas):
)
def instantiateMvar(varfont, location):
def instantiateMVAR(varfont, location):
log.info("Instantiating MVAR table")
mvar = varfont["MVAR"].table
fvarAxes = varfont["fvar"].axes
defaultDeltas, varIndexMapping = instantiateItemVariationStore(
mvar.VarStore, fvarAxes, location
)
varStore = mvar.VarStore
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location)
setMvarDeltas(varfont, defaultDeltas)
if varIndexMapping:
if varStore.VarRegionList.Region:
varIndexMapping = varStore.optimize()
for rec in mvar.ValueRecord:
rec.VarIdx = varIndexMapping[rec.VarIdx]
else:
# Delete table if no more regions left.
del varfont["MVAR"]
def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder):
oldMapping = getattr(table, attrName).mapping
newMapping = [varIndexMapping[oldMapping[glyphName]] for glyphName in glyphOrder]
setattr(table, attrName, builder.buildVarIdxMap(newMapping, glyphOrder))
# TODO(anthrotype) Add support for HVAR/VVAR in CFF2
def _instantiateVHVAR(varfont, location, tableFields):
tableTag = tableFields.tableTag
fvarAxes = varfont["fvar"].axes
# Deltas from gvar table have already been applied to the hmtx/vmtx. For full
# instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
if set(location).issuperset(axis.axisTag for axis in fvarAxes):
log.info("Dropping %s table", tableTag)
del varfont[tableTag]
return
log.info("Instantiating %s table", tableTag)
vhvar = varfont[tableTag].table
varStore = vhvar.VarStore
# since deltas were already applied, the return value here is ignored
instantiateItemVariationStore(varStore, fvarAxes, location)
if varStore.VarRegionList.Region:
# Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap
# or AdvHeightMap. If a direct, implicit glyphID->VariationIndex mapping is
# used for advances, skip re-optimizing and maintain original VariationIndex.
if getattr(vhvar, tableFields.advMapping):
varIndexMapping = varStore.optimize()
glyphOrder = varfont.getGlyphOrder()
_remapVarIdxMap(vhvar, tableFields.advMapping, varIndexMapping, glyphOrder)
if getattr(vhvar, tableFields.sb1): # left or top sidebearings
_remapVarIdxMap(vhvar, tableFields.sb1, varIndexMapping, glyphOrder)
if getattr(vhvar, tableFields.sb2): # right or bottom sidebearings
_remapVarIdxMap(vhvar, tableFields.sb2, varIndexMapping, glyphOrder)
if tableTag == "VVAR" and getattr(vhvar, tableFields.vOrigMapping):
_remapVarIdxMap(
vhvar, tableFields.vOrigMapping, varIndexMapping, glyphOrder
)
else:
del varfont[tableTag]
def instantiateHVAR(varfont, location):
return _instantiateVHVAR(varfont, location, varLib.HVAR_FIELDS)
def instantiateVVAR(varfont, location):
return _instantiateVHVAR(varfont, location, varLib.VVAR_FIELDS)
class _TupleVarStoreAdapter(object):
def __init__(self, regions, axisOrder, tupleVarData, itemCounts):
self.regions = regions
@ -249,6 +300,8 @@ class _TupleVarStoreAdapter(object):
)
regionList = builder.buildVarRegionList(self.regions, self.axisOrder)
itemVarStore = builder.buildVarStore(regionList, varDatas)
# remove unused regions from VarRegionList
itemVarStore.prune_regions()
return itemVarStore
@ -258,6 +311,10 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
Remove regions in which all axes were instanced, and scale the deltas of
the remaining regions where only some of the axes were instanced.
The number of VarData subtables, and the number of items within each, are
not modified, in order to keep the existing VariationIndex valid.
One may call VarStore.optimize() method after this to further optimize those.
Args:
varStore: An otTables.VarStore object (Item Variation Store)
fvarAxes: list of fvar's Axis objects
@ -267,8 +324,6 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
Returns:
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).
"""
tupleVarStore = _TupleVarStoreAdapter.fromItemVarStore(itemVarStore, fvarAxes)
defaultDeltaArray = tupleVarStore.instantiate(location)
@ -278,18 +333,12 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
assert itemVarStore.VarDataCount == newItemVarStore.VarDataCount
itemVarStore.VarData = newItemVarStore.VarData
if itemVarStore.VarRegionList.Region:
# optimize VarStore, and get a map from old to new VarIdx after optimization
varIndexMapping = itemVarStore.optimize()
else:
varIndexMapping = None # VarStore is empty
defaultDeltas = {
((major << 16) + minor): delta
for major, deltas in enumerate(defaultDeltaArray)
for minor, delta in enumerate(deltas)
}
return defaultDeltas, varIndexMapping
return defaultDeltas
def instantiateOTL(varfont, location):
@ -305,11 +354,10 @@ def instantiateOTL(varfont, location):
log.info(msg)
gdef = varfont["GDEF"].table
varStore = gdef.VarStore
fvarAxes = varfont["fvar"].axes
defaultDeltas, varIndexMapping = instantiateItemVariationStore(
gdef.VarStore, fvarAxes, location
)
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location)
# When VF are built, big lookups may overflow and be broken into multiple
# subtables. MutatorMerger (which inherits from AligningMerger) reattaches
@ -321,11 +369,12 @@ def instantiateOTL(varfont, location):
# LigatureCarets, and optionally deletes all VariationIndex tables if the
# VarStore is fully instanced.
merger = MutatorMerger(
varfont, defaultDeltas, deleteVariations=(varIndexMapping is None)
varfont, defaultDeltas, deleteVariations=(not varStore.VarRegionList.Region)
)
merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"])
if varIndexMapping:
if varStore.VarRegionList.Region:
varIndexMapping = varStore.optimize()
gdef.remap_device_varidxes(varIndexMapping)
if "GPOS" in varfont:
varfont["GPOS"].table.remap_device_varidxes(varIndexMapping)
@ -439,6 +488,9 @@ def sanityCheckVariableTables(varfont):
if "gvar" in varfont:
if "glyf" not in varfont:
raise ValueError("Can't have gvar without glyf")
# TODO(anthrotype) Remove once we do support partial instancing CFF2
if "CFF2" in varfont:
raise NotImplementedError("Instancing CFF2 variable fonts is not supported yet")
def instantiateVariableFont(varfont, axis_limits, inplace=False, optimize=True):
@ -461,15 +513,18 @@ def instantiateVariableFont(varfont, axis_limits, inplace=False, optimize=True):
instantiateCvar(varfont, axis_limits)
if "MVAR" in varfont:
instantiateMvar(varfont, axis_limits)
instantiateMVAR(varfont, axis_limits)
if "HVAR" in varfont:
instantiateHVAR(varfont, axis_limits)
if "VVAR" in varfont:
instantiateVVAR(varfont, axis_limits)
instantiateOTL(varfont, axis_limits)
instantiateFeatureVariations(varfont, axis_limits)
# TODO: actually process HVAR instead of dropping it
del varfont["HVAR"]
return varfont

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.39">
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
@ -12,12 +12,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0x45c905be"/>
<checkSumAdjustment value="0x9180a393"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 5 00:05:14 2019"/>
<modified value="Mon Mar 25 12:51:29 2019"/>
<modified value="Sat Apr 20 17:03:24 2019"/>
<xMin value="40"/>
<yMin value="-200"/>
<xMax value="450"/>
@ -570,7 +570,7 @@
</VarRegionList>
<!-- VarDataCount=1 -->
<VarData index="0">
<!-- ItemCount=3 -->
<!-- ItemCount=2 -->
<NumShorts value="0"/>
<!-- VarRegionCount=5 -->
<VarRegionIndex index="0" value="2"/>
@ -578,18 +578,22 @@
<VarRegionIndex index="2" value="4"/>
<VarRegionIndex index="3" value="5"/>
<VarRegionIndex index="4" value="6"/>
<Item index="0" value="[0, 0, 0, 0, 0]"/>
<Item index="1" value="[-4, -48, -11, 31, 55]"/>
<Item index="2" value="[0, 0, 0, 0, 0]"/>
<Item index="0" value="[-4, -48, -11, 31, 55]"/>
<Item index="1" value="[0, 0, 0, 0, 0]"/>
</VarData>
</VarStore>
<AdvWidthMap>
<Map glyph=".notdef" outer="0" inner="1"/>
<Map glyph="hyphen" outer="0" inner="0"/>
<Map glyph="space" outer="0" inner="1"/>
</AdvWidthMap>
</HVAR>
<MVAR>
<Version value="0x00010000"/>
<Reserved value="0"/>
<ValueRecordSize value="8"/>
<!-- ValueRecordCount=3 -->
<!-- ValueRecordCount=4 -->
<VarStore Format="1">
<Format value="1"/>
<VarRegionList>
@ -632,7 +636,7 @@
</VarRegionAxis>
</Region>
</VarRegionList>
<!-- VarDataCount=1 -->
<!-- VarDataCount=2 -->
<VarData index="0">
<!-- ItemCount=3 -->
<NumShorts value="1"/>
@ -644,7 +648,7 @@
<Item index="1" value="[100, 0, -20]"/>
<Item index="2" value="[50, -30, -20]"/>
</VarData>
<VarData>
<VarData index="1">
<!-- ItemCount=1 -->
<NumShorts value="0"/>
<!-- VarRegionCount=1 -->
@ -1016,4 +1020,10 @@
</glyphVariations>
</gvar>
<vmtx>
<mtx name=".notdef" height="1000" tsb="100"/>
<mtx name="hyphen" height="536" tsb="229"/>
<mtx name="space" height="600" tsb="0"/>
</vmtx>
</ttFont>

View File

@ -67,7 +67,7 @@ class InstantiateGvarTest(object):
(247, 229),
(0, 0),
(274, 0),
(0, 1000),
(0, 536),
(0, 0),
]
},
@ -83,7 +83,7 @@ class InstantiateGvarTest(object):
(265, 229),
(0, 0),
(298, 0),
(0, 1000),
(0, 536),
(0, 0),
]
},
@ -101,7 +101,7 @@ class InstantiateGvarTest(object):
(282, 229),
(0, 0),
(322, 0),
(0, 1000),
(0, 536),
(0, 0),
]
},
@ -133,7 +133,7 @@ class InstantiateGvarTest(object):
(265, 229),
(0, 0),
(298, 0),
(0, 1000),
(0, 536),
(0, 0),
]
@ -169,7 +169,7 @@ class InstantiateCvarTest(object):
assert "cvar" not in varfont
class InstantiateMvarTest(object):
class InstantiateMVARTest(object):
@pytest.mark.parametrize(
"location, expected",
[
@ -217,7 +217,7 @@ class InstantiateMvarTest(object):
assert mvar.VarStore.VarData[1].VarRegionCount == 1
assert all(len(item) == 1 for item in mvar.VarStore.VarData[1].Item)
instancer.instantiateMvar(varfont, location)
instancer.instantiateMVAR(varfont, location)
for mvar_tag, expected_value in expected.items():
table_tag, item_name = MVAR_ENTRIES[mvar_tag]
@ -268,7 +268,7 @@ class InstantiateMvarTest(object):
],
)
def test_full_instance(self, varfont, location, expected):
instancer.instantiateMvar(varfont, location)
instancer.instantiateMVAR(varfont, location)
for mvar_tag, expected_value in expected.items():
table_tag, item_name = MVAR_ENTRIES[mvar_tag]
@ -277,6 +277,62 @@ class InstantiateMvarTest(object):
assert "MVAR" not in varfont
class InstantiateHVARTest(object):
# the 'expectedDeltas' below refer to the VarData item deltas for the "hyphen"
# glyph in the PartialInstancerTest-VF.ttx test font, that are left after
# partial instancing
@pytest.mark.parametrize(
"location, expectedRegions, expectedDeltas",
[
({"wght": -1.0}, [{"wdth": (-1.0, -1.0, 0)}], [-59]),
({"wght": 0}, [{"wdth": (-1.0, -1.0, 0)}], [-48]),
({"wght": 1.0}, [{"wdth": (-1.0, -1.0, 0)}], [7]),
(
{"wdth": -1.0},
[
{"wght": (-1.0, -1.0, 0.0)},
{"wght": (0.0, 0.61, 1.0)},
{"wght": (0.61, 1.0, 1.0)},
],
[-11, 31, 51],
),
({"wdth": 0}, [{"wght": (0.61, 1.0, 1.0)}], [-4]),
],
)
def test_partial_instance(
self, varfont, fvarAxes, location, expectedRegions, expectedDeltas
):
instancer.instantiateHVAR(varfont, location)
assert "HVAR" in varfont
hvar = varfont["HVAR"].table
varStore = hvar.VarStore
regions = varStore.VarRegionList.Region
assert [reg.get_support(fvarAxes) for reg in regions] == expectedRegions
assert len(varStore.VarData) == 1
assert varStore.VarData[0].ItemCount == 2
assert hvar.AdvWidthMap is not None
advWithMap = hvar.AdvWidthMap.mapping
assert advWithMap[".notdef"] == advWithMap["space"]
varIdx = advWithMap[".notdef"]
# these glyphs have no metrics variations in the test font
assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == (
[0] * varStore.VarData[0].VarRegionCount
)
varIdx = advWithMap["hyphen"]
assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == expectedDeltas
def test_full_instance(self, varfont):
instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})
assert "HVAR" not in varfont
class InstantiateItemVariationStoreTest(object):
def test_VarRegion_get_support(self):
axisOrder = ["wght", "wdth", "opsz"]
@ -328,7 +384,7 @@ class InstantiateItemVariationStoreTest(object):
def test_instantiate_default_deltas(
self, varStore, fvarAxes, location, expected_deltas, num_regions
):
defaultDeltas, _ = instancer.instantiateItemVariationStore(
defaultDeltas = instancer.instantiateItemVariationStore(
varStore, fvarAxes, location
)