fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-musllinux_1_2_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cffLib/CFF2ToCFF.py +65 -10
  4. fontTools/cffLib/__init__.py +61 -26
  5. fontTools/cffLib/specializer.py +4 -1
  6. fontTools/cffLib/transforms.py +11 -6
  7. fontTools/config/__init__.py +15 -0
  8. fontTools/cu2qu/cu2qu.c +6567 -5579
  9. fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
  10. fontTools/cu2qu/cu2qu.py +36 -4
  11. fontTools/cu2qu/ufo.py +14 -0
  12. fontTools/designspaceLib/__init__.py +8 -3
  13. fontTools/designspaceLib/statNames.py +14 -7
  14. fontTools/feaLib/ast.py +24 -15
  15. fontTools/feaLib/builder.py +139 -66
  16. fontTools/feaLib/error.py +1 -1
  17. fontTools/feaLib/lexer.c +7038 -7995
  18. fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
  19. fontTools/feaLib/parser.py +75 -40
  20. fontTools/feaLib/variableScalar.py +6 -1
  21. fontTools/fontBuilder.py +50 -44
  22. fontTools/merge/__init__.py +1 -1
  23. fontTools/merge/cmap.py +33 -1
  24. fontTools/merge/tables.py +12 -1
  25. fontTools/misc/bezierTools.c +14913 -17013
  26. fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
  27. fontTools/misc/bezierTools.py +4 -1
  28. fontTools/misc/configTools.py +3 -1
  29. fontTools/misc/enumTools.py +23 -0
  30. fontTools/misc/etree.py +4 -27
  31. fontTools/misc/filesystem/__init__.py +68 -0
  32. fontTools/misc/filesystem/_base.py +134 -0
  33. fontTools/misc/filesystem/_copy.py +45 -0
  34. fontTools/misc/filesystem/_errors.py +54 -0
  35. fontTools/misc/filesystem/_info.py +75 -0
  36. fontTools/misc/filesystem/_osfs.py +164 -0
  37. fontTools/misc/filesystem/_path.py +67 -0
  38. fontTools/misc/filesystem/_subfs.py +92 -0
  39. fontTools/misc/filesystem/_tempfs.py +34 -0
  40. fontTools/misc/filesystem/_tools.py +34 -0
  41. fontTools/misc/filesystem/_walk.py +55 -0
  42. fontTools/misc/filesystem/_zipfs.py +204 -0
  43. fontTools/misc/fixedTools.py +1 -1
  44. fontTools/misc/loggingTools.py +1 -1
  45. fontTools/misc/psCharStrings.py +17 -2
  46. fontTools/misc/sstruct.py +2 -6
  47. fontTools/misc/symfont.py +6 -8
  48. fontTools/misc/testTools.py +5 -1
  49. fontTools/misc/textTools.py +4 -2
  50. fontTools/misc/visitor.py +32 -16
  51. fontTools/misc/xmlWriter.py +44 -8
  52. fontTools/mtiLib/__init__.py +1 -3
  53. fontTools/otlLib/builder.py +402 -155
  54. fontTools/otlLib/optimize/gpos.py +49 -63
  55. fontTools/pens/filterPen.py +218 -26
  56. fontTools/pens/momentsPen.c +5514 -5584
  57. fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
  58. fontTools/pens/pointPen.py +61 -18
  59. fontTools/pens/roundingPen.py +2 -2
  60. fontTools/pens/t2CharStringPen.py +31 -11
  61. fontTools/qu2cu/qu2cu.c +6581 -6168
  62. fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
  63. fontTools/subset/__init__.py +283 -25
  64. fontTools/subset/svg.py +2 -3
  65. fontTools/ttLib/__init__.py +4 -0
  66. fontTools/ttLib/__main__.py +47 -8
  67. fontTools/ttLib/removeOverlaps.py +7 -5
  68. fontTools/ttLib/reorderGlyphs.py +8 -7
  69. fontTools/ttLib/sfnt.py +11 -9
  70. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  71. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  72. fontTools/ttLib/tables/S__i_l_f.py +2 -2
  73. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  74. fontTools/ttLib/tables/T_S_I__1.py +2 -5
  75. fontTools/ttLib/tables/T_S_I__5.py +18 -7
  76. fontTools/ttLib/tables/__init__.py +1 -0
  77. fontTools/ttLib/tables/_a_v_a_r.py +12 -3
  78. fontTools/ttLib/tables/_c_m_a_p.py +20 -7
  79. fontTools/ttLib/tables/_c_v_t.py +3 -2
  80. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  81. fontTools/ttLib/tables/_g_l_y_f.py +45 -21
  82. fontTools/ttLib/tables/_g_v_a_r.py +67 -19
  83. fontTools/ttLib/tables/_h_d_m_x.py +4 -4
  84. fontTools/ttLib/tables/_h_m_t_x.py +7 -3
  85. fontTools/ttLib/tables/_l_o_c_a.py +2 -2
  86. fontTools/ttLib/tables/_n_a_m_e.py +11 -6
  87. fontTools/ttLib/tables/_p_o_s_t.py +9 -7
  88. fontTools/ttLib/tables/otBase.py +5 -12
  89. fontTools/ttLib/tables/otConverters.py +5 -2
  90. fontTools/ttLib/tables/otData.py +1 -1
  91. fontTools/ttLib/tables/otTables.py +33 -30
  92. fontTools/ttLib/tables/otTraverse.py +2 -1
  93. fontTools/ttLib/tables/sbixStrike.py +3 -3
  94. fontTools/ttLib/ttFont.py +666 -120
  95. fontTools/ttLib/ttGlyphSet.py +0 -10
  96. fontTools/ttLib/woff2.py +10 -13
  97. fontTools/ttx.py +13 -1
  98. fontTools/ufoLib/__init__.py +300 -202
  99. fontTools/ufoLib/converters.py +103 -30
  100. fontTools/ufoLib/errors.py +8 -0
  101. fontTools/ufoLib/etree.py +1 -1
  102. fontTools/ufoLib/filenames.py +171 -106
  103. fontTools/ufoLib/glifLib.py +303 -205
  104. fontTools/ufoLib/kerning.py +98 -48
  105. fontTools/ufoLib/utils.py +46 -15
  106. fontTools/ufoLib/validators.py +121 -99
  107. fontTools/unicodedata/Blocks.py +35 -20
  108. fontTools/unicodedata/Mirrored.py +446 -0
  109. fontTools/unicodedata/ScriptExtensions.py +63 -37
  110. fontTools/unicodedata/Scripts.py +173 -152
  111. fontTools/unicodedata/__init__.py +10 -2
  112. fontTools/varLib/__init__.py +198 -109
  113. fontTools/varLib/avar/__init__.py +0 -0
  114. fontTools/varLib/avar/__main__.py +72 -0
  115. fontTools/varLib/avar/build.py +79 -0
  116. fontTools/varLib/avar/map.py +108 -0
  117. fontTools/varLib/avar/plan.py +1004 -0
  118. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  119. fontTools/varLib/avarPlanner.py +3 -999
  120. fontTools/varLib/featureVars.py +21 -7
  121. fontTools/varLib/hvar.py +113 -0
  122. fontTools/varLib/instancer/__init__.py +180 -65
  123. fontTools/varLib/interpolatableHelpers.py +3 -0
  124. fontTools/varLib/iup.c +7564 -6903
  125. fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
  126. fontTools/varLib/models.py +17 -2
  127. fontTools/varLib/mutator.py +11 -0
  128. fontTools/varLib/varStore.py +10 -38
  129. fontTools/voltLib/__main__.py +206 -0
  130. fontTools/voltLib/ast.py +4 -0
  131. fontTools/voltLib/parser.py +16 -8
  132. fontTools/voltLib/voltToFea.py +347 -166
  133. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
  134. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
  135. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
  136. fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
  137. {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
  138. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
  139. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
  140. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
@@ -95,6 +95,14 @@ def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"):
95
95
 
96
96
  addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags)
