painter.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. """Functions that help us work with Quantum Painter's file formats.
  2. """
  3. import math
  4. import re
  5. from string import Template
  6. from PIL import Image, ImageOps
  7. # The list of valid formats Quantum Painter supports
  8. valid_formats = {
  9. 'rgb888': {
  10. 'image_format': 'IMAGE_FORMAT_RGB888',
  11. 'bpp': 24,
  12. 'has_palette': False,
  13. 'num_colors': 16777216,
  14. 'image_format_byte': 0x09, # see qp_internal_formats.h
  15. },
  16. 'rgb565': {
  17. 'image_format': 'IMAGE_FORMAT_RGB565',
  18. 'bpp': 16,
  19. 'has_palette': False,
  20. 'num_colors': 65536,
  21. 'image_format_byte': 0x08, # see qp_internal_formats.h
  22. },
  23. 'pal256': {
  24. 'image_format': 'IMAGE_FORMAT_PALETTE',
  25. 'bpp': 8,
  26. 'has_palette': True,
  27. 'num_colors': 256,
  28. 'image_format_byte': 0x07, # see qp_internal_formats.h
  29. },
  30. 'pal16': {
  31. 'image_format': 'IMAGE_FORMAT_PALETTE',
  32. 'bpp': 4,
  33. 'has_palette': True,
  34. 'num_colors': 16,
  35. 'image_format_byte': 0x06, # see qp_internal_formats.h
  36. },
  37. 'pal4': {
  38. 'image_format': 'IMAGE_FORMAT_PALETTE',
  39. 'bpp': 2,
  40. 'has_palette': True,
  41. 'num_colors': 4,
  42. 'image_format_byte': 0x05, # see qp_internal_formats.h
  43. },
  44. 'pal2': {
  45. 'image_format': 'IMAGE_FORMAT_PALETTE',
  46. 'bpp': 1,
  47. 'has_palette': True,
  48. 'num_colors': 2,
  49. 'image_format_byte': 0x04, # see qp_internal_formats.h
  50. },
  51. 'mono256': {
  52. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  53. 'bpp': 8,
  54. 'has_palette': False,
  55. 'num_colors': 256,
  56. 'image_format_byte': 0x03, # see qp_internal_formats.h
  57. },
  58. 'mono16': {
  59. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  60. 'bpp': 4,
  61. 'has_palette': False,
  62. 'num_colors': 16,
  63. 'image_format_byte': 0x02, # see qp_internal_formats.h
  64. },
  65. 'mono4': {
  66. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  67. 'bpp': 2,
  68. 'has_palette': False,
  69. 'num_colors': 4,
  70. 'image_format_byte': 0x01, # see qp_internal_formats.h
  71. },
  72. 'mono2': {
  73. 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
  74. 'bpp': 1,
  75. 'has_palette': False,
  76. 'num_colors': 2,
  77. 'image_format_byte': 0x00, # see qp_internal_formats.h
  78. }
  79. }
  80. license_template = """\
  81. // Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
  82. // SPDX-License-Identifier: GPL-2.0-or-later
  83. // This file was auto-generated by `${generator_command}`
  84. """
  85. def render_license(subs):
  86. license_txt = Template(license_template)
  87. return license_txt.substitute(subs)
  88. header_file_template = """\
  89. ${license}
  90. #pragma once
  91. #include <qp.h>
  92. extern const uint32_t ${var_prefix}_${sane_name}_length;
  93. extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}];
  94. """
  95. def render_header(subs):
  96. header_txt = Template(header_file_template)
  97. return header_txt.substitute(subs)
  98. source_file_template = """\
  99. ${license}
  100. #include <qp.h>
  101. const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};
  102. // clang-format off
  103. const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = {
  104. ${bytes_lines}
  105. };
  106. // clang-format on
  107. """
  108. def render_source(subs):
  109. source_txt = Template(source_file_template)
  110. return source_txt.substitute(subs)
  111. def render_bytes(bytes, newline_after=16):
  112. lines = ''
  113. for n in range(len(bytes)):
  114. if n % newline_after == 0 and n > 0 and n != len(bytes):
  115. lines = lines + "\n "
  116. elif n == 0:
  117. lines = lines + " "
  118. lines = lines + " 0x{0:02X},".format(bytes[n])
  119. return lines.rstrip()
  120. def clean_output(str):
  121. str = re.sub(r'\r', '', str)
  122. str = re.sub(r'[\n]{3,}', r'\n\n', str)
  123. return str
  124. def rescale_byte(val, maxval):
  125. """Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval].
  126. """
  127. return int(round(val * maxval / 255.0))
  128. def convert_requested_format(im, format):
  129. """Convert an image to the requested format.
  130. """
  131. # Work out the requested format
  132. ncolors = format["num_colors"]
  133. image_format = format["image_format"]
  134. # -- Check if ncolors is valid
  135. # Formats accepting several options
  136. if image_format in ['IMAGE_FORMAT_GRAYSCALE', 'IMAGE_FORMAT_PALETTE']:
  137. valid = [2, 4, 8, 16, 256]
  138. # Formats expecting a particular number
  139. else:
  140. # Read number from specs dict, instead of hardcoding
  141. for _, fmt in valid_formats.items():
  142. if fmt["image_format"] == image_format:
  143. # has to be an iterable, to use `in`
  144. valid = [fmt["num_colors"]]
  145. break
  146. if ncolors not in valid:
  147. raise ValueError(f"Number of colors must be: {', '.join(valid)}.")
  148. # Work out where we're getting the bytes from
  149. if image_format == 'IMAGE_FORMAT_GRAYSCALE':
  150. # If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel
  151. im = ImageOps.grayscale(im)
  152. im = im.convert("RGB")
  153. elif image_format == 'IMAGE_FORMAT_PALETTE':
  154. # If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes
  155. im = im.convert("RGB")
  156. im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors)
  157. elif image_format in ['IMAGE_FORMAT_RGB565', 'IMAGE_FORMAT_RGB888']:
  158. # Convert input to RGB
  159. im = im.convert("RGB")
  160. return im
  161. def rgb_to565(r, g, b):
  162. msb = ((r >> 3 & 0x1F) << 3) + (g >> 5 & 0x07)
  163. lsb = ((g >> 2 & 0x07) << 5) + (b >> 3 & 0x1F)
  164. return [msb, lsb]
  165. def convert_image_bytes(im, format):
  166. """Convert the supplied image to the equivalent bytes required by the QMK firmware.
  167. """
  168. # Work out the requested format
  169. ncolors = format["num_colors"]
  170. image_format = format["image_format"]
  171. shifter = int(math.log2(ncolors))
  172. pixels_per_byte = int(8 / math.log2(ncolors))
  173. bytes_per_pixel = math.ceil(math.log2(ncolors) / 8)
  174. (width, height) = im.size
  175. if (pixels_per_byte != 0):
  176. expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
  177. else:
  178. expected_byte_count = width * height * bytes_per_pixel
  179. if image_format == 'IMAGE_FORMAT_GRAYSCALE':
  180. # Take the red channel
  181. image_bytes = im.tobytes("raw", "R")
  182. image_bytes_len = len(image_bytes)
  183. # No palette
  184. palette = None
  185. bytearray = []
  186. for x in range(expected_byte_count):
  187. byte = 0
  188. for n in range(pixels_per_byte):
  189. byte_offset = x * pixels_per_byte + n
  190. if byte_offset < image_bytes_len:
  191. # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together
  192. byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter))
  193. bytearray.append(byte)
  194. elif image_format == 'IMAGE_FORMAT_PALETTE':
  195. # Convert each pixel to the palette bytes
  196. image_bytes = im.tobytes("raw", "P")
  197. image_bytes_len = len(image_bytes)
  198. # Export the palette
  199. palette = []
  200. pal = im.getpalette()
  201. for n in range(0, ncolors * 3, 3):
  202. palette.append((pal[n + 0], pal[n + 1], pal[n + 2]))
  203. bytearray = []
  204. for x in range(expected_byte_count):
  205. byte = 0
  206. for n in range(pixels_per_byte):
  207. byte_offset = x * pixels_per_byte + n
  208. if byte_offset < image_bytes_len:
  209. # If color, each input byte is the index into the color palette -- pack them together
  210. byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
  211. bytearray.append(byte)
  212. if image_format == 'IMAGE_FORMAT_RGB565':
  213. # Take the red, green, and blue channels
  214. red = im.tobytes("raw", "R")
  215. green = im.tobytes("raw", "G")
  216. blue = im.tobytes("raw", "B")
  217. # No palette
  218. palette = None
  219. bytearray = [byte for r, g, b in zip(red, green, blue) for byte in rgb_to565(r, g, b)]
  220. if image_format == 'IMAGE_FORMAT_RGB888':
  221. # Take the red, green, and blue channels
  222. red = im.tobytes("raw", "R")
  223. green = im.tobytes("raw", "G")
  224. blue = im.tobytes("raw", "B")
  225. # No palette
  226. palette = None
  227. bytearray = [byte for r, g, b in zip(red, green, blue) for byte in (r, g, b)]
  228. if len(bytearray) != expected_byte_count:
  229. raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")
  230. return (palette, bytearray)
  231. def compress_bytes_qmk_rle(bytearray):
  232. debug_dump = False
  233. output = []
  234. temp = []
  235. repeat = False
  236. def append_byte(c):
  237. if debug_dump:
  238. print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c)
  239. output.append(c)
  240. def append_range(r):
  241. append_byte(127 + len(r))
  242. if debug_dump:
  243. print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']')
  244. output.extend(r)
  245. for n in range(0, len(bytearray) + 1):
  246. end = True if n == len(bytearray) else False
  247. if not end:
  248. c = bytearray[n]
  249. temp.append(c)
  250. if len(temp) <= 1:
  251. continue
  252. if debug_dump:
  253. print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']')
  254. if repeat:
  255. if temp[-1] != temp[-2]:
  256. repeat = False
  257. if not repeat or len(temp) == 128 or end:
  258. append_byte(len(temp) if end else len(temp) - 1)
  259. append_byte(temp[0])
  260. temp = [temp[-1]]
  261. repeat = False
  262. else:
  263. if len(temp) >= 2 and temp[-1] == temp[-2]:
  264. repeat = True
  265. if len(temp) > 2:
  266. append_range(temp[0:(len(temp) - 2)])
  267. temp = [temp[-1], temp[-1]]
  268. continue
  269. if len(temp) == 128 or end:
  270. append_range(temp)
  271. temp = []
  272. repeat = False
  273. return output