painter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. """Functions that help us work with Quantum Painter's file formats.
  2. """
  3. import datetime
  4. import math
  5. import re
  6. from string import Template
  7. from PIL import Image, ImageOps
  8. # The list of valid formats Quantum Painter supports
  9. valid_formats = {
  10. 'rgb888': {
  11. 'image_format': 'IMAGE_FORMAT_RGB888',
  12. 'bpp': 24,
  13. 'has_palette': False,
  14. 'num_colors': 16777216,
  15. 'image_format_byte': 0x09, # see qp_internal_formats.h
  16. },
  17. 'rgb565': {
  18. 'image_format': 'IMAGE_FORMAT_RGB565',
  19. 'bpp': 16,
  20. 'has_palette': False,
  21. 'num_colors': 65536,
  22. 'image_format_byte': 0x08, # see qp_internal_formats.h
  23. },
  24. 'pal256': {
  25. 'image_format': 'IMAGE_FORMAT_PALETTE',
  26. 'bpp': 8,
  27. 'has_palette': True,
  28. 'num_colors': 256,
  29. 'image_format_byte': 0x07, # see qp_internal_formats.h
  30. },
  31. 'pal16': {
  32. 'image_format': 'IMAGE_FORMAT_PALETTE',
  33. 'bpp': 4,
  34. 'has_palette': True,
  35. 'num_colors': 16,
  36. 'image_format_byte': 0x06, # see qp_internal_formats.h
  37. },
  38. 'pal4': {
  39. 'image_format': 'IMAGE_FORMAT_PALETTE',
  40. 'bpp': 2,
  41. 'has_palette': True,
  42. 'num_colors': 4,
  43. 'image_format_byte': 0x05, # see qp_internal_formats.h
  44. },
  45. 'pal2': {
  46. 'image_format': 'IMAGE_FORMAT_PALETTE',
  47. 'bpp': 1,
  48. 'has_palette': True,
  49. 'num_colors': 2,
  50. 'image_format_byte': 0x04, # see qp_internal_formats.h
  51. },
  52. 'mono256': {
  53. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  54. 'bpp': 8,
  55. 'has_palette': False,
  56. 'num_colors': 256,
  57. 'image_format_byte': 0x03, # see qp_internal_formats.h
  58. },
  59. 'mono16': {
  60. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  61. 'bpp': 4,
  62. 'has_palette': False,
  63. 'num_colors': 16,
  64. 'image_format_byte': 0x02, # see qp_internal_formats.h
  65. },
  66. 'mono4': {
  67. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  68. 'bpp': 2,
  69. 'has_palette': False,
  70. 'num_colors': 4,
  71. 'image_format_byte': 0x01, # see qp_internal_formats.h
  72. },
  73. 'mono2': {
  74. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  75. 'bpp': 1,
  76. 'has_palette': False,
  77. 'num_colors': 2,
  78. 'image_format_byte': 0x00, # see qp_internal_formats.h
  79. }
  80. }
  81. def _render_text(values):
  82. # FIXME: May need more chars with GIFs containing lots of frames (or longer durations)
  83. return "|".join([f"{i:4d}" for i in values])
  84. def _render_numeration(metadata):
  85. return _render_text(range(len(metadata)))
  86. def _render_values(metadata, key):
  87. return _render_text([i[key] for i in metadata])
  88. def _render_image_metadata(metadata):
  89. size = metadata.pop(0)
  90. lines = [
  91. "// Image's metadata",
  92. "// ----------------",
  93. f"// Width: {size['width']}",
  94. f"// Height: {size['height']}",
  95. ]
  96. if len(metadata) == 1:
  97. lines.append("// Single frame")
  98. else:
  99. lines.extend([
  100. f"// Frame: {_render_numeration(metadata)}",
  101. f"// Duration(ms): {_render_values(metadata, 'delay')}",
  102. f"// Compression: {_render_values(metadata, 'compression')} >> See qp.h, painter_compression_t",
  103. f"// Delta: {_render_values(metadata, 'delta')}",
  104. ])
  105. deltas = []
  106. for i, v in enumerate(metadata):
  107. # Not a delta frame, go to next one
  108. if not v["delta"]:
  109. continue
  110. # Unpack rect's coords
  111. l, t, r, b = v["delta_rect"]
  112. delta_px = (r - l) * (b - t)
  113. px = size["width"] * size["height"]
  114. # FIXME: May need need more chars here too
  115. deltas.append(f"// Frame {i:3d}: ({l:3d}, {t:3d}) - ({r:3d}, {b:3d}) >> {delta_px:4d}/{px:4d} pixels ({100*delta_px/px:.2f}%)")
  116. if deltas:
  117. lines.append("// Areas on delta frames")
  118. lines.extend(deltas)
  119. return "\n".join(lines)
  120. def generate_subs(cli, out_bytes, *, font_metadata=None, image_metadata=None, command):
  121. if font_metadata is not None and image_metadata is not None:
  122. raise ValueError("Cant generate subs for font and image at the same time")
  123. subs = {
  124. "year": datetime.date.today().strftime("%Y"),
  125. "input_file": cli.args.input.name,
  126. "sane_name": re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
  127. "byte_count": len(out_bytes),
  128. "bytes_lines": render_bytes(out_bytes),
  129. "format": cli.args.format,
  130. "generator_command": command,
  131. }
  132. if font_metadata is not None:
  133. subs.update({
  134. "generated_type": "font",
  135. "var_prefix": "font",
  136. # not using triple quotes to avoid extra indentation/weird formatted code
  137. "metadata": "\n".join([
  138. "// Font's metadata",
  139. "// ---------------",
  140. f"// Glyphs: {', '.join([i for i in font_metadata['glyphs']])}",
  141. ]),
  142. })
  143. elif image_metadata is not None:
  144. subs.update({
  145. "generated_type": "image",
  146. "var_prefix": "gfx",
  147. "generator_command": command,
  148. "metadata": _render_image_metadata(image_metadata),
  149. })
  150. else:
  151. raise ValueError("Pass metadata for either an image or a font")
  152. subs.update({"license": render_license(subs)})
  153. return subs
  154. license_template = """\
  155. // Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
  156. // SPDX-License-Identifier: GPL-2.0-or-later
  157. // This file was auto-generated by `${generator_command}`
  158. """
  159. def render_license(subs):
  160. license_txt = Template(license_template)
  161. return license_txt.substitute(subs)
  162. header_file_template = """\
  163. ${license}
  164. #pragma once
  165. #include <qp.h>
  166. extern const uint32_t ${var_prefix}_${sane_name}_length;
  167. extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}];
  168. """
  169. def render_header(subs):
  170. header_txt = Template(header_file_template)
  171. return header_txt.substitute(subs)
  172. source_file_template = """\
  173. ${license}
  174. ${metadata}
  175. #include <qp.h>
  176. const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};
  177. // clang-format off
  178. const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = {
  179. ${bytes_lines}
  180. };
  181. // clang-format on
  182. """
  183. def render_source(subs):
  184. source_txt = Template(source_file_template)
  185. return source_txt.substitute(subs)
  186. def render_bytes(bytes, newline_after=16):
  187. lines = ''
  188. for n in range(len(bytes)):
  189. if n % newline_after == 0 and n > 0 and n != len(bytes):
  190. lines = lines + "\n "
  191. elif n == 0:
  192. lines = lines + " "
  193. lines = lines + " 0x{0:02X},".format(bytes[n])
  194. return lines.rstrip()
  195. def clean_output(str):
  196. str = re.sub(r'\r', '', str)
  197. str = re.sub(r'[\n]{3,}', r'\n\n', str)
  198. return str
  199. def rescale_byte(val, maxval):
  200. """Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval].
  201. """
  202. return int(round(val * maxval / 255.0))
  203. def convert_requested_format(im, format):
  204. """Convert an image to the requested format.
  205. """
  206. # Work out the requested format
  207. ncolors = format["num_colors"]
  208. image_format = format["image_format"]
  209. # -- Check if ncolors is valid
  210. # Formats accepting several options
  211. if image_format in ['IMAGE_FORMAT_GRAYSCALE', 'IMAGE_FORMAT_PALETTE']:
  212. valid = [2, 4, 8, 16, 256]
  213. # Formats expecting a particular number
  214. else:
  215. # Read number from specs dict, instead of hardcoding
  216. for _, fmt in valid_formats.items():
  217. if fmt["image_format"] == image_format:
  218. # has to be an iterable, to use `in`
  219. valid = [fmt["num_colors"]]
  220. break
  221. if ncolors not in valid:
  222. raise ValueError(f"Number of colors must be: {', '.join(valid)}.")
  223. # Work out where we're getting the bytes from
  224. if image_format == 'IMAGE_FORMAT_GRAYSCALE':
  225. # If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel
  226. im = ImageOps.grayscale(im)
  227. im = im.convert("RGB")
  228. elif image_format == 'IMAGE_FORMAT_PALETTE':
  229. # If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes
  230. im = im.convert("RGB")
  231. im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors)
  232. elif image_format in ['IMAGE_FORMAT_RGB565', 'IMAGE_FORMAT_RGB888']:
  233. # Convert input to RGB
  234. im = im.convert("RGB")
  235. return im
  236. def rgb_to565(r, g, b):
  237. msb = ((r >> 3 & 0x1F) << 3) + (g >> 5 & 0x07)
  238. lsb = ((g >> 2 & 0x07) << 5) + (b >> 3 & 0x1F)
  239. return [msb, lsb]
  240. def convert_image_bytes(im, format):
  241. """Convert the supplied image to the equivalent bytes required by the QMK firmware.
  242. """
  243. # Work out the requested format
  244. ncolors = format["num_colors"]
  245. image_format = format["image_format"]
  246. shifter = int(math.log2(ncolors))
  247. pixels_per_byte = int(8 / math.log2(ncolors))
  248. bytes_per_pixel = math.ceil(math.log2(ncolors) / 8)
  249. (width, height) = im.size
  250. if (pixels_per_byte != 0):
  251. expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
  252. else:
  253. expected_byte_count = width * height * bytes_per_pixel
  254. if image_format == 'IMAGE_FORMAT_GRAYSCALE':
  255. # Take the red channel
  256. image_bytes = im.tobytes("raw", "R")
  257. image_bytes_len = len(image_bytes)
  258. # No palette
  259. palette = None
  260. bytearray = []
  261. for x in range(expected_byte_count):
  262. byte = 0
  263. for n in range(pixels_per_byte):
  264. byte_offset = x * pixels_per_byte + n
  265. if byte_offset < image_bytes_len:
  266. # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together
  267. byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter))
  268. bytearray.append(byte)
  269. elif image_format == 'IMAGE_FORMAT_PALETTE':
  270. # Convert each pixel to the palette bytes
  271. image_bytes = im.tobytes("raw", "P")
  272. image_bytes_len = len(image_bytes)
  273. # Export the palette
  274. palette = []
  275. pal = im.getpalette()
  276. for n in range(0, ncolors * 3, 3):
  277. palette.append((pal[n + 0], pal[n + 1], pal[n + 2]))
  278. bytearray = []
  279. for x in range(expected_byte_count):
  280. byte = 0
  281. for n in range(pixels_per_byte):
  282. byte_offset = x * pixels_per_byte + n
  283. if byte_offset < image_bytes_len:
  284. # If color, each input byte is the index into the color palette -- pack them together
  285. byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
  286. bytearray.append(byte)
  287. if image_format == 'IMAGE_FORMAT_RGB565':
  288. # Take the red, green, and blue channels
  289. red = im.tobytes("raw", "R")
  290. green = im.tobytes("raw", "G")
  291. blue = im.tobytes("raw", "B")
  292. # No palette
  293. palette = None
  294. bytearray = [byte for r, g, b in zip(red, green, blue) for byte in rgb_to565(r, g, b)]
  295. if image_format == 'IMAGE_FORMAT_RGB888':
  296. # Take the red, green, and blue channels
  297. red = im.tobytes("raw", "R")
  298. green = im.tobytes("raw", "G")
  299. blue = im.tobytes("raw", "B")
  300. # No palette
  301. palette = None
  302. bytearray = [byte for r, g, b in zip(red, green, blue) for byte in (r, g, b)]
  303. if len(bytearray) != expected_byte_count:
  304. raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")
  305. return (palette, bytearray)
  306. def compress_bytes_qmk_rle(bytearray):
  307. debug_dump = False
  308. output = []
  309. temp = []
  310. repeat = False
  311. def append_byte(c):
  312. if debug_dump:
  313. print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c)
  314. output.append(c)
  315. def append_range(r):
  316. append_byte(127 + len(r))
  317. if debug_dump:
  318. print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']')
  319. output.extend(r)
  320. for n in range(0, len(bytearray) + 1):
  321. end = True if n == len(bytearray) else False
  322. if not end:
  323. c = bytearray[n]
  324. temp.append(c)
  325. if len(temp) <= 1:
  326. continue
  327. if debug_dump:
  328. print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']')
  329. if repeat:
  330. if temp[-1] != temp[-2]:
  331. repeat = False
  332. if not repeat or len(temp) == 128 or end:
  333. append_byte(len(temp) if end else len(temp) - 1)
  334. append_byte(temp[0])
  335. temp = [temp[-1]]
  336. repeat = False
  337. else:
  338. if len(temp) >= 2 and temp[-1] == temp[-2]:
  339. repeat = True
  340. if len(temp) > 2:
  341. append_range(temp[0:(len(temp) - 2)])
  342. temp = [temp[-1], temp[-1]]
  343. continue
  344. if len(temp) == 128 or end:
  345. append_range(temp)
  346. temp = []
  347. repeat = False
  348. return output