97
97
 
98
+ # Update OS/2.usMaxContext in case the font didn't have features before, but
99
+ # does now, if the OS/2 table exists. The table may be required, but
100
+ # fontTools needs to be able to deal with non-standard fonts. Since feature
101
+ # variations are always 1:1 mappings, we can set the value to at least 1
102
+ # instead of recomputing it with `otlLib.maxContextCalc.maxCtxFont()`.
103
+ if (os2 := font.get("OS/2")) is not None:
104
+ os2.usMaxContext = max(1, os2.usMaxContext)
105
+
98
106
 
99
107
  def _existingVariableFeatures(table):
100
108
  existingFeatureVarsTags = set()
@@ -392,10 +400,13 @@ def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="r
392
400
 
393
401
  for scriptRecord in table.ScriptList.ScriptRecord:
394
402
  if scriptRecord.Script.DefaultLangSys is None:
395
- raise VarLibError(
396
- "Feature variations require that the script "
397
- f"'{scriptRecord.ScriptTag}' defines a default language system."
398
- )
403
+ # We need to have a default LangSys to attach variations to.
404
+ langSys = ot.LangSys()
405
+ langSys.LookupOrder = None
406
+ langSys.ReqFeatureIndex = 0xFFFF
407
+ langSys.FeatureIndex = []
408
+ langSys.FeatureCount = 0
409
+ scriptRecord.Script.DefaultLangSys = langSys
399
410
  langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
400
411
  for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
401
412
  langSys.FeatureIndex.append(varFeatureIndex)
