painter.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 convert_image_bytes(im, format):
  162. """Convert the supplied image to the equivalent bytes required by the QMK firmware.
  163. """
  164. # Work out the requested format
  165. ncolors = format["num_colors"]
  166. image_format = format["image_format"]
  167. shifter = int(math.log2(ncolors))
  168. pixels_per_byte = int(8 / math.log2(ncolors))
  169. bytes_per_pixel = math.ceil(math.log2(ncolors) / 8)
  170. (width, height) = im.size
  171. if (pixels_per_byte != 0):
  172. expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
  173. else:
  174. expected_byte_count = width * height * bytes_per_pixel
  175. if image_format == 'IMAGE_FORMAT_GRAYSCALE':
  176. # Take the red channel
  177. image_bytes = im.tobytes("raw", "R")
  178. image_bytes_len = len(image_bytes)
  179. # No palette
  180. palette = None
  181. bytearray = []
  182. for x in range(expected_byte_count):
  183. byte = 0
  184. for n in range(pixels_per_byte):
  185. byte_offset = x * pixels_per_byte + n
  186. if byte_offset < image_bytes_len:
  187. # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together
  188. byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter))
  189. bytearray.append(byte)
  190. elif image_format == 'IMAGE_FORMAT_PALETTE':
  191. # Convert each pixel to the palette bytes
  192. image_bytes = im.tobytes("raw", "P")
  193. image_bytes_len = len(image_bytes)
  194. # Export the palette
  195. palette = []
  196. pal = im.getpalette()
  197. for n in range(0, ncolors * 3, 3):
  198. palette.append((pal[n + 0], pal[n + 1], pal[n + 2]))
  199. bytearray = []
  200. for x in range(expected_byte_count):
  201. byte = 0
  202. for n in range(pixels_per_byte):
  203. byte_offset = x * pixels_per_byte + n
  204. if byte_offset < image_bytes_len:
  205. # If color, each input byte is the index into the color palette -- pack them together
  206. byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
  207. bytearray.append(byte)
  208. if image_format == 'IMAGE_FORMAT_RGB565':
  209. # Take the red, green, and blue channels
  210. image_bytes_red = im.tobytes("raw", "R")
  211. image_bytes_green = im.tobytes("raw", "G")
  212. image_bytes_blue = im.tobytes("raw", "B")
  213. image_pixels_len = len(image_bytes_red)
  214. # No palette
  215. palette = None
  216. bytearray = []
  217. for x in range(image_pixels_len):
  218. # 5 bits of red, 3 MSb of green
  219. byte = ((image_bytes_red[x] >> 3 & 0x1F) << 3) + (image_bytes_green[x] >> 5 & 0x07)
  220. bytearray.append(byte)
  221. # 3 LSb of green, 5 bits of blue
  222. byte = ((image_bytes_green[x] >> 2 & 0x07) << 5) + (image_bytes_blue[x] >> 3 & 0x1F)
  223. bytearray.append(byte)
  224. if image_format == 'IMAGE_FORMAT_RGB888':
  225. # Take the red, green, and blue channels
  226. image_bytes_red = im.tobytes("raw", "R")
  227. image_bytes_green = im.tobytes("raw", "G")
  228. image_bytes_blue = im.tobytes("raw", "B")
  229. image_pixels_len = len(image_bytes_red)
  230. # No palette
  231. palette = None
  232. bytearray = []
  233. for x in range(image_pixels_len):
  234. byte = image_bytes_red[x]
  235. bytearray.append(byte)
  236. byte = image_bytes_green[x]
  237. bytearray.append(byte)
  238. byte = image_bytes_blue[x]
  239. bytearray.append(byte)
  240. if len(bytearray) != expected_byte_count:
  241. raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")
  242. return (palette, bytearray)
  243. def compress_bytes_qmk_rle(bytearray):
  244. debug_dump = False
  245. output = []
  246. temp = []
  247. repeat = False
  248. def append_byte(c):
  249. if debug_dump:
  250. print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c)
  251. output.append(c)
  252. def append_range(r):
  253. append_byte(127 + len(r))
  254. if debug_dump:
  255. print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']')
  256. output.extend(r)
  257. for n in range(0, len(bytearray) + 1):
  258. end = True if n == len(bytearray) else False
  259. if not end:
  260. c = bytearray[n]
  261. temp.append(c)
  262. if len(temp) <= 1:
  263. continue
  264. if debug_dump:
  265. print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']')
  266. if repeat:
  267. if temp[-1] != temp[-2]:
  268. repeat = False
  269. if not repeat or len(temp) == 128 or end:
  270. append_byte(len(temp) if end else len(temp) - 1)
  271. append_byte(temp[0])
  272. temp = [temp[-1]]
  273. repeat = False
  274. else:
  275. if len(temp) >= 2 and temp[-1] == temp[-2]:
  276. repeat = True
  277. if len(temp) > 2:
  278. append_range(temp[0:(len(temp) - 2)])
  279. temp = [temp[-1], temp[-1]]
  280. continue
  281. if len(temp) == 128 or end:
  282. append_range(temp)
  283. temp = []
  284. repeat = False
  285. return output