xref: /aosp_15_r20/external/fonttools/Lib/fontTools/cu2qu/cli.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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("--version", action="version", version=fontTools.__version__)
71    parser.add_argument(
72        "infiles",
73        nargs="+",
74        metavar="INPUT",
75        help="one or more input UFO source file(s).",
76    )
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    )
86    parser.add_argument(
87        "-m",
88        "--mixed",
89        default=False,
90        action="store_true",
91        help="whether to used mixed quadratic and cubic curves",
92    )
93    parser.add_argument(
94        "--keep-direction",
95        dest="reverse_direction",
96        action="store_false",
97        help="do not reverse the contour direction",
98    )
99
100    mode_parser = parser.add_mutually_exclusive_group()
101    mode_parser.add_argument(
102        "-i",
103        "--interpolatable",
104        action="store_true",
105        help="whether curve conversion should keep interpolation compatibility",
106    )
107    mode_parser.add_argument(
108        "-j",
109        "--jobs",
110        type=int,
111        nargs="?",
112        default=1,
113        const=_cpu_count(),
114        metavar="N",
115        help="Convert using N multiple processes (default: %(default)s)",
116    )
117
118    output_parser = parser.add_mutually_exclusive_group()
119    output_parser.add_argument(
120        "-o",
121        "--output-file",
122        default=None,
123        metavar="OUTPUT",
124        help=(
125            "output filename for the converted UFO. By default fonts are "
126            "modified in place. This only works with a single input."
127        ),
128    )
129    output_parser.add_argument(
130        "-d",
131        "--output-dir",
132        default=None,
133        metavar="DIRECTORY",
134        help="output directory where to save converted UFOs",
135    )
136
137    options = parser.parse_args(args)
138
139    if ufo_module is None:
140        parser.error("Either ufoLib2 or defcon are required to run this script.")
141
142    if not options.verbose:
143        level = "WARNING"
144    elif options.verbose == 1:
145        level = "INFO"
146    else:
147        level = "DEBUG"
148    logging.basicConfig(level=level)
149
150    if len(options.infiles) > 1 and options.output_file:
151        parser.error("-o/--output-file can't be used with multile inputs")
152
153    if options.output_dir:
154        output_dir = options.output_dir
155        if not os.path.exists(output_dir):
156            os.mkdir(output_dir)
157        elif not os.path.isdir(output_dir):
158            parser.error("'%s' is not a directory" % output_dir)
159        output_paths = [
160            os.path.join(output_dir, os.path.basename(p)) for p in options.infiles
161        ]
162    elif options.output_file:
163        output_paths = [options.output_file]
164    else:
165        # save in-place
166        output_paths = [None] * len(options.infiles)
167
168    kwargs = dict(
169        dump_stats=options.verbose > 0,
170        max_err_em=options.conversion_error,
171        reverse_direction=options.reverse_direction,
172        all_quadratic=False if options.mixed else True,
173    )
174
175    if options.interpolatable:
176        logger.info("Converting curves compatibly")
177        ufos = [open_ufo(infile) for infile in options.infiles]
178        if fonts_to_quadratic(ufos, **kwargs):
179            for ufo, output_path in zip(ufos, output_paths):
180                logger.info("Saving %s", output_path)
181                if output_path:
182                    ufo.save(output_path)
183                else:
184                    ufo.save()
185        else:
186            for input_path, output_path in zip(options.infiles, output_paths):
187                if output_path:
188                    _copytree(input_path, output_path)
189    else:
190        jobs = min(len(options.infiles), options.jobs) if options.jobs > 1 else 1
191        if jobs > 1:
192            func = partial(_font_to_quadratic, **kwargs)
193            logger.info("Running %d parallel processes", jobs)
194            with closing(mp.Pool(jobs)) as pool:
195                pool.starmap(func, zip(options.infiles, output_paths))
196        else:
197            for input_path, output_path in zip(options.infiles, output_paths):
198                _font_to_quadratic(input_path, output_path, **kwargs)
199