From 07351d12e6df8c6f627072aac013c5f27fca8f80 Mon Sep 17 00:00:00 2001 From: Harry Dalton Date: Wed, 11 Sep 2024 10:49:00 +0100 Subject: [PATCH] Fix visual artefacts with partial L2 instancing Closes #3634 To produce inferred deltas that will be correct given OpenType's gvar semantics, fontTool's IUP optimisation module checks the equality of some points. However, this happens before the points are rounded, whereas the point comparison that happens at runtime will occur after the points are rounded (as is necessary to serialise glyf), which leads to diverging semantics and so diverging and incorrect implied deltas. This leads to significant visual artefacts, e.g. where large deltas that should be inferred based on previous values are instead interpreted as 0 at runtime. I suspect this has gone undetected as the subsetter normally works with rounded points; in the rarer case that partial VF instancing is occurring with a different default position, however, varLib.instancer will calculate and apply the relevant deltas to the font's original coordinates to effect the new default position, which leads to unrounded points in memory. This commit ensures that we round directly before optimising (but still after calculating `glyf` metrics, for backward compatibility). --- Lib/fontTools/varLib/instancer/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index 82676d419..5819560a5 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -897,7 +897,18 @@ def _instantiateGvarGlyph( return if optimize: + # IUP semantics depend on point equality, and so round prior to + # optimization to ensure that comparisons that happen now will be the + # same as those that happen at render time. This is especially needed + # when floating point deltas have been applied to the default position. + # See https://github.com/fonttools/fonttools/issues/3634 + # Rounding must happen only after calculating glyf metrics above, to + # preserve backwards compatibility. + # See 0010a3cd9aa25f84a3a6250dafb119743d32aa40 + coordinates.toInt() + isComposite = glyf[glyphname].isComposite() + for var in tupleVarStore: var.optimize(coordinates, endPts, isComposite=isComposite)