Merge branch 'fonttools:main' into ttf2otf

This commit is contained in:
ftCLI 2024-09-12 08:51:52 +02:00 committed by GitHub
commit 9f7025af8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1197 additions and 110 deletions

View File

@ -118,7 +118,7 @@ jobs:
# so that all artifacts are downloaded in the same directory specified by 'path' # so that all artifacts are downloaded in the same directory specified by 'path'
merge-multiple: true merge-multiple: true
path: dist path: dist
- uses: pypa/gh-action-pypi-publish@v1.9.0 - uses: pypa/gh-action-pypi-publish@v1.10.1
with: with:
user: __token__ user: __token__
password: ${{ secrets.PYPI_PASSWORD }} password: ${{ secrets.PYPI_PASSWORD }}

View File

@ -3,3 +3,4 @@ colorLib.builder: Build COLR/CPAL tables from scratch
##################################################### #####################################################
.. automodule:: fontTools.colorLib.builder .. automodule:: fontTools.colorLib.builder
:no-inherited-members:

View File

@ -31,16 +31,20 @@ needs_sphinx = "1.3"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
"sphinx.ext.napoleon",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx.ext.coverage", "sphinx.ext.coverage",
"sphinx.ext.autosectionlabel", "sphinx.ext.autosectionlabel",
] ]
autodoc_mock_imports = ["gtk", "reportlab"] autodoc_mock_imports = ["gtk", "reportlab"]
autodoc_default_options = {"members": True, "inherited-members": True} autodoc_default_options = {
"members": True,
"inherited-members": True,
"show-inheritance": True,
}
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
@ -78,7 +82,7 @@ release = "4.0"
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language = "en"
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.

View File

@ -297,7 +297,7 @@ Example of all axis elements together
``<output>`` element ``<output>`` element
................... ....................
- Defines the output location of an axis mapping. - Defines the output location of an axis mapping.
- Child element of ``<mapping>`` - Child element of ``<mapping>``

View File

@ -1,4 +1,5 @@
:orphan: :orphan:
.. _developerinfo: .. _developerinfo:
.. image:: ../../Icons/FontToolsIconGreenCircle.png .. image:: ../../Icons/FontToolsIconGreenCircle.png
:width: 200px :width: 200px

View File

@ -6,7 +6,7 @@
---fontTools Documentation--- ---fontTools Documentation---
======= =============================
About About
----- -----

View File

@ -14,4 +14,4 @@ ttFont: Read/write OpenType and TrueType fonts
.. automodule:: fontTools.ttLib.ttFont .. automodule:: fontTools.ttLib.ttFont
:members: getTableModule, registerCustomTableClass, unregisterCustomTableClass, getCustomTableClass, getClassTag, newTable, tagToIdentifier, identifierToTag, tagToXML, xmlToTag, sortedTagList, reorderFontTables :members: getTableModule, registerCustomTableClass, unregisterCustomTableClass, getCustomTableClass, getClassTag, newTable, tagToIdentifier, identifierToTag, tagToXML, xmlToTag, sortedTagList, reorderFontTables
:exclude-members: TTFont, GlyphOrder

View File

