orxport.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/env python3
  2. import getopt
  3. import os
  4. import sys
  5. import emoji
  6. import export
  7. import jsonutils
  8. import log
  9. import orx.manifest
  10. import orx.params
  11. from cache import Cache
  12. VERSION = '0.5.0'
  13. RENDERERS = ['inkscape', 'resvg', 'imagemagick']
  14. DEF_INPUT = 'in'
  15. DEF_MANIFEST = 'manifest.orx'
  16. DEF_OUTPUT = 'out'
  17. DEF_OUTPUT_NAMING = '%f/%s'
  18. DEF_OUTPUT_FORMATS = ['svg']
  19. DEF_LICENSE_ENABLED = True
  20. DEF_PARAMS = 'parameters.orx'
  21. DEF_NUM_THREADS = 1
  22. DEF_RENDERER = 'inkscape'
  23. DEF_MAX_BATCH = 1000
  24. HELP = f'''orxporter {VERSION}
  25. by Mutant Standard
  26. (mutant.tech)
  27. USAGE: orxport.py [options...]
  28. HELP:
  29. ----------------------------------------------------
  30. -h Prints this help message.
  31. Also look at /docs for full documentation.
  32. INPUT AND OUTPUT:
  33. ----------------------------------------------------
  34. -i Input images (default: {DEF_INPUT})
  35. -m Manifest file (default: {DEF_MANIFEST})
  36. -o Output directory (default: {DEF_OUTPUT})
  37. IMAGE BUILD:
  38. ----------------------------------------------------
  39. -F Format (default: {DEF_OUTPUT_FORMATS[0]})
  40. comma separated with no spaces (ie. 'svg,png-64,jxl-128')
  41. - svg (SVG)
  42. - png-SIZE (PNG)
  43. - pngc-SIZE (Crushed PNG)
  44. - jxl-SIZE (Lossless JPEG XL)
  45. - webp-SIZE (Lossless WebP)
  46. -f Directory/filename naming system for output (default: {DEF_OUTPUT_NAMING})
  47. See the documentation for how this works.
  48. -r SVG renderer (default: {DEF_RENDERER})
  49. - resvg
  50. - imagemagick
  51. - inkscape
  52. -l Do not embed license metadata given in manifest
  53. -p Parameters file
  54. You can attach a parameters file instead of doing the 4 flags above.
  55. Adding this will overwrite anything entered in the previous 4 flags.
  56. -t Number of threads working on export tasks (default: {DEF_NUM_THREADS})
  57. -C Cache directory
  58. Uses the argument as the directory for the export cache.
  59. JSON BUILD:
  60. ----------------------------------------------------
  61. -j <FILE> export JSON replica of directory structure
  62. -J <FILE> export JSON metadata for mutstd website
  63. Using JSON flags will override any image build flags, so run image and JSON builds separately.
  64. OTHER OPTIONS:
  65. ----------------------------------------------------
  66. -e <FILTER> emoji filter
  67. -q <WIDTHxHEIGHT> ensure source images have certain size
  68. -b <NUM> maximum files per exiftool call (default: {DEF_MAX_BATCH})
  69. --force-desc ensure all emoji have a text description
  70. TERMINAL OPTIONS:
  71. ----------------------------------------------------
  72. -c disable ANSI color codes
  73. --verbose verbose printing
  74. '''
  75. def main():
  76. input_path = DEF_INPUT
  77. manifest_path = DEF_MANIFEST
  78. output_path = DEF_OUTPUT
  79. output_naming = DEF_OUTPUT_NAMING
  80. output_formats = DEF_OUTPUT_FORMATS
  81. renderer = DEF_RENDERER
  82. license_enabled = DEF_LICENSE_ENABLED
  83. params_path = None
  84. emoji_filter = []
  85. emoji_filter_text = "" # for error messaging only
  86. json_out = None
  87. json_web_out = None
  88. src_size = None
  89. num_threads = DEF_NUM_THREADS
  90. force_desc = False
  91. max_batch = DEF_MAX_BATCH
  92. cache = False
  93. verbose = False
  94. try:
  95. opts, _ = getopt.getopt(sys.argv[1:],
  96. 'hm:i:o:f:F:ce:j:J:q:t:r:b:p:lC:',
  97. ['help', 'force-desc', 'verbose'])
  98. for opt, arg in opts:
  99. if opt in ['-h', '--help']:
  100. print(HELP)
  101. sys.exit()
  102. # basics
  103. elif opt == '-m':
  104. manifest_path = arg
  105. elif opt == '-i':
  106. input_path = arg
  107. elif opt == '-o':
  108. output_path = arg
  109. # images
  110. elif opt == '-F':
  111. output_formats = arg.split(',')
  112. elif opt == '-f':
  113. output_naming = arg
  114. elif opt == '-r':
  115. renderer = arg
  116. elif opt == '-l':
  117. license_enabled = False
  118. elif opt == '-p':
  119. params_path = arg
  120. elif opt == '-t':
  121. num_threads = int(arg)
  122. if num_threads <= 0:
  123. raise ValueError
  124. elif opt == '-C':
  125. cache = Cache(cache_dir=arg)
  126. # JSON
  127. elif opt == '-j':
  128. json_out = arg
  129. elif opt == '-J':
  130. json_web_out = arg
  131. # other emoji stuff
  132. elif opt == '-e':
  133. k, v = arg.split('=')
  134. v = v.split(',')
  135. emoji_filter.append((k, v))
  136. emoji_filter_text = arg
  137. elif opt == '-q':
  138. t1, t2 = arg.split('x')
  139. src_size = int(t1), int(t2)
  140. elif opt == '-b':
  141. max_batch = int(arg)
  142. if max_batch <= 0:
  143. raise ValueError
  144. elif opt == '--force-desc':
  145. force_desc = True
  146. # terminal stuff
  147. elif opt == '-c':
  148. log.use_color = False
  149. elif opt == '--verbose':
  150. verbose = True
  151. except Exception as e:
  152. log.out(f'x∆∆x {e}\n', 31)
  153. sys.exit(2)
  154. # try to get all of the basic stuff and do the main execution
  155. # -----------------------------------------------------------
  156. try:
  157. log.out(f'o∆∆o', 32) #hello
  158. # validate basic input that can't be checked while in progress
  159. if renderer not in RENDERERS:
  160. raise Exception(f"There's a mistake in your command arguments. '{renderer}' is not a renderer you can use in orxporter.")
  161. # create a Manifest
  162. # ie. parse the manifest file and get the information we need from it
  163. log.out(f'Loading manifest file...', 36)
  164. m = orx.manifest.Manifest(os.path.dirname(manifest_path),
  165. os.path.basename(manifest_path))
  166. log.out(f'-> {len(m.emoji)} emoji defined.')
  167. # filter emoji (if any filter is present)
  168. filtered_emoji = [e for e in m.emoji if emoji.match(e, emoji_filter)]
  169. if emoji_filter:
  170. if filtered_emoji: # if more than 0
  171. log.out(f'-> {len(filtered_emoji)} / {len(m.emoji)} emoji match the filter you gave.', 34)
  172. else:
  173. raise ValueError(f"Your filter ('{emoji_filter_text}') returned no results.")
  174. # ensure that descriptions are present if --force-desc flag is there
  175. if force_desc:
  176. nondesc = [e.get('code', str(e)) for e in filtered_emoji if 'desc' not in e]
  177. if nondesc:
  178. raise ValueError('You have emoji without a description: ' +
  179. ', '.join(nondesc))
  180. # JSON out or image out
  181. if json_out:
  182. jsonutils.write_emoji(filtered_emoji, json_out)
  183. elif json_web_out:
  184. jsonutils.write_web(filtered_emoji, json_web_out)
  185. else:
  186. if params_path:
  187. log.out(f'Loading image export parameters...', 36)
  188. p = orx.params.Parameters(os.path.dirname(params_path),
  189. os.path.basename(params_path))
  190. else:
  191. # convert the non-parameter flags into an orx expression to be turned into a parameters object.
  192. log.out(f'Compiling image export parameters...', 36)
  193. license_text=""
  194. if license_enabled == True:
  195. license_text = "yes"
  196. else:
  197. license_text = "no"
  198. makeshift_params = f"dest structure = {output_naming} format = {' '.join(output_formats)} license = {license_text}"
  199. p = orx.params.Parameters(string = makeshift_params)
  200. path = os.path.join(output_path, output_naming)
  201. log.out(f'{len(p.dests)} destination(s) defined.', 32)
  202. log.out(f"-> {', '.join(output_formats)}") # print formats
  203. log.out(f"-> to '{path}'") # print out path
  204. export.export(m, filtered_emoji, input_path, output_formats,
  205. path, src_size,
  206. num_threads, renderer, max_batch, verbose,
  207. license_enabled, cache)
  208. except (KeyboardInterrupt, SystemExit) as e:
  209. log.out(f'>∆∆< Cancelled!\n{e}', 93)
  210. sys.exit(1)
  211. # Where all the exceptions eventually go~
  212. except Exception as e:
  213. log.out(f'x∆∆x {e}\n', 31)
  214. raise e # TEMP: for developer stuff
  215. sys.exit(1)
  216. # yay! finished!
  217. log.out('All done! ^∆∆^\n', 32) # goodbye
  218. if __name__ == '__main__':
  219. main()