1import os
2import argparse
3import logging
4import shutil
5import multiprocessing as mp
6from contextlib import closing
7from functools import partial
8
9import fontTools
10from .ufo import font_to_quadratic, fonts_to_quadratic
11
12ufo_module = None
13try:
14    import ufoLib2 as ufo_module
15except ImportError:
16    try:
17        import defcon as ufo_module
18    except ImportError as e:
19        pass
20
21
22logger = logging.getLogger("fontTools.cu2qu")
23
24
25def _cpu_count():
26    try:
27        return mp.cpu_count()
28    except NotImplementedError:  # pragma: no cover
29        return 1
30
31
32def _font_to_quadratic(input_path, output_path=None, **kwargs):
33    ufo = ufo_module.Font(input_path)
34    logger.info('Converting curves for %s', input_path)
35    if font_to_quadratic(ufo, **kwargs):
36        logger.info("Saving %s", output_path)
37        if output_path:
38            ufo.save(output_path)
39        else:
40            ufo.save()  # save in-place
41    elif output_path:
42        _copytree(input_path, output_path)
43
44
45def _samepath(path1, path2):
46    # TODO on python3+, there's os.path.samefile
47    path1 = os.path.normcase(os.path.abspath(os.path.realpath(path1)))
48    path2 = os.path.normcase(os.path.abspath(os.path.realpath(path2)))
49    return path1 == path2
50
51
52def _copytree(input_path, output_path):
53    if _samepath(input_path, output_path):
54        logger.debug("input and output paths are the same file; skipped copy")
55        return
56    if os.path.exists(output_path):
57        shutil.rmtree(output_path)
58    shutil.copytree(input_path, output_path)
59
60
61def main(args=None):
62    """Convert a UFO font from cubic to quadratic curves"""
63    parser = argparse.ArgumentParser(prog="cu2qu")
64    parser.add_argument(
65        "--version", action="version", version=fontTools.__version__)
66    parser.add_argument(
67        "infiles",
68        nargs="+",
69        metavar="INPUT",
70        help="one or more input UFO source file(s).")
71    parser.add_argument("-v", "--verbose", action="count", default=0)
72    parser.add_argument(
73        "-e",
74        "--conversion-error",
75        type=float,
76        metavar="ERROR",
77        default=None,
78        help="maxiumum approximation error measured in EM (default: 0.001)")
79    parser.add_argument(
80        "--keep-direction",
81        dest="reverse_direction",
82        action="store_false",
83        help="do not reverse the contour direction")
84
85    mode_parser = parser.add_mutually_exclusive_group()
86    mode_parser.add_argument(
87        "-i",
88        "--interpolatable",
89        action="store_true",
90        help="whether curve conversion should keep interpolation compatibility"
91    )
92    mode_parser.add_argument(
93        "-j",
94        "--jobs",
95        type=int,
96        nargs="?",
97        default=1,
98        const=_cpu_count(),
99        metavar="N",
100        help="Convert using N multiple processes (default: %(default)s)")
101
102    output_parser = parser.add_mutually_exclusive_group()
103    output_parser.add_argument(
104        "-o",
105        "--output-file",
106        default=None,
107        metavar="OUTPUT",
108        help=("output filename for the converted UFO. By default fonts are "
109              "modified in place. This only works with a single input."))
110    output_parser.add_argument(
111        "-d",
112        "--output-dir",
113        default=None,
114        metavar="DIRECTORY",
115        help="output directory where to save converted UFOs")
116
117    options = parser.parse_args(args)
118
119    if ufo_module is None:
120        parser.error("Either ufoLib2 or defcon are required to run this script.")
121
122    if not options.verbose:
123        level = "WARNING"
124    elif options.verbose == 1:
125        level = "INFO"
126    else:
127        level = "DEBUG"
128    logging.basicConfig(level=level)
129
130    if len(options.infiles) > 1 and options.output_file:
131        parser.error("-o/--output-file can't be used with multile inputs")
132
133    if options.output_dir:
134        output_dir = options.output_dir
135        if not os.path.exists(output_dir):
136            os.mkdir(output_dir)
137        elif not os.path.isdir(output_dir):
138            parser.error("'%s' is not a directory" % output_dir)
139        output_paths = [
140            os.path.join(output_dir, os.path.basename(p))
141            for p in options.infiles
142        ]
143    elif options.output_file:
144        output_paths = [options.output_file]
145    else:
146        # save in-place
147        output_paths = [None] * len(options.infiles)
148
149    kwargs = dict(dump_stats=options.verbose > 0,
150                  max_err_em=options.conversion_error,
151                  reverse_direction=options.reverse_direction)
152
153    if options.interpolatable:
154        logger.info('Converting curves compatibly')
155        ufos = [ufo_module.Font(infile) for infile in options.infiles]
156        if fonts_to_quadratic(ufos, **kwargs):
157            for ufo, output_path in zip(ufos, output_paths):
158                logger.info("Saving %s", output_path)
159                if output_path:
160                    ufo.save(output_path)
161                else:
162                    ufo.save()
163        else:
164            for input_path, output_path in zip(options.infiles, output_paths):
165                if output_path:
166                    _copytree(input_path, output_path)
167    else:
168        jobs = min(len(options.infiles),
169                   options.jobs) if options.jobs > 1 else 1
170        if jobs > 1:
171            func = partial(_font_to_quadratic, **kwargs)
172            logger.info('Running %d parallel processes', jobs)
173            with closing(mp.Pool(jobs)) as pool:
174                pool.starmap(func, zip(options.infiles, output_paths))
175        else:
176            for input_path, output_path in zip(options.infiles, output_paths):
177                _font_to_quadratic(input_path, output_path, **kwargs)
178