@@ -597,9 +608,12 @@ def buildFeatureRecord(featureTag, lookupListIndices):
597
608
  def buildFeatureVariationRecord(conditionTable, substitutionRecords):
598
609
  """Build a FeatureVariationRecord."""
599
610
  fvr = ot.FeatureVariationRecord()
600
- fvr.ConditionSet = ot.ConditionSet()
601
- fvr.ConditionSet.ConditionTable = conditionTable
602
- fvr.ConditionSet.ConditionCount = len(conditionTable)
611
+ if len(conditionTable) != 0:
612
+ fvr.ConditionSet = ot.ConditionSet()
613
+ fvr.ConditionSet.ConditionTable = conditionTable
614
+ fvr.ConditionSet.ConditionCount = len(conditionTable)
615
+ else:
616
+ fvr.ConditionSet = None
603
617
  fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution()
604
618
  fvr.FeatureTableSubstitution.Version = 0x00010000
605
619
  fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords
@@ -0,0 +1,113 @@
1
+ from fontTools.misc.roundTools import noRound
2
+ from fontTools.ttLib import TTFont, newTable
3
+ from fontTools.ttLib.tables import otTables as ot
4
+ from fontTools.ttLib.tables.otBase import OTTableWriter
5
+ from fontTools.varLib import HVAR_FIELDS, VVAR_FIELDS, _add_VHVAR
6
+ from fontTools.varLib import builder, models, varStore
7
+ from fontTools.misc.fixedTools import fixedToFloat as fi2fl
8
+ from fontTools.misc.cliTools import makeOutputFileName
9
+ from functools import partial
10
+ import logging
11
+
12
+ log = logging.getLogger("fontTools.varLib.avar")
13
+
14
+
15
+ def _get_advance_metrics(font, axisTags, tableFields):
16
+ # There's two ways we can go from here:
17
+ # 1. For each glyph, at each master peak, compute the value of the
18
+ # advance width at that peak. Then pass these all to a VariationModel
19
+ # builder to compute back the deltas.
20
+ # 2. For each master peak, pull out the deltas of the advance width directly,
21
+ # and feed these to the VarStoreBuilder, forgoing the remodeling step.
22
+ # We'll go with the second option, as it's simpler, faster, and more direct.
23
+ gvar = font["gvar"]
24
+ vhAdvanceDeltasAndSupports = {}
25
+ glyphOrder = font.getGlyphOrder()
26
+ phantomIndex = tableFields.phantomIndex
27
+ for glyphName in glyphOrder:
28
+ supports = []
29
+ deltas = []
30
+ variations = gvar.variations.get(glyphName, [])
31
+
32
+ for tv in variations:
33
+ supports.append(tv.axes)
34
+ phantoms = tv.coordinates[-4:]
35
+ phantoms = phantoms[phantomIndex * 2 : phantomIndex * 2 + 2]
36
+ assert len(phantoms) == 2
37
+ phantoms[0] = phantoms[0][phantomIndex] if phantoms[0] is not None else 0
38
+ phantoms[1] = phantoms[1][phantomIndex] if phantoms[1] is not None else 0
39
+ deltas.append(phantoms[1] - phantoms[0])
40
+
41
+ vhAdvanceDeltasAndSupports[glyphName] = (deltas, supports)
42
+
43
+ vOrigDeltasAndSupports = None # TODO
44
+
45
+ return vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports
46
+
47
+
48
+ def add_HVAR(font):
49
+ if "HVAR" in font:
50
+ del font["HVAR"]
51
+ axisTags = [axis.axisTag for axis in font["fvar"].axes]
52
+ getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, HVAR_FIELDS)
53
+ _add_VHVAR(font, axisTags, HVAR_FIELDS, getAdvanceMetrics)
54
+
55
+
56
+ def add_VVAR(font):
57
+ if "VVAR" in font:
58
+ del font["VVAR"]
59
+ getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, VVAR_FIELDS)
60
+ axisTags = [axis.axisTag for axis in font["fvar"].axes]
61
+ _add_VHVAR(font, axisTags, VVAR_FIELDS, getAdvanceMetrics)
62
+
63
+
64
+ def main(args=None):
65
+ """Add `HVAR` table to variable font."""
66
+
67
+ if args is None:
68
+ import sys
69
+
70
+ args = sys.argv[1:]
71
+
72
+ from fontTools import configLogger
73
+ from fontTools.designspaceLib import DesignSpaceDocument
74
+ import argparse
75
+
76
+ parser = argparse.ArgumentParser(
77
+ "fonttools varLib.hvar",
78
+ description="Add `HVAR` table from to variable font.",
79
+ )
80
+ parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
81
+ parser.add_argument(
82
+ "-o",
83
+ "--output-file",
84
+ type=str,
85
+ help="Output font file name.",
86
+ )
87
+
88
+ options = parser.parse_args(args)
89
+
90
+ configLogger(level="WARNING")
91
+
92
+ font = TTFont(options.font)
93
+ if not "fvar" in font:
94
+ log.error("Not a variable font.")
95
+ return 1
96
+
97
+ add_HVAR(font)
98
+ if "vmtx" in font:
99
+ add_VVAR(font)
100
+
101
+ if options.output_file is None:
102
+ outfile = makeOutputFileName(options.font, overWrite=True, suffix=".hvar")
103
+ else:
104
+ outfile = options.output_file
105
+ if outfile:
106
+ log.info("Saving %s", outfile)
107
+ font.save(outfile)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ import sys
112
+
113
+ sys.exit(main())
@@ -1,4 +1,4 @@
1
- """ Partially instantiate a variable font.
1
+ """Partially instantiate a variable font.
2
2
 
