Merge pull request #126 from anthrotype/cli
skip converting twice; add 'cu2qu' console script
This commit is contained in:
commit
b072f0ab5e
6
Lib/cu2qu/__main__.py
Normal file
6
Lib/cu2qu/__main__.py
Normal 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
139
Lib/cu2qu/cli.py
Normal 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)
|
@ -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
|
||||
|
||||
|
||||
|
2
setup.py
2
setup.py
@ -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
76
tests/cli_test.py
Normal 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"]))
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user