@ -1,3 +1,9 @@
"""
designSpaceDocument
- Read and write designspace files
"""
from __future__ import annotations from __future__ import annotations
import collections import collections
@ -15,11 +21,6 @@ from fontTools.misc import plistlib
from fontTools.misc.loggingTools import LogMixin from fontTools.misc.loggingTools import LogMixin
from fontTools.misc.textTools import tobytes, tostr from fontTools.misc.textTools import tobytes, tostr
"""
designSpaceDocument
- read and write designspace files
"""
__all__ = [ __all__ = [
"AxisDescriptor", "AxisDescriptor",

View File

@ -122,15 +122,16 @@ Other options
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
For the other options listed below, to see the current value of the option, For the other options listed below, to see the current value of the option,
pass a value of '?' to it, with or without a '='. pass a value of '?' to it, with or without a '='. In some environments,
you might need to escape the question mark, like this: '--glyph-names\?'.
Examples:: Examples::
$ pyftsubset --glyph-names? $ pyftsubset --glyph-names?
Current setting for 'glyph-names' is: False Current setting for 'glyph-names' is: False
$ ./pyftsubset --name-IDs=? $ pyftsubset --name-IDs=?
Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6]
$ ./pyftsubset --hinting? --no-hinting --hinting? $ pyftsubset --hinting? --no-hinting --hinting?
Current setting for 'hinting' is: True Current setting for 'hinting' is: True
Current setting for 'hinting' is: False Current setting for 'hinting' is: False

View File

@ -1175,17 +1175,8 @@ class NameRecordVisitor(TTVisitor):
@NameRecordVisitor.register_attrs( @NameRecordVisitor.register_attrs(
( (
(otTables.FeatureParamsSize, ("SubfamilyID", "SubfamilyNameID")), (otTables.FeatureParamsSize, ("SubfamilyNameID",)),
(otTables.FeatureParamsStylisticSet, ("UINameID",)), (otTables.FeatureParamsStylisticSet, ("UINameID",)),
(
otTables.FeatureParamsCharacterVariants,
(
"FeatUILabelNameID",
"FeatUITooltipTextNameID",
"SampleTextNameID",
"FirstParamUILabelNameID",
),
),
(otTables.STAT, ("ElidedFallbackNameID",)), (otTables.STAT, ("ElidedFallbackNameID",)),
(otTables.AxisRecord, ("AxisNameID",)), (otTables.AxisRecord, ("AxisNameID",)),
(otTables.AxisValue, ("ValueNameID",)), (otTables.AxisValue, ("ValueNameID",)),
@ -1197,6 +1188,22 @@ def visit(visitor, obj, attr, value):
visitor.seen.add(value) visitor.seen.add(value)
@NameRecordVisitor.register(otTables.FeatureParamsCharacterVariants)
def visit(visitor, obj):
for attr in ("FeatUILabelNameID", "FeatUITooltipTextNameID", "SampleTextNameID"):
value = getattr(obj, attr)
visitor.seen.add(value)
# also include the sequence of UI strings for individual variants, if any
if obj.FirstParamUILabelNameID == 0 or obj.NumNamedParameters == 0:
return
visitor.seen.update(
range(
obj.FirstParamUILabelNameID,
obj.FirstParamUILabelNameID + obj.NumNamedParameters,
)
)
@NameRecordVisitor.register(ttLib.getTableClass("fvar")) @NameRecordVisitor.register(ttLib.getTableClass("fvar"))
def visit(visitor, obj): def visit(visitor, obj):
for inst in obj.instances: for inst in obj.instances:

View File

@ -1,26 +1,3 @@
import os
from copy import deepcopy
from os import fsdecode
import logging
import zipfile
import enum
from collections import OrderedDict
import fs
import fs.base
import fs.subfs
import fs.errors
import fs.copy
import fs.osfs
import fs.zipfs
import fs.tempfs
import fs.tools
from fontTools.misc import plistlib
from fontTools.ufoLib.validators import *
from fontTools.ufoLib.filenames import userNameToFileName
from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
from fontTools.ufoLib.errors import UFOLibError
from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
""" """
A library for importing .ufo files and their descendants. A library for importing .ufo files and their descendants.
Refer to http://unifiedfontobject.com for the UFO specification. Refer to http://unifiedfontobject.com for the UFO specification.
@ -51,6 +28,29 @@ fontinfo.plist values between the possible format versions.
convertFontInfoValueForAttributeFromVersion3ToVersion2 convertFontInfoValueForAttributeFromVersion3ToVersion2
""" """
import os
from copy import deepcopy
from os import fsdecode
import logging
import zipfile
import enum
from collections import OrderedDict
import fs
import fs.base
import fs.subfs
import fs.errors
import fs.copy
import fs.osfs
import fs.zipfs
import fs.tempfs
import fs.tools
from fontTools.misc import plistlib
from fontTools.ufoLib.validators import *
from fontTools.ufoLib.filenames import userNameToFileName
from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
from fontTools.ufoLib.errors import UFOLibError
from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
__all__ = [ __all__ = [
"makeUFOPath", "makeUFOPath",
"UFOLibError", "UFOLibError",

View File

@ -869,7 +869,7 @@ def _add_COLR(font, model, master_fonts, axisTags, colr_layer_reuse=True):
colr.VarIndexMap = builder.buildDeltaSetIndexMap(varIdxes) colr.VarIndexMap = builder.buildDeltaSetIndexMap(varIdxes)
def load_designspace(designspace, log_enabled=True): def load_designspace(designspace, log_enabled=True, *, require_sources=True):
# TODO: remove this and always assume 'designspace' is a DesignSpaceDocument, # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
# never a file path, as that's already handled by caller # never a file path, as that's already handled by caller
if hasattr(designspace, "sources"): # Assume a DesignspaceDocument if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
@ -878,7 +878,7 @@ def load_designspace(designspace, log_enabled=True):
ds = DesignSpaceDocument.fromfile(designspace) ds = DesignSpaceDocument.fromfile(designspace)
masters = ds.sources masters = ds.sources
if not masters: if require_sources and not masters:
raise VarLibValidationError("Designspace must have at least one source.") raise VarLibValidationError("Designspace must have at least one source.")
instances = ds.instances instances = ds.instances
@ -978,7 +978,7 @@ def load_designspace(designspace, log_enabled=True):
"More than one base master found in Designspace." "More than one base master found in Designspace."
) )
base_idx = i base_idx = i
if base_idx is None: if require_sources and base_idx is None:
raise VarLibValidationError( raise VarLibValidationError(
"Base master not found; no master at default location?" "Base master not found; no master at default location?"
) )

View File

@ -1,10 +1,187 @@
from fontTools.varLib import _add_avar, load_designspace from fontTools.varLib import _add_avar, load_designspace
from fontTools.varLib.models import VariationModel
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.misc.fixedTools import fixedToFloat as fi2fl
from fontTools.misc.cliTools import makeOutputFileName from fontTools.misc.cliTools import makeOutputFileName
from itertools import product
import logging import logging
log = logging.getLogger("fontTools.varLib.avar") log = logging.getLogger("fontTools.varLib.avar")
def _denormalize(v, axis):
if v >= 0:
return axis.defaultValue + v * (axis.maxValue - axis.defaultValue)
else:
return axis.defaultValue + v * (axis.defaultValue - axis.minValue)
def _pruneLocations(locations, poles, axisTags):
# Now we have all the input locations, find which ones are
# not needed and remove them.
# Note: This algorithm is heavily tied to how VariationModel
# is implemented. It assumes that input was extracted from
# VariationModel-generated object, like an ItemVariationStore
# created by fontmake using varLib.models.VariationModel.
# Some CoPilot blabbering:
# I *think* I can prove that this algorithm is correct, but
# I'm not 100% sure. It's possible that there are edge cases
# where this algorithm will fail. I'm not sure how to prove
# that it's correct, but I'm also not sure how to prove that
# it's incorrect. I'm not sure how to write a test case that
# would prove that it's incorrect. I'm not sure how to write
# a test case that would prove that it's correct.
model = VariationModel(locations, axisTags)
modelMapping = model.mapping
modelSupports = model.supports
pins = {tuple(k.items()): None for k in poles}
for location in poles:
i = locations.index(location)
i = modelMapping[i]
support = modelSupports[i]
supportAxes = set(support.keys())
for axisTag, (minV, _, maxV) in support.items():
for v in (minV, maxV):
if v in (-1, 0, 1):
continue
for pin in pins.keys():
pinLocation = dict(pin)
pinAxes = set(pinLocation.keys())
if pinAxes != supportAxes:
continue
if axisTag not in pinAxes:
continue
if pinLocation[axisTag] == v:
break
else:
# No pin found. Go through the previous masters
# and find a suitable pin. Going backwards is
# better because it can find a pin that is close
# to the pole in more dimensions, and reducing
# the total number of pins needed.
for candidateIdx in range(i - 1, -1, -1):
candidate = modelSupports[candidateIdx]
candidateAxes = set(candidate.keys())
if candidateAxes != supportAxes:
continue
if axisTag not in candidateAxes:
continue
candidate = {
k: defaultV for k, (_, defaultV, _) in candidate.items()
}
if candidate[axisTag] == v:
pins[tuple(candidate.items())] = None
break
else:
assert False, "No pin found"
return [dict(t) for t in pins.keys()]
def mappings_from_avar(font, denormalize=True):
fvarAxes = font["fvar"].axes
axisMap = {a.axisTag: a for a in fvarAxes}
axisTags = [a.axisTag for a in fvarAxes]
axisIndexes = {a.axisTag: i for i, a in enumerate(fvarAxes)}
if "avar" not in font:
return {}, {}
avar = font["avar"]
axisMaps = {
tag: seg
for tag, seg in avar.segments.items()
if seg and seg != {-1: -1, 0: 0, 1: 1}
}
mappings = []
if getattr(avar, "majorVersion", 1) == 2:
varStore = avar.table.VarStore
regions = varStore.VarRegionList.Region
# Find all the input locations; this finds "poles", that are
# locations of the peaks, and "corners", that are locations
# of the corners of the regions. These two sets of locations
# together constitute inputLocations to consider.
poles = {(): None} # Just using it as an ordered set
inputLocations = set({()})
for varData in varStore.VarData:
regionIndices = varData.VarRegionIndex
for regionIndex in regionIndices:
peakLocation = []
corners = []
region = regions[regionIndex]
for axisIndex, axis in enumerate(region.VarRegionAxis):
if axis.PeakCoord == 0:
continue
axisTag = axisTags[axisIndex]
peakLocation.append((axisTag, axis.PeakCoord))
corner = []
if axis.StartCoord != 0:
corner.append((axisTag, axis.StartCoord))
if axis.EndCoord != 0:
corner.append((axisTag, axis.EndCoord))
corners.append(corner)
corners = set(product(*corners))
peakLocation = tuple(peakLocation)
poles[peakLocation] = None
inputLocations.add(peakLocation)
inputLocations.update(corners)
# Sort them by number of axes, then by axis order
inputLocations = [
dict(t)
for t in sorted(
inputLocations,
key=lambda t: (len(t), tuple(axisIndexes[tag] for tag, _ in t)),
)
]
poles = [dict(t) for t in poles.keys()]
inputLocations = _pruneLocations(inputLocations, list(poles), axisTags)
# Find the output locations, at input locations
varIdxMap = avar.table.VarIdxMap
instancer = VarStoreInstancer(varStore, fvarAxes)
for location in inputLocations:
instancer.setLocation(location)
outputLocation = {}
for axisIndex, axisTag in enumerate(axisTags):
varIdx = axisIndex
if varIdxMap is not None:
varIdx = varIdxMap[varIdx]
delta = instancer[varIdx]
if delta != 0:
v = location.get(axisTag, 0)
v = v + fi2fl(delta, 14)
# See https://github.com/fonttools/fonttools/pull/3598#issuecomment-2266082009
# v = max(-1, min(1, v))
outputLocation[axisTag] = v
mappings.append((location, outputLocation))
# Remove base master we added, if it maps to the default location
assert mappings[0][0] == {}
if mappings[0][1] == {}:
mappings.pop(0)
if denormalize:
for tag, seg in axisMaps.items():
if tag not in axisMap:
raise ValueError(f"Unknown axis tag {tag}")
denorm = lambda v: _denormalize(v, axisMap[tag])
axisMaps[tag] = {denorm(k): denorm(v) for k, v in seg.items()}
for i, (inputLoc, outputLoc) in enumerate(mappings):
inputLoc = {
tag: _denormalize(val, axisMap[tag]) for tag, val in inputLoc.items()
}
outputLoc = {
tag: _denormalize(val, axisMap[tag]) for tag, val in outputLoc.items()
}
mappings[i] = (inputLoc, outputLoc)
return axisMaps, mappings
def main(args=None): def main(args=None):
"""Add `avar` table from designspace file to variable font.""" """Add `avar` table from designspace file to variable font."""
@ -24,7 +201,11 @@ def main(args=None):
) )
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.") parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
parser.add_argument( parser.add_argument(
"designspace", metavar="family.designspace", help="Designspace file." "designspace",
metavar="family.designspace",
help="Designspace file.",
nargs="?",
default=None,
) )
parser.add_argument( parser.add_argument(
"-o", "-o",
@ -45,9 +226,18 @@ def main(args=None):
log.error("Not a variable font.") log.error("Not a variable font.")
return 1 return 1
if options.designspace is None:
from pprint import pprint
segments, mappings = mappings_from_avar(font)
pprint(segments)
pprint(mappings)
print(len(mappings), "mappings")
return
axisTags = [a.axisTag for a in font["fvar"].axes] axisTags = [a.axisTag for a in font["fvar"].axes]
ds = load_designspace(options.designspace) ds = load_designspace(options.designspace, require_sources=False)
if "avar" in font: if "avar" in font:
log.warning("avar table already present, overwriting.") log.warning("avar table already present, overwriting.")

View File

@ -135,6 +135,7 @@ def test_gen(
kinkiness=DEFAULT_KINKINESS, kinkiness=DEFAULT_KINKINESS,
upem=DEFAULT_UPEM, upem=DEFAULT_UPEM,
show_all=False, show_all=False,
discrete_axes=[],
): ):
if tolerance >= 10: if tolerance >= 10:
tolerance *= 0.01 tolerance *= 0.01
@ -150,7 +151,9 @@ def test_gen(
# ... risks the sparse master being the first one, and only processing a subset of the glyphs # ... risks the sparse master being the first one, and only processing a subset of the glyphs
glyphs = {g for glyphset in glyphsets for g in glyphset.keys()} glyphs = {g for glyphset in glyphsets for g in glyphset.keys()}
parents, order = find_parents_and_order(glyphsets, locations) parents, order = find_parents_and_order(
glyphsets, locations, discrete_axes=discrete_axes
)
def grand_parent(i, glyphname): def grand_parent(i, glyphname):
if i is None: if i is None:
@ -701,6 +704,7 @@ def main(args=None):
fonts = [] fonts = []
names = [] names = []
locations = [] locations = []
discrete_axes = set()
upem = DEFAULT_UPEM upem = DEFAULT_UPEM
original_args_inputs = tuple(args.inputs) original_args_inputs = tuple(args.inputs)
@ -713,8 +717,13 @@ def main(args=None):
designspace = DesignSpaceDocument.fromfile(args.inputs[0]) designspace = DesignSpaceDocument.fromfile(args.inputs[0])
args.inputs = [master.path for master in designspace.sources] args.inputs = [master.path for master in designspace.sources]
locations = [master.location for master in designspace.sources] locations = [master.location for master in designspace.sources]
discrete_axes = {
a.name for a in designspace.axes if not hasattr(a, "minimum")
}
axis_triples = { axis_triples = {
a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes a.name: (a.minimum, a.default, a.maximum)
for a in designspace.axes
if a.name not in discrete_axes
} }
axis_mappings = {a.name: a.map for a in designspace.axes} axis_mappings = {a.name: a.map for a in designspace.axes}
axis_triples = { axis_triples = {
@ -879,7 +888,13 @@ def main(args=None):
glyphset[gn] = None glyphset[gn] = None
# Normalize locations # Normalize locations
locations = [normalizeLocation(loc, axis_triples) for loc in locations] locations = [
{
**normalizeLocation(loc, axis_triples),
**{k: v for k, v in loc.items() if k in discrete_axes},
}
for loc in locations
]
tolerance = args.tolerance or DEFAULT_TOLERANCE tolerance = args.tolerance or DEFAULT_TOLERANCE
kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS
@ -896,6 +911,7 @@ def main(args=None):
tolerance=tolerance, tolerance=tolerance,
kinkiness=kinkiness, kinkiness=kinkiness,
show_all=args.show_all, show_all=args.show_all,
discrete_axes=discrete_axes,
) )
problems = defaultdict(list) problems = defaultdict(list)

View File

@ -293,17 +293,19 @@ def add_isomorphisms(points, isomorphisms, reverse):
) )
def find_parents_and_order(glyphsets, locations): def find_parents_and_order(glyphsets, locations, *, discrete_axes=set()):
parents = [None] + list(range(len(glyphsets) - 1)) parents = [None] + list(range(len(glyphsets) - 1))
order = list(range(len(glyphsets))) order = list(range(len(glyphsets)))
if locations: if locations:
# Order base master first # Order base master first
bases = (i for i, l in enumerate(locations) if all(v == 0 for v in l.values())) bases = [
i
for i, l in enumerate(locations)
if all(v == 0 for k, v in l.items() if k not in discrete_axes)
]
if bases: if bases:
base = next(bases) logging.info("Found %s base masters: %s", len(bases), bases)
logging.info("Base master index %s, location %s", base, locations[base])
else: else:
base = 0
logging.warning("No base master location found") logging.warning("No base master location found")
# Form a minimum spanning tree of the locations # Form a minimum spanning tree of the locations
@ -317,9 +319,17 @@ def find_parents_and_order(glyphsets, locations):
axes = sorted(axes) axes = sorted(axes)
vectors = [tuple(l.get(k, 0) for k in axes) for l in locations] vectors = [tuple(l.get(k, 0) for k in axes) for l in locations]
for i, j in itertools.combinations(range(len(locations)), 2): for i, j in itertools.combinations(range(len(locations)), 2):
i_discrete_location = {
k: v for k, v in zip(axes, vectors[i]) if k in discrete_axes
}
j_discrete_location = {
k: v for k, v in zip(axes, vectors[j]) if k in discrete_axes
}
if i_discrete_location != j_discrete_location:
continue
graph[i][j] = vdiff_hypot2(vectors[i], vectors[j]) graph[i][j] = vdiff_hypot2(vectors[i], vectors[j])
tree = minimum_spanning_tree(graph) tree = minimum_spanning_tree(graph, overwrite=True)
rows, cols = tree.nonzero() rows, cols = tree.nonzero()
graph = defaultdict(set) graph = defaultdict(set)
for row, col in zip(rows, cols): for row, col in zip(rows, cols):
@ -330,7 +340,7 @@ def find_parents_and_order(glyphsets, locations):
parents = [None] * len(locations) parents = [None] * len(locations)
order = [] order = []
visited = set() visited = set()
queue = deque([base]) queue = deque(bases)
while queue: while queue:
i = queue.popleft() i = queue.popleft()
visited.add(i) visited.add(i)
@ -339,6 +349,9 @@ def find_parents_and_order(glyphsets, locations):
if j not in visited: if j not in visited:
parents[j] = i parents[j] = i
queue.append(j) queue.append(j)
assert len(order) == len(
parents
), "Not all masters are reachable; report an issue"
except ImportError: except ImportError:
pass pass

View File

@ -209,10 +209,14 @@ def supportScalar(location, support, ot=True, extrapolate=False, axisRanges=None
class VariationModel(object): class VariationModel(object):
"""Locations must have the base master at the origin (ie. 0). """Locations must have the base master at the origin (ie. 0).
If axis-ranges are not provided, values are assumed to be normalized to
the range [-1, 1].
If the extrapolate argument is set to True, then values are extrapolated If the extrapolate argument is set to True, then values are extrapolated
outside the axis range. outside the axis range.
>>> from pprint import pprint >>> from pprint import pprint
>>> axisRanges = {'wght': (-180, +180), 'wdth': (-1, +1)}
>>> locations = [ \ >>> locations = [ \
{'wght':100}, \ {'wght':100}, \
{'wght':-100}, \ {'wght':-100}, \
@ -224,7 +228,7 @@ class VariationModel(object):
{'wght':+180,'wdth':.3}, \ {'wght':+180,'wdth':.3}, \
{'wght':+180}, \ {'wght':+180}, \
] ]
>>> model = VariationModel(locations, axisOrder=['wght']) >>> model = VariationModel(locations, axisOrder=['wght'], axisRanges=axisRanges)
>>> pprint(model.locations) >>> pprint(model.locations)
[{}, [{},
{'wght': -100}, {'wght': -100},
@ -252,14 +256,22 @@ class VariationModel(object):
7: 0.6666666666666667}] 7: 0.6666666666666667}]
""" """
def __init__(self, locations, axisOrder=None, extrapolate=False): def __init__(
self, locations, axisOrder=None, extrapolate=False, *, axisRanges=None
):
if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations): if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
raise VariationModelError("Locations must be unique.") raise VariationModelError("Locations must be unique.")
self.origLocations = locations self.origLocations = locations
self.axisOrder = axisOrder if axisOrder is not None else [] self.axisOrder = axisOrder if axisOrder is not None else []
self.extrapolate = extrapolate self.extrapolate = extrapolate
self.axisRanges = self.computeAxisRanges(locations) if extrapolate else None if axisRanges is None:
if extrapolate:
axisRanges = self.computeAxisRanges(locations)
else:
allAxes = {axis for loc in locations for axis in loc.keys()}
axisRanges = {axis: (-1, 1) for axis in allAxes}
self.axisRanges = axisRanges
locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations] locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
keyFunc = self.getMasterLocationsSortKeyFunc( keyFunc = self.getMasterLocationsSortKeyFunc(
@ -374,7 +386,7 @@ class VariationModel(object):
locAxes = set(region.keys()) locAxes = set(region.keys())
# Walk over previous masters now # Walk over previous masters now
for prev_region in regions[:i]: for prev_region in regions[:i]:
# Master with extra axes do not participte # Master with different axes do not participte
if set(prev_region.keys()) != locAxes: if set(prev_region.keys()) != locAxes:
continue continue
# If it's NOT in the current box, it does not participate # If it's NOT in the current box, it does not participate
@ -425,23 +437,16 @@ class VariationModel(object):
def _locationsToRegions(self): def _locationsToRegions(self):
locations = self.locations locations = self.locations
# Compute min/max across each axis, use it as total range. axisRanges = self.axisRanges
# TODO Take this as input from outside?
minV = {}
maxV = {}
for l in locations:
for k, v in l.items():
minV[k] = min(v, minV.get(k, v))
maxV[k] = max(v, maxV.get(k, v))
regions = [] regions = []
for loc in locations: for loc in locations:
region = {} region = {}
for axis, locV in loc.items(): for axis, locV in loc.items():
if locV > 0: if locV > 0:
region[axis] = (0, locV, maxV[axis]) region[axis] = (0, locV, axisRanges[axis][1])
else: else:
region[axis] = (minV[axis], locV, 0) region[axis] = (axisRanges[axis][0], locV, 0)
regions.append(region) regions.append(region)
return regions return regions

View File

@ -1112,12 +1112,12 @@ class BuilderTest(unittest.TestCase):
var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0] var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1] var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875) assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 1.0)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0) assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0] var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1] var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875) assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 1.0)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5) assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 1.0)
# With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to # With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
# the right, but leaving the wdth axis alone: # the right, but leaving the wdth axis alone:
@ -1129,12 +1129,12 @@ class BuilderTest(unittest.TestCase):
var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0] var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1] var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625) assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 1.0)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0) assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0] var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1] var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625) assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 1.0)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5) assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 1.0)
def test_ligatureCaretByPos_variable_scalar(self): def test_ligatureCaretByPos_variable_scalar(self):
"""Test that the `avar` table is consulted when normalizing user-space """Test that the `avar` table is consulted when normalizing user-space
@ -1158,7 +1158,7 @@ class BuilderTest(unittest.TestCase):
var_region_list = table.VarStore.VarRegionList var_region_list = table.VarStore.VarRegionList
var_region_axis = var_region_list.Region[0].VarRegionAxis[0] var_region_axis = var_region_list.Region[0].VarRegionAxis[0]
assert self.get_region(var_region_axis) == (0.0, 0.875, 0.875) assert self.get_region(var_region_axis) == (0.0, 0.875, 1.0)
def generate_feature_file_test(name): def generate_feature_file_test(name):

