You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

268 lines
8.1 KiB

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