8000 Add `varLib.hvar` module by behdad · Pull Request #3780 · fonttools/fonttools · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add varLib.hvar module #3780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 92 additions & 89 deletions Lib/fontTools/varLib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

.. code-block:: sh

$ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs
$ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs

Then you can make a variable-font this way:

.. code-block:: sh

$ fonttools varLib master_ufo/NotoSansArabic.designspace
$ fonttools varLib master_ufo/NotoSansArabic.designspace

API *will* change in near future.
"""
Expand Down Expand Up @@ -479,7 +479,15 @@ def _merge_TTHinting(font, masterModel, master_ttfs):

_MetricsFields = namedtuple(
"_MetricsFields",
["tableTag", "metricsTag", "sb1", "sb2", "advMapping", "vOrigMapping"],
[
"tableTag",
"metricsTag",
"sb1",
"sb2",
"advMapping",
"vOrigMapping",
"phantomIndex",
],
)

HVAR_FIELDS = _MetricsFields(
Expand All @@ -489,6 +497,7 @@ def _merge_TTHinting(font, masterModel, master_ttfs):
sb2="RsbMap",
advMapping="AdvWidthMap",
vOrigMapping=None,
phantomIndex=0,
)

VVAR_FIELDS = _MetricsFields(
Expand All @@ -498,109 +507,43 @@ def _merge_TTHinting(font, masterModel, master_ttfs):
sb2="BsbMap",
advMapping="AdvHeightMap",
vOrigMapping="VOrgMap",
phantomIndex=1,
)


def _add_HVAR(font, masterModel, master_ttfs, axisTags):
_add_VHVAR(font, masterModel, master_ttfs, axisTags, HVAR_FIELDS)
getAdvanceMetrics = partial(
_get_advance_metrics, font, masterModel, master_ttfs, axisTags, HVAR_FIELDS
)
_add_VHVAR(font, axisTags, HVAR_FIELDS, getAdvanceMetrics)


def _add_VVAR(font, masterModel, master_ttfs, axisTags):
_add_VHVAR(font, masterModel, master_ttfs, axisTags, VVAR_FIELDS)
getAdvanceMetrics = partial(
_get_advance_metrics, font, masterModel, master_ttfs, axisTags, VVAR_FIELDS
)
_add_VHVAR(font, axisTags, VVAR_FIELDS, getAdvanceMetrics)


def _add_VHVAR(font, masterModel, master_ttfs, axisTags, tableFields):
def _add_VHVAR(font, axisTags, tableFields, getAdvanceMetrics):
tableTag = tableFields.tableTag
assert tableTag not in font
glyphOrder = font.getGlyphOrder()
log.info("Generating " + tableTag)
VHVAR = newTable(tableTag)
tableClass = getattr(ot, tableTag)
vhvar = VHVAR.table = tableClass()
vhvar.Version = 0x00010000

glyphOrder = font.getGlyphOrder()

# Build list of source font advance widths for each glyph
metricsTag = tableFields.metricsTag
advMetricses = [m[metricsTag].metrics for m in master_ttfs]

# Build list of source font vertical origin coords for each glyph
if tableTag == "VVAR" and "VORG" in master_ttfs[0]:
vOrigMetricses = [m["VORG"].VOriginRecords for m in master_ttfs]
defaultYOrigs = [m["VORG"].defaultVertOriginY for m in master_ttfs]
vOrigMetricses = list(zip(vOrigMetricses, defaultYOrigs))
else:
vOrigMetricses = None

metricsStore, advanceMapping, vOrigMapping = _get_advance_metrics(
font,
masterModel,
master_ttfs,
axisTags,
glyphOrder,
advMetricses,
vOrigMetricses,
)
vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports = getAdvanceMetrics()

vhvar.VarStore = metricsStore
if advanceMapping is None:
setattr(vhvar, tableFields.advMapping, None)
if vOrigDeltasAndSupports:
singleModel = False
else:
setattr(vhvar, tableFields.advMapping, advanceMapping)
if vOrigMapping is not None:
setattr(vhvar, tableFields.vOrigMapping, vOrigMapping)
setattr(vhvar, tableFields.sb1, None)
setattr(vhvar, tableFields.sb2, None)

font[tableTag] = VHVAR
return


def _get_advance_metrics(
font,
masterModel,
master_ttfs,
axisTags,
glyphOrder,
advMetricses,
vOrigMetricses=None,
):
vhAdvanceDeltasAndSupports = {}
vOrigDeltasAndSupports = {}
# HACK: we treat width 65535 as a sentinel value to signal that a glyph
# from a non-default master should not participate in computing {H,V}VAR,
# as if it were missing. Allows to variate other glyph-related data independently
# from glyph metrics
sparse_advance = 0xFFFF
for glyph in glyphOrder:
vhAdvances = [
(
metrics[glyph][0]
if glyph in metrics and metrics[glyph][0] != sparse_advance
else None
)
for metrics in advMetricses
]
vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
vhAdvances, round=round
singleModel = models.allEqual(
id(v[1]) for v in vhAdvanceDeltasAndSupports.values()
)

singleModel = models.allEqual(id(v[1]) for v in vhAdvanceDeltasAndSupports.values())

if vOrigMetricses:
singleModel = False
for glyph in glyphOrder:
# We need to supply a vOrigs tuple with non-None default values
# for each glyph. vOrigMetricses contains values only for those
# glyphs which have a non-default vOrig.
vOrigs = [
metrics[glyph] if glyph in metrics else defaultVOrig
for metrics, defaultVOrig in vOrigMetricses
]
vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
vOrigs, round=round
)

directStore = None
if singleModel:
# Build direct mapping
Expand All @@ -621,7 +564,7 @@ def _get_advance_metrics(
storeBuilder.setSupports(supports)
advMapping[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)

if vOrigMetricses:
if vOrigDeltasAndSupports:
vOrigMap = {}
for glyphName in glyphOrder:
deltas, supports = vOrigDeltasAndSupports[glyphName]
Expand All @@ -633,7 +576,7 @@ def _get_advance_metrics(
advMapping = [mapping2[advMapping[g]] for g in glyphOrder]
advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder)

if vOrigMetricses:
if vOrigDeltasAndSupports:
vOrigMap = [mapping2[vOrigMap[g]] for g in glyphOrder]

useDirect = False
Expand All @@ -657,10 +600,70 @@ def _get_advance_metrics(
advanceMapping = None
else:
metricsStore = indirectStore
if vOrigMetricses:
if vOrigDeltasAndSupports:
vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder)

return metricsStore, advanceMapping, vOrigMapping
vhvar.VarStore = metricsStore
setattr(vhvar, tableFields.advMapping, advanceMapping)
if vOrigMapping is not None:
setattr(vhvar, tableFields.vOrigMapping, vOrigMapping)
setattr(vhvar, tableFields.sb1, None)
setattr(vhvar, tableFields.sb2, None)

font[tableTag] = VHVAR
return


def _get_advance_metrics(font, masterModel, master_ttfs, axisTags, tableFields):
tableTag = tableFields.tableTag
glyphOrder = font.getGlyphOrder()

# Build list of source font advance widths for each glyph
metricsTag = tableFields.metricsTag
advMetricses = [m[metricsTag].metrics for m in master_ttfs]

# Build list of source font vertical origin coords for each glyph
if tableTag == "VVAR" and "VORG" in master_ttfs[0]:
vOrigMetricses = [m["VORG"].VOriginRecords for m in master_ttfs]
defaultYOrigs = [m["VORG"].defaultVertOriginY for m in master_ttfs]
vOrigMetricses = list(zip(vOrigMetricses, defaultYOrigs))
else:
vOrigMetricses = None

vhAdvanceDeltasAndSupports = {}
vOrigDeltasAndSupports = {}
# HACK: we treat width 65535 as a sentinel value to signal that a glyph
# from a non-default master should not participate in computing {H,V}VAR,
# as if it were missing. Allows to variate other glyph-related data independently
# from glyph metrics
sparse_advance = 0xFFFF
for glyph in glyphOrder:
vhAdvances = [
(
metrics[glyph][0]
if glyph in metrics and metrics[glyph][0] != sparse_advance
else None
)
for metrics in advMetricses
]
vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
vhAdvances, round=round
)

if vOrigMetricses:
for glyph in glyphOrder:
# We need to supply a vOrigs tuple with non-None default values
# for each glyph. vOrigMetricses contains values only for those
# glyphs which have a non-default vOrig.
vOrigs = [
metrics[glyph] if glyph in metrics else defaultVOrig
for metrics, defaultVOrig in vOrigMetricses
]
vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
vOrigs, round=round
)

return vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports


def _add_MVAR(font, masterModel, master_ttfs, axisTags):
Expand Down
113 changes: 113 additions & 0 deletions Lib/fontTools/varLib/hvar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from fontTools.misc.roundTools import noRound
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import OTTableWriter
from fontTools.varLib import HVAR_FIELDS, VVAR_FIELDS, _add_VHVAR
from fontTools.varLib import builder, models, varStore
from fontTools.misc.fixedTools import fixedToFloat as fi2fl
from fontTools.misc.cliTools import makeOutputFileName
from functools import partial
import logging

log = logging.getLogger("fontTools.varLib.avar")


def _get_advance_metrics(font, axisTags, tableFields):
# There's two ways we can go from here:
# 1. For each glyph, at each master peak, compute the value of the
# advance width at that peak. Then pass these all to a VariationModel
# builder to compute back the deltas.
# 2. For each master peak, pull out the deltas of the advance width directly,
# and feed these to the VarStoreBuilder, forgoing the remoding step.
# We'll go with the second option, as it's simpler, faster, and more direct.
gvar = font["gvar"]
vhAdvanceDeltasAndSupports = {}
glyphOrder = font.getGlyphOrder()
phantomIndex = tableFields.phantomIndex
for glyphName in glyphOrder:
supports = []
deltas = []
variations = gvar.variations.get(glyphName, [])

for tv in variations:
supports.append(tv.axes)
phantoms = tv.coordinates[-4:]
phantoms = phantoms[phantomIndex * 2 : phantomIndex * 2 + 2]
assert len(phantoms) == 2
phantoms[0] = phantoms[0][phantomIndex] if phantoms[0] is not None else 0
phantoms[1] = phantoms[1][phantomIndex] if phantoms[1] is not None else 0
deltas.append(phantoms[1] - phantoms[0])

vhAdvanceDeltasAndSupports[glyphName] = (deltas, supports)

vOrigDeltasAndSupports = None # TODO

return vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports


def add_HVAR(font):
if "HVAR" in font:
del font["HVAR"]
axisTags = [axis.axisTag for axis in font["fvar"].axes]
getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, HVAR_FIELDS)
_add_VHVAR(font, axisTags, HVAR_FIELDS, getAdvanceMetrics)


def add_VVAR(font):
if "VVAR" in font:
del font["VVAR"]
getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, HVAR_FIELDS)
axisTags = [axis.axisTag for axis in font["fvar"].axes]
_add_VHVAR(font, axisTags, VVAR_FIELDS, getAdvanceMetrics)


def main(args=None):
"""Add `HVAR` table to variable font."""

if args is None:
import sys

args = sys.argv[1:]

from fontTools import configLogger
from fontTools.designspaceLib import DesignSpaceDocument
import argparse

parser = argparse.ArgumentParser(
"fonttools varLib.hvar",
description="Add `HVAR` table from to variable font.",
)
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
parser.add_argument(
"-o",
"--output-file",
type=str,
help="Output font file name.",
)

options = parser.parse_args(args)

configLogger(level="WARNING")

font = TTFont(options.font)
if not "fvar" in font:
log.error("Not a variable font.")
return 1

add_HVAR(font)
if "vmtx" in font:
add_VVAR(font)

if options.output_file is None:
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".hvar")
else:
outfile = options.output_file
if outfile:
log.info("Saving %s", outfile)
font.save(outfile)


if __name__ == "__main__":
import sys

sys.exit(main())
2 changes: 1 addition & 1 deletion Lib/fontTools/varLib/varStore.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def setModel(self, model):
def setSupports(self, supports):
self._model = None
self._supports = list(supports)
if not self._supports[0]:
if self._supports and not self._supports[0]:
del self._supports[0] # Drop base master support
self._cache = None
self._data = None
Expand Down
Loading
Loading
0