fonttools 4.59.1__cp313-cp313-musllinux_1_2_aarch64.whl → 4.59.2__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.

Potentially problematic release.


This version of fonttools might be problematic. Click here for more details.

fontTools/cu2qu/cu2qu.py CHANGED
@@ -37,7 +37,7 @@ NAN = float("NaN")
37
37
  @cython.cfunc
38
38
  @cython.inline
39
39
  @cython.returns(cython.double)
40
- @cython.locals(v1=cython.complex, v2=cython.complex)
40
+ @cython.locals(v1=cython.complex, v2=cython.complex, result=cython.double)
41
41
  def dot(v1, v2):
42
42
  """Return the dot product of two vectors.
43
43
 
@@ -48,7 +48,16 @@ def dot(v1, v2):
48
48
  Returns:
49
49
  double: Dot product.
50
50
  """
51
- return (v1 * v2.conjugate()).real
51
+ result = (v1 * v2.conjugate()).real
52
+ # When vectors are perpendicular (i.e. dot product is 0), the above expression may
53
+ # yield slightly different results when running in pure Python vs C/Cython,
54
+ # both of which are correct within IEEE-754 floating-point precision.
55
+ # It's probably due to the different order of operations and roundings in each
56
+ # implementation. Because we are using the result in a denominator and catching
57
+ # ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here.
58
+ if abs(result) < 1e-15:
59
+ result = 0.0
60
+ return result
52
61
 
53
62
 
54
63
  @cython.cfunc
@@ -273,6 +282,12 @@ def calc_intersect(a, b, c, d):
273
282
  try:
274
283
  h = dot(p, a - c) / dot(p, cd)
275
284
  except ZeroDivisionError:
285
+ # if 3 or 4 points are equal, we do have an intersection despite the zero-div:
286
+ # return one of the off-curves so that the algorithm can attempt a one-curve
287
+ # solution if it's within tolerance:
288
+ # https://github.com/linebender/kurbo/pull/484
289
+ if b == c and (a == b or c == d):
290
+ return b
276
291
  return complex(NAN, NAN)
277
292
  return c + cd * h
278
293
 
@@ -32,6 +32,7 @@ from fontTools.otlLib.builder import (
32
32
  AnySubstBuilder,
33
33
  )
34
34
  from fontTools.otlLib.error import OpenTypeLibError
35
+ from fontTools.varLib.errors import VarLibError
35
36
  from fontTools.varLib.varStore import OnlineVarStoreBuilder
36
37
  from fontTools.varLib.builder import buildVarDevTable
37
38
  from fontTools.varLib.featureVars import addFeatureVariationsRaw
@@ -1728,9 +1729,14 @@ class Builder(object):
1728
1729
  if not varscalar.does_vary:
1729
1730
  return varscalar.default, None
1730
1731
 
1731
- default, index = varscalar.add_to_variation_store(
1732
- self.varstorebuilder, self.model_cache, self.font.get("avar")
1733
- )
1732
+ try:
1733
+ default, index = varscalar.add_to_variation_store(
1734
+ self.varstorebuilder, self.model_cache, self.font.get("avar")
1735
+ )
1736
+ except VarLibError as e:
1737
+ raise FeatureLibError(
1738
+ "Failed to compute deltas for variable scalar", location
1739
+ ) from e
1734
1740
 
1735
1741
  device = None
1736
1742
  if index is not None and index != 0xFFFFFFFF:
@@ -2198,7 +2198,7 @@ class Parser(object):
2198
2198
  raise FeatureLibError(
2199
2199
  "Expected an equals sign", self.cur_token_location_
2200
2200
  )
2201
- value = self.expect_number_()
2201
+ value = self.expect_integer_or_float_()
2202
2202
  location[axis] = value
2203
2203
  if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
2204
2204
  # Lexer has just read the value as a glyph name. We'll correct it later
@@ -2230,6 +2230,16 @@ class Parser(object):
2230
2230
  "Expected a floating-point number", self.cur_token_location_
2231
2231
  )
2232
2232
 
