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