From f7ca9f28ac553ebb8820d49ed33e0bb041dc8e62 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 7 Jun 2023 17:50:33 +0100 Subject: [PATCH 1/6] vector: add isclose method to compare vs another Vector using math.isclose --- Lib/fontTools/misc/vector.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/fontTools/misc/vector.py b/Lib/fontTools/misc/vector.py index 81c148418..666ff15cf 100644 --- a/Lib/fontTools/misc/vector.py +++ b/Lib/fontTools/misc/vector.py @@ -134,6 +134,11 @@ class Vector(tuple): "can't set attribute, the 'values' attribute has been deprecated", ) + def isclose(self, other: "Vector", **kwargs) -> bool: + """Return True if the vector is close to another Vector.""" + assert len(self) == len(other) + return all(math.isclose(a, b, **kwargs) for a, b in zip(self, other)) + def _operator_rsub(a, b): return operator.sub(b, a) From 97e626b23e9feb604fa55823b0e7bf1b78fd40ad Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 7 Jun 2023 17:55:56 +0100 Subject: [PATCH 2/6] ttGlyphPen: drop implied before rounding, allow not to round at all We want to be able to compute implied oncurves both before coordinates have been rounded to integer and afterwards. Also when ufo2ft builds interpolatable master TTFs, we want to turn off the rounding so that varLib can call dropImpliedOnCurvePoints on the un-rounded coordinates, so we need an option to disable rounding the coordinates in the ttGlyphPens --- Lib/fontTools/pens/ttGlyphPen.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index 1042f21d6..f06bcf5b9 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -127,7 +127,12 @@ class _TTGlyphBasePen: components.append(component) return components - def glyph(self, componentFlags: int = 0x04, dropImpliedOnCurves=False) -> Glyph: + def glyph( + self, + componentFlags: int = 0x04, + dropImpliedOnCurves: bool = False, + roundCoordinates: bool = True, + ) -> Glyph: """ Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. @@ -144,8 +149,6 @@ class _TTGlyphBasePen: glyph.coordinates = GlyphCoordinates(self.points) glyph.endPtsOfContours = self.endPts glyph.flags = array("B", self.types) - glyph.coordinates.toInt() - self.init() if components: @@ -159,6 +162,8 @@ class _TTGlyphBasePen: glyph.program.fromBytecode(b"") if dropImpliedOnCurves: dropImpliedOnCurvePoints(glyph) + if roundCoordinates: + glyph.coordinates.toInt() return glyph From 1ca554332c9f1aa9c5737640aabae84d555a9a69 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 7 Jun 2023 18:05:48 +0100 Subject: [PATCH 3/6] _getCoordinatesAndControls: make sure coords are rounded toInt() as gvar expects ufo2ft will no longer send varLib already-rounded master glyf tables (to give it an opportunity to compute implied oncurves on the pre-rounded coords) so when retrieving coordinates off the glyf table in order to compute gvar deltas we have to round --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 6 +++++- Tests/varLib/instancer/instancer_test.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index b953061d9..3493ca974 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -369,7 +369,9 @@ class table__g_l_y_f(DefaultTable.DefaultTable): (0, bottomSideY), ] - def _getCoordinatesAndControls(self, glyphName, hMetrics, vMetrics=None): + def _getCoordinatesAndControls( + self, glyphName, hMetrics, vMetrics=None, roundCoordinates=True + ): """Return glyph coordinates and controls as expected by "gvar" table. The coordinates includes four "phantom points" for the glyph metrics, @@ -442,6 +444,8 @@ class table__g_l_y_f(DefaultTable.DefaultTable): # Add phantom points for (left, right, top, bottom) positions. phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics) coords.extend(phantomPoints) + if roundCoordinates: + coords.toInt() return coords, controls def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None): diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index 499f9d4f0..e574384de 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -51,8 +51,15 @@ def fvarAxes(): def _get_coordinates(varfont, glyphname): # converts GlyphCoordinates to a list of (x, y) tuples, so that pytest's # assert will give us a nicer diff - with pytest.deprecated_call(): - return list(varfont["glyf"].getCoordinatesAndControls(glyphname, varfont)[0]) + return list( + varfont["glyf"]._getCoordinatesAndControls( + glyphname, + varfont["hmtx"].metrics, + varfont["vmtx"].metrics, + # the tests expect float coordinates + roundCoordinates=False, + )[0] + ) class InstantiateGvarTest(object): From 86777525a62f639a7e1fd5e1b1b465178be8680a Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 7 Jun 2023 18:13:58 +0100 Subject: [PATCH 4/6] drop oncurve if it's impliable either before OR after rounding ofter the rounding gives us an opportunity to make an oncurve impliable, for points that were not equidistant become so after round, so why not take it! --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 3493ca974..4554efc2e 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -15,6 +15,7 @@ from fontTools.misc.fixedTools import ( strToFixedToFloat as str2fl, otRound, ) +from fontTools.misc.vector import Vector from numbers import Number from . import DefaultTable from . import ttProgram @@ -28,6 +29,7 @@ from fontTools.misc import xmlWriter from fontTools.misc.filenames import userNameToFileName from fontTools.misc.loggingTools import deprecateFunction from enum import IntFlag +from functools import partial from types import SimpleNamespace from typing import Set @@ -1537,6 +1539,19 @@ class Glyph(object): return result if result is NotImplemented else not result +# Vector.__round__ uses the built-in (Banker's) `round` but we want +# to use otRound below +_roundv = partial(Vector.__round__, round=otRound) + + +def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool: + # True if p1 is in the middle of p0 and p2, either before or after rounding + p0 = Vector(p0) + p1 = Vector(p1) + p2 = Vector(p2) + return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2 + + def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: """Drop impliable on-curve points from the (simple) glyph or glyphs. @@ -1597,12 +1612,8 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: nxt = i + 1 if i < last else start if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: continue - p0 = coords[prv] - p1 = coords[i] - p2 = coords[nxt] - if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose( - p1[1] - p0[1], p2[1] - p1[1] - ): + # we may drop the ith on-curve if halfway between previous/next off-curves + if not _is_mid_point(coords[prv], coords[i], coords[nxt]): continue may_drop.add(i) From 5c0f05cc42fe0e4f149a7488419d4a3cf5ef8c47 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 8 Jun 2023 11:41:07 +0100 Subject: [PATCH 5/6] allow to pass 'round' parameter in ttGlyphPen, optimize for noRound --- Lib/fontTools/pens/ttGlyphPen.py | 8 ++++---- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index f06bcf5b9..de2ccaeeb 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -1,5 +1,5 @@ from array import array -from typing import Any, Dict, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat from fontTools.misc.loggingTools import LogMixin from fontTools.pens.pointPen import AbstractPointPen @@ -131,7 +131,8 @@ class _TTGlyphBasePen: self, componentFlags: int = 0x04, dropImpliedOnCurves: bool = False, - roundCoordinates: bool = True, + *, + round: Callable[[float], int] = otRound, ) -> Glyph: """ Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. @@ -162,8 +163,7 @@ class _TTGlyphBasePen: glyph.program.fromBytecode(b"") if dropImpliedOnCurves: dropImpliedOnCurvePoints(glyph) - if roundCoordinates: - glyph.coordinates.toInt() + glyph.coordinates.toInt(round=round) return glyph diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 4554efc2e..46cb2fd61 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -13,8 +13,8 @@ from fontTools.misc.fixedTools import ( floatToFixed as fl2fi, floatToFixedToStr as fl2str, strToFixedToFloat as str2fl, - otRound, ) +from fontTools.misc.roundTools import noRound, otRound from fontTools.misc.vector import Vector from numbers import Number from . import DefaultTable @@ -2322,6 +2322,8 @@ class GlyphCoordinates(object): self._a.extend(p) def toInt(self, *, round=otRound): + if round is noRound: + return a = self._a for i in range(len(a)): a[i] = round(a[i]) From dfec4abf6d0d893c9ce2b01b9e8383831a684ff2 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 8 Jun 2023 11:47:47 +0100 Subject: [PATCH 6/6] glyf: use 'round' parameter in _getCoordinatesAndControls as Behdad suggested in review --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 5 ++--- Tests/varLib/instancer/instancer_test.py | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 46cb2fd61..5c0e5f7f8 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -372,7 +372,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): ] def _getCoordinatesAndControls( - self, glyphName, hMetrics, vMetrics=None, roundCoordinates=True + self, glyphName, hMetrics, vMetrics=None, *, round=otRound ): """Return glyph coordinates and controls as expected by "gvar" table. @@ -446,8 +446,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): # Add phantom points for (left, right, top, bottom) positions. phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics) coords.extend(phantomPoints) - if roundCoordinates: - coords.toInt() + coords.toInt(round=round) return coords, controls def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None): diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index e574384de..ab64c379d 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -1,4 +1,5 @@ from fontTools.misc.fixedTools import floatToFixedToFloat +from fontTools.misc.roundTools import noRound from fontTools.misc.testTools import stripVariableItemsFromTTX from fontTools.misc.textTools import Tag from fontTools import ttLib @@ -57,7 +58,7 @@ def _get_coordinates(varfont, glyphname): varfont["hmtx"].metrics, varfont["vmtx"].metrics, # the tests expect float coordinates - roundCoordinates=False, + round=noRound, )[0] )