Skip to content

Commit

Permalink
Merge pull request #3146 from fonttools/drop-implied-oncurves-interpo…
Browse files Browse the repository at this point in the history
…latable

implied oncurve points for interpolatable glyphs
  • Loading branch information
anthrotype committed Jun 1, 2023
2 parents 02a0636 + a039e1d commit 84cebca
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 45 deletions.
47 changes: 2 additions & 45 deletions Lib/fontTools/pens/ttGlyphPen.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,56 +11,13 @@
from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints
import math


__all__ = ["TTGlyphPen", "TTGlyphPointPen"]


def drop_implied_oncurves(glyph):
drop = set()
start = 0
flags = glyph.flags
coords = glyph.coordinates
for last in glyph.endPtsOfContours:
for i in range(start, last + 1):
if not (flags[i] & flagOnCurve):
continue
prv = i - 1 if i > start else last
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]
):
continue

drop.add(i)
if drop:
# Do the actual dropping
glyph.coordinates = GlyphCoordinates(
coords[i] for i in range(len(coords)) if i not in drop
)
glyph.flags = array("B", (flags[i] for i in range(len(flags)) if i not in drop))

endPts = glyph.endPtsOfContours
newEndPts = []
i = 0
delta = 0
for d in sorted(drop):
while d > endPts[i]:
newEndPts.append(endPts[i] - delta)
i += 1
delta += 1
while i < len(endPts):
newEndPts.append(endPts[i] - delta)
i += 1
glyph.endPtsOfContours = newEndPts


class _TTGlyphBasePen:
def __init__(
self,
Expand Down Expand Up @@ -190,7 +147,7 @@ def glyph(self, componentFlags: int = 0x04, dropImpliedOnCurves=False) -> Glyph:

glyph.coordinates.toInt()
if dropImpliedOnCurves:
drop_implied_oncurves(glyph)
dropImpliedOnCurvePoints(glyph)

self.init()

Expand Down
81 changes: 81 additions & 0 deletions Lib/fontTools/ttLib/tables/_g_l_y_f.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import struct
import array
import logging
import math
import os
from fontTools.misc import xmlWriter
from fontTools.misc.filenames import userNameToFileName
from fontTools.misc.loggingTools import deprecateFunction
from enum import IntFlag
from typing import Set

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -1530,6 +1532,85 @@ def __ne__(self, other):
return result if result is NotImplemented else not result


def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
"""Drop impliable on-curve points from the (simple) glyph or glyphs.
In TrueType glyf outlines, on-curve points can be implied when they are located at
the midpoint of the line connecting two consecutive off-curve points.
If more than one glyphs are passed, these are assumed to be interpolatable masters
of the same glyph impliable, and thus only the on-curve points that are impliable
for all of them will actually be implied.
The input glyph(s) is/are modified in-place.
Args:
interpolatable_glyphs: The glyph or glyphs to modify in-place.
Returns:
The set of point indices that were dropped if any.
Reference:
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
"""
assert len(interpolatable_glyphs) > 0

drop = None
for glyph in interpolatable_glyphs:
may_drop = set()
start = 0
flags = glyph.flags
coords = glyph.coordinates
for last in glyph.endPtsOfContours:
for i in range(start, last + 1):
if not (flags[i] & flagOnCurve):
continue
prv = i - 1 if i > start else last
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]
):
continue

may_drop.add(i)
# we only want to drop if ALL interpolatable glyphs have the same implied oncurves
if drop is None:
drop = may_drop
else:
drop.intersection_update(may_drop)

if drop:
# Do the actual dropping
for glyph in interpolatable_glyphs:
coords = glyph.coordinates
glyph.coordinates = GlyphCoordinates(
coords[i] for i in range(len(coords)) if i not in drop
)
glyph.flags = array.array(
"B", (flags[i] for i in range(len(flags)) if i not in drop)
)

endPts = glyph.endPtsOfContours
newEndPts = []
i = 0
delta = 0
for d in sorted(drop):
while d > endPts[i]:
newEndPts.append(endPts[i] - delta)
i += 1
delta += 1
while i < len(endPts):
newEndPts.append(endPts[i] - delta)
i += 1
glyph.endPtsOfContours = newEndPts

return drop


class GlyphComponent(object):
"""Represents a component within a composite glyph.
Expand Down
123 changes: 123 additions & 0 deletions Tests/ttLib/tables/_g_l_y_f_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fontTools.misc.fixedTools import otRound
from fontTools.misc.testTools import getXML, parseXML
from fontTools.misc.transform import Transform
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen
from fontTools.pens.pointPen import PointToSegmentPen
Expand All @@ -8,6 +9,7 @@
Glyph,
GlyphCoordinates,
GlyphComponent,
dropImpliedOnCurvePoints,
flagOnCurve,
flagCubic,
ARGS_ARE_XY_VALUES,
Expand All @@ -20,6 +22,7 @@
from fontTools.ttLib.tables import ttProgram
import sys
import array
from copy import deepcopy
from io import StringIO, BytesIO
import itertools
import pytest
Expand Down Expand Up @@ -813,6 +816,126 @@ def test_spline(self):
]


def build_interpolatable_glyphs(contours, *transforms):
# given a list of lists of (point, flag) tuples (one per contour), build a Glyph
# then make len(transforms) copies transformed accordingly, and return a
# list of such interpolatable glyphs.
glyph1 = Glyph()
glyph1.numberOfContours = len(contours)
glyph1.coordinates = GlyphCoordinates(
[pt for contour in contours for pt, _flag in contour]
)
glyph1.flags = array.array(
"B", [flag for contour in contours for _pt, flag in contour]
)
glyph1.endPtsOfContours = [
sum(len(contour) for contour in contours[: i + 1]) - 1
for i in range(len(contours))
]
result = [glyph1]
for t in transforms:
glyph = deepcopy(glyph1)
glyph.coordinates.transform((t[0:2], t[2:4]))
glyph.coordinates.translate(t[4:6])
result.append(glyph)
return result


def test_dropImpliedOnCurvePoints_all_quad_off_curves():
# Two interpolatable glyphs with same structure, the coordinates of one are 2x the
# other; all the on-curve points are impliable in each one, thus are dropped from
# both, leaving contours with off-curve points only.
glyph1, glyph2 = build_interpolatable_glyphs(
[
[
((0, 1), flagOnCurve),
((1, 1), 0),
((1, 0), flagOnCurve),
((1, -1), 0),
((0, -1), flagOnCurve),
((-1, -1), 0),
((-1, 0), flagOnCurve),
((-1, 1), 0),
]
],
Transform().scale(2.0),
)

assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 2, 4, 6}

assert glyph1.flags == glyph2.flags == array.array("B", [0, 0, 0, 0])
assert glyph1.coordinates == GlyphCoordinates([(1, 1), (1, -1), (-1, -1), (-1, 1)])
assert glyph2.coordinates == GlyphCoordinates([(2, 2), (2, -2), (-2, -2), (-2, 2)])
assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [3]


def test_dropImpliedOnCurvePoints_all_cubic_off_curves():
# same as above this time using cubic curves
glyph1, glyph2 = build_interpolatable_glyphs(
[
[
((0, 1), flagOnCurve),
((1, 1), flagCubic),
((1, 1), flagCubic),
((1, 0), flagOnCurve),
((1, -1), flagCubic),
((1, -1), flagCubic),
((0, -1), flagOnCurve),
((-1, -1), flagCubic),
((-1, -1), flagCubic),
((-1, 0), flagOnCurve),
((-1, 1), flagCubic),
((-1, 1), flagCubic),
]
],
Transform().translate(10.0),
)

assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 3, 6, 9}

assert glyph1.flags == glyph2.flags == array.array("B", [flagCubic] * 8)
assert glyph1.coordinates == GlyphCoordinates(
[(1, 1), (1, 1), (1, -1), (1, -1), (-1, -1), (-1, -1), (-1, 1), (-1, 1)]
)
assert glyph2.coordinates == GlyphCoordinates(
[(11, 1), (11, 1), (11, -1), (11, -1), (9, -1), (9, -1), (9, 1), (9, 1)]
)
assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [7]


def test_dropImpliedOnCurvePoints_not_all_impliable():
# same input as in in test_dropImpliedOnCurvePoints_all_quad_off_curves but we
# perturbate one of the glyphs such that the 2nd on-curve is no longer half-way
# between the neighboring off-curves.
glyph1, glyph2, glyph3 = build_interpolatable_glyphs(
[
[
((0, 1), flagOnCurve),
((1, 1), 0),
((1, 0), flagOnCurve),
((1, -1), 0),
((0, -1), flagOnCurve),
((-1, -1), 0),
((-1, 0), flagOnCurve),
((-1, 1), 0),
]
],
Transform().translate(10.0),
Transform().translate(10.0).scale(2.0),
)
p2 = glyph2.coordinates[2]
glyph2.coordinates[2] = (p2[0] + 2.0, p2[1] - 2.0)

assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == {
0,
# 2, this is NOT implied because it's no longer impliable for all glyphs
4,
6,
}

assert glyph2.flags == array.array("B", [0, flagOnCurve, 0, 0, 0])


if __name__ == "__main__":
import sys

Expand Down

0 comments on commit 84cebca

Please sign in to comment.