View File

@ -16,7 +16,7 @@
<VarRegionAxis index="0"> <VarRegionAxis index="0">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.875"/> <PeakCoord value="0.875"/>
<EndCoord value="0.875"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
<VarRegionAxis index="1"> <VarRegionAxis index="1">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>

View File

@ -12,7 +12,7 @@
<VarRegionAxis index="0"> <VarRegionAxis index="0">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.875"/> <PeakCoord value="0.875"/>
<EndCoord value="0.875"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
<VarRegionAxis index="1"> <VarRegionAxis index="1">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
@ -24,12 +24,12 @@
<VarRegionAxis index="0"> <VarRegionAxis index="0">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.875"/> <PeakCoord value="0.875"/>
<EndCoord value="0.875"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
<VarRegionAxis index="1"> <VarRegionAxis index="1">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.5"/> <PeakCoord value="0.5"/>
<EndCoord value="0.5"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
</Region> </Region>
</VarRegionList> </VarRegionList>

View File

@ -12,7 +12,7 @@
<VarRegionAxis index="0"> <VarRegionAxis index="0">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.875"/> <PeakCoord value="0.875"/>
<EndCoord value="0.875"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
<VarRegionAxis index="1"> <VarRegionAxis index="1">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
@ -24,12 +24,12 @@
<VarRegionAxis index="0"> <VarRegionAxis index="0">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.875"/> <PeakCoord value="0.875"/>
<EndCoord value="0.875"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
<VarRegionAxis index="1"> <VarRegionAxis index="1">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.5"/> <PeakCoord value="0.5"/>
<EndCoord value="0.5"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
</Region> </Region>
</VarRegionList> </VarRegionList>