2233
+ def expect_integer_or_float_(self):
2234
+ if self.next_token_type_ == Lexer.FLOAT:
2235
+ return self.expect_float_()
2236
+ elif self.next_token_type_ is Lexer.NUMBER:
2237
+ return self.expect_number_()
2238
+ else:
2239
+ raise FeatureLibError(
2240
+ "Expected an integer or floating-point number", self.cur_token_location_
2241
+ )
2242
+
2233
2243
  def expect_decipoint_(self):
2234
2244
  if self.next_token_type_ == Lexer.FLOAT:
2235
2245
  return self.expect_float_()
@@ -17,7 +17,12 @@ class VariableScalar:
17
17
  def __repr__(self):
18
18
  items = []
19
19
  for location, value in self.values.items():
20
- loc = ",".join(["%s=%i" % (ax, loc) for ax, loc in location])
20
+ loc = ",".join(
21
+ [
22
+ f"{ax}={int(coord) if float(coord).is_integer() else coord}"
23
+ for ax, coord in location
24
+ ]
25
+ )
21
26
  items.append("%s:%i" % (loc, value))
22
27
  return "(" + (" ".join(items)) + ")"
23
28
 
@@ -1,5 +1,7 @@
1
1
  """fontTools.misc.textTools.py -- miscellaneous routines."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import ast
4
6
  import string
5
7
 
@@ -118,14 +120,14 @@ def pad(data, size):
118
120
  return data
119
121
 
120
122
 
121
- def tostr(s, encoding="ascii", errors="strict"):
123
+ def tostr(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> str:
122
124
  if not isinstance(s, str):
123
125
  return s.decode(encoding, errors)
124
126
  else:
125
127
  return s
126
128
 
127
129
 
128
- def tobytes(s, encoding="ascii", errors="strict"):
130
+ def tobytes(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> bytes:
129
131
  if isinstance(s, str):
130
132
  return s.encode(encoding, errors)
131
133
  else:
@@ -1530,6 +1530,7 @@ def subset_glyphs(self, s):
1530
1530
  if self.MarkFilteringSet not in s.used_mark_sets:
1531
1531
  self.MarkFilteringSet = None
1532
1532
  self.LookupFlag &= ~0x10
1533
+ self.LookupFlag |= 0x8
1533
1534
  else:
1534
1535
  self.MarkFilteringSet = s.used_mark_sets.index(self.MarkFilteringSet)
1535
1536
  return bool(self.SubTableCount)
@@ -143,7 +143,7 @@ class table__a_v_a_r(BaseTTXConverter):
143
143
  else:
144
144
  super().fromXML(name, attrs, content, ttFont)
145
145
 
146
- def renormalizeLocation(self, location, font):
146
+ def renormalizeLocation(self, location, font, dropZeroes=True):
147
147
 
148
148
  majorVersion = getattr(self, "majorVersion", 1)
149
149
 
@@ -185,7 +185,9 @@ class table__a_v_a_r(BaseTTXConverter):
185
185
  out.append(v)
186
186
 
187
187
  mappedLocation = {
188
- axis.axisTag: fi2fl(v, 14) for v, axis in zip(out, axes) if v != 0
188
+ axis.axisTag: fi2fl(v, 14)
189
+ for v, axis in zip(out, axes)
190
+ if v != 0 or not dropZeroes
189
191
  }
190
192
 
191
193
  return mappedLocation
@@ -1,4 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
2
4
  from fontTools.misc import sstruct
3
5
  from fontTools.misc.textTools import (
4
6
  bytechr,
@@ -63,7 +65,7 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
63
65
  )
64
66
  stringData = data[stringOffset:]
65
67
  data = data[6:]
66
- self.names = []
68
+ self.names: list[NameRecord] = []
67
69
  for i in range(n):
68
70
  if len(data) < 12:
69
71
  log.error("skipping malformed name record #%d", i)
@@ -112,7 +114,9 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
112
114
  self.names.append(name)
113
115
  name.fromXML(name, attrs, content, ttFont)
114
116
 
115
- def getName(self, nameID, platformID, platEncID, langID=None):
117
+ def getName(
118
+ self, nameID: int, platformID: int, platEncID: int, langID: int | None = None
119
+ ) -> "NameRecord | None":
116
120
  for namerecord in self.names:
117
121
  if (
118
122
  namerecord.nameID == nameID
@@ -123,8 +127,9 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
123
127
  return namerecord
124
128
  return None # not found
125
129
 
126
- def getDebugName(self, nameID):
127
- englishName = someName = None
130
+ def getDebugName(self, nameID: int) -> str | None:
131
+ englishName: str | None = None
132
+ someName: str | None = None
128
133
  for name in self.names:
129
134
  if name.nameID != nameID:
130
135
  continue
@@ -513,7 +518,7 @@ class NameRecord(object):
513
518
  self.platformID == 3 and self.platEncID in [0, 1, 10]
514
519
  )
515
520
 
516
- def toUnicode(self, errors="strict"):
521
+ def toUnicode(self, errors: str = "strict") -> str:
517
522
  """
