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