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

[varlib] add --drop-implied-oncurves option #3147

Merged
merged 4 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions Lib/fontTools/pens/ttGlyphPen.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,7 @@ 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()
if dropImpliedOnCurves:
dropImpliedOnCurvePoints(glyph)

self.init()

Expand All @@ -160,6 +157,8 @@ def glyph(self, componentFlags: int = 0x04, dropImpliedOnCurves=False) -> Glyph:
glyph.numberOfContours = len(glyph.endPtsOfContours)
glyph.program = ttProgram.Program()
glyph.program.fromBytecode(b"")
if dropImpliedOnCurves:
dropImpliedOnCurvePoints(glyph)

return glyph

Expand Down
40 changes: 34 additions & 6 deletions Lib/fontTools/ttLib/tables/_g_l_y_f.py
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,8 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
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.
Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more
contours are considered.
The input glyph(s) is/are modified in-place.

Args:
Expand All @@ -1549,16 +1551,40 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
Returns:
The set of point indices that were dropped if any.

Raises:
ValueError if simple glyphs are not in fact interpolatable because they have
different point flags or number of contours.

Reference:
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
"""
assert len(interpolatable_glyphs) > 0

numContours = None
flags = None
drop = None
for glyph in interpolatable_glyphs:
simple_glyphs = []
for i, glyph in enumerate(interpolatable_glyphs):
if glyph.numberOfContours < 1:
# ignore composite or empty glyphs
continue

if numContours is None:
numContours = glyph.numberOfContours
elif glyph.numberOfContours != numContours:
raise ValueError(
f"Incompatible number of contours for glyph at master index {i}: "
f"expected {numContours}, found {glyph.numberOfContours}"
)

if flags is None:
flags = glyph.flags
elif glyph.flags != flags:
anthrotype marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
f"Incompatible flags for simple glyph at master index {i}: "
f"expected {flags}, found {glyph.flags}"
)

may_drop = set()
start = 0
flags = glyph.flags
coords = glyph.coordinates
for last in glyph.endPtsOfContours:
for i in range(start, last + 1):
Expand All @@ -1583,9 +1609,11 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
else:
drop.intersection_update(may_drop)

simple_glyphs.append(glyph)

if drop:
# Do the actual dropping
for glyph in interpolatable_glyphs:
for glyph in simple_glyphs:
coords = glyph.coordinates
glyph.coordinates = GlyphCoordinates(
coords[i] for i in range(len(coords)) if i not in drop
Expand All @@ -1608,7 +1636,7 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
i += 1
glyph.endPtsOfContours = newEndPts

return drop
return drop if drop is not None else set()


class GlyphComponent(object):
Expand Down
63 changes: 61 additions & 2 deletions Lib/fontTools/varLib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from fontTools.misc.textTools import Tag, tostr
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates, dropImpliedOnCurvePoints
from fontTools.ttLib.tables.ttProgram import Program
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import otTables as ot
Expand All @@ -40,7 +40,7 @@
from fontTools.colorLib.builder import buildColrV1
from fontTools.colorLib.unbuilder import unbuildColrV1
from functools import partial
from collections import OrderedDict, namedtuple
from collections import OrderedDict, defaultdict, namedtuple
import os.path
import logging
from copy import deepcopy
Expand Down Expand Up @@ -965,13 +965,54 @@
font["post"].italicAngle = italicAngle


def drop_implied_oncurve_points(*masters: TTFont) -> int:
"""Drop impliable on-curve points from all the simple glyphs in masters.

In TrueType glyf outlines, on-curve points can be implied when they are located
exactly at the midpoint of the line connecting two consecutive off-curve points.

The input masters' glyf tables are assumed to contain same-named glyphs that are
interpolatable. Oncurve points are only dropped if they can be implied for all
the masters. The fonts are modified in-place.

Args:
masters: The TTFont(s) to modify

Returns:
The total number of points that were dropped if any.

Reference:
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
"""

count = 0
glyph_masters = defaultdict(list)
# multiple DS source may point to the same TTFont object and we want to
# avoid processing the same glyph twice as they are modified in-place
for font in {id(m): m for m in masters}.values():
glyf = font["glyf"]
for glyphName in glyf.keys():
glyph_masters[glyphName].append(glyf[glyphName])
count = 0
for glyphName, glyphs in glyph_masters.items():
try:
dropped = dropImpliedOnCurvePoints(*glyphs)
except ValueError as e:

Check warning on line 1000 in Lib/fontTools/varLib/__init__.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/varLib/__init__.py#L1000

Added line #L1000 was not covered by tests
# we don't fail for incompatible glyphs in _add_gvar so we shouldn't here
log.warning("Failed to drop implied oncurves for %r: %s", glyphName, e)

Check warning on line 1002 in Lib/fontTools/varLib/__init__.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/varLib/__init__.py#L1002

Added line #L1002 was not covered by tests
else:
count += len(dropped)
return count


def build_many(
designspace: DesignSpaceDocument,
master_finder=lambda s: s,
exclude=[],
optimize=True,
skip_vf=lambda vf_name: False,
colr_layer_reuse=True,
drop_implied_oncurves=False,
):
"""
Build variable fonts from a designspace file, version 5 which can define
Expand Down Expand Up @@ -1015,6 +1056,7 @@
exclude=exclude,
optimize=optimize,
colr_layer_reuse=colr_layer_reuse,
drop_implied_oncurves=drop_implied_oncurves,
)[0]
if doBuildStatFromDSv5:
buildVFStatTable(vf, designspace, name)
Expand All @@ -1028,6 +1070,7 @@
exclude=[],
optimize=True,
colr_layer_reuse=True,
drop_implied_oncurves=False,
):
"""
Build variation font from a designspace file.
Expand Down Expand Up @@ -1055,6 +1098,13 @@
except AttributeError:
master_ttfs.append(None) # in-memory fonts have no path

if drop_implied_oncurves and "glyf" in master_fonts[ds.base_idx]:
drop_count = drop_implied_oncurve_points(*master_fonts)
log.info(
"Dropped %s on-curve points from simple glyphs in the 'glyf' table",
drop_count,
)

# Copy the base master to work from it
vf = deepcopy(master_fonts[ds.base_idx])

Expand Down Expand Up @@ -1228,6 +1278,14 @@
action="store_false",
help="do not rebuild variable COLR table to optimize COLR layer reuse",
)
parser.add_argument(
"--drop-implied-oncurves",
action="store_true",
help=(
"drop on-curve points that can be implied when exactly in the middle of "
"two off-curve points (only applies to TrueType fonts)"
),
)
parser.add_argument(
"--master-finder",
default="master_ttf_interpolatable/{stem}.ttf",
Expand Down Expand Up @@ -1312,6 +1370,7 @@
exclude=options.exclude,
optimize=options.optimize,
colr_layer_reuse=options.colr_layer_reuse,
drop_implied_oncurves=options.drop_implied_oncurves,
)

for vf_name, vf in vfs.items():
Expand Down
54 changes: 52 additions & 2 deletions Tests/ttLib/tables/_g_l_y_f_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,13 +860,17 @@ def test_dropImpliedOnCurvePoints_all_quad_off_curves():
],
Transform().scale(2.0),
)
# also add an empty glyph (will be ignored); we use this trick for 'sparse' masters
glyph3 = Glyph()
glyph3.numberOfContours = 0

assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 2, 4, 6}
assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == {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]
assert glyph3.numberOfContours == 0


def test_dropImpliedOnCurvePoints_all_cubic_off_curves():
Expand All @@ -890,8 +894,10 @@ def test_dropImpliedOnCurvePoints_all_cubic_off_curves():
],
Transform().translate(10.0),
)
glyph3 = Glyph()
glyph3.numberOfContours = 0

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

assert glyph1.flags == glyph2.flags == array.array("B", [flagCubic] * 8)
assert glyph1.coordinates == GlyphCoordinates(
Expand All @@ -901,6 +907,7 @@ def test_dropImpliedOnCurvePoints_all_cubic_off_curves():
[(11, 1), (11, 1), (11, -1), (11, -1), (9, -1), (9, -1), (9, 1), (9, 1)]
)
assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [7]
assert glyph3.numberOfContours == 0


def test_dropImpliedOnCurvePoints_not_all_impliable():
Expand Down Expand Up @@ -936,6 +943,49 @@ def test_dropImpliedOnCurvePoints_not_all_impliable():
assert glyph2.flags == array.array("B", [0, flagOnCurve, 0, 0, 0])


def test_dropImpliedOnCurvePoints_all_empty_glyphs():
glyph1 = Glyph()
glyph1.numberOfContours = 0
glyph2 = Glyph()
glyph2.numberOfContours = 0

assert dropImpliedOnCurvePoints(glyph1, glyph2) == set()


def test_dropImpliedOnCurvePoints_incompatible_number_of_contours():
glyph1 = Glyph()
glyph1.numberOfContours = 1
glyph1.endPtsOfContours = [3]
glyph1.flags = array.array("B", [1, 1, 1, 1])
glyph1.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)])

glyph2 = Glyph()
glyph2.numberOfContours = 2
glyph2.endPtsOfContours = [1, 3]
glyph2.flags = array.array("B", [1, 1, 1, 1])
glyph2.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)])

with pytest.raises(ValueError, match="Incompatible number of contours"):
dropImpliedOnCurvePoints(glyph1, glyph2)


def test_dropImpliedOnCurvePoints_incompatible_flags():
glyph1 = Glyph()
glyph1.numberOfContours = 1
glyph1.endPtsOfContours = [3]
glyph1.flags = array.array("B", [1, 1, 1, 1])
glyph1.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)])

glyph2 = Glyph()
glyph2.numberOfContours = 1
glyph2.endPtsOfContours = [3]
glyph2.flags = array.array("B", [0, 0, 0, 0])
glyph2.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)])

with pytest.raises(ValueError, match="Incompatible flags"):
dropImpliedOnCurvePoints(glyph1, glyph2)


if __name__ == "__main__":
import sys

Expand Down
20 changes: 20 additions & 0 deletions Tests/varLib/data/DropOnCurves.designspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version='1.0' encoding='utf-8'?>
<designspace format="3">
<axes>
<axis default="400" maximum="1000" minimum="400" name="weight" tag="wght" />
</axes>
<sources>
<source familyname="Test Family" filename="master_ufo/TestFamily-Master1.ttx" name="master_1" stylename="Master1">
<location>
<dimension name="weight" xvalue="400" />
</location>
</source>
<source familyname="Test Family" filename="master_ufo/TestFamily-Master2.ttx" name="master_2" stylename="Master2">
<location>
<dimension name="weight" xvalue="1000" />
</location>
</source>
</sources>
<instances>
</instances>
</designspace>