518
523
  If self.string is a Unicode string, return it; otherwise try decoding the
519
524
  bytes in self.string to a Unicode string using the encoding of this
@@ -533,7 +538,7 @@ class NameRecord(object):
533
538
  and saving it back will not change them.
534
539
  """
535
540
 
536
- def isascii(b):
541
+ def isascii(b: int) -> bool:
537
542
  return (b >= 0x20 and b <= 0x7E) or b in [0x09, 0x0A, 0x0D]
538
543
 
539
544
  encoding = self.getEncoding()
@@ -30,7 +30,11 @@ from fontTools.misc.fixedTools import floatToFixed as fl2fi
30
30
  from fontTools.misc.textTools import Tag, tostr
31
31
  from fontTools.ttLib import TTFont, newTable
32
32
  from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
33
- from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates, dropImpliedOnCurvePoints
33
+ from fontTools.ttLib.tables._g_l_y_f import (
34
+ GlyphCoordinates,
35
+ dropImpliedOnCurvePoints,
36
+ USE_MY_METRICS,
37
+ )
34
38
  from fontTools.ttLib.tables.ttProgram import Program
35
39
  from fontTools.ttLib.tables.TupleVariation import TupleVariation
36
40
  from fontTools.ttLib.tables import otTables as ot
@@ -489,6 +493,77 @@ def _merge_TTHinting(font, masterModel, master_ttfs):
489
493
  cvar.variations = variations
490
494
 
491
495
 
496
+ def _has_inconsistent_use_my_metrics_flag(
497
+ master_glyf, glyph_name, flagged_components, expected_num_components
498
+ ) -> bool:
499
+ master_glyph = master_glyf.get(glyph_name)
500
+ # 'sparse' glyph master doesn't contribute. Besides when components don't match
501
+ # the VF build is going to fail anyway, so be lenient here.
502
+ if (
503
+ master_glyph is not None
504
+ and master_glyph.isComposite()
505
+ and len(master_glyph.components) == expected_num_components
506
+ ):
507
+ for i, base_glyph in flagged_components:
508
+ comp = master_glyph.components[i]
509
+ if comp.glyphName != base_glyph:
510
+ break
511
+ if not (comp.flags & USE_MY_METRICS):
512
+ return True
513
+ return False
514
+
515
+
516
+ def _unset_inconsistent_use_my_metrics_flags(vf, master_fonts):
517
+ """Clear USE_MY_METRICS on composite components if inconsistent across masters.
518
+
519
+ If a composite glyph's component has USE_MY_METRICS set differently among
520
+ the masters, the flag is removed from the variable font's glyf table so that
521
+ advance widths are not determined by that single component's phantom points.
522
+ """
523
+ glyf = vf["glyf"]
524
+ master_glyfs = [m["glyf"] for m in master_fonts if "glyf" in m]
525
+ if not master_glyfs:
526
+ # Should not happen: at least the base master (as copied into vf) has glyf
527
+ return
528
+
529
+ for glyph_name in glyf.keys():
530
+ glyph = glyf[glyph_name]
531
+ if not glyph.isComposite():
532
+ continue
533
+
534
+ # collect indices of component(s) that carry the USE_MY_METRICS flag.
535
+ # This is supposed to be 1 component per composite, but you never know.
536
+ flagged_components = [
537
+ (i, comp.glyphName)
538
+ for i, comp in enumerate(glyph.components)
539
+ if (comp.flags & USE_MY_METRICS)
540
+ ]
541
+ if not flagged_components:
542
+ # Nothing to fix
543
+ continue
544
+
545
+ # Verify that for all master glyf tables that contribute this glyph, the
546
+ # corresponding component (same glyphName and index) also carries USE_MY_METRICS
547
+ # and unset the flag if not.
548
+ expected_num_components = len(glyph.components)
549
+ if any(
550
+ _has_inconsistent_use_my_metrics_flag(
551
+ master_glyf, glyph_name, flagged_components, expected_num_components
552
+ )
553
+ for master_glyf in master_glyfs
554
+ ):
555
+ comp_names = [name for _, name in flagged_components]
556
+ log.info(
557
+ "Composite glyph '%s' has inconsistent USE_MY_METRICS flags across "
558
+ "masters; clearing the flag on component%s %s",
559
+ glyph_name,
560
+ "s" if len(comp_names) > 1 else "",
561
+ comp_names if len(comp_names) > 1 else comp_names[0],
562
+ )
563
+ for i, _ in flagged_components:
564
+ glyph.components[i].flags &= ~USE_MY_METRICS
565
+
566
+
492
567
  _MetricsFields = namedtuple(
493
568
  "_MetricsFields",
494
569
  [
@@ -1205,6 +1280,10 @@ def build(
1205
1280
  if "DSIG" in vf:
1206
1281
  del vf["DSIG"]
1207
1282
 
1283
+ # Clear USE_MY_METRICS composite flags if set inconsistently across masters.
1284
+ if "glyf" in vf:
1285
+ _unset_inconsistent_use_my_metrics_flags(vf, master_fonts)
1286
+
1208
1287
  # TODO append masters as named-instances as well; needs .designspace change.
1209
1288
  fvar = _add_fvar(vf, ds.axes, ds.instances)
1210
1289
  if "STAT" not in exclude:
@@ -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 (
@@ -435,7 +433,27 @@ class AxisLimits(_BaseAxisLimits):
435
433
 
436
434
  avarSegments = {}
437
435
  if usingAvar and "avar" in varfont:
438
- 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], avarSegments.get(tag, None))
452
+ for tag, value in location.items()
453
+ }
454
+ return NormalizedAxisLimits(
455
+ **avar.renormalizeLocation(location, varfont, dropZeroes=False)
456
+ )
439
457
 
440
458
  normalizedLimits = {}
441
459
 
@@ -1122,7 +1140,8 @@ def _instantiateVHVAR(varfont, axisLimits, tableFields, *, round=round):
1122
1140
  varIdx = advMapping.mapping[glyphName]
1123
1141
  else:
1124
1142
  varIdx = varfont.getGlyphID(glyphName)
1125
- metrics[glyphName] = (advanceWidth + round(defaultDeltas[varIdx]), sb)
1143
+ delta = round(defaultDeltas[varIdx])
1144
+ metrics[glyphName] = (max(0, advanceWidth + delta), sb)
1126
1145
 
1127
1146
  if (
1128
1147
  tableTag == "VVAR"
@@ -1384,7 +1403,6 @@ def _isValidAvarSegmentMap(axisTag, segmentMap):
1384
1403
 
1385
1404
 
1386
1405
  def downgradeCFF2ToCFF(varfont):
1387
-
1388
1406
  # Save these properties
1389
1407
  recalcTimestamp = varfont.recalcTimestamp
1390
1408
  recalcBBoxes = varfont.recalcBBoxes
@@ -1433,8 +1451,6 @@ def instantiateAvar(varfont, axisLimits):
1433
1451
  # 'axisLimits' dict must contain user-space (non-normalized) coordinates.
1434
1452
 
1435
1453
  avar = varfont["avar"]
1436
- if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore:
1437
- raise NotImplementedError("avar table with VarStore is not supported")
1438
1454
 
1439
1455
  segments = avar.segments
1440
1456
 
@@ -1445,6 +1461,9 @@ def instantiateAvar(varfont, axisLimits):
1445
1461
  del varfont["avar"]
1446
1462
  return
1447
1463
 
1464
+ if getattr(avar, "majorVersion", 1) >= 2 and avar.table.VarStore:
1465
+ raise NotImplementedError("avar table with VarStore is not supported")
1466
+
1448
1467
  log.info("Instantiating avar table")
1449
1468
  for axis in pinnedAxes:
1450
1469
  if axis in segments:
@@ -1646,6 +1665,7 @@ def instantiateVariableFont(
1646
1665
  updateFontNames=False,
1647
1666
  *,
1648
1667
  downgradeCFF2=False,
1668
+ static=False,
1649
1669
  ):
1650
1670
  """Instantiate variable font, either fully or partially.