3
3
  The module exports an `instantiateVariableFont` function and CLI that allow to
4
4
  create full instances (i.e. static fonts) from variable fonts, as well as "partial"
@@ -36,7 +36,7 @@ If the input location specifies all the axes, the resulting instance is no longe
36
36
  'variable' (same as using fontools varLib.mutator):
37
37
  .. code-block:: pycon
38
38
 
39
- >>>
39
+ >>>
40
40
  >> instance = instancer.instantiateVariableFont(
41
41
  ... varfont, {"wght": 700, "wdth": 67.5}
42
42
  ... )
@@ -56,8 +56,10 @@ From the console script, this is equivalent to passing `wght=drop` as input.
56
56
 
57
57
  This module is similar to fontTools.varLib.mutator, which it's intended to supersede.
58
58
  Note that, unlike varLib.mutator, when an axis is not mentioned in the input
59
- location, the varLib.instancer will keep the axis and the corresponding deltas,
60
- whereas mutator implicitly drops the axis at its default coordinate.
59
+ location, by default the varLib.instancer will keep the axis and the corresponding
60
+ deltas, whereas mutator implicitly drops the axis at its default coordinate.
61
+ To obtain the same behavior as mutator, pass the `static=True` parameter or
62
+ the `--static` CLI option.
61
63
 
62
64
  The module supports all the following "levels" of instancing, which can of
63
65
  course be combined:
@@ -72,7 +74,7 @@ L1
72
74
  L2
73
75
  dropping one or more axes while pinning them at non-default locations;
74
76
  .. code-block:: pycon
75
-
77
+
76
78
  >>>
77
79
  >> font = instancer.instantiateVariableFont(varfont, {"wght": 700})
78
80
 
@@ -81,22 +83,18 @@ L3
81
83
  a new minimum or maximum, potentially -- though not necessarily -- dropping
82
84
  entire regions of variations that fall completely outside this new range.
83
85
  .. code-block:: pycon
84
-
86
+
85
87
  >>>
86
88
  >> font = instancer.instantiateVariableFont(varfont, {"wght": (100, 300)})
87
89
 
88
90
  L4
89
91
  moving the default location of an axis, by specifying (min,defalt,max) values:
90
92
  .. code-block:: pycon
91
-
93
+
92
94
  >>>
93
95
  >> font = instancer.instantiateVariableFont(varfont, {"wght": (100, 300, 700)})
94
96
 