View File

@ -2,7 +2,6 @@ import contextlib
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from subprocess import run
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import pytest import pytest
@ -28,18 +27,17 @@ def test_main(tmpdir: Path):
input = tmpdir / "in.ttf" input = tmpdir / "in.ttf"
fb.save(str(input)) fb.save(str(input))
output = tmpdir / "out.ttf" output = tmpdir / "out.ttf"
run( args = [
[ "--gpos-compression-level",
"fonttools", "5",
"otlLib.optimize", str(input),
"--gpos-compression-level", "-o",
"5", str(output),
str(input), ]
"-o", from fontTools.otlLib.optimize import main
str(output),
], ret = main(args)
check=True, assert ret in (0, None)
)
assert output.exists() assert output.exists()

View File

@ -0,0 +1,733 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.53">
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name="space"/>
<GlyphID id="2" name="eng"/>
<GlyphID id="3" name="Eng.UCStyle"/>
<GlyphID id="4" name="Eng.BaselineHook"/>
<GlyphID id="5" name="Eng"/>
<GlyphID id="6" name="Eng.Kom"/>
<GlyphID id="7" name="eng.BaselineHook.sc"/>
<GlyphID id="8" name="eng.UCStyle.sc"/>
<GlyphID id="9" name="eng.Kom.sc"/>
</GlyphOrder>
<head>
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="6.101"/>
<checkSumAdjustment value="0x60016654"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000111"/>
<unitsPerEm value="2048"/>
<created value="Wed Feb 2 15:47:19 2022"/>
<modified value="Wed Feb 9 14:12:57 2022"/>
<xMin value="-1387"/>
<yMin value="-1148"/>
<xMax value="4805"/>
<yMax value="2620"/>
<macStyle value="00000000 00000000"/>
<lowestRecPPEM value="6"/>
<fontDirectionHint value="2"/>
<indexToLocFormat value="0"/>
<glyphDataFormat value="0"/>
</head>
<hhea>
<tableVersion value="0x00010000"/>
<ascent value="2500"/>
<descent value="-800"/>
<lineGap value="0"/>
<advanceWidthMax value="4885"/>
<minLeftSideBearing value="-1387"/>
<minRightSideBearing value="-1060"/>
<xMaxExtent value="4805"/>
<caretSlopeRise value="1"/>
<caretSlopeRun value="0"/>
<caretOffset value="0"/>
<reserved0 value="0"/>
<reserved1 value="0"/>
<reserved2 value="0"/>
<reserved3 value="0"/>
<metricDataFormat value="0"/>
<numberOfHMetrics value="10"/>
</hhea>
<maxp>
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="0x10000"/>
<numGlyphs value="10"/>
<maxPoints value="191"/>
<maxContours value="24"/>
<maxCompositePoints value="116"/>
<maxCompositeContours value="7"/>
<maxZones value="1"/>
<maxTwilightPoints value="0"/>
<maxStorage value="0"/>
<maxFunctionDefs value="0"/>
<maxInstructionDefs value="0"/>
<maxStackElements value="0"/>
<maxSizeOfInstructions value="0"/>
<maxComponentElements value="5"/>
<maxComponentDepth value="1"/>
</maxp>
<OS_2>
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
will be recalculated by the compiler -->
<version value="4"/>
<xAvgCharWidth value="1146"/>
<usWeightClass value="400"/>
<usWidthClass value="5"/>
<fsType value="00000000 00000000"/>
<ySubscriptXSize value="1433"/>
<ySubscriptYSize value="1331"/>
<ySubscriptXOffset value="0"/>
<ySubscriptYOffset value="286"/>
<ySuperscriptXSize value="1433"/>
<ySuperscriptYSize value="1331"/>
<ySuperscriptXOffset value="0"/>
<ySuperscriptYOffset value="976"/>
<yStrikeoutSize value="100"/>
<yStrikeoutPosition value="700"/>
<sFamilyClass value="0"/>
<panose>
<bFamilyType value="2"/>
<bSerifStyle value="0"/>
<bWeight value="0"/>
<bProportion value="0"/>
<bContrast value="0"/>
<bStrokeVariation value="0"/>
<bArmStyle value="0"/>
<bLetterForm value="0"/>
<bMidline value="0"/>
<bXHeight value="0"/>
</panose>
<ulUnicodeRange1 value="00000000 00000000 00000000 00000101"/>
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
<achVendID value="SIL "/>
<fsSelection value="00000000 11000000"/>
<usFirstCharIndex value="32"/>
<usLastCharIndex value="331"/>
<sTypoAscender value="2500"/>
<sTypoDescender value="-800"/>
<sTypoLineGap value="0"/>
<usWinAscent value="2500"/>
<usWinDescent value="800"/>
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
<sxHeight value="1040"/>
<sCapHeight value="1485"/>
<usDefaultChar value="0"/>
<usBreakChar value="32"/>
<usMaxContext value="7"/>
</OS_2>
<hmtx>
<mtx name=".notdef" width="1400" lsb="100"/>
<mtx name="Eng" width="1460" lsb="135"/>
<mtx name="Eng.BaselineHook" width="1496" lsb="135"/>
<mtx name="Eng.Kom" width="1496" lsb="135"/>
<mtx name="Eng.UCStyle" width="1500" lsb="160"/>
<mtx name="eng" width="1185" lsb="105"/>
<mtx name="eng.BaselineHook.sc" width="1396" lsb="129"/>
<mtx name="eng.Kom.sc" width="1396" lsb="129"/>
<mtx name="eng.UCStyle.sc" width="1400" lsb="153"/>
<mtx name="space" width="550" lsb="0"/>
</hmtx>
<cmap>
<tableVersion version="0"/>
<cmap_format_4 platformID="0" platEncID="3" language="0">
<map code="0x20" name="space"/><!-- SPACE -->
<map code="0x14a" name="Eng"/><!-- LATIN CAPITAL LETTER ENG -->
<map code="0x14b" name="eng"/><!-- LATIN SMALL LETTER ENG -->
</cmap_format_4>
<cmap_format_4 platformID="3" platEncID="1" language="0">
<map code="0x20" name="space"/><!-- SPACE -->
<map code="0x14a" name="Eng"/><!-- LATIN CAPITAL LETTER ENG -->
<map code="0x14b" name="eng"/><!-- LATIN SMALL LETTER ENG -->
</cmap_format_4>
</cmap>
<loca>
<!-- The 'loca' table will be calculated by the compiler -->
</loca>
<glyf>
<!-- The xMin, yMin, xMax and yMax values
will be recalculated by the compiler. -->
<TTGlyph name=".notdef"/><!-- contains no outline data -->
<TTGlyph name="Eng" xMin="135" yMin="-470" xMax="1275" yMax="1485">
<contour>
<pt x="135" y="1455" on="1"/>
<pt x="320" y="1455" on="1"/>
<pt x="330" y="1425" on="0"/>
<pt x="349" y="1321" on="0"/>
<pt x="366" y="1215" on="0"/>
<pt x="370" y="1180" on="1"/>
<pt x="490" y="1343" on="0"/>
<pt x="747" y="1485" on="0"/>
<pt x="880" y="1485" on="1"/>
<pt x="1066" y="1485" on="0"/>
<pt x="1275" y="1186" on="0"/>
<pt x="1275" y="892" on="1"/>
<pt x="1275" y="145" on="1"/>
<pt x="1275" y="-174" on="0"/>
<pt x="1044" y="-470" on="0"/>
<pt x="845" y="-470" on="1"/>
<pt x="784" y="-470" on="0"/>
<pt x="674" y="-436" on="0"/>
<pt x="650" y="-420" on="1"/>
<pt x="715" y="-265" on="1"/>
<pt x="740" y="-284" on="0"/>
<pt x="824" y="-300" on="0"/>
<pt x="870" y="-300" on="1"/>
<pt x="924" y="-300" on="0"/>
<pt x="1022" y="-223" on="0"/>
<pt x="1085" y="-55" on="0"/>
<pt x="1085" y="80" on="1"/>
<pt x="1085" y="785" on="1"/>
<pt x="1085" y="964" on="0"/>
<pt x="1040" y="1184" on="0"/>
<pt x="923" y="1285" on="0"/>
<pt x="815" y="1285" on="1"/>
<pt x="743" y="1285" on="0"/>
<pt x="609" y="1202" on="0"/>
<pt x="494" y="1068" on="0"/>
<pt x="411" y="913" on="0"/>
<pt x="390" y="840" on="1"/>
<pt x="390" y="0" on="1"/>
<pt x="185" y="0" on="1"/>
<pt x="196" y="60" on="0"/>
<pt x="200" y="282" on="0"/>
<pt x="200" y="430" on="1"/>
<pt x="200" y="880" on="1"/>
<pt x="200" y="1089" on="0"/>
<pt x="156" y="1381" on="0"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="Eng.BaselineHook" xMin="135" yMin="-25" xMax="1305" yMax="1485">
<contour>
<pt x="135" y="1455" on="1"/>
<pt x="320" y="1455" on="1"/>
<pt x="330" y="1425" on="0"/>
<pt x="349" y="1321" on="0"/>
<pt x="366" y="1215" on="0"/>
<pt x="370" y="1180" on="1"/>
<pt x="490" y="1343" on="0"/>
<pt x="770" y="1485" on="0"/>
<pt x="910" y="1485" on="1"/>
<pt x="1096" y="1485" on="0"/>
<pt x="1305" y="1186" on="0"/>
<pt x="1305" y="892" on="1"/>
<pt x="1305" y="714" on="0"/>
<pt x="1305" y="590" on="0"/>
<pt x="1305" y="590" on="1"/>
<pt x="1305" y="271" on="0"/>
<pt x="1089" y="-25" on="0"/>
<pt x="905" y="-25" on="1"/>
<pt x="844" y="-25" on="0"/>
<pt x="734" y="9" on="0"/>
<pt x="710" y="25" on="1"/>
<pt x="775" y="180" on="1"/>
<pt x="800" y="161" on="0"/>
<pt x="884" y="145" on="0"/>
<pt x="930" y="145" on="1"/>
<pt x="974" y="145" on="0"/>
<pt x="1059" y="222" on="0"/>
<pt x="1115" y="390" on="0"/>
<pt x="1115" y="525" on="1"/>
<pt x="1115" y="785" on="1"/>
<pt x="1115" y="964" on="0"/>
<pt x="1070" y="1184" on="0"/>
<pt x="953" y="1285" on="0"/>
<pt x="845" y="1285" on="1"/>
<pt x="770" y="1285" on="0"/>
<pt x="625" y="1202" on="0"/>
<pt x="501" y="1068" on="0"/>
<pt x="411" y="913" on="0"/>
<pt x="390" y="840" on="1"/>
<pt x="390" y="0" on="1"/>
<pt x="185" y="0" on="1"/>
<pt x="196" y="60" on="0"/>
<pt x="200" y="282" on="0"/>
<pt x="200" y="430" on="1"/>
<pt x="200" y="880" on="1"/>
<pt x="200" y="1089" on="0"/>
<pt x="156" y="1381" on="0"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="Eng.Kom" xMin="135" yMin="-25" xMax="1305" yMax="1485">
<contour>
<pt x="135" y="1455" on="1"/>
<pt x="320" y="1455" on="1"/>
<pt x="330" y="1425" on="0"/>
<pt x="347" y="1331" on="0"/>
<pt x="362" y="1235" on="0"/>
<pt x="366" y="1200" on="1"/>
<pt x="482" y="1350" on="0"/>
<pt x="770" y="1485" on="0"/>
<pt x="910" y="1485" on="1"/>
<pt x="1096" y="1485" on="0"/>
<pt x="1305" y="1186" on="0"/>
<pt x="1305" y="892" on="1"/>
<pt x="1305" y="752" on="0"/>
<pt x="1305" y="640" on="0"/>
<pt x="1305" y="640" on="1"/>
<pt x="1305" y="403" on="0"/>
<pt x="1175" y="110" on="0"/>
<pt x="958" y="-25" on="0"/>
<pt x="825" y="-25" on="1"/>
<pt x="756" y="-25" on="0"/>
<pt x="634" y="9" on="0"/>
<pt x="610" y="25" on="1"/>
<pt x="675" y="180" on="1"/>
<pt x="700" y="161" on="0"/>
<pt x="796" y="145" on="0"/>
<pt x="850" y="145" on="1"/>
<pt x="904" y="145" on="0"/>
<pt x="1027" y="226" on="0"/>
<pt x="1115" y="415" on="0"/>
<pt x="1115" y="575" on="1"/>
<pt x="1115" y="785" on="1"/>
<pt x="1115" y="964" on="0"/>
<pt x="1070" y="1184" on="0"/>
<pt x="953" y="1285" on="0"/>
<pt x="845" y="1285" on="1"/>
<pt x="770" y="1285" on="0"/>
<pt x="626" y="1209" on="0"/>
<pt x="504" y="1085" on="0"/>
<pt x="413" y="940" on="0"/>
<pt x="390" y="870" on="1"/>
<pt x="390" y="480" on="1"/>
<pt x="185" y="480" on="1"/>
<pt x="196" y="540" on="0"/>
<pt x="200" y="750" on="0"/>
<pt x="200" y="860" on="1"/>
<pt x="200" y="880" on="1"/>
<pt x="200" y="1089" on="0"/>
<pt x="156" y="1381" on="0"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="Eng.UCStyle" xMin="160" yMin="-470" xMax="1315" yMax="1460">
<contour>
<pt x="200" y="1355" on="1"/>
<pt x="340" y="1460" on="1"/>
<pt x="1275" y="100" on="1"/>
<pt x="1135" y="0" on="1"/>
</contour>
<contour>
<pt x="340" y="1460" on="1"/>
<pt x="340" y="0" on="1"/>
<pt x="160" y="0" on="1"/>
<pt x="171" y="60" on="0"/>
<pt x="175" y="287" on="0"/>
<pt x="175" y="435" on="1"/>
<pt x="175" y="1025" on="1"/>
<pt x="175" y="1173" on="0"/>
<pt x="171" y="1400" on="0"/>
<pt x="160" y="1460" on="1"/>
</contour>
<contour>
<pt x="1135" y="1460" on="1"/>
<pt x="1315" y="1460" on="1"/>
<pt x="1305" y="1400" on="0"/>
<pt x="1300" y="1173" on="0"/>
<pt x="1300" y="1025" on="1"/>
<pt x="1300" y="25" on="1"/>
<pt x="1300" y="-161" on="0"/>
<pt x="1208" y="-378" on="0"/>
<pt x="1040" y="-470" on="0"/>
<pt x="926" y="-470" on="1"/>
<pt x="869" y="-470" on="0"/>
<pt x="734" y="-431" on="0"/>
<pt x="687" y="-395" on="1"/>
<pt x="722" y="-230" on="1"/>
<pt x="758" y="-257" on="0"/>
<pt x="875" y="-306" on="0"/>
<pt x="936" y="-306" on="1"/>
<pt x="1020" y="-306" on="0"/>
<pt x="1135" y="-165" on="0"/>
<pt x="1135" y="-25" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="eng" xMin="105" yMin="-470" xMax="1050" yMax="1040">
<contour>
<pt x="105" y="1020" on="1"/>
<pt x="285" y="1020" on="1"/>
<pt x="293" y="999" on="0"/>
<pt x="308" y="925" on="0"/>
<pt x="322" y="849" on="0"/>
<pt x="325" y="825" on="1"/>
<pt x="421" y="939" on="0"/>
<pt x="615" y="1040" on="0"/>
<pt x="720" y="1040" on="1"/>
<pt x="880" y="1040" on="0"/>
<pt x="1050" y="819" on="0"/>
<pt x="1050" y="560" on="1"/>
<pt x="1050" y="110" on="1"/>
<pt x="1050" y="-115" on="0"/>
<pt x="948" y="-367" on="0"/>
<pt x="766" y="-470" on="0"/>
<pt x="645" y="-470" on="1"/>
<pt x="600" y="-470" on="0"/>
<pt x="496" y="-440" on="0"/>
<pt x="455" y="-415" on="1"/>
<pt x="520" y="-265" on="1"/>
<pt x="572" y="-305" on="0"/>
<pt x="660" y="-305" on="1"/>
<pt x="724" y="-305" on="0"/>
<pt x="815" y="-247" on="0"/>
<pt x="865" y="-95" on="0"/>
<pt x="865" y="45" on="1"/>
<pt x="865" y="485" on="1"/>
<pt x="865" y="646" on="0"/>
<pt x="821" y="809" on="0"/>
<pt x="732" y="865" on="0"/>
<pt x="665" y="865" on="1"/>
<pt x="593" y="865" on="0"/>
<pt x="461" y="779" on="0"/>
<pt x="362" y="648" on="0"/>
<pt x="340" y="580" on="1"/>
<pt x="340" y="0" on="1"/>
<pt x="155" y="0" on="1"/>
<pt x="155" y="615" on="1"/>
<pt x="155" y="762" on="0"/>
<pt x="121" y="968" on="0"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="eng.BaselineHook.sc" xMin="129" yMin="-21" xMax="1202" yMax="1241">
<contour>
<pt x="129" y="1216" on="1"/>
<pt x="310" y="1216" on="1"/>
<pt x="322" y="1180" on="0"/>
<pt x="348" y="1044" on="0"/>
<pt x="355" y="997" on="1"/>
<pt x="463" y="1127" on="0"/>
<pt x="713" y="1241" on="0"/>
<pt x="839" y="1241" on="1"/>
<pt x="1009" y="1241" on="0"/>
<pt x="1202" y="991" on="0"/>
<pt x="1202" y="744" on="1"/>
<pt x="1202" y="495" on="1"/>
<pt x="1202" y="228" on="0"/>
<pt x="1003" y="-21" on="0"/>
<pt x="834" y="-21" on="1"/>
<pt x="777" y="-21" on="0"/>
<pt x="674" y="10" on="0"/>
<pt x="649" y="24" on="1"/>
<pt x="715" y="166" on="1"/>
<pt x="739" y="150" on="0"/>
<pt x="815" y="134" on="0"/>
<pt x="856" y="134" on="1"/>
<pt x="914" y="134" on="0"/>
<pt x="1017" y="277" on="0"/>
<pt x="1017" y="441" on="1"/>
<pt x="1017" y="655" on="1"/>
<pt x="1017" y="801" on="0"/>
<pt x="978" y="979" on="0"/>
<pt x="875" y="1061" on="0"/>
<pt x="780" y="1061" on="1"/>
<pt x="713" y="1061" on="0"/>
<pt x="585" y="993" on="0"/>
<pt x="475" y="885" on="0"/>
<pt x="394" y="759" on="0"/>
<pt x="374" y="700" on="1"/>
<pt x="374" y="0" on="1"/>
<pt x="175" y="0" on="1"/>
<pt x="186" y="56" on="0"/>
<pt x="190" y="242" on="0"/>
<pt x="190" y="363" on="1"/>
<pt x="190" y="734" on="1"/>
<pt x="190" y="906" on="0"/>
<pt x="148" y="1152" on="0"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="eng.Kom.sc" xMin="129" yMin="-21" xMax="1202" yMax="1241">
<contour>
<pt x="129" y="1216" on="1"/>
<pt x="310" y="1216" on="1"/>
<pt x="322" y="1179" on="0"/>
<pt x="344" y="1054" on="0"/>
<pt x="351" y="1009" on="1"/>
<pt x="457" y="1132" on="0"/>
<pt x="713" y="1241" on="0"/>
<pt x="839" y="1241" on="1"/>
<pt x="1009" y="1241" on="0"/>
<pt x="1202" y="991" on="0"/>
<pt x="1202" y="744" on="1"/>
<pt x="1202" y="536" on="1"/>
<pt x="1202" y="338" on="0"/>
<pt x="1083" y="92" on="0"/>
<pt x="884" y="-21" on="0"/>
<pt x="762" y="-21" on="1"/>
<pt x="698" y="-21" on="0"/>
<pt x="583" y="10" on="0"/>
<pt x="559" y="24" on="1"/>
<pt x="625" y="166" on="1"/>
<pt x="649" y="150" on="0"/>
<pt x="736" y="134" on="0"/>
<pt x="785" y="134" on="1"/>
<pt x="832" y="134" on="0"/>
<pt x="940" y="200" on="0"/>
<pt x="1017" y="353" on="0"/>
<pt x="1017" y="482" on="1"/>
<pt x="1017" y="655" on="1"/>
<pt x="1017" y="801" on="0"/>
<pt x="978" y="979" on="0"/>
<pt x="875" y="1061" on="0"/>
<pt x="780" y="1061" on="1"/>
<pt x="693" y="1061" on="0"/>
<pt x="529" y="954" on="0"/>
<pt x="404" y="798" on="0"/>
<pt x="374" y="723" on="1"/>
<pt x="374" y="396" on="1"/>
<pt x="175" y="396" on="1"/>
<pt x="186" y="452" on="0"/>
<pt x="190" y="627" on="0"/>
<pt x="190" y="717" on="1"/>
<pt x="190" y="734" on="1"/>
<pt x="190" y="906" on="0"/>
<pt x="148" y="1152" on="0"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="eng.UCStyle.sc" xMin="153" yMin="-388" xMax="1213" yMax="1222">
<contour>
<pt x="187" y="1128" on="1"/>
<pt x="325" y="1222" on="1"/>
<pt x="1178" y="88" on="1"/>
<pt x="1040" y="-2" on="1"/>
</contour>
<contour>
<pt x="329" y="1221" on="1"/>
<pt x="329" y="0" on="1"/>
<pt x="153" y="0" on="1"/>
<pt x="164" y="56" on="0"/>
<pt x="167" y="245" on="0"/>
<pt x="167" y="366" on="1"/>
<pt x="167" y="853" on="1"/>
<pt x="167" y="975" on="0"/>
<pt x="164" y="1164" on="0"/>
<pt x="153" y="1221" on="1"/>
</contour>
<contour>
<pt x="1036" y="1221" on="1"/>
<pt x="1213" y="1221" on="1"/>
<pt x="1203" y="1165" on="0"/>
<pt x="1198" y="975" on="0"/>
<pt x="1198" y="853" on="1"/>
<pt x="1198" y="28" on="1"/>
<pt x="1198" y="-129" on="0"/>
<pt x="1113" y="-311" on="0"/>
<pt x="959" y="-388" on="0"/>
<pt x="854" y="-388" on="1"/>
<pt x="802" y="-388" on="0"/>
<pt x="675" y="-353" on="0"/>
<pt x="630" y="-322" on="1"/>
<pt x="664" y="-171" on="1"/>
<pt x="699" y="-194" on="0"/>
<pt x="808" y="-238" on="0"/>
<pt x="862" y="-238" on="1"/>
<pt x="936" y="-238" on="0"/>
<pt x="1036" y="-125" on="0"/>
<pt x="1036" y="-13" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="space"/><!-- contains no outline data -->
</glyf>
<name>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright (c) 2004-2022 SIL International
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
Andika
</namerecord>
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
SIL International: Andika Regular: 2022
</namerecord>
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
Andika
</namerecord>
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
Version 6.101
</namerecord>
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
Andika
</namerecord>
<namerecord nameID="321" platformID="3" platEncID="1" langID="0x409">
Capital Eng
</namerecord>
<namerecord nameID="322" platformID="3" platEncID="1" langID="0x409">
Alternate forms of capital Eng
</namerecord>
<namerecord nameID="323" platformID="3" platEncID="1" langID="0x409">
Ŋ
</namerecord>
<namerecord nameID="324" platformID="3" platEncID="1" langID="0x409">
Lowercase no descender
</namerecord>
<namerecord nameID="325" platformID="3" platEncID="1" langID="0x409">
Capital form
</namerecord>
<namerecord nameID="326" platformID="3" platEncID="1" langID="0x409">
Lowercase short stem
</namerecord>
</name>
<post>
<formatType value="2.0"/>
<italicAngle value="0.0"/>
<underlinePosition value="-110"/>
<underlineThickness value="80"/>
<isFixedPitch value="0"/>
<minMemType42 value="0"/>
<maxMemType42 value="0"/>
<minMemType1 value="0"/>
<maxMemType1 value="0"/>
<psNames>
<!-- This file uses unique glyph names based on the information
found in the 'post' table. Since these names might not be unique,
we have to invent artificial names in case of clashes. In order to
be able to retain the original information, we need a name to
ps name mapping for those cases where they differ. That's what
you see below.
-->
</psNames>
<extraNames>
<!-- following are the name that are not taken from the standard Mac glyph order -->
<psName name="eng"/>
<psName name="Eng.UCStyle"/>
<psName name="Eng.BaselineHook"/>
<psName name="Eng"/>
<psName name="Eng.Kom"/>
<psName name="eng.BaselineHook.sc"/>
<psName name="eng.UCStyle.sc"/>
<psName name="eng.Kom.sc"/>
</extraNames>
</post>
<gasp>
<gaspRange rangeMaxPPEM="65535" rangeGaspBehavior="15"/>
</gasp>
<GDEF>
<Version value="0x00010000"/>
<GlyphClassDef>
<ClassDef glyph=".notdef" class="1"/>
<ClassDef glyph="Eng" class="1"/>
<ClassDef glyph="Eng.BaselineHook" class="1"/>
<ClassDef glyph="Eng.Kom" class="1"/>
<ClassDef glyph="Eng.UCStyle" class="1"/>
<ClassDef glyph="eng" class="1"/>
<ClassDef glyph="eng.BaselineHook.sc" class="1"/>
<ClassDef glyph="eng.Kom.sc" class="1"/>
<ClassDef glyph="eng.UCStyle.sc" class="1"/>
<ClassDef glyph="space" class="1"/>
</GlyphClassDef>
</GDEF>
<GSUB>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=3 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="1">
<ScriptTag value="cyrl"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="2">
<ScriptTag value="latn"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="cv43"/>
<Feature>
<FeatureParamsCharacterVariants Format="0">
<Format value="0"/>
<FeatUILabelNameID value="321"/> <!-- Capital Eng -->
<FeatUITooltipTextNameID value="322"/> <!-- Alternate forms of capital Eng -->
<SampleTextNameID value="323"/> <!-- Ŋ -->
<NumNamedParameters value="3"/>
<FirstParamUILabelNameID value="324"/> <!-- Lowercase no descender -->
<!-- CharCount=0 -->
</FeatureParamsCharacterVariants>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<LookupType value="3"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<AlternateSubst index="0">
<AlternateSet glyph="Eng">
<Alternate glyph="Eng.BaselineHook"/>
<Alternate glyph="Eng.UCStyle"/>
<Alternate glyph="Eng.Kom"/>
</AlternateSet>
</AlternateSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>