1651
1671
 
@@ -1689,12 +1709,23 @@ def instantiateVariableFont(
1689
1709
  software that does not support CFF2. Defaults to False. Note that this
1690
1710
  operation also removes overlaps within glyph shapes, as CFF does not support
1691
1711
  overlaps but CFF2 does.
1712
+ static (bool): if True, generate a full instance (static font) instead of a partial
1713
+ instance (variable font).
1692
1714
  """
1693
1715
  # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool
1694
1716
  overlap = OverlapMode(int(overlap))
1695
1717
 
1696
1718
  sanityCheckVariableTables(varfont)
1697
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
+
1698
1729
  axisLimits = AxisLimits(axisLimits).limitAxesAndPopulateDefaults(varfont)
1699
1730
 
1700
1731
  log.info("Restricted limits: %s", axisLimits)
@@ -1886,6 +1917,12 @@ def parseArgs(args):
1886
1917
  default=None,
1887
1918
  help="Output instance TTF file (default: INPUT-instance.ttf).",
1888
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
+ )
1889
1926
  parser.add_argument(
1890
1927
  "--no-optimize",
1891
1928
  dest="optimize",
@@ -1983,7 +2020,7 @@ def main(args=None):
1983
2020
  recalcBBoxes=options.recalc_bounds,
1984
2021
  )
1985
2022
 
1986
- isFullInstance = {
2023
+ isFullInstance = options.static or {
1987
2024
  axisTag
1988
2025
  for axisTag, limit in axisLimits.items()
1989
2026
  if limit is None or limit[0] == limit[2]
@@ -1997,6 +2034,7 @@ def main(args=None):
1997
2034
  overlap=options.overlap,
1998
2035
  updateFontNames=options.update_name_table,
1999
2036
  downgradeCFF2=options.downgrade_cff2,
2037
+ static=options.static,
2000
2038
  )
2001
2039
 
2002
2040
  suffix = "-instance" if isFullInstance else "-partial"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fonttools
3
- Version: 4.59.1
3
+ Version: 4.59.2
4
4
  Summary: Tools to manipulate font files
5
5
  Home-page: http://github.com/fonttools/fonttools
6
6
  Author: Just van Rossum
@@ -388,6 +388,20 @@ Have fun!
388
388
  Changelog
389
389
  ~~~~~~~~~
390
390
 
391
+ 4.59.2 (released 2025-08-27)
392
+ ----------------------------
393
+
394
+ - [varLib] Clear ``USE_MY_METRICS`` component flags when inconsistent across masters (#3912).
395
+ - [varLib.instancer] Avoid negative advance width/height values when instatiating HVAR/VVAR,
396
+ (unlikely in well-behaved fonts) (#3918).
397
+ - [subset] Fix shaping behaviour when pruning empty mark sets (#3915, harfbuzz/harfbuzz#5499).
398
+ - [cu2qu] Fixed ``dot()`` product of perpendicular vectors not always returning exactly 0.0
399
+ in all Python implementations (#3911)
400
+ - [varLib.instancer] Implemented fully-instantiating ``avar2`` fonts (#3909).
401
+ - [feaLib] Allow float values in ``VariableScalar``'s axis locations (#3906, #3907).
402
+ - [cu2qu] Handle special case in ``calc_intersect`` for degenerate cubic curves where 3 to 4
403
+ control points are equal (#3904).
404
+
391
405
  4.59.1 (released 2025-08-14)
392
406
  ----------------------------
393
407