export.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import os
  2. import pathlib
  3. import queue
  4. import re
  5. import subprocess
  6. import threading
  7. import time
  8. import log
  9. import png
  10. import svg
  11. import util
  12. class ExportThread:
  13. def __init__(self, queue, name, total, m, input_path, formats, path):
  14. self.queue = queue
  15. self.name = name
  16. self.total = total
  17. self.m = m
  18. self.input_path = input_path
  19. self.formats = formats
  20. self.path = path
  21. self.err = None
  22. self.kill_flag = False
  23. self.thread = threading.Thread(target=self.run)
  24. self.thread.start()
  25. def kill(self):
  26. self.kill_flag = True
  27. def join(self):
  28. self.thread.join()
  29. def msg(self, s, color=37, indent=0):
  30. log.out(s, color, indent, self.name)
  31. def export_svg(self, emoji_svg, path, license=None):
  32. if license:
  33. self.msg('* Writing license metadata...', indent=4)
  34. final_svg = svg.license(emoji_svg, license)
  35. else:
  36. final_svg = emoji_svg
  37. self.msg('* Exporting to file: ' + path, indent=4)
  38. try:
  39. out = open(path, 'w')
  40. out.write(final_svg)
  41. out.close()
  42. except Exception:
  43. raise Exception('Could not write to file: ' + path)
  44. def export_png(self, emoji_svg, size, path, license=None):
  45. self.msg('* Saving svg to temporary file because Inkscape has a bug '
  46. 'since 2005...', indent=4)
  47. tmp_name = '.tmp' + self.name + '.svg'
  48. try:
  49. f = open(tmp_name, 'w')
  50. f.write(emoji_svg)
  51. f.close()
  52. except IOError:
  53. raise Exception('Could not write to temporary file: ' + tmp_name)
  54. self.msg(f'* Exporting at {size}px to {path}...', indent=4)
  55. cmd = ['inkscape', os.path.abspath(tmp_name),
  56. '--export-png=' + os.path.abspath(path),
  57. '-h', str(size), '-w', str(size)]
  58. try:
  59. r = subprocess.run(cmd, stdout=subprocess.DEVNULL).returncode
  60. except Exception as e:
  61. raise Exception('Rasteriser invocation failed: ' + str(e))
  62. if r:
  63. raise Exception('Rasteriser returned error code: ' + str(r))
  64. self.msg('* Deleting temporary file...', indent=4)
  65. os.remove(tmp_name)
  66. if license:
  67. self.msg('* Writing license metadata...', indent=4)
  68. png.license(path, license)
  69. def export_emoji(self, emoji, emoji_svg, f, path, license):
  70. final_path = format_path(path, emoji, f)
  71. self.msg('* Export path is ' + final_path, indent=4)
  72. try:
  73. dirname = os.path.dirname(final_path)
  74. if dirname:
  75. os.makedirs(dirname, exist_ok=True)
  76. except IOError:
  77. raise Exception('Could not create directory: ' + dirname)
  78. if f == 'svg':
  79. self.export_svg(emoji_svg, final_path, license.get('svg'))
  80. elif f.startswith('png-'):
  81. try:
  82. size = int(f[4:])
  83. except ValueError:
  84. raise ValueError('Invalid format: ' + f)
  85. self.export_png(emoji_svg, size, final_path, license.get('png'))
  86. else:
  87. raise ValueError('Invalid format: ' + f)
  88. def run(self):
  89. try:
  90. while not self.kill_flag:
  91. try:
  92. i, emoji = self.queue.get_nowait()
  93. except queue.Empty:
  94. break
  95. self.msg(f'[{i+1} / {self.total}] Exporting '
  96. f'{emoji.get("code", "<UNNAMED>")}...', 32)
  97. try:
  98. format_path(self.path, emoji, 'svg')
  99. except SkipException as ex:
  100. if str(ex):
  101. self.msg(f'Skipping: {ex}', 34, 4)
  102. else:
  103. self.msg('Skipping', 34, 4)
  104. continue
  105. if 'src' not in emoji:
  106. raise ValueError('Missing src attribute')
  107. srcpath = os.path.join(self.m.homedir, self.input_path,
  108. emoji['src'])
  109. self.msg('* Loading source file: ' + srcpath, indent=4)
  110. try:
  111. emoji_svg = open(srcpath, 'r').read()
  112. except Exception:
  113. raise ValueError('Could not load file: ' + srcpath)
  114. if 'color' in emoji:
  115. self.msg('* Converting colormap...', indent=4)
  116. cmap = self.m.colormaps[emoji['color']]
  117. pfrom = self.m.palettes[cmap['src']]
  118. pto = self.m.palettes[cmap['dst']]
  119. emoji_svg = svg.ctrans(emoji_svg, pfrom, pto)
  120. for f in self.formats:
  121. self.msg('-> ' + f, 35)
  122. self.export_emoji(emoji, emoji_svg, f, self.path, self.m.license)
  123. except Exception as e:
  124. self.err = e
  125. class SkipException(Exception):
  126. def __init__(self, s=''):
  127. self.s = s
  128. def __str__(self):
  129. return self.s
  130. def format_resolve(code, emoji, f):
  131. if code[0] == '(':
  132. inside = code[1:-1]
  133. if inside not in emoji:
  134. raise ValueError('Missing property: ' + inside)
  135. return emoji[inside]
  136. if code == 'c':
  137. if 'color' not in emoji:
  138. raise ValueError('Cannot resolve %c - no colormap')
  139. return emoji['color']
  140. if code == 'd':
  141. if 'src' not in emoji:
  142. raise ValueError('Cannot resolve %d - no emoji source file defined')
  143. return str(pathlib.Path(emoji['src']).parent)
  144. if code == 'f':
  145. return f
  146. if code == 's':
  147. if 'code' not in emoji:
  148. raise ValueError('Cannot resolve %s - no shortcode')
  149. return emoji['code']
  150. if code == 'u':
  151. if 'unicode' not in emoji:
  152. raise ValueError('Cannot resolve %u - no unicode codepoint defined')
  153. if '!' in emoji['unicode']:
  154. raise SkipException('Cannot resolve %u (explicitly undefined)')
  155. return util.uni_to_hex_filename(emoji['unicode'])
  156. raise ValueError('Cannot resolve format code: ' + code)
  157. def format_path(path, emoji, f):
  158. res = path
  159. if f == 'svg':
  160. res = res + '.svg'
  161. elif f.startswith('png-'):
  162. res = res + '.png'
  163. else:
  164. raise ValueError('Invalid export format: ' + f)
  165. for match, fcode in set(re.findall(r'(%(\(.*\)|.))', res)):
  166. repl = format_resolve(fcode, emoji, f)
  167. res = res.replace(match, repl)
  168. return res
  169. def export(m, filtered_emoji, input_path, formats, path, src_size=None,
  170. num_threads=1):
  171. # 1st pass
  172. log.out('Performing sanity check...', 36)
  173. for i, e in enumerate(filtered_emoji):
  174. log.out(f'[{i+1} / {len(filtered_emoji)}] Checking '
  175. f'{e.get("code", "<UNNAMED>")}...', 32)
  176. try:
  177. format_path(path, e, 'svg')
  178. except SkipException as ex:
  179. if str(ex):
  180. log.out(f'Skipping: {ex})', 34, 4)
  181. else:
  182. log.out('Skipping', 34)
  183. continue
  184. if 'src' not in e:
  185. raise ValueError('Missing src attribute')
  186. srcpath = os.path.join(m.homedir, input_path, e['src'])
  187. try:
  188. emoji_svg = open(srcpath, 'r').read()
  189. except Exception:
  190. raise ValueError('Could not load file: ' + srcpath)
  191. if src_size is not None:
  192. imgsize = svg.size(emoji_svg)
  193. if imgsize != src_size:
  194. raise ValueError('Source image size is {}, expected {}'.format(
  195. str(imgsize[0]) + 'x' + str(imgsize[1]),
  196. str(src_size[0]) + 'x' + str(src_size[1])))
  197. # 2nd pass
  198. log.out('Exporting emoji...', 36)
  199. emoji_queue = queue.Queue()
  200. for entry in enumerate(filtered_emoji):
  201. emoji_queue.put(entry)
  202. log.show_threads = num_threads > 1
  203. threads = []
  204. for i in range(num_threads):
  205. log.out(f'Init thread {i}...', 35)
  206. threads.append(ExportThread(emoji_queue, str(i), len(filtered_emoji),
  207. m, input_path, formats, path))
  208. while True:
  209. done = emoji_queue.empty()
  210. for t in threads:
  211. if t.err is not None:
  212. for u in threads:
  213. u.kill()
  214. u.join()
  215. raise ValueError(f'Thread {t.name} failed: {t.err}')
  216. if done:
  217. break
  218. time.sleep(0.01)
  219. log.out(f'Waiting for all threads to finish...', 35)
  220. for t in threads:
  221. t.join()