View File

@ -2071,5 +2071,28 @@ def test_prune_unused_user_name_IDs_with_keep_all(ttf_path):
assert nameIDs == keepNameIDs assert nameIDs == keepNameIDs
def test_cvXX_feature_params_nameIDs_are_retained():
# https://github.com/fonttools/fonttools/issues/3616
font = TTFont()
ttx = pathlib.Path(__file__).parent / "data" / "Andika-Regular.subset.ttx"
font.importXML(ttx)
keepNameIDs = {n.nameID for n in font["name"].names}
options = subset.Options()
options.glyph_names = True
# that's where the FeatureParamsCharacteVariants are stored
options.layout_features.append("cv43")
subsetter = subset.Subsetter(options)
subsetter.populate(glyphs=font.getGlyphOrder())
subsetter.subset(font)
# we expect that all nameIDs are retained, including all the nameIDs
# used by the FeatureParamsCharacterVariants
nameIDs = {n.nameID for n in font["name"].names}
assert nameIDs == keepNameIDs
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(unittest.main()) sys.exit(unittest.main())

94
Tests/varLib/avar_test.py Normal file
View File

@ -0,0 +1,94 @@
from fontTools.ttLib import TTFont
from fontTools.varLib.models import VariationModel
from fontTools.varLib.avar import _pruneLocations, mappings_from_avar
import os
import unittest
import pytest
TESTS = [
(
[
{"wght": 1},
{"wght": 0.5},
],
[
{"wght": 0.5},
],
[
{"wght": 0.5},
],
),
(
[
{"wght": 1, "wdth": 1},
{"wght": 0.5, "wdth": 1},
],
[
{"wght": 1, "wdth": 1},
],
[
{"wght": 1, "wdth": 1},
{"wght": 0.5, "wdth": 1},
],
),
(
[
{"wght": 1},
{"wdth": 1},
{"wght": 0.5, "wdth": 0.5},
],
[
{"wght": 0.5, "wdth": 0.5},
],
[
{"wght": 0.5, "wdth": 0.5},
],
),
]
@pytest.mark.parametrize("locations, poles, expected", TESTS)
def test_pruneLocations(locations, poles, expected):
axisTags = set()
for location in locations:
axisTags.update(location.keys())
axisTags = sorted(axisTags)
locations = [{}] + locations
pruned = _pruneLocations(locations, poles, axisTags)
assert pruned == expected, (pruned, expected)
@pytest.mark.parametrize("locations, poles, expected", TESTS)
def test_roundtrip(locations, poles, expected):
axisTags = set()
for location in locations:
axisTags.update(location.keys())
axisTags = sorted(axisTags)
locations = [{}] + locations
expected = [{}] + expected
model1 = VariationModel(locations, axisTags)
model2 = VariationModel(expected, axisTags)
for location in poles:
i = model1.locations.index(location)
support1 = model1.supports[i]
i = model2.locations.index(location)
support2 = model2.supports[i]
assert support1 == support2, (support1, support2)
def test_mappings_from_avar():
CWD = os.path.abspath(os.path.dirname(__file__))
DATADIR = os.path.join(CWD, "..", "ttLib", "tables", "data")
varfont_path = os.path.join(DATADIR, "Amstelvar-avar2.subset.ttf")
font = TTFont(varfont_path)
mappings = mappings_from_avar(font)
assert len(mappings) == 2, mappings

