Merge remote-tracking branch 'cu2qu-origin/fonttools-merge' into cu2qu

This commit is contained in:
Cosimo Lupo 2020-04-01 18:45:11 +01:00
commit b8f1f7bf5e
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
5 changed files with 861 additions and 0 deletions

View File

@ -0,0 +1,6 @@
import sys
from cu2qu.cli import main
if __name__ == "__main__":
sys.exit(main())

161
fonttools-merge/cli.py Normal file
View File

@ -0,0 +1,161 @@
from __future__ import print_function, division, absolute_import
import os
import argparse
import logging
import shutil
import multiprocessing as mp
from contextlib import closing
from functools import partial
import cu2qu
from cu2qu.ufo import font_to_quadratic, fonts_to_quadratic
import defcon
logger = logging.getLogger("cu2qu")
def _cpu_count():
try:
return mp.cpu_count()
except NotImplementedError: # pragma: no cover
return 1
def _font_to_quadratic(zipped_paths, **kwargs):
input_path, output_path = zipped_paths
ufo = defcon.Font(input_path)
logger.info('Converting curves for %s', input_path)
if font_to_quadratic(ufo, **kwargs):
logger.info("Saving %s", output_path)
ufo.save(output_path)
else:
_copytree(input_path, output_path)
def _samepath(path1, path2):
# TODO on python3+, there's os.path.samefile
path1 = os.path.normcase(os.path.abspath(os.path.realpath(path1)))
path2 = os.path.normcase(os.path.abspath(os.path.realpath(path2)))
return path1 == path2
def _copytree(input_path, output_path):
if _samepath(input_path, output_path):
logger.debug("input and output paths are the same file; skipped copy")
return
if os.path.exists(output_path):
shutil.rmtree(output_path)
shutil.copytree(input_path, output_path)
def main(args=None):
parser = argparse.ArgumentParser(prog="cu2qu")
parser.add_argument(
"--version", action="version", version=cu2qu.__version__)
parser.add_argument(
"infiles",
nargs="+",
metavar="INPUT",
help="one or more input UFO source file(s).")
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument(
"-e",
"--conversion-error",
type=float,
metavar="ERROR",
default=None,
help="maxiumum approximation error measured in EM (default: 0.001)")
parser.add_argument(
"--keep-direction",
dest="reverse_direction",
action="store_false",
help="do not reverse the contour direction")
mode_parser = parser.add_mutually_exclusive_group()
mode_parser.add_argument(
"-i",
"--interpolatable",
action="store_true",
help="whether curve conversion should keep interpolation compatibility"
)
mode_parser.add_argument(
"-j",
"--jobs",
type=int,
nargs="?",
default=1,
const=_cpu_count(),
metavar="N",
help="Convert using N multiple processes (default: %(default)s)")
output_parser = parser.add_mutually_exclusive_group()
output_parser.add_argument(
"-o",
"--output-file",
default=None,
metavar="OUTPUT",
help=("output filename for the converted UFO. By default fonts are "
"modified in place. This only works with a single input."))
output_parser.add_argument(
"-d",
"--output-dir",
default=None,
metavar="DIRECTORY",
help="output directory where to save converted UFOs")
options = parser.parse_args(args)
if not options.verbose:
level = "WARNING"
elif options.verbose == 1:
level = "INFO"
else:
level = "DEBUG"
logging.basicConfig(level=level)
if len(options.infiles) > 1 and options.output_file:
parser.error("-o/--output-file can't be used with multile inputs")
if options.output_dir:
output_dir = options.output_dir
if not os.path.exists(output_dir):
os.mkdir(output_dir)
elif not os.path.isdir(output_dir):
parser.error("'%s' is not a directory" % output_dir)
output_paths = [
os.path.join(output_dir, os.path.basename(p))
for p in options.infiles
]
elif options.output_file:
output_paths = [options.output_file]
else:
# save in-place
output_paths = list(options.infiles)
kwargs = dict(dump_stats=options.verbose > 0,
max_err_em=options.conversion_error,
reverse_direction=options.reverse_direction)
if options.interpolatable:
logger.info('Converting curves compatibly')
ufos = [defcon.Font(infile) for infile in options.infiles]
if fonts_to_quadratic(ufos, **kwargs):
for ufo, output_path in zip(ufos, output_paths):
logger.info("Saving %s", output_path)
ufo.save(output_path)
else:
for input_path, output_path in zip(options.infiles, output_paths):
_copytree(input_path, output_path)
else:
jobs = min(len(options.infiles),
options.jobs) if options.jobs > 1 else 1
if jobs > 1:
func = partial(_font_to_quadratic, **kwargs)
logger.info('Running %d parallel processes', jobs)
with closing(mp.Pool(jobs)) as pool:
# can't use Pool.starmap as it's 3.3+ only
pool.map(func, zip(options.infiles, output_paths))
else:
for paths in zip(options.infiles, output_paths):
_font_to_quadratic(paths, **kwargs)

