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'
merge-multiple: true
path: dist
- uses: pypa/gh-action-pypi-publish@v1.9.0
- uses: pypa/gh-action-pypi-publish@v1.10.1
with:
user: __token__
password: ${{ secrets.PYPI_PASSWORD }}

View File

@ -3,3 +3,4 @@ colorLib.builder: Build COLR/CPAL tables from scratch
#####################################################
.. 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
# ones.
extensions = [
"sphinx.ext.napoleon",
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx.ext.coverage",
"sphinx.ext.autosectionlabel",
]
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.
templates_path = ["_templates"]
@ -78,7 +82,7 @@ release = "4.0"
#
# This is also used if you do content translation via gettext catalogs.
# 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
# directories to ignore when looking for source files.

View File

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

View File

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

View File

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

View File

@ -14,4 +14,4 @@ ttFont: Read/write OpenType and TrueType fonts
.. automodule:: fontTools.ttLib.ttFont
: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
import collections
@ -15,11 +21,6 @@ from fontTools.misc import plistlib
from fontTools.misc.loggingTools import LogMixin
from fontTools.misc.textTools import tobytes, tostr
"""
designSpaceDocument
- read and write designspace files
"""
__all__ = [
"AxisDescriptor",

View File

@ -122,15 +122,16 @@ Other options
^^^^^^^^^^^^^
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::
$ pyftsubset --glyph-names?
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]
$ ./pyftsubset --hinting? --no-hinting --hinting?
$ pyftsubset --hinting? --no-hinting --hinting?
Current setting for 'hinting' is: True
Current setting for 'hinting' is: False

View File

