logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

painter.py (14186B)


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