View File

@ -0,0 +1,85 @@
from __future__ import print_function, division, absolute_import
import defcon
from . import DATADIR
import pytest
import py
from cu2qu.ufo import CURVE_TYPE_LIB_KEY
from cu2qu.cli import main
TEST_UFOS = [
py.path.local(DATADIR).join("RobotoSubset-Regular.ufo"),
py.path.local(DATADIR).join("RobotoSubset-Bold.ufo"),
]
@pytest.fixture
def test_paths(tmpdir):
result = []
for path in TEST_UFOS:
new_path = tmpdir / path.basename
path.copy(new_path)
result.append(new_path)
return result
class MainTest(object):
@staticmethod
def run_main(*args):
main([str(p) for p in args if p])
def test_single_input_no_output(self, test_paths):
ufo_path = test_paths[0]
self.run_main(ufo_path)
font = defcon.Font(str(ufo_path))
assert font.lib[CURVE_TYPE_LIB_KEY] == "quadratic"
def test_single_input_output_file(self, tmpdir):
input_path = TEST_UFOS[0]
output_path = tmpdir / input_path.basename
self.run_main('-o', output_path, input_path)
assert output_path.check(dir=1)
def test_multiple_inputs_output_dir(self, tmpdir):
output_dir = tmpdir / "output_dir"
self.run_main('-d', output_dir, *TEST_UFOS)
assert output_dir.check(dir=1)
outputs = set(p.basename for p in output_dir.listdir())
assert "RobotoSubset-Regular.ufo" in outputs
assert "RobotoSubset-Bold.ufo" in outputs
def test_interpolatable_inplace(self, test_paths):
self.run_main('-i', *test_paths)
self.run_main('-i', *test_paths) # idempotent
@pytest.mark.parametrize(
"mode", ["", "-i"], ids=["normal", "interpolatable"])
def test_copytree(self, mode, tmpdir):
output_dir = tmpdir / "output_dir"
self.run_main(mode, '-d', output_dir, *TEST_UFOS)
output_dir_2 = tmpdir / "output_dir_2"
# no conversion when curves are already quadratic, just copy
self.run_main(mode, '-d', output_dir_2, *output_dir.listdir())
# running again overwrites existing with the copy
self.run_main(mode, '-d', output_dir_2, *output_dir.listdir())
def test_multiprocessing(self, tmpdir, test_paths):
self.run_main(*(test_paths + ["-j"]))
def test_keep_direction(self, test_paths):
self.run_main('--keep-direction', *test_paths)
def test_conversion_error(self, test_paths):
self.run_main('--conversion-error', 0.002, *test_paths)
def test_conversion_error_short(self, test_paths):
self.run_main('-e', 0.003, test_paths[0])

324
fonttools-merge/ufo.py Normal file
View File

@ -0,0 +1,324 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Converts cubic bezier curves to quadratic splines.
Conversion is performed such that the quadratic splines keep the same end-curve
tangents as the original cubics. The approach is iterative, increasing the
number of segments for a spline until the error gets below a bound.
Respective curves from multiple fonts will be converted at once to ensure that
the resulting splines are interpolation-compatible.
"""
from __future__ import print_function, division, absolute_import
import logging
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import PointToSegmentPen
from fontTools.pens.reverseContourPen import ReverseContourPen
from cu2qu import curves_to_quadratic
from cu2qu.errors import (
UnequalZipLengthsError, IncompatibleSegmentNumberError,
IncompatibleSegmentTypesError, IncompatibleGlyphsError,
IncompatibleFontsError)
__all__ = ['fonts_to_quadratic', 'font_to_quadratic']
DEFAULT_MAX_ERR = 0.001
CURVE_TYPE_LIB_KEY = "com.github.googlei18n.cu2qu.curve_type"
logger = logging.getLogger(__name__)
_zip = zip
def zip(*args):
"""Ensure each argument to zip has the same length. Also make sure a list is
returned for python 2/3 compatibility.
"""
if len(set(len(a) for a in args)) != 1:
raise UnequalZipLengthsError(*args)
return list(_zip(*args))
class GetSegmentsPen(AbstractPen):
"""Pen to collect segments into lists of points for conversion.
Curves always include their initial on-curve point, so some points are
duplicated between segments.
"""
def __init__(self):
self._last_pt = None
self.segments = []
def _add_segment(self, tag, *args):
if tag in ['move', 'line', 'qcurve', 'curve']:
self._last_pt = args[-1]
self.segments.append((tag, args))
def moveTo(self, pt):
self._add_segment('move', pt)
def lineTo(self, pt):
self._add_segment('line', pt)
def qCurveTo(self, *points):
self._add_segment('qcurve', self._last_pt, *points)
def curveTo(self, *points):
self._add_segment('curve', self._last_pt, *points)
def closePath(self):
self._add_segment('close')
def endPath(self):
self._add_segment('end')
def addComponent(self, glyphName, transformation):
pass
def _get_segments(glyph):
"""Get a glyph's segments as extracted by GetSegmentsPen."""
pen = GetSegmentsPen()
# glyph.draw(pen)
# We can't simply draw the glyph with the pen, but we must initialize the
# PointToSegmentPen explicitly with outputImpliedClosingLine=True.
# By default PointToSegmentPen does not outputImpliedClosingLine -- unless
# last and first point on closed contour are duplicated. Because we are
# converting multiple glyphs at the same time, we want to make sure
# this function returns the same number of segments, whether or not
# the last and first point overlap.
# https://github.com/googlefonts/fontmake/issues/572
# https://github.com/fonttools/fonttools/pull/1720
pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=True)
glyph.drawPoints(pointPen)
return pen.segments
def _set_segments(glyph, segments, reverse_direction):
"""Draw segments as extracted by GetSegmentsPen back to a glyph."""
glyph.clearContours()
pen = glyph.getPen()
if reverse_direction:
pen = ReverseContourPen(pen)
for tag, args in segments:
if tag == 'move':
pen.moveTo(*args)
elif tag == 'line':
pen.lineTo(*args)
elif tag == 'curve':
pen.curveTo(*args[1:])
elif tag == 'qcurve':
pen.qCurveTo(*args[1:])
elif tag == 'close':
pen.closePath()
elif tag == 'end':
pen.endPath()
else:
raise AssertionError('Unhandled segment type "%s"' % tag)
def _segments_to_quadratic(segments, max_err, stats):
"""Return quadratic approximations of cubic segments."""
assert all(s[0] == 'curve' for s in segments), 'Non-cubic given to convert'
new_points = curves_to_quadratic([s[1] for s in segments], max_err)
n = len(new_points[0])
assert all(len(s) == n for s in new_points[1:]), 'Converted incompatibly'
spline_length = str(n - 2)
stats[spline_length] = stats.get(spline_length, 0) + 1
return [('qcurve', p) for p in new_points]
def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats):
"""Do the actual conversion of a set of compatible glyphs, after arguments
have been set up.
Return True if the glyphs were modified, else return False.
"""
try:
segments_by_location = zip(*[_get_segments(g) for g in glyphs])
except UnequalZipLengthsError:
raise IncompatibleSegmentNumberError(glyphs)
if not any(segments_by_location):
return False
# always modify input glyphs if reverse_direction is True
glyphs_modified = reverse_direction
new_segments_by_location = []
incompatible = {}
for i, segments in enumerate(segments_by_location):
tag = segments[0][0]
if not all(s[0] == tag for s in segments[1:]):
incompatible[i] = [s[0] for s in segments]
elif tag == 'curve':
segments = _segments_to_quadratic(segments, max_err, stats)
glyphs_modified = True
new_segments_by_location.append(segments)
if glyphs_modified:
new_segments_by_glyph = zip(*new_segments_by_location)
for glyph, new_segments in zip(glyphs, new_segments_by_glyph):
_set_segments(glyph, new_segments, reverse_direction)
if incompatible:
raise IncompatibleSegmentTypesError(glyphs, segments=incompatible)
return glyphs_modified
def glyphs_to_quadratic(
glyphs, max_err=None, reverse_direction=False, stats=None):
"""Convert the curves of a set of compatible of glyphs to quadratic.
All curves will be converted to quadratic at once, ensuring interpolation
compatibility. If this is not required, calling glyphs_to_quadratic with one
glyph at a time may yield slightly more optimized results.
Return True if glyphs were modified, else return False.
Raises IncompatibleGlyphsError if glyphs have non-interpolatable outlines.
"""
if stats is None:
stats = {}
if not max_err:
# assume 1000 is the default UPEM
max_err = DEFAULT_MAX_ERR * 1000
if isinstance(max_err, (list, tuple)):
max_errors = max_err
else:
max_errors = [max_err] * len(glyphs)
assert len(max_errors) == len(glyphs)
return _glyphs_to_quadratic(glyphs, max_errors, reverse_direction, stats)
def fonts_to_quadratic(
fonts, max_err_em=None, max_err=None, reverse_direction=False,
stats=None, dump_stats=False, remember_curve_type=True):
"""Convert the curves of a collection of fonts to quadratic.
All curves will be converted to quadratic at once, ensuring interpolation
compatibility. If this is not required, calling fonts_to_quadratic with one
font at a time may yield slightly more optimized results.
Return True if fonts were modified, else return False.
By default, cu2qu stores the curve type in the fonts' lib, under a private
key "com.github.googlei18n.cu2qu.curve_type", and will not try to convert
them again if the curve type is already set to "quadratic".
Setting 'remember_curve_type' to False disables this optimization.
Raises IncompatibleFontsError if same-named glyphs from different fonts
have non-interpolatable outlines.
"""
if remember_curve_type:
curve_types = {f.lib.get(CURVE_TYPE_LIB_KEY, "cubic") for f in fonts}
if len(curve_types) == 1:
curve_type = next(iter(curve_types))
if curve_type == "quadratic":
logger.info("Curves already converted to quadratic")
return False
elif curve_type == "cubic":
pass # keep converting
else:
raise NotImplementedError(curve_type)
elif len(curve_types) > 1:
# going to crash later if they do differ
logger.warning("fonts may contain different curve types")
if stats is None:
stats = {}
if max_err_em and max_err:
raise TypeError('Only one of max_err and max_err_em can be specified.')
if not (max_err_em or max_err):
max_err_em = DEFAULT_MAX_ERR
if isinstance(max_err, (list, tuple)):
assert len(max_err) == len(fonts)
max_errors = max_err
elif max_err:
max_errors = [max_err] * len(fonts)
if isinstance(max_err_em, (list, tuple)):
assert len(fonts) == len(max_err_em)
max_errors = [f.info.unitsPerEm * e
for f, e in zip(fonts, max_err_em)]
elif max_err_em:
max_errors = [f.info.unitsPerEm * max_err_em for f in fonts]
modified = False
glyph_errors = {}
for name in set().union(*(f.keys() for f in fonts)):
glyphs = []
cur_max_errors = []
for font, error in zip(fonts, max_errors):
if name in font:
glyphs.append(font[name])
cur_max_errors.append(error)
try:
modified |= _glyphs_to_quadratic(
glyphs, cur_max_errors, reverse_direction, stats)
except IncompatibleGlyphsError as exc:
logger.error(exc)
glyph_errors[name] = exc
if glyph_errors:
raise IncompatibleFontsError(glyph_errors)
if modified and dump_stats:
spline_lengths = sorted(stats.keys())
logger.info('New spline lengths: %s' % (', '.join(
'%s: %d' % (l, stats[l]) for l in spline_lengths)))
if remember_curve_type:
for font in fonts:
curve_type = font.lib.get(CURVE_TYPE_LIB_KEY, "cubic")
if curve_type != "quadratic":
font.lib[CURVE_TYPE_LIB_KEY] = "quadratic"
modified = True
return modified
def glyph_to_quadratic(glyph, **kwargs):
"""Convenience wrapper around glyphs_to_quadratic, for just one glyph.
Return True if the glyph was modified, else return False.
"""
return glyphs_to_quadratic([glyph], **kwargs)
def font_to_quadratic(font, **kwargs):
"""Convenience wrapper around fonts_to_quadratic, for just one font.
Return True if the font was modified, else return False.
"""
return fonts_to_quadratic([font], **kwargs)

285
fonttools-merge/ufo_test.py Normal file
View File

@ -0,0 +1,285 @@
from __future__ import print_function, division, absolute_import
import os
from fontTools.misc.loggingTools import CapturingLogHandler
from defcon import Font, Glyph
from cu2qu.ufo import (
fonts_to_quadratic,
font_to_quadratic,
glyphs_to_quadratic,
glyph_to_quadratic,
logger,
CURVE_TYPE_LIB_KEY,
)
from cu2qu.errors import (
IncompatibleSegmentNumberError,
IncompatibleSegmentTypesError,
IncompatibleFontsError,
)
from . import DATADIR
import pytest
TEST_UFOS = [
os.path.join(DATADIR, "RobotoSubset-Regular.ufo"),
os.path.join(DATADIR, "RobotoSubset-Bold.ufo"),
]
@pytest.fixture
def fonts():
return [Font(ufo) for ufo in TEST_UFOS]
class FontsToQuadraticTest(object):
def test_modified(self, fonts):
modified = fonts_to_quadratic(fonts)
assert modified
def test_stats(self, fonts):
stats = {}
fonts_to_quadratic(fonts, stats=stats)
assert stats == {'1': 1, '2': 79, '3': 130, '4': 2}
def test_dump_stats(self, fonts):
with CapturingLogHandler(logger, "INFO") as captor:
fonts_to_quadratic(fonts, dump_stats=True)
assert captor.assertRegex("New spline lengths:")
def test_remember_curve_type(self, fonts):
fonts_to_quadratic(fonts, remember_curve_type=True)
assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "quadratic"
with CapturingLogHandler(logger, "INFO") as captor:
fonts_to_quadratic(fonts, remember_curve_type=True)
assert captor.assertRegex("already converted")
def test_no_remember_curve_type(self, fonts):
assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
fonts_to_quadratic(fonts, remember_curve_type=False)
assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
def test_different_glyphsets(self, fonts):
del fonts[0]['a']
assert 'a' not in fonts[0]
assert 'a' in fonts[1]
assert fonts_to_quadratic(fonts)
def test_max_err_em_float(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err_em=0.002, stats=stats)
assert stats == {'1': 5, '2': 193, '3': 14}
def test_max_err_em_list(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err_em=[0.002, 0.002], stats=stats)
assert stats == {'1': 5, '2': 193, '3': 14}
def test_max_err_float(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err=4.096, stats=stats)
assert stats == {'1': 5, '2': 193, '3': 14}
def test_max_err_list(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err=[4.096, 4.096], stats=stats)
assert stats == {'1': 5, '2': 193, '3': 14}
def test_both_max_err_and_max_err_em(self, fonts):
with pytest.raises(TypeError, match="Only one .* can be specified"):
fonts_to_quadratic(fonts, max_err=1.000, max_err_em=0.001)
def test_single_font(self, fonts):
assert font_to_quadratic(fonts[0], max_err_em=0.002,
reverse_direction=True)
class GlyphsToQuadraticTest(object):
@pytest.mark.parametrize(
["glyph", "expected"],
[('A', False), # contains no curves, it is not modified
('a', True)],
ids=['lines-only', 'has-curves']
)
def test_modified(self, fonts, glyph, expected):
glyphs = [f[glyph] for f in fonts]
assert glyphs_to_quadratic(glyphs) == expected
def test_stats(self, fonts):
stats = {}
glyphs_to_quadratic([f['a'] for f in fonts], stats=stats)
assert stats == {'2': 1, '3': 7, '4': 3, '5': 1}
def test_max_err_float(self, fonts):
glyphs = [f['a'] for f in fonts]
stats = {}
glyphs_to_quadratic(glyphs, max_err=4.096, stats=stats)
assert stats == {'2': 11, '3': 1}
def test_max_err_list(self, fonts):
glyphs = [f['a'] for f in fonts]
stats = {}
glyphs_to_quadratic(glyphs, max_err=[4.096, 4.096], stats=stats)
assert stats == {'2': 11, '3': 1}
def test_reverse_direction(self, fonts):
glyphs = [f['A'] for f in fonts]
assert glyphs_to_quadratic(glyphs, reverse_direction=True)
def test_single_glyph(self, fonts):
assert glyph_to_quadratic(fonts[0]['a'], max_err=4.096,
reverse_direction=True)
@pytest.mark.parametrize(
["outlines", "exception", "message"],
[
[
[
[
('moveTo', ((0, 0),)),
('curveTo', ((1, 1), (2, 2), (3, 3))),
('curveTo', ((4, 4), (5, 5), (6, 6))),
('closePath', ()),
],
[
('moveTo', ((7, 7),)),
('curveTo', ((8, 8), (9, 9), (10, 10))),
('closePath', ()),
]
],
IncompatibleSegmentNumberError,
"have different number of segments",
],
[
[
[
('moveTo', ((0, 0),)),
('curveTo', ((1, 1), (2, 2), (3, 3))),
('closePath', ()),
],
[
('moveTo', ((4, 4),)),
('lineTo', ((5, 5),)),
('closePath', ()),
],
],
IncompatibleSegmentTypesError,
"have incompatible segment types",
],
],
ids=[
"unequal-length",
"different-segment-types",
]
)
def test_incompatible_glyphs(self, outlines, exception, message):
glyphs = []
for i, outline in enumerate(outlines):
glyph = Glyph()
glyph.name = "glyph%d" % i
pen = glyph.getPen()
for operator, args in outline:
getattr(pen, operator)(*args)
glyphs.append(glyph)
with pytest.raises(exception) as excinfo:
glyphs_to_quadratic(glyphs)
assert excinfo.match(message)
def test_incompatible_fonts(self):
font1 = Font()
font1.info.unitsPerEm = 1000
glyph1 = font1.newGlyph("a")
pen1 = glyph1.getPen()
for operator, args in [("moveTo", ((0, 0),)),
("lineTo", ((1, 1),)),
("endPath", ())]:
getattr(pen1, operator)(*args)
font2 = Font()
font2.info.unitsPerEm = 1000
glyph2 = font2.newGlyph("a")
pen2 = glyph2.getPen()
for operator, args in [("moveTo", ((0, 0),)),
("curveTo", ((1, 1), (2, 2), (3, 3))),
("endPath", ())]:
getattr(pen2, operator)(*args)
with pytest.raises(IncompatibleFontsError) as excinfo:
fonts_to_quadratic([font1, font2])
assert excinfo.match("fonts contains incompatible glyphs: 'a'")
assert hasattr(excinfo.value, "glyph_errors")
error = excinfo.value.glyph_errors['a']
assert isinstance(error, IncompatibleSegmentTypesError)
assert error.segments == {1: ["line", "curve"]}
def test_already_quadratic(self):
glyph = Glyph()
pen = glyph.getPen()
pen.moveTo((0, 0))
pen.qCurveTo((1, 1), (2, 2))
pen.closePath()
assert not glyph_to_quadratic(glyph)
def test_open_paths(self):
glyph = Glyph()
pen = glyph.getPen()
pen.moveTo((0, 0))
pen.lineTo((1, 1))
pen.curveTo((2, 2), (3, 3), (4, 4))
pen.endPath()
assert glyph_to_quadratic(glyph)
# open contour is still open
assert glyph[-1][0].segmentType == "move"
def test_ignore_components(self):
glyph = Glyph()
pen = glyph.getPen()
pen.addComponent('a', (1, 0, 0, 1, 0, 0))
pen.moveTo((0, 0))
pen.curveTo((1, 1), (2, 2), (3, 3))
pen.closePath()
assert glyph_to_quadratic(glyph)
assert len(glyph.components) == 1
def test_overlapping_start_end_points(self):
# https://github.com/googlefonts/fontmake/issues/572
glyph1 = Glyph()
pen = glyph1.getPointPen()
pen.beginPath()
pen.addPoint((0, 651), segmentType="line")
pen.addPoint((0, 101), segmentType="line")
pen.addPoint((0, 101), segmentType="line")
pen.addPoint((0, 651), segmentType="line")
pen.endPath()
glyph2 = Glyph()
pen = glyph2.getPointPen()
pen.beginPath()
pen.addPoint((1, 651), segmentType="line")
pen.addPoint((2, 101), segmentType="line")
pen.addPoint((3, 101), segmentType="line")
pen.addPoint((4, 651), segmentType="line")
pen.endPath()
glyphs = [glyph1, glyph2]
assert glyphs_to_quadratic(glyphs, reverse_direction=True)
assert [[(p.x, p.y) for p in glyph[0]] for glyph in glyphs] == [
[
(0, 651),
(0, 651),
(0, 101),
(0, 101),
],
[
(1, 651),
(4, 651),
(3, 101),
(2, 101)
],
]