View File

@ -23,7 +23,7 @@
<VarRegionAxis index="0"> <VarRegionAxis index="0">
<StartCoord value="0.0"/> <StartCoord value="0.0"/>
<PeakCoord value="0.38"/> <PeakCoord value="0.38"/>
<EndCoord value="0.38"/> <EndCoord value="1.0"/>
</VarRegionAxis> </VarRegionAxis>
</Region> </Region>
</VarRegionList> </VarRegionList>

View File

@ -6,4 +6,4 @@ mypy>=0.782
readme_renderer[md]>=43.0 readme_renderer[md]>=43.0
# Pin black as each version could change formatting, breaking CI randomly. # Pin black as each version could change formatting, breaking CI randomly.
black==24.4.2 black==24.8.0

View File

@ -11,10 +11,10 @@ fs==2.4.16
skia-pathops==0.8.0.post1; platform_python_implementation != "PyPy" skia-pathops==0.8.0.post1; platform_python_implementation != "PyPy"
# this is only required to run Tests/cu2qu/{ufo,cli}_test.py # this is only required to run Tests/cu2qu/{ufo,cli}_test.py
ufoLib2==0.16.0 ufoLib2==0.16.0
ufo2ft==3.2.5 ufo2ft==3.2.7
pyobjc==10.3.1; sys_platform == "darwin" pyobjc==10.3.1; sys_platform == "darwin"
freetype-py==2.4.0 freetype-py==2.4.0
uharfbuzz==0.39.3 uharfbuzz==0.39.3
glyphsLib==6.7.1 # this is only required to run Tests/varLib/interpolatable_test.py glyphsLib==6.8.0 # this is only required to run Tests/varLib/interpolatable_test.py
lxml==5.2.2 lxml==5.3.0
sympy==1.13.0 sympy==1.13.2