@ -1175,17 +1175,8 @@ class NameRecordVisitor(TTVisitor):
@NameRecordVisitor.register_attrs(
(
(otTables.FeatureParamsSize, ("SubfamilyID", "SubfamilyNameID")),
(otTables.FeatureParamsSize, ("SubfamilyNameID",)),
(otTables.FeatureParamsStylisticSet, ("UINameID",)),
(
otTables.FeatureParamsCharacterVariants,
(
"FeatUILabelNameID",
"FeatUITooltipTextNameID",
"SampleTextNameID",
"FirstParamUILabelNameID",
),
),
(otTables.STAT, ("ElidedFallbackNameID",)),
(otTables.AxisRecord, ("AxisNameID",)),
(otTables.AxisValue, ("ValueNameID",)),
@ -1197,6 +1188,22 @@ def visit(visitor, obj, attr, 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"))
def visit(visitor, obj):
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.
Refer to http://unifiedfontobject.com for the UFO specification.
@ -51,6 +28,29 @@ fontinfo.plist values between the possible format versions.
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__ = [
"makeUFOPath",
"UFOLibError",

View File

@ -869,7 +869,7 @@ def _add_COLR(font, model, master_fonts, axisTags, colr_layer_reuse=True):
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,
# never a file path, as that's already handled by caller
if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
@ -878,7 +878,7 @@ def load_designspace(designspace, log_enabled=True):
ds = DesignSpaceDocument.fromfile(designspace)
masters = ds.sources
if not masters:
if require_sources and not masters:
raise VarLibValidationError("Designspace must have at least one source.")
instances = ds.instances
@ -978,7 +978,7 @@ def load_designspace(designspace, log_enabled=True):
"More than one base master found in Designspace."
)
base_idx = i
if base_idx is None:
if require_sources and base_idx is None:
raise VarLibValidationError(
"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.models import VariationModel
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.misc.fixedTools import fixedToFloat as fi2fl
from fontTools.misc.cliTools import makeOutputFileName
from itertools import product
import logging
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):
"""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(
"designspace", metavar="family.designspace", help="Designspace file."
"designspace",
metavar="family.designspace",
help="Designspace file.",
nargs="?",
default=None,
)
parser.add_argument(
"-o",
@ -45,9 +226,18 @@ def main(args=None):
log.error("Not a variable font.")
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]
ds = load_designspace(options.designspace)
ds = load_designspace(options.designspace, require_sources=False)
if "avar" in font:
log.warning("avar table already present, overwriting.")

View File

@ -135,6 +135,7 @@ def test_gen(
kinkiness=DEFAULT_KINKINESS,
upem=DEFAULT_UPEM,
show_all=False,
discrete_axes=[],
):
if tolerance >= 10:
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
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):
if i is None:
@ -701,6 +704,7 @@ def main(args=None):
fonts = []
names = []
locations = []
discrete_axes = set()
upem = DEFAULT_UPEM
original_args_inputs = tuple(args.inputs)
@ -713,8 +717,13 @@ def main(args=None):
designspace = DesignSpaceDocument.fromfile(args.inputs[0])
args.inputs = [master.path 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 = {
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_triples = {
@ -879,7 +888,13 @@ def main(args=None):
glyphset[gn] = None
# 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
kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS
@ -896,6 +911,7 @@ def main(args=None):
tolerance=tolerance,
kinkiness=kinkiness,
show_all=args.show_all,
discrete_axes=discrete_axes,
)
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))
order = list(range(len(glyphsets)))
if locations:
# 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:
base = next(bases)
logging.info("Base master index %s, location %s", base, locations[base])
logging.info("Found %s base masters: %s", len(bases), bases)
else:
base = 0
logging.warning("No base master location found")
# Form a minimum spanning tree of the locations
@ -317,9 +319,17 @@ def find_parents_and_order(glyphsets, locations):
axes = sorted(axes)
vectors = [tuple(l.get(k, 0) for k in axes) for l in locations]
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])
tree = minimum_spanning_tree(graph)
tree = minimum_spanning_tree(graph, overwrite=True)
rows, cols = tree.nonzero()
graph = defaultdict(set)
for row, col in zip(rows, cols):
@ -330,7 +340,7 @@ def find_parents_and_order(glyphsets, locations):
parents = [None] * len(locations)
order = []
visited = set()
queue = deque([base])
queue = deque(bases)
while queue:
i = queue.popleft()
visited.add(i)
@ -339,6 +349,9 @@ def find_parents_and_order(glyphsets, locations):
if j not in visited:
parents[j] = i
queue.append(j)
assert len(order) == len(
parents
), "Not all masters are reachable; report an issue"
except ImportError:
pass

View File

@ -209,10 +209,14 @@ def supportScalar(location, support, ot=True, extrapolate=False, axisRanges=None
class VariationModel(object):
"""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
outside the axis range.
>>> from pprint import pprint
>>> axisRanges = {'wght': (-180, +180), 'wdth': (-1, +1)}
>>> locations = [ \
{'wght':100}, \
{'wght':-100}, \
@ -224,7 +228,7 @@ class VariationModel(object):
{'wght':+180,'wdth':.3}, \
{'wght':+180}, \
]
>>> model = VariationModel(locations, axisOrder=['wght'])
>>> model = VariationModel(locations, axisOrder=['wght'], axisRanges=axisRanges)
>>> pprint(model.locations)
[{},
{'wght': -100},
@ -252,14 +256,22 @@ class VariationModel(object):
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):
raise VariationModelError("Locations must be unique.")
self.origLocations = locations
self.axisOrder = axisOrder if axisOrder is not None else []
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]
keyFunc = self.getMasterLocationsSortKeyFunc(
@ -374,7 +386,7 @@ class VariationModel(object):
locAxes = set(region.keys())
# Walk over previous masters now
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:
continue
# If it's NOT in the current box, it does not participate
@ -425,23 +437,16 @@ class VariationModel(object):
def _locationsToRegions(self):
locations = self.locations
# Compute min/max across each axis, use it as total range.
# 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))
axisRanges = self.axisRanges
regions = []
for loc in locations:
region = {}
for axis, locV in loc.items():
if locV > 0:
region[axis] = (0, locV, maxV[axis])
region[axis] = (0, locV, axisRanges[axis][1])
else:
region[axis] = (minV[axis], locV, 0)
region[axis] = (axisRanges[axis][0], locV, 0)
regions.append(region)
return regions

View File

@ -1112,12 +1112,12 @@ class BuilderTest(unittest.TestCase):
var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
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)
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
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_wdth) == (0.0, 0.5, 0.5)
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, 1.0)
# With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
# 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_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
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)
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
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_wdth) == (0.0, 0.5, 0.5)
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, 1.0)
def test_ligatureCaretByPos_variable_scalar(self):
"""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_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):

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import contextlib
import logging
import os
from pathlib import Path
from subprocess import run
from typing import List, Optional, Tuple
import pytest
@ -28,18 +27,17 @@ def test_main(tmpdir: Path):
input = tmpdir / "in.ttf"
fb.save(str(input))
output = tmpdir / "out.ttf"
run(
[
"fonttools",
"otlLib.optimize",
args = [
"--gpos-compression-level",
"5",
str(input),
"-o",
str(output),
],
check=True,
)
]
from fontTools.otlLib.optimize import main
ret = main(args)
assert ret in (0, None)
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
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__":
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">
<StartCoord value="0.0"/>
<PeakCoord value="0.38"/>
<EndCoord value="0.38"/>
<EndCoord value="1.0"/>
</VarRegionAxis>
</Region>
</VarRegionList>

View File

@ -6,4 +6,4 @@ mypy>=0.782
readme_renderer[md]>=43.0
# 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"
# this is only required to run Tests/cu2qu/{ufo,cli}_test.py
ufoLib2==0.16.0
ufo2ft==3.2.5
ufo2ft==3.2.7
pyobjc==10.3.1; sys_platform == "darwin"
freetype-py==2.4.0
uharfbuzz==0.39.3
glyphsLib==6.7.1 # this is only required to run Tests/varLib/interpolatable_test.py
lxml==5.2.2
sympy==1.13.0
glyphsLib==6.8.0 # this is only required to run Tests/varLib/interpolatable_test.py
lxml==5.3.0
sympy==1.13.2