95
- Currently only TrueType-flavored variable fonts (i.e. containing 'glyf' table)
96
- are supported, but support for CFF2 variable fonts will be added soon.
97
-
98
- The discussion and implementation of these features are tracked at
99
- https://github.com/fonttools/fonttools/issues/1537
97
+ Both TrueType-flavored (glyf+gvar) variable and CFF2 variable fonts are supported.
100
98
  """
101
99
 
102
100
  from fontTools.misc.fixedTools import (
@@ -120,6 +118,7 @@ from fontTools.cffLib.specializer import (
120
118
  specializeCommands,
121
119
  generalizeCommands,
122
120
  )
121
+ from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF
123
122
  from fontTools.varLib import builder
124
123
  from fontTools.varLib.mvar import MVAR_ENTRIES
125
124
  from fontTools.varLib.merger import MutatorMerger
@@ -136,6 +135,7 @@ from enum import IntEnum
136
135
  import logging
137
136
  import os
138
137
  import re
138
+ import io
139
139
  from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union
140
140
  import warnings
141
141
 
@@ -433,7 +433,27 @@ class AxisLimits(_BaseAxisLimits):
433
433
 
434
434
  avarSegments = {}
435
435
  if usingAvar and "avar" in varfont:
436
- avarSegments = varfont["avar"].segments
436
+ avar = varfont["avar"]
437
+ avarSegments = avar.segments
438
+
439
+ if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore:
440
+ pinnedAxes = set(self.pinnedLocation())
441
+ if not pinnedAxes.issuperset(avarSegments):
442
+ raise NotImplementedError(
443
+ "Partial-instancing avar2 table is not supported"
444
+ )
445
+
446
+ # TODO: Merge this with the main codepath.
447
+
448
+ # Full instancing of avar2 font. Use avar table to normalize location and return.
449
+ location = self.pinnedLocation()
450
+ location = {
451
+ tag: normalize(value, axes[tag], None)
452
+ for tag, value in location.items()
453
+ }
454
+ return NormalizedAxisLimits(
455
+ **avar.renormalizeLocation(location, varfont, dropZeroes=False)
456
+ )
437
457
 
438
458
  normalizedLimits = {}
439
459
 
@@ -643,7 +663,11 @@ def instantiateCFF2(
643
663
  # the Private dicts.
644
664
  #
645
665
  # Then prune unused things and possibly drop the VarStore if it's empty.
646
- # In which case, downgrade to CFF table if requested.
666
+ #
667
+ # If the downgrade parameter is True, no actual downgrading is done, but
668
+ # the function returns True if the VarStore was empty after instantiation,
669
+ # and hence a downgrade to CFF is possible. In all other cases it returns
670
+ # False.
647
671
 
648
672
  log.info("Instantiating CFF2 table")
649
673
 
@@ -675,6 +699,7 @@ def instantiateCFF2(
675
699
  privateDicts.append(fd.Private)
676
700
 
677
701
  allCommands = []
702
+ allCommandPrivates = []
678
703
  for cs in charStrings:
679
704
  assert cs.private.vstore.otVarStore is varStore # Or in many places!!
680
705
  commands = programToCommands(cs.program, getNumRegions=getNumRegions)
@@ -683,6 +708,7 @@ def instantiateCFF2(
683
708
  if specialize:
684
709
  commands = specializeCommands(commands, generalizeFirst=not generalize)
685
710
  allCommands.append(commands)
711
+ allCommandPrivates.append(cs.private)
686
712
 
687
713
  def storeBlendsToVarStore(arg):
688
714
  if not isinstance(arg, list):
@@ -721,9 +747,7 @@ def instantiateCFF2(
721
747
  minor = varDataCursor[major]
722
748
  varDataCursor[major] += 1
723
749
 
724
- varIdx = (major << 16) + minor
725
-
726
- defaultValue += round(defaultDeltas[varIdx])
750
+ defaultValue += round(defaultDeltas[major][minor])
727
751
  newDefaults.append(defaultValue)
728
752
 
729
753
  varData = varStore.VarData[major]
@@ -742,8 +766,8 @@ def instantiateCFF2(
742
766
  assert varData.ItemCount == 0
743
767
 
744
768
  # Add charstring blend lists to VarStore so we can instantiate them
745
- for commands in allCommands:
746
- vsindex = 0
769
+ for commands, private in zip(allCommands, allCommandPrivates):
770
+ vsindex = getattr(private, "vsindex", 0)
747
771
  for command in commands:
748
772
  if command[0] == "vsindex":
749
773
  vsindex = command[1][0]
@@ -752,7 +776,6 @@ def instantiateCFF2(
752
776
  storeBlendsToVarStore(arg)
753
777
 
754
778
  # Add private blend lists to VarStore so we can instantiate values
755
- vsindex = 0
756
779
  for opcode, name, arg_type, default, converter in privateDictOperators2:
757
780
  if arg_type not in ("number", "delta", "array"):
758
781
  continue
@@ -763,6 +786,7 @@ def instantiateCFF2(
763
786
  continue
764
787
  values = getattr(private, name)
765
788
 
789
+ # This is safe here since "vsindex" is the first in the privateDictOperators2
766
790
  if name == "vsindex":
767
791
  vsindex = values[0]
768
792
  continue
@@ -779,12 +803,14 @@ def instantiateCFF2(
779
803
  storeBlendsToVarStore(value + [count])
780
804
 
781
805
  # Instantiate VarStore
782
- defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
806
+ defaultDeltas = instantiateItemVariationStore(
807
+ varStore, fvarAxes, axisLimits, hierarchical=True
808
+ )
783
809
 
784
810
  # Read back new charstring blends from the instantiated VarStore
785
811
  varDataCursor = [0] * len(varStore.VarData)
786
- for commands in allCommands:
787
- vsindex = 0
812
+ for commands, private in zip(allCommands, allCommandPrivates):
813
+ vsindex = getattr(private, "vsindex", 0)
788
814
  for command in commands:
789
815
  if command[0] == "vsindex":
790
816
  vsindex = command[1][0]
@@ -799,9 +825,16 @@ def instantiateCFF2(
799
825
  if arg_type not in ("number", "delta", "array"):
800
826
  continue
801
827
 
828
+ vsindex = 0
802
829
  for private in privateDicts:
803
830
  if not hasattr(private, name):
804
831
  continue
832
+
833
+ # This is safe here since "vsindex" is the first in the privateDictOperators2
834
+ if name == "vsindex":
835
+ vsindex = values[0]
836
+ continue
837
+
805
838
  values = getattr(private, name)
806
839
  if arg_type == "number":
807
840
  values = [values]
@@ -830,18 +863,13 @@ def instantiateCFF2(
830
863
  varData.Item = []
831
864
  varData.ItemCount = 0
832
865
 
833
- # Remove vsindex commands that are no longer needed, collect those that are.
834
- usedVsindex = set()
835
- for commands in allCommands:
836
- if any(isinstance(arg, list) for command in commands for arg in command[1]):
837
- vsindex = 0
838
- for command in commands:
839
- if command[0] == "vsindex":
840
- vsindex = command[1][0]
841
- continue
842
- if any(isinstance(arg, list) for arg in command[1]):
843
- usedVsindex.add(vsindex)
844
- else:
866
+ # Collect surviving vsindexes
867
+ usedVsindex = set(
868
+ i for i in range(len(varStore.VarData)) if varStore.VarData[i].VarRegionCount
869
+ )
870
+ # Remove vsindex commands that are no longer needed
871
+ for commands, private in zip(allCommands, allCommandPrivates):
872
+ if not any(isinstance(arg, list) for command in commands for arg in command[1]):
845
873
  commands[:] = [command for command in commands if command[0] != "vsindex"]
846
874
 
847
875
  # Remove unused VarData and update vsindex values
@@ -854,10 +882,14 @@ def instantiateCFF2(
854
882
  for command in commands:
855
883
  if command[0] == "vsindex":
856
884
  command[1][0] = vsindexMapping[command[1][0]]
885
+ for private in privateDicts:
886
+ if hasattr(private, "vsindex"):
887
+ private.vsindex = vsindexMapping[private.vsindex]
857
888
 
858
889
  # Remove initial vsindex commands that are implied
859
- for commands in allCommands:
860
- if commands and commands[0] == ("vsindex", [0]):
890
+ for commands, private in zip(allCommands, allCommandPrivates):
891
+ vsindex = getattr(private, "vsindex", 0)
892
+ if commands and commands[0] == ("vsindex", [vsindex]):
861
893
  commands.pop(0)
862
894
 
863
895
  # Ship the charstrings!
@@ -874,9 +906,9 @@ def instantiateCFF2(
874
906
  del private.vstore
875
907
 
876
908
  if downgrade:
877
- from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF
909
+ return True
878
910
 
879
- convertCFF2ToCFF(varfont)
911
+ return False
880
912
 
881
913
 
882
914
  def _instantiateGvarGlyph(
@@ -1108,7 +1140,8 @@ def _instantiateVHVAR(varfont, axisLimits, tableFields, *, round=round):
1108
1140
  varIdx = advMapping.mapping[glyphName]
1109
1141
  else:
1110
1142
  varIdx = varfont.getGlyphID(glyphName)
1111
- metrics[glyphName] = (advanceWidth + round(defaultDeltas[varIdx]), sb)
1143
+ delta = round(defaultDeltas[varIdx])
1144
+ metrics[glyphName] = (max(0, advanceWidth + delta), sb)
1112
1145
 
1113
1146
  if (
1114
1147
  tableTag == "VVAR"
@@ -1238,7 +1271,9 @@ class _TupleVarStoreAdapter(object):
1238
1271
  return itemVarStore
1239
1272
 
1240
1273
 
1241
- def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
1274
+ def instantiateItemVariationStore(
1275
+ itemVarStore, fvarAxes, axisLimits, hierarchical=False
1276
+ ):
1242
1277
  """Compute deltas at partial location, and update varStore in-place.
