From 599f3398de33f93370c25dfb1cf8d07a5df46aaa Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2024 16:49:05 +0100 Subject: [PATCH 01/39] Update sphinx from 7.4.3 to 8.0.2 --- Doc/docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt index 4269223fb..c43b92471 100644 --- a/Doc/docs-requirements.txt +++ b/Doc/docs-requirements.txt @@ -1,4 +1,4 @@ -sphinx==7.4.3 +sphinx==8.0.2 sphinx_rtd_theme==2.0.0 reportlab==4.2.2 freetype-py==2.4.0 From d4f79043aa67f97686e8a1b9b8f347ef2cfbee0e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2024 16:49:05 +0100 Subject: [PATCH 02/39] Update black from 24.4.2 to 24.8.0 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index a2eea1b01..13bebe81f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -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 From ca175df7bcbeb167316fa62ea293dc01a1a1323c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2024 16:49:06 +0100 Subject: [PATCH 03/39] Update ufo2ft from 3.2.5 to 3.2.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d9632cad0..297f5639f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ 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 From 6f358c720246fece19cc879d45cbf6b1b1334b11 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2024 16:49:07 +0100 Subject: [PATCH 04/39] Update glyphslib from 6.7.1 to 6.8.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 297f5639f..96ac92626 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ 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 +glyphsLib==6.8.0 # this is only required to run Tests/varLib/interpolatable_test.py lxml==5.2.2 sympy==1.13.0 From c6aef32488c470cc44b7a1c770630b360d02e7cd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2024 16:49:08 +0100 Subject: [PATCH 05/39] Update lxml from 5.2.2 to 5.3.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 96ac92626..acc2bfbb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,5 +16,5 @@ pyobjc==10.3.1; sys_platform == "darwin" freetype-py==2.4.0 uharfbuzz==0.39.3 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 From 7220fcbdc78653e93bd627d96f35bde00f98bb5b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 12 Aug 2024 16:49:08 +0100 Subject: [PATCH 06/39] Update sympy from 1.13.0 to 1.13.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index acc2bfbb3..2fde2798d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ freetype-py==2.4.0 uharfbuzz==0.39.3 glyphsLib==6.8.0 # this is only required to run Tests/varLib/interpolatable_test.py lxml==5.3.0 -sympy==1.13.0 +sympy==1.13.2 From 6ec2b67152d205b98ca153b22def04b0ed266af9 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Thu, 15 Aug 2024 11:32:05 +0100 Subject: [PATCH 07/39] Remove duplicate sidebar entries for ttLib.ttFont.TTFont and .GlyphOrder. --- Doc/source/ttLib/ttFont.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/source/ttLib/ttFont.rst b/Doc/source/ttLib/ttFont.rst index a3b4c9d0e..d094b29dc 100644 --- a/Doc/source/ttLib/ttFont.rst +++ b/Doc/source/ttLib/ttFont.rst @@ -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 From ead2a18d4b0d5b2c85ace7d96fb59d7692a25e06 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 3 Aug 2024 17:34:01 -0600 Subject: [PATCH 08/39] [varLib.interpolatable] Support discrete axes in .designspace Fixes https://github.com/fonttools/fonttools/issues/3597 --- Lib/fontTools/varLib/interpolatable.py | 22 ++++++++++++--- Lib/fontTools/varLib/interpolatableHelpers.py | 27 ++++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py index e16a95b49..60dbfeb7f 100644 --- a/Lib/fontTools/varLib/interpolatable.py +++ b/Lib/fontTools/varLib/interpolatable.py @@ -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) diff --git a/Lib/fontTools/varLib/interpolatableHelpers.py b/Lib/fontTools/varLib/interpolatableHelpers.py index f71b32afd..5cf22cf87 100644 --- a/Lib/fontTools/varLib/interpolatableHelpers.py +++ b/Lib/fontTools/varLib/interpolatableHelpers.py @@ -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 From 0c2652011e51bd78d153b41287bb845530dd0fdf Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 5 Aug 2024 11:27:12 -0600 Subject: [PATCH 09/39] [varLib.models] By default, assume OpenType-like normalized space See: https://github.com/fonttools/fonttools/pull/2846#issuecomment-2267750076 I *think* this is an improvement, and no one should have been relying on the broken existing behavior. Docs need updating. --- Lib/fontTools/varLib/models.py | 31 +++++++++++-------- Tests/feaLib/builder_test.py | 14 ++++----- Tests/feaLib/data/variable_mark_anchor.ttx | 2 +- Tests/feaLib/data/variable_scalar_anchor.ttx | 6 ++-- .../data/variable_scalar_valuerecord.ttx | 6 ++-- Tests/varLib/data/test_results/BuildAvar2.ttx | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index 819596991..fadfa242c 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -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( @@ -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 diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index e2f6a1d4d..6c1fdab24 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -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): diff --git a/Tests/feaLib/data/variable_mark_anchor.ttx b/Tests/feaLib/data/variable_mark_anchor.ttx index 962cff741..d29fc43a1 100644 --- a/Tests/feaLib/data/variable_mark_anchor.ttx +++ b/Tests/feaLib/data/variable_mark_anchor.ttx @@ -16,7 +16,7 @@ - + diff --git a/Tests/feaLib/data/variable_scalar_anchor.ttx b/Tests/feaLib/data/variable_scalar_anchor.ttx index 6bb55691f..92a456d3b 100644 --- a/Tests/feaLib/data/variable_scalar_anchor.ttx +++ b/Tests/feaLib/data/variable_scalar_anchor.ttx @@ -12,7 +12,7 @@ - + @@ -24,12 +24,12 @@ - + - + diff --git a/Tests/feaLib/data/variable_scalar_valuerecord.ttx b/Tests/feaLib/data/variable_scalar_valuerecord.ttx index e3251f691..94bd3867d 100644 --- a/Tests/feaLib/data/variable_scalar_valuerecord.ttx +++ b/Tests/feaLib/data/variable_scalar_valuerecord.ttx @@ -12,7 +12,7 @@ - + @@ -24,12 +24,12 @@ - + - + diff --git a/Tests/varLib/data/test_results/BuildAvar2.ttx b/Tests/varLib/data/test_results/BuildAvar2.ttx index 27a41bfbb..493d9bdbb 100644 --- a/Tests/varLib/data/test_results/BuildAvar2.ttx +++ b/Tests/varLib/data/test_results/BuildAvar2.ttx @@ -23,7 +23,7 @@ - + From 31b5ce1f8ec5c520d0c50894ebedb99fba50d27c Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Tue, 20 Aug 2024 16:12:00 +0100 Subject: [PATCH 10/39] Docs, minor: fix Sphinx warnings. --- Doc/source/colorLib/index.rst | 1 + Doc/source/designspaceLib/xml.rst | 2 +- Doc/source/developer.rst | 1 + Doc/source/index.rst | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/source/colorLib/index.rst b/Doc/source/colorLib/index.rst index 5a9bf8a16..f9447f622 100644 --- a/Doc/source/colorLib/index.rst +++ b/Doc/source/colorLib/index.rst @@ -3,3 +3,4 @@ colorLib.builder: Build COLR/CPAL tables from scratch ##################################################### .. automodule:: fontTools.colorLib.builder + :no-inherited-members: diff --git a/Doc/source/designspaceLib/xml.rst b/Doc/source/designspaceLib/xml.rst index 7b59dbb17..d8c76be99 100644 --- a/Doc/source/designspaceLib/xml.rst +++ b/Doc/source/designspaceLib/xml.rst @@ -297,7 +297,7 @@ Example of all axis elements together ```` element -................... +.................... - Defines the output location of an axis mapping. - Child element of ```` diff --git a/Doc/source/developer.rst b/Doc/source/developer.rst index e480706af..abaea79af 100644 --- a/Doc/source/developer.rst +++ b/Doc/source/developer.rst @@ -1,4 +1,5 @@ :orphan: + .. _developerinfo: .. image:: ../../Icons/FontToolsIconGreenCircle.png :width: 200px diff --git a/Doc/source/index.rst b/Doc/source/index.rst index 51dfc3c11..e74abf219 100644 --- a/Doc/source/index.rst +++ b/Doc/source/index.rst @@ -6,7 +6,7 @@ ---fontTools Documentation--- -======= +============================= About ----- From 6f7d949d5cd408e8dbcaaa703d783e0a07641395 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Tue, 20 Aug 2024 16:14:46 +0100 Subject: [PATCH 11/39] Docs: update Sphinx config, to show inheritance. This should simplify cross-module readability, and also makes errors caused by autodoc easier to spot. --- Doc/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/source/conf.py b/Doc/source/conf.py index 982af8032..d07a34aca 100644 --- a/Doc/source/conf.py +++ b/Doc/source/conf.py @@ -40,7 +40,7 @@ extensions = [ 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 +78,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. From 868f50daf4ca71a1bb9a63556b399985518c6410 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Tue, 20 Aug 2024 16:15:50 +0100 Subject: [PATCH 12/39] Pin the Sphinx dependency to v7; v8 breaks the RTD theme. See issue #3606 --- Doc/docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt index c43b92471..4269223fb 100644 --- a/Doc/docs-requirements.txt +++ b/Doc/docs-requirements.txt @@ -1,4 +1,4 @@ -sphinx==8.0.2 +sphinx==7.4.3 sphinx_rtd_theme==2.0.0 reportlab==4.2.2 freetype-py==2.4.0 From 1781cf8f7503f27ebf122b173764a8dee0da5620 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Tue, 20 Aug 2024 17:00:52 +0100 Subject: [PATCH 13/39] Docs, minor: reformat line for lint GH action. --- Doc/source/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Doc/source/conf.py b/Doc/source/conf.py index d07a34aca..cee66549b 100644 --- a/Doc/source/conf.py +++ b/Doc/source/conf.py @@ -40,7 +40,11 @@ extensions = [ autodoc_mock_imports = ["gtk", "reportlab"] -autodoc_default_options = {"members": True, "inherited-members": True, "show-inheritance": 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"] From ecd2d8e559cf814a417313b7c799274eda6685b0 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 20 Aug 2024 10:47:59 -0600 Subject: [PATCH 14/39] [Tests] Do not require fonttools command to be available I typically run tests like: $ python setup.py build_ext -i && PYTHONPATH=Lib pytest Previously, this particular test and only this, required that a `pip install -e .` has had happened. Not anymore. --- Tests/otlLib/optimize_test.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Tests/otlLib/optimize_test.py b/Tests/otlLib/optimize_test.py index a2e433225..a1ab88da7 100644 --- a/Tests/otlLib/optimize_test.py +++ b/Tests/otlLib/optimize_test.py @@ -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", - "--gpos-compression-level", - "5", - str(input), - "-o", - str(output), - ], - check=True, - ) + args = [ + "--gpos-compression-level", + "5", + str(input), + "-o", + str(output), + ] + from fontTools.otlLib.optimize import main + + ret = main(args) + assert ret in (0, None) assert output.exists() From 150d4fc195a19f383ce4380b87beb483b3938554 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 14:10:52 -0600 Subject: [PATCH 15/39] [varLib.avar] Sketch of code to reconstruct mappings from binary https://github.com/Lorp/fencer/issues/25 --- Lib/fontTools/varLib/avar.py | 106 ++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 60f0d7e70..541bc5b83 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -1,10 +1,104 @@ from fontTools.varLib import _add_avar, load_designspace +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): + return axis.defaultValue + v * ( + (axis.maxValue if v >= 0 else axis.minValue) - axis.defaultValue + ) + + +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 + 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[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)) + inputLocations.update(corners) + + inputLocations = [ + dict(t) + for t in sorted( + inputLocations, + key=lambda t: (len(t), tuple(axisIndexes[tag] for tag, _ in t)), + ) + ] + + 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) + v = max(-1, min(1, v)) + outputLocation[axisTag] = v + mappings.append((location, outputLocation)) + # Filter out empty mappings + mappings = [io for io in mappings if io[1]] + + 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 +118,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,6 +143,12 @@ def main(args=None): log.error("Not a variable font.") return 1 + if options.designspace is None: + from pprint import pprint + + pprint(mappings_from_avar(font)) + return + axisTags = [a.axisTag for a in font["fvar"].axes] ds = load_designspace(options.designspace) From 40f6760e8a7f370de6e979ad43113805e99d9749 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 14:19:50 -0600 Subject: [PATCH 16/39] [varLib.avar] Don't clamp values https://github.com/fonttools/fonttools/pull/3598#issuecomment-2266082009 --- Lib/fontTools/varLib/avar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 541bc5b83..1dfed52f5 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -74,7 +74,8 @@ def mappings_from_avar(font, denormalize=True): if delta != 0: v = location.get(axisTag, 0) v = v + fi2fl(delta, 14) - v = max(-1, min(1, v)) + # See https://github.com/fonttools/fonttools/pull/3598#issuecomment-2266082009 + # v = max(-1, min(1, v)) outputLocation[axisTag] = v mappings.append((location, outputLocation)) # Filter out empty mappings From 297f73aeaf824151253e68e04f0690975c8f22e8 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 14:25:19 -0600 Subject: [PATCH 17/39] [varLib.avar] Err, don't drop empty pins https://github.com/fonttools/fonttools/issues/3086#issuecomment-2263626285 --- Lib/fontTools/varLib/avar.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 1dfed52f5..c53eab076 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -78,8 +78,6 @@ def mappings_from_avar(font, denormalize=True): # v = max(-1, min(1, v)) outputLocation[axisTag] = v mappings.append((location, outputLocation)) - # Filter out empty mappings - mappings = [io for io in mappings if io[1]] if denormalize: for tag, seg in axisMaps.items(): From 0127a235af4afdba309f2f76ea8098467fd4dd58 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 14:39:41 -0600 Subject: [PATCH 18/39] [varLib.avar] Don't require a full .designspace with sources --- Lib/fontTools/varLib/__init__.py | 6 +++--- Lib/fontTools/varLib/avar.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 6d0e00ee1..5c96fd01c 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -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?" ) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index c53eab076..b773bc558 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -150,7 +150,7 @@ def main(args=None): 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.") From e606adfffe5e495b4f646f651b260b7b0945c3d3 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 16:44:09 -0600 Subject: [PATCH 19/39] [varLib.avar] Add peakLocation as well, oops --- Lib/fontTools/varLib/avar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index b773bc558..65eef94c6 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -36,14 +36,14 @@ def mappings_from_avar(font, denormalize=True): for varData in varStore.VarData: regionIndices = varData.VarRegionIndex for regionIndex in regionIndices: - peakLocation = {} + peakLocation = [] corners = [] region = regions[regionIndex] for axisIndex, axis in enumerate(region.VarRegionAxis): if axis.PeakCoord == 0: continue axisTag = axisTags[axisIndex] - peakLocation[axisTag] = axis.PeakCoord + peakLocation.append((axisTag, axis.PeakCoord)) corner = [] if axis.StartCoord != 0: corner.append((axisTag, axis.StartCoord)) @@ -51,6 +51,7 @@ def mappings_from_avar(font, denormalize=True): corner.append((axisTag, axis.EndCoord)) corners.append(corner) corners = set(product(*corners)) + inputLocations.add(tuple(peakLocation)) inputLocations.update(corners) inputLocations = [ From cb031514eab8492d5fe256fa3508e8155ac9ec92 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 17:12:09 -0600 Subject: [PATCH 20/39] [varLib.avar] Try dropping unnecessary "masters" Untested, as I don't have a test font that exercises this. --- Lib/fontTools/varLib/avar.py | 53 ++++++++++++++++++++++++++++++++-- Lib/fontTools/varLib/models.py | 2 +- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 65eef94c6..751a7f556 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -1,4 +1,5 @@ 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 @@ -28,6 +29,7 @@ def mappings_from_avar(font, denormalize=True): if seg and seg != {-1: -1, 0: 0, 1: 1} } mappings = [] + poles = set() if getattr(avar, "majorVersion", 1) == 2: varStore = avar.table.VarStore @@ -51,7 +53,9 @@ def mappings_from_avar(font, denormalize=True): corner.append((axisTag, axis.EndCoord)) corners.append(corner) corners = set(product(*corners)) - inputLocations.add(tuple(peakLocation)) + peakLocation = tuple(peakLocation) + poles.add(peakLocation) + inputLocations.add(peakLocation) inputLocations.update(corners) inputLocations = [ @@ -80,6 +84,48 @@ def mappings_from_avar(font, denormalize=True): outputLocation[axisTag] = v mappings.append((location, outputLocation)) + # Now we have all the mappings, find which ones are redundant + # and remove them. + inputLocations.insert(0, {}) + model = VariationModel(inputLocations, axisTags) + modelMapping = model.mapping + modelSupports = model.supports + pins = set(poles) + for pole in poles: + location = dict(pole) + i = inputLocations.index(location) + i = modelMapping[i] + support = modelSupports[i] + supportAxes = set(support.keys()) + for supportIndex, (axisTag, (minV, _, maxV)) in enumerate(support.items()): + for v in (minV, maxV): + for pin in pins: + 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. + for candidateIdx in range(supportIndex): + candidate = modelSupports[candidateIdx] + candidateAxes = set(candidate.keys()) + if candidateAxes != supportAxes: + continue + if axisTag not in candidateAxes: + continue + if candidateLocation[axisTag] == v: + pins.add(tuple(candidateLocation.items())) + mappings = [ + (inputLoc, outputLoc) + for inputLoc, outputLoc in mappings + if tuple(inputLoc.items()) in pins + ] + if denormalize: for tag, seg in axisMaps.items(): if tag not in axisMap: @@ -146,7 +192,10 @@ def main(args=None): if options.designspace is None: from pprint import pprint - pprint(mappings_from_avar(font)) + segments, mappings = mappings_from_avar(font) + pprint(segments) + pprint(mappings) + print(len(mappings)) return axisTags = [a.axisTag for a in font["fvar"].axes] diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index fadfa242c..52433a66a 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -386,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 From b8306b1d82835fdeb81f6910482ff76022363342 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 17:41:18 -0600 Subject: [PATCH 21/39] [varLib.avar] Fix normalization And see if I can make it deterministic. It still isn't. --- Lib/fontTools/varLib/avar.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 751a7f556..ebaeb8499 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -10,9 +10,10 @@ log = logging.getLogger("fontTools.varLib.avar") def _denormalize(v, axis): - return axis.defaultValue + v * ( - (axis.maxValue if v >= 0 else axis.minValue) - axis.defaultValue - ) + if v >= 0: + return axis.defaultValue + v * (axis.maxValue - axis.defaultValue) + else: + return axis.defaultValue + v * (axis.defaultValue - axis.minValue) def mappings_from_avar(font, denormalize=True): @@ -29,7 +30,7 @@ def mappings_from_avar(font, denormalize=True): if seg and seg != {-1: -1, 0: 0, 1: 1} } mappings = [] - poles = set() + poles = dict() # Just using it as an ordered set if getattr(avar, "majorVersion", 1) == 2: varStore = avar.table.VarStore @@ -54,7 +55,7 @@ def mappings_from_avar(font, denormalize=True): corners.append(corner) corners = set(product(*corners)) peakLocation = tuple(peakLocation) - poles.add(peakLocation) + poles[peakLocation] = None inputLocations.add(peakLocation) inputLocations.update(corners) @@ -90,8 +91,8 @@ def mappings_from_avar(font, denormalize=True): model = VariationModel(inputLocations, axisTags) modelMapping = model.mapping modelSupports = model.supports - pins = set(poles) - for pole in poles: + pins = set(poles.keys()) + for pole in poles.keys(): location = dict(pole) i = inputLocations.index(location) i = modelMapping[i] From 9f19a19c4eef8a1d383270938112d0626b323c9d Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 17:55:56 -0600 Subject: [PATCH 22/39] [varLib.avar] Introduce base master earlier --- Lib/fontTools/varLib/avar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index ebaeb8499..7662271a2 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -35,7 +35,7 @@ def mappings_from_avar(font, denormalize=True): if getattr(avar, "majorVersion", 1) == 2: varStore = avar.table.VarStore regions = varStore.VarRegionList.Region - inputLocations = set() + inputLocations = set({()}) for varData in varStore.VarData: regionIndices = varData.VarRegionIndex for regionIndex in regionIndices: @@ -87,7 +87,6 @@ def mappings_from_avar(font, denormalize=True): # Now we have all the mappings, find which ones are redundant # and remove them. - inputLocations.insert(0, {}) model = VariationModel(inputLocations, axisTags) modelMapping = model.mapping modelSupports = model.supports From 34e38c3d0379a03acaa4d23b35ea46e6077d9afc Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 17:57:38 -0600 Subject: [PATCH 23/39] [varLib.avar] Do less work No need to compute outputLocation for unneeded inputLocations. --- Lib/fontTools/varLib/avar.py | 48 +++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 7662271a2..ccb9b8cbe 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -67,26 +67,8 @@ def mappings_from_avar(font, denormalize=True): ) ] - 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)) - - # Now we have all the mappings, find which ones are redundant - # and remove them. + # Now we have all the input locations, find which ones are + # not needed and remove them. model = VariationModel(inputLocations, axisTags) modelMapping = model.mapping modelSupports = model.supports @@ -120,11 +102,26 @@ def mappings_from_avar(font, denormalize=True): continue if candidateLocation[axisTag] == v: pins.add(tuple(candidateLocation.items())) - mappings = [ - (inputLoc, outputLoc) - for inputLoc, outputLoc in mappings - if tuple(inputLoc.items()) in pins - ] + inputLocations = [dict(t) for t in pins] + + # Find the output 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)) if denormalize: for tag, seg in axisMaps.items(): @@ -195,7 +192,6 @@ def main(args=None): segments, mappings = mappings_from_avar(font) pprint(segments) pprint(mappings) - print(len(mappings)) return axisTags = [a.axisTag for a in font["fvar"].axes] From bd76b4a24bbd8c06f1b96d9303c5a89bf966390e Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 18:16:11 -0600 Subject: [PATCH 24/39] [varLib.avar] Handle default location mapping somewhere else --- Lib/fontTools/varLib/avar.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index ccb9b8cbe..eccfb8cba 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -30,7 +30,7 @@ def mappings_from_avar(font, denormalize=True): if seg and seg != {-1: -1, 0: 0, 1: 1} } mappings = [] - poles = dict() # Just using it as an ordered set + poles = {(): None} # Just using it as an ordered set if getattr(avar, "majorVersion", 1) == 2: varStore = avar.table.VarStore @@ -72,7 +72,7 @@ def mappings_from_avar(font, denormalize=True): model = VariationModel(inputLocations, axisTags) modelMapping = model.mapping modelSupports = model.supports - pins = set(poles.keys()) + pins = poles.copy() for pole in poles.keys(): location = dict(pole) i = inputLocations.index(location) @@ -81,7 +81,7 @@ def mappings_from_avar(font, denormalize=True): supportAxes = set(support.keys()) for supportIndex, (axisTag, (minV, _, maxV)) in enumerate(support.items()): for v in (minV, maxV): - for pin in pins: + for pin in pins.keys(): pinLocation = dict(pin) pinAxes = set(pinLocation.keys()) if pinAxes != supportAxes: @@ -101,8 +101,8 @@ def mappings_from_avar(font, denormalize=True): if axisTag not in candidateAxes: continue if candidateLocation[axisTag] == v: - pins.add(tuple(candidateLocation.items())) - inputLocations = [dict(t) for t in pins] + pins[tuple(candidateLocation.items())] = None + inputLocations = [dict(t) for t in pins.keys()] # Find the output locations varIdxMap = avar.table.VarIdxMap @@ -123,6 +123,11 @@ def mappings_from_avar(font, denormalize=True): outputLocation[axisTag] = v mappings.append((location, outputLocation)) + # Remove base master we added, if it mapped 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: From 65ab19468dc72bc0504fd8a9530a42c830e1a11c Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 18:24:18 -0600 Subject: [PATCH 25/39] [varLib.avar] Comments and a bug fix Code was in wrong block. --- Lib/fontTools/varLib/avar.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index eccfb8cba..20c14fb58 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -30,11 +30,17 @@ def mappings_from_avar(font, denormalize=True): if seg and seg != {-1: -1, 0: 0, 1: 1} } mappings = [] - poles = {(): None} # Just using it as an ordered set 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 @@ -59,6 +65,7 @@ def mappings_from_avar(font, denormalize=True): inputLocations.add(peakLocation) inputLocations.update(corners) + # Sort them by number of axes, then by axis order inputLocations = [ dict(t) for t in sorted( @@ -92,8 +99,11 @@ def mappings_from_avar(font, denormalize=True): break else: # No pin found. Go through the previous masters - # and find a suitable pin. - for candidateIdx in range(supportIndex): + # 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(supportIndex - 1, -1, -1): candidate = modelSupports[candidateIdx] candidateAxes = set(candidate.keys()) if candidateAxes != supportAxes: @@ -104,7 +114,7 @@ def mappings_from_avar(font, denormalize=True): pins[tuple(candidateLocation.items())] = None inputLocations = [dict(t) for t in pins.keys()] - # Find the output locations + # Find the output locations, at input locations varIdxMap = avar.table.VarIdxMap instancer = VarStoreInstancer(varStore, fvarAxes) for location in inputLocations: @@ -123,10 +133,10 @@ def mappings_from_avar(font, denormalize=True): outputLocation[axisTag] = v mappings.append((location, outputLocation)) - # Remove base master we added, if it mapped to the default location - assert mappings[0][0] == {} - if mappings[0][1] == {}: - mappings.pop(0) + # 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(): From 700b6a7b0e5f2eeeb4a1fb51f4e3b306e83672f0 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 18:38:37 -0600 Subject: [PATCH 26/39] [varLib.avar] Refactor code So we can test it. --- Lib/fontTools/varLib/avar.py | 83 +++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 20c14fb58..bcc4a570e 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -16,6 +16,48 @@ def _denormalize(v, axis): 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. + model = VariationModel(locations, axisTags) + modelMapping = model.mapping + modelSupports = model.supports + pins = poles.copy() + for pole in poles.keys(): + location = dict(pole) + i = locations.index(location) + i = modelMapping[i] + support = modelSupports[i] + supportAxes = set(support.keys()) + for supportIndex, (axisTag, (minV, _, maxV)) in enumerate(support.items()): + for v in (minV, maxV): + 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(supportIndex - 1, -1, -1): + candidate = modelSupports[candidateIdx] + candidateAxes = set(candidate.keys()) + if candidateAxes != supportAxes: + continue + if axisTag not in candidateAxes: + continue + if candidateLocation[axisTag] == v: + pins[tuple(candidateLocation.items())] = None + 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} @@ -73,46 +115,7 @@ def mappings_from_avar(font, denormalize=True): key=lambda t: (len(t), tuple(axisIndexes[tag] for tag, _ in t)), ) ] - - # Now we have all the input locations, find which ones are - # not needed and remove them. - model = VariationModel(inputLocations, axisTags) - modelMapping = model.mapping - modelSupports = model.supports - pins = poles.copy() - for pole in poles.keys(): - location = dict(pole) - i = inputLocations.index(location) - i = modelMapping[i] - support = modelSupports[i] - supportAxes = set(support.keys()) - for supportIndex, (axisTag, (minV, _, maxV)) in enumerate(support.items()): - for v in (minV, maxV): - 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(supportIndex - 1, -1, -1): - candidate = modelSupports[candidateIdx] - candidateAxes = set(candidate.keys()) - if candidateAxes != supportAxes: - continue - if axisTag not in candidateAxes: - continue - if candidateLocation[axisTag] == v: - pins[tuple(candidateLocation.items())] = None - inputLocations = [dict(t) for t in pins.keys()] + inputLocations = _pruneLocations(inputLocations, poles, axisTags) # Find the output locations, at input locations varIdxMap = avar.table.VarIdxMap From 2742c6287cb54621b9fa668bb22458fa5a5541a8 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 19:32:45 -0600 Subject: [PATCH 27/39] [varLib.avar] Test & fix _pruneLocations --- Lib/fontTools/varLib/avar.py | 49 ++++++++++++++++++------------ Tests/varLib/avar_test.py | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 Tests/varLib/avar_test.py diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index bcc4a570e..1233068c3 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -22,15 +22,16 @@ def _pruneLocations(locations, poles, axisTags): model = VariationModel(locations, axisTags) modelMapping = model.mapping modelSupports = model.supports - pins = poles.copy() - for pole in poles.keys(): - location = dict(pole) + 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 supportIndex, (axisTag, (minV, _, maxV)) in enumerate(support.items()): + 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()) @@ -40,21 +41,27 @@ def _pruneLocations(locations, poles, axisTags): 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(supportIndex - 1, -1, -1): - candidate = modelSupports[candidateIdx] - candidateAxes = set(candidate.keys()) - if candidateAxes != supportAxes: - continue - if axisTag not in candidateAxes: - continue - if candidateLocation[axisTag] == v: - pins[tuple(candidateLocation.items())] = None + 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()] @@ -115,7 +122,8 @@ def mappings_from_avar(font, denormalize=True): key=lambda t: (len(t), tuple(axisIndexes[tag] for tag, _ in t)), ) ] - inputLocations = _pruneLocations(inputLocations, poles, axisTags) + 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 @@ -210,6 +218,7 @@ def main(args=None): segments, mappings = mappings_from_avar(font) pprint(segments) pprint(mappings) + print(len(mappings), "mappings") return axisTags = [a.axisTag for a in font["fvar"].axes] diff --git a/Tests/varLib/avar_test.py b/Tests/varLib/avar_test.py new file mode 100644 index 000000000..b4e1132fe --- /dev/null +++ b/Tests/varLib/avar_test.py @@ -0,0 +1,59 @@ +from fontTools.varLib.avar import _pruneLocations +import unittest +import pytest + + +@pytest.mark.parametrize( + "locations, poles, expected", + [ + ( + [ + {"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}, + ], + ), + ], +) +def test_pruneLocations(locations, poles, expected): + axisTags = set() + for location in locations: + axisTags.update(location.keys()) + axisTags = sorted(axisTags) + + locations = [{}] + locations + + output = _pruneLocations(locations, poles, axisTags) + + assert output == expected, (output, expected) From 132654c9e504df3ce5bb9cd0a80d8c82cb7a9a98 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 2 Aug 2024 20:00:21 -0600 Subject: [PATCH 28/39] [varLib.avar] Comment --- Lib/fontTools/varLib/avar.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/fontTools/varLib/avar.py b/Lib/fontTools/varLib/avar.py index 1233068c3..164a4a805 100644 --- a/Lib/fontTools/varLib/avar.py +++ b/Lib/fontTools/varLib/avar.py @@ -19,6 +19,20 @@ def _denormalize(v, axis): 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 From 8d58f7f730e368fa04b58804f6e4affa2510e36a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 4 Aug 2024 15:55:27 -0600 Subject: [PATCH 29/39] [varLib.avar] Add roundtrip test Fails currently. --- Tests/varLib/avar_test.py | 113 +++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 45 deletions(-) diff --git a/Tests/varLib/avar_test.py b/Tests/varLib/avar_test.py index b4e1132fe..c1208dce1 100644 --- a/Tests/varLib/avar_test.py +++ b/Tests/varLib/avar_test.py @@ -1,51 +1,51 @@ +from fontTools.varLib.models import VariationModel from fontTools.varLib.avar import _pruneLocations 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", - [ - ( - [ - {"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: @@ -54,6 +54,29 @@ def test_pruneLocations(locations, poles, expected): locations = [{}] + locations - output = _pruneLocations(locations, poles, axisTags) + pruned = _pruneLocations(locations, poles, axisTags) - assert output == expected, (output, expected) + 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) From dab890e681420c21e4edc13fa37e0c48f8635076 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 30 Aug 2024 08:46:21 -0700 Subject: [PATCH 30/39] [varLib.avar] Add another test --- Tests/varLib/avar_test.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Tests/varLib/avar_test.py b/Tests/varLib/avar_test.py index c1208dce1..92f7b3a7d 100644 --- a/Tests/varLib/avar_test.py +++ b/Tests/varLib/avar_test.py @@ -1,5 +1,7 @@ +from fontTools.ttLib import TTFont from fontTools.varLib.models import VariationModel -from fontTools.varLib.avar import _pruneLocations +from fontTools.varLib.avar import _pruneLocations, mappings_from_avar +import os import unittest import pytest @@ -80,3 +82,13 @@ def test_roundtrip(locations, poles, expected): 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 From 00ad60b4c3c499a8845a9aaeb1a068a9974d3904 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 30 Aug 2024 18:13:26 +0100 Subject: [PATCH 31/39] subset_test: add failing test to reproduce issue #3616 If we subset this test font (a subset of Google Fonts' Andika-Regular.ttf) and request to keep 'cv43', only the FirstParaUILabelNameID (324) is currently kept, the other two (325 amd 326) get incorectly dropped. All referenced nameIDs should be kept. This will be fixed with https://github.com/fonttools/fonttools/pull/3617 ``` > assert nameIDs == keepNameIDs E assert {0, 1, 2, 3, 4, 5, 6, 321, 322, 323, 324} == {0, 1, 2, 3, 4, 5, 6, 321, 322, 323, 324, 325, 326} E Extra items in the right set: E 325 E 326 E Full diff: E - {0, 1, 2, 3, 4, 5, 6, 321, 322, 323, 324, 325, 326} E ? ---------- E + {0, 1, 2, 3, 4, 5, 6, 321, 322, 323, 324} ``` --- Tests/subset/data/Andika-Regular.subset.ttx | 733 ++++++++++++++++++++ Tests/subset/subset_test.py | 23 + 2 files changed, 756 insertions(+) create mode 100644 Tests/subset/data/Andika-Regular.subset.ttx diff --git a/Tests/subset/data/Andika-Regular.subset.ttx b/Tests/subset/data/Andika-Regular.subset.ttx new file mode 100644 index 000000000..8fd28a7a9 --- /dev/null +++ b/Tests/subset/data/Andika-Regular.subset.ttx @@ -0,0 +1,733 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (c) 2004-2022 SIL International + + + Andika + + + Regular + + + SIL International: Andika Regular: 2022 + + + Andika + + + Version 6.101 + + + Andika + + + Capital Eng + + + Alternate forms of capital Eng + + + Ŋ + + + Lowercase no descender + + + Capital form + + + Lowercase short stem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py index abb82687e..8fdee83b4 100644 --- a/Tests/subset/subset_test.py +++ b/Tests/subset/subset_test.py @@ -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()) From 821f37329e01a46645fc0fd06fd969709398e4fa Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 27 Aug 2024 11:28:31 +0100 Subject: [PATCH 32/39] NameRecordVisitor: include whole sequence of UI labels for character variants, not just first Fixes #3616 --- Lib/fontTools/ttLib/tables/_n_a_m_e.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index bbb4f5364..bf45df2ed 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -1177,15 +1177,6 @@ class NameRecordVisitor(TTVisitor): ( (otTables.FeatureParamsSize, ("SubfamilyID", "SubfamilyNameID")), (otTables.FeatureParamsStylisticSet, ("UINameID",)), - ( - otTables.FeatureParamsCharacterVariants, - ( - "FeatUILabelNameID", - "FeatUITooltipTextNameID", - "SampleTextNameID", - "FirstParamUILabelNameID", - ), - ), (otTables.STAT, ("ElidedFallbackNameID",)), (otTables.AxisRecord, ("AxisNameID",)), (otTables.AxisValue, ("ValueNameID",)), @@ -1197,6 +1188,19 @@ 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 + last_name_id = obj.FirstParamUILabelNameID + obj.NumNamedParameters - 1 + if last_name_id >= 256 and last_namd_id <= 0x7FFF: + visitor.seen.update(range(obj.FirstParamUILabelNameID, last_name_id + 1)) + + @NameRecordVisitor.register(ttLib.getTableClass("fvar")) def visit(visitor, obj): for inst in obj.instances: From 8f01590353b2929ad94e2cf883ef4570ddd069b6 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 27 Aug 2024 17:01:31 +0100 Subject: [PATCH 33/39] FeatureParamsSize.SubfamilyID is *not* actually a NameID --- Lib/fontTools/ttLib/tables/_n_a_m_e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index bf45df2ed..d7b53fbc3 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -1175,7 +1175,7 @@ class NameRecordVisitor(TTVisitor): @NameRecordVisitor.register_attrs( ( - (otTables.FeatureParamsSize, ("SubfamilyID", "SubfamilyNameID")), + (otTables.FeatureParamsSize, ("SubfamilyNameID",)), (otTables.FeatureParamsStylisticSet, ("UINameID",)), (otTables.STAT, ("ElidedFallbackNameID",)), (otTables.AxisRecord, ("AxisNameID",)), From afd73dd2b1620d4e54765f2c1cba6eaaf0b2431b Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 30 Aug 2024 17:04:39 +0100 Subject: [PATCH 34/39] NameRecordVisitor: remove unnecessary check for out of bounds nameIDs --- Lib/fontTools/ttLib/tables/_n_a_m_e.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index d7b53fbc3..e30086adb 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -1196,9 +1196,12 @@ def visit(visitor, obj): # also include the sequence of UI strings for individual variants, if any if obj.FirstParamUILabelNameID == 0 or obj.NumNamedParameters == 0: return - last_name_id = obj.FirstParamUILabelNameID + obj.NumNamedParameters - 1 - if last_name_id >= 256 and last_namd_id <= 0x7FFF: - visitor.seen.update(range(obj.FirstParamUILabelNameID, last_name_id + 1)) + visitor.seen.update( + range( + obj.FirstParamUILabelNameID, + obj.FirstParamUILabelNameID + obj.NumNamedParameters, + ) + ) @NameRecordVisitor.register(ttLib.getTableClass("fvar")) From cd1c31ff6cdb37dcfd4388d63f539d784918a24b Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 2 Sep 2024 17:13:48 +0100 Subject: [PATCH 35/39] Docs: reorder Sphinx extensions. Napoleon must precede autodoc or it will trigger superfluous warnings about indentation. --- Doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/source/conf.py b/Doc/source/conf.py index cee66549b..4976bd813 100644 --- a/Doc/source/conf.py +++ b/Doc/source/conf.py @@ -31,9 +31,9 @@ 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", ] From c19b1c51480d3e12b7f11a9c586c842f4cb3a26c Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 2 Sep 2024 17:18:41 +0100 Subject: [PATCH 36/39] Docs: move module docstrings to first line of file, as per PEP 257. --- Lib/fontTools/designspaceLib/__init__.py | 11 +++--- Lib/fontTools/ufoLib/__init__.py | 46 ++++++++++++------------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py index 342f1decd..0a1e782f5 100644 --- a/Lib/fontTools/designspaceLib/__init__.py +++ b/Lib/fontTools/designspaceLib/__init__.py @@ -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", diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py index c2d2b0b26..a014c9317 100755 --- a/Lib/fontTools/ufoLib/__init__.py +++ b/Lib/fontTools/ufoLib/__init__.py @@ -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", From af27eb0aef1a4a4573420ba4205de1a46c4963e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:43:06 +0000 Subject: [PATCH 37/39] Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.1 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.9.0 to 1.10.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.9.0...v1.10.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index dcc187d45..4b87bf598 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -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 }} From d871fd3a083f2521a1a6d536fc26338bdf871e68 Mon Sep 17 00:00:00 2001 From: Roel Nieskens Date: Tue, 10 Sep 2024 09:28:07 +0200 Subject: [PATCH 38/39] Remove dotslash from examples --- Lib/fontTools/subset/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 4aa60ad84..c32e6fe05 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -128,9 +128,9 @@ 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 From 11343ed64c10d3905f4fb4c8d2810c9612ee8acd Mon Sep 17 00:00:00 2001 From: Roel Nieskens Date: Tue, 10 Sep 2024 09:28:38 +0200 Subject: [PATCH 39/39] Add instructions to escape question mark Arguments without it will not work in zsh and possibly other shells. --- Lib/fontTools/subset/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index c32e6fe05..5a1af9a6c 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -122,7 +122,8 @@ 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::