Merge pull request #126 from anthrotype/cli

skip converting twice; add 'cu2qu' console script
This commit is contained in:
Cosimo Lupo 2018-04-10 22:06:54 +02:00 committed by GitHub
commit b072f0ab5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 265 additions and 1 deletions

6
Lib/cu2qu/__main__.py Normal file
View File

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

139
Lib/cu2qu/cli.py Normal file
View File

@ -0,0 +1,139 @@
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)
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_paths = [
os.path.join(options.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)
if options.interpolatable:
logger.info('Converting curves compatibly')
ufos = [defcon.Font(infile) for infile in options.infiles]
if fonts_to_quadratic(ufos, dump_stats=True):
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, dump_stats=False)
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, dump_stats=True)

View File

@ -40,6 +40,7 @@ from cu2qu.errors import (
__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__)
@ -208,7 +209,7 @@ def glyphs_to_quadratic(
def fonts_to_quadratic(
fonts, max_err_em=None, max_err=None, reverse_direction=False,
stats=None, dump_stats=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
@ -217,10 +218,30 @@ def fonts_to_quadratic(
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 = {}
@ -265,6 +286,13 @@ def fonts_to_quadratic(
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

View File

@ -179,6 +179,8 @@ setup(
"fonttools>=3.18.0",
"ufoLib>=2.1.1",
],
extras_require={"cli": ["defcon>=0.4.0"]},
entry_points={"console_scripts": ["cu2qu = cu2qu.cli:main [cli]"]},
cmdclass={
"release": release,
"bump_version": bump_version,

76
tests/cli_test.py Normal file
View File

@ -0,0 +1,76 @@
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"]))

View File

@ -9,6 +9,7 @@ from cu2qu.ufo import (
glyphs_to_quadratic,
glyph_to_quadratic,
logger,
CURVE_TYPE_LIB_KEY,
)
from cu2qu.errors import (
IncompatibleSegmentNumberError,
@ -47,6 +48,18 @@ class FontsToQuadraticTest(object):
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]