1243
1278
 
1244
1279
  Remove regions in which all axes were instanced, or fall outside the new axis
@@ -1270,12 +1305,19 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
1270
1305
  assert itemVarStore.VarDataCount == newItemVarStore.VarDataCount
1271
1306
  itemVarStore.VarData = newItemVarStore.VarData
1272
1307
 
1273
- defaultDeltas = {
1274
- ((major << 16) + minor): delta
1275
- for major, deltas in enumerate(defaultDeltaArray)
1276
- for minor, delta in enumerate(deltas)
1277
- }
1278
- defaultDeltas[itemVarStore.NO_VARIATION_INDEX] = 0
1308
+ if not hierarchical:
1309
+ defaultDeltas = {
1310
+ ((major << 16) + minor): delta
1311
+ for major, deltas in enumerate(defaultDeltaArray)
1312
+ for minor, delta in enumerate(deltas)
1313
+ }
1314
+ defaultDeltas[itemVarStore.NO_VARIATION_INDEX] = 0
1315
+ else:
1316
+ defaultDeltas = {0xFFFF: {0xFFFF: 0}} # NO_VARIATION_INDEX
1317
+ for major, deltas in enumerate(defaultDeltaArray):
1318
+ defaultDeltasForMajor = defaultDeltas.setdefault(major, {})
1319
+ for minor, delta in enumerate(deltas):
1320
+ defaultDeltasForMajor[minor] = delta
1279
1321
  return defaultDeltas
1280
1322
 
1281
1323
 
@@ -1360,12 +1402,55 @@ def _isValidAvarSegmentMap(axisTag, segmentMap):
1360
1402
  return True
1361
1403
 
1362
1404
 
1405
+ def downgradeCFF2ToCFF(varfont):
1406
+ # Save these properties
1407
+ recalcTimestamp = varfont.recalcTimestamp
1408
+ recalcBBoxes = varfont.recalcBBoxes
1409
+
1410
+ # Disable them
1411
+ varfont.recalcTimestamp = False
1412
+ varfont.recalcBBoxes = False
1413
+
1414
+ # Save to memory, reload, downgrade and save again, reload.
1415
+ # We do this dance because the convertCFF2ToCFF changes glyph
1416
+ # names, so following save would fail if any other table was
1417
+ # loaded and referencing glyph names.
1418
+ #
1419
+ # The second save+load is unfortunate but also necessary.
1420
+
1421
+ stream = io.BytesIO()
1422
+ log.info("Saving CFF2 font to memory for downgrade")
1423
+ varfont.save(stream)
1424
+ stream.seek(0)
1425
+ varfont = TTFont(stream, recalcTimestamp=False, recalcBBoxes=False)
1426
+
1427
+ convertCFF2ToCFF(varfont)
1428
+
1429
+ stream = io.BytesIO()
1430
+ log.info("Saving downgraded CFF font to memory")
1431
+ varfont.save(stream)
1432
+ stream.seek(0)
1433
+ varfont = TTFont(stream, recalcTimestamp=False, recalcBBoxes=False)
1434
+
1435
+ # Uncomment, to see test all tables can be loaded. This fails without
1436
+ # the extra save+load above.
1437
+ """
1438
+ for tag in varfont.keys():
1439
+ print("Loading", tag)
1440
+ varfont[tag]
1441
+ """
1442
+
1443
+ # Restore them
1444
+ varfont.recalcTimestamp = recalcTimestamp
1445
+ varfont.recalcBBoxes = recalcBBoxes
1446
+
1447
+ return varfont
1448
+
1449
+
1363
1450
  def instantiateAvar(varfont, axisLimits):
1364
1451
  # 'axisLimits' dict must contain user-space (non-normalized) coordinates.
1365
1452
 
1366
1453
  avar = varfont["avar"]
1367
- if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore:
1368
- raise NotImplementedError("avar table with VarStore is not supported")
1369
1454
 
1370
1455
  segments = avar.segments
1371
1456
 
@@ -1376,6 +1461,9 @@ def instantiateAvar(varfont, axisLimits):
1376
1461
  del varfont["avar"]
1377
1462
  return
1378
1463
 
1464
+ if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore:
1465
+ raise NotImplementedError("avar table with VarStore is not supported")
1466
+
1379
1467
  log.info("Instantiating avar table")
1380
1468
  for axis in pinnedAxes:
1381
1469
  if axis in segments:
@@ -1577,6 +1665,7 @@ def instantiateVariableFont(
1577
1665
  updateFontNames=False,
1578
1666
  *,
1579
1667
  downgradeCFF2=False,
1668
+ static=False,
1580
1669
  ):
1581
1670
  """Instantiate variable font, either fully or partially.
1582
1671
 
@@ -1620,12 +1709,23 @@ def instantiateVariableFont(
1620
1709
  software that does not support CFF2. Defaults to False. Note that this
1621
1710
  operation also removes overlaps within glyph shapes, as CFF does not support
1622
1711
  overlaps but CFF2 does.
1712
+ static (bool): if True, generate a full instance (static font) instead of a partial
1713
+ instance (variable font).
1623
1714
  """
1624
1715
  # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool
1625
1716
  overlap = OverlapMode(int(overlap))
1626
1717
 
1627
1718
  sanityCheckVariableTables(varfont)
1628
1719
 
1720
+ if static:
1721
+ unspecified = []
1722
+ for axis in varfont["fvar"].axes:
1723
+ if axis.axisTag not in axisLimits:
1724
+ axisLimits[axis.axisTag] = None
1725
+ unspecified.append(axis.axisTag)
1726
+ if unspecified:
1727
+ log.info("Pinning unspecified axes to default: %s", unspecified)
1728
+
1629
1729
  axisLimits = AxisLimits(axisLimits).limitAxesAndPopulateDefaults(varfont)
1630
1730
 
1631
1731
  log.info("Restricted limits: %s", axisLimits)
@@ -1648,7 +1748,9 @@ def instantiateVariableFont(
1648
1748
  instantiateVARC(varfont, normalizedLimits)
1649
1749
 
1650
1750
  if "CFF2" in varfont:
1651
- instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2)
1751
+ downgradeCFF2 = instantiateCFF2(
1752
+ varfont, normalizedLimits, downgrade=downgradeCFF2
1753
+ )
1652
1754
 
1653
1755
  if "gvar" in varfont:
1654
1756
  instantiateGvar(varfont, normalizedLimits, optimize=optimize)
@@ -1678,19 +1780,6 @@ def instantiateVariableFont(
1678
1780
 
1679
1781
  instantiateFvar(varfont, axisLimits)
1680
1782
 
1681
- if "fvar" not in varfont:
1682
- if "glyf" in varfont:
1683
- if overlap == OverlapMode.KEEP_AND_SET_FLAGS:
1684
- setMacOverlapFlags(varfont["glyf"])
1685
- elif overlap in (OverlapMode.REMOVE, OverlapMode.REMOVE_AND_IGNORE_ERRORS):
1686
- from fontTools.ttLib.removeOverlaps import removeOverlaps
1687
-
1688
- log.info("Removing overlaps from glyf table")
1689
- removeOverlaps(
1690
- varfont,
1691
- ignoreErrors=(overlap == OverlapMode.REMOVE_AND_IGNORE_ERRORS),
1692
- )
1693
-
1694
1783
  if "OS/2" in varfont:
1695
1784
  varfont["OS/2"].recalcAvgCharWidth(varfont)
1696
1785
 
@@ -1703,6 +1792,25 @@ def instantiateVariableFont(
1703
1792
  # name table has been updated.
1704
1793
  setRibbiBits(varfont)
1705
1794
 
1795
+ if downgradeCFF2:
1796
+ origVarfont = varfont
1797
+ varfont = downgradeCFF2ToCFF(varfont)
1798
+ if inplace:
1799
+ origVarfont.__dict__ = varfont.__dict__.copy()
1800
+
1801
+ if "fvar" not in varfont:
1802
+ if overlap == OverlapMode.KEEP_AND_SET_FLAGS:
1803
+ if "glyf" in varfont:
1804
+ setMacOverlapFlags(varfont["glyf"])
1805
+ elif overlap in (OverlapMode.REMOVE, OverlapMode.REMOVE_AND_IGNORE_ERRORS):
1806
+ from fontTools.ttLib.removeOverlaps import removeOverlaps
1807
+
1808
+ log.info("Removing glyph outlines overlaps")
1809
+ removeOverlaps(
1810
+ varfont,
1811
+ ignoreErrors=(overlap == OverlapMode.REMOVE_AND_IGNORE_ERRORS),
1812
+ )
1813
+
1706
1814
  return varfont
1707
1815
 
1708
1816
 
@@ -1809,6 +1917,12 @@ def parseArgs(args):
1809
1917
  default=None,
1810
1918
  help="Output instance TTF file (default: INPUT-instance.ttf).",
1811
1919
  )
1920
+ parser.add_argument(
1921
+ "--static",
1922
+ dest="static",
1923
+ action="store_true",
1924
+ help="Make a static font: pin unspecified axes to their default location.",
1925
+ )
1812
1926
  parser.add_argument(
1813
1927
  "--no-optimize",
1814
1928
  dest="optimize",
@@ -1906,13 +2020,13 @@ def main(args=None):
1906
2020
  recalcBBoxes=options.recalc_bounds,
1907
2021
  )
1908
2022
 
1909
- isFullInstance = {
2023
+ isFullInstance = options.static or {
1910
2024
  axisTag
1911
2025
  for axisTag, limit in axisLimits.items()
1912
2026
  if limit is None or limit[0] == limit[2]
1913
2027
  }.issuperset(axis.axisTag for axis in varfont["fvar"].axes)
1914
2028
 
1915
- instantiateVariableFont(
2029
+ varfont = instantiateVariableFont(
1916
2030
  varfont,
1917
2031
  axisLimits,
1918
2032
  inplace=True,
@@ -1920,6 +2034,7 @@ def main(args=None):
1920
2034
  overlap=options.overlap,
1921
2035
  updateFontNames=options.update_name_table,
1922
2036
  downgradeCFF2=options.downgrade_cff2,
2037
+ static=options.static,
1923
2038
  )
1924
2039
 
1925
2040
  suffix = "-instance" if isFullInstance else "-partial"
@@ -174,6 +174,9 @@ def min_cost_perfect_bipartite_matching_bruteforce(G):
174
174
  return best, best_cost
175
175
 
176
176
 
177
+ # Prefer `scipy.optimize.linear_sum_assignment` for performance.
178
+ # `Munkres` is also supported as a fallback for minimalistic systems
179
+ # where installing SciPy is not feasible.
177
180
  try:
178
181
  from scipy.optimize import linear_sum_assignment
179
182