Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop impliable oncurves either before or after rounding #3156

Merged
merged 6 commits into from
Jun 8, 2023
5 changes: 5 additions & 0 deletions Lib/fontTools/misc/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ def values(self, values):
"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)
Expand Down
11 changes: 8 additions & 3 deletions Lib/fontTools/pens/ttGlyphPen.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ def _buildComponents(self, componentFlags):
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,
anthrotype marked this conversation as resolved.
Show resolved Hide resolved
) -> Glyph:
"""
Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.

Expand All @@ -144,8 +149,6 @@ def glyph(self, componentFlags: int = 0x04, dropImpliedOnCurves=False) -> Glyph:
glyph.coordinates = GlyphCoordinates(self.points)
glyph.endPtsOfContours = self.endPts
glyph.flags = array("B", self.types)
glyph.coordinates.toInt()

self.init()

if components:
Expand All @@ -159,6 +162,8 @@ def glyph(self, componentFlags: int = 0x04, dropImpliedOnCurves=False) -> Glyph:
glyph.program.fromBytecode(b"")
if dropImpliedOnCurves:
dropImpliedOnCurvePoints(glyph)
if roundCoordinates:
glyph.coordinates.toInt()

return glyph

Expand Down
29 changes: 22 additions & 7 deletions Lib/fontTools/ttLib/tables/_g_l_y_f.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
strToFixedToFloat as str2fl,
otRound,
)
from fontTools.misc.vector import Vector
from numbers import Number
from . import DefaultTable
from . import ttProgram
Expand All @@ -28,6 +29,7 @@
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

Expand Down Expand Up @@ -369,7 +371,9 @@ def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None):
(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,
Expand Down Expand Up @@ -442,6 +446,8 @@ def _getCoordinatesAndControls(self, glyphName, hMetrics, vMetrics=None):
# 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):
Expand Down Expand Up @@ -1533,6 +1539,19 @@ def __ne__(self, other):
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.

Expand Down Expand Up @@ -1593,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)
Expand Down
11 changes: 9 additions & 2 deletions Tests/varLib/instancer/instancer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down