Merge pull request #3146 from fonttools/drop-implied-oncurves-interpolatable
implied oncurve points for interpolatable glyphs
This commit is contained in:
commit
84cebca6a1
@ -11,56 +11,13 @@ from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic
|
||||
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,
|
||||
@ -190,7 +147,7 @@ class _TTGlyphBasePen:
|
||||
|
||||
glyph.coordinates.toInt()
|
||||
if dropImpliedOnCurves:
|
||||
drop_implied_oncurves(glyph)
|
||||
dropImpliedOnCurvePoints(glyph)
|
||||
|
||||
self.init()
|
||||
|
||||
|
@ -22,11 +22,13 @@ import sys
|
||||
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__)
|
||||
|
||||
@ -1530,6 +1532,85 @@ class Glyph(object):
|
||||
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.
|
||||
|
||||
|
@ -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
|
||||
@ -8,6 +9,7 @@ from fontTools.ttLib.tables._g_l_y_f import (
|
||||
Glyph,
|
||||
GlyphCoordinates,
|
||||
GlyphComponent,
|
||||
dropImpliedOnCurvePoints,
|
||||
flagOnCurve,
|
||||
flagCubic,
|
||||
ARGS_ARE_XY_VALUES,
|
||||
@ -20,6 +22,7 @@ from fontTools.ttLib.tables._g_l_y_f import (
|
||||
from fontTools.ttLib.tables import ttProgram
|
||||
import sys
|
||||
import array
|
||||
from copy import deepcopy
|
||||
from io import StringIO, BytesIO
|
||||
import itertools
|
||||
import pytest
|
||||
@ -813,6 +816,126 @@ class GlyphCubicTest:
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user