cli.py 5.9 KB

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