diff options
Diffstat (limited to 'lib/python/qmk/painter.py')
| -rw-r--r-- | lib/python/qmk/painter.py | 268 |
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 | """ | ||
| 3 | import math | ||
| 4 | import re | ||
| 5 | from string import Template | ||
| 6 | from PIL import Image, ImageOps | ||
| 7 | |||
| 8 | # The list of valid formats Quantum Painter supports | ||
| 9 | valid_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 | |||
| 68 | license_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 | |||
| 76 | def render_license(subs): | ||
| 77 | license_txt = Template(license_template) | ||
| 78 | return license_txt.substitute(subs) | ||
| 79 | |||
| 80 | |||
| 81 | header_file_template = """\ | ||
| 82 | ${license} | ||
| 83 | #pragma once | ||
| 84 | |||
| 85 | #include <qp.h> | ||
| 86 | |||
| 87 | extern const uint32_t ${var_prefix}_${sane_name}_length; | ||
| 88 | extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}]; | ||
| 89 | """ | ||
| 90 | |||
| 91 | |||
| 92 | def render_header(subs): | ||
| 93 | header_txt = Template(header_file_template) | ||
| 94 | return header_txt.substitute(subs) | ||
| 95 | |||
| 96 | |||
| 97 | source_file_template = """\ | ||
| 98 | ${license} | ||
| 99 | #include <qp.h> | ||
| 100 | |||
| 101 | const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count}; | ||
| 102 | |||
| 103 | // clang-format off | ||
| 104 | const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = { | ||
| 105 | ${bytes_lines} | ||
| 106 | }; | ||
| 107 | // clang-format on | ||
| 108 | """ | ||
| 109 | |||
| 110 | |||
| 111 | def render_source(subs): | ||
| 112 | source_txt = Template(source_file_template) | ||
| 113 | return source_txt.substitute(subs) | ||
| 114 | |||
| 115 | |||
| 116 | def 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 | |||
| 127 | def 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 | |||
| 133 | def 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 | |||
| 139 | def 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 | |||
| 164 | def 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 | |||
| 221 | def 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 | ||
