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 @@ +"""Functions that help us work with Quantum Painter's file formats. +""" +import math +import re +from string import Template +from PIL import Image, ImageOps + +# The list of valid formats Quantum Painter supports +valid_formats = { +    'pal256': { +        'image_format': 'IMAGE_FORMAT_PALETTE', +        'bpp': 8, +        'has_palette': True, +        'num_colors': 256, +        'image_format_byte': 0x07,  # see qp_internal_formats.h +    }, +    'pal16': { +        'image_format': 'IMAGE_FORMAT_PALETTE', +        'bpp': 4, +        'has_palette': True, +        'num_colors': 16, +        'image_format_byte': 0x06,  # see qp_internal_formats.h +    }, +    'pal4': { +        'image_format': 'IMAGE_FORMAT_PALETTE', +        'bpp': 2, +        'has_palette': True, +        'num_colors': 4, +        'image_format_byte': 0x05,  # see qp_internal_formats.h +    }, +    'pal2': { +        'image_format': 'IMAGE_FORMAT_PALETTE', +        'bpp': 1, +        'has_palette': True, +        'num_colors': 2, +        'image_format_byte': 0x04,  # see qp_internal_formats.h +    }, +    'mono256': { +        'image_format': 'IMAGE_FORMAT_GRAYSCALE', +        'bpp': 8, +        'has_palette': False, +        'num_colors': 256, +        'image_format_byte': 0x03,  # see qp_internal_formats.h +    }, +    'mono16': { +        'image_format': 'IMAGE_FORMAT_GRAYSCALE', +        'bpp': 4, +        'has_palette': False, +        'num_colors': 16, +        'image_format_byte': 0x02,  # see qp_internal_formats.h +    }, +    'mono4': { +        'image_format': 'IMAGE_FORMAT_GRAYSCALE', +        'bpp': 2, +        'has_palette': False, +        'num_colors': 4, +        'image_format_byte': 0x01,  # see qp_internal_formats.h +    }, +    'mono2': { +        'image_format': 'IMAGE_FORMAT_GRAYSCALE', +        'bpp': 1, +        'has_palette': False, +        'num_colors': 2, +        'image_format_byte': 0x00,  # see qp_internal_formats.h +    } +} + +license_template = """\ +// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright +// SPDX-License-Identifier: GPL-2.0-or-later + +// This file was auto-generated by `${generator_command}` +""" + + +def render_license(subs): +    license_txt = Template(license_template) +    return license_txt.substitute(subs) + + +header_file_template = """\ +${license} +#pragma once + +#include <qp.h> + +extern const uint32_t ${var_prefix}_${sane_name}_length; +extern const uint8_t  ${var_prefix}_${sane_name}[${byte_count}]; +""" + + +def render_header(subs): +    header_txt = Template(header_file_template) +    return header_txt.substitute(subs) + + +source_file_template = """\ +${license} +#include <qp.h> + +const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count}; + +// clang-format off +const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = { +${bytes_lines} +}; +// clang-format on +""" + + +def render_source(subs): +    source_txt = Template(source_file_template) +    return source_txt.substitute(subs) + + +def render_bytes(bytes, newline_after=16): +    lines = '' +    for n in range(len(bytes)): +        if n % newline_after == 0 and n > 0 and n != len(bytes): +            lines = lines + "\n   " +        elif n == 0: +            lines = lines + "   " +        lines = lines + " 0x{0:02X},".format(bytes[n]) +    return lines.rstrip() + + +def clean_output(str): +    str = re.sub(r'\r', '', str) +    str = re.sub(r'[\n]{3,}', r'\n\n', str) +    return str + + +def rescale_byte(val, maxval): +    """Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval]. +    """ +    return int(round(val * maxval / 255.0)) + + +def convert_requested_format(im, format): +    """Convert an image to the requested format. +    """ + +    # Work out the requested format +    ncolors = format["num_colors"] +    image_format = format["image_format"] + +    # Ensure we have a valid number of colors for the palette +    if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0): +        raise ValueError("Number of colors must be 2, 4, 16, or 256.") + +    # Work out where we're getting the bytes from +    if image_format == 'IMAGE_FORMAT_GRAYSCALE': +        # If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel +        im = ImageOps.grayscale(im) +        im = im.convert("RGB") +    elif image_format == 'IMAGE_FORMAT_PALETTE': +        # If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes +        im = im.convert("RGB") +        im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors) + +    return im + + +def convert_image_bytes(im, format): +    """Convert the supplied image to the equivalent bytes required by the QMK firmware. +    """ + +    # Work out the requested format +    ncolors = format["num_colors"] +    image_format = format["image_format"] +    shifter = int(math.log2(ncolors)) +    pixels_per_byte = int(8 / math.log2(ncolors)) +    (width, height) = im.size +    expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte + +    if image_format == 'IMAGE_FORMAT_GRAYSCALE': +        # Take the red channel +        image_bytes = im.tobytes("raw", "R") +        image_bytes_len = len(image_bytes) + +        # No palette +        palette = None + +        bytearray = [] +        for x in range(expected_byte_count): +            byte = 0 +            for n in range(pixels_per_byte): +                byte_offset = x * pixels_per_byte + n +                if byte_offset < image_bytes_len: +                    # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together +                    byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter)) +            bytearray.append(byte) + +    elif image_format == 'IMAGE_FORMAT_PALETTE': +        # Convert each pixel to the palette bytes +        image_bytes = im.tobytes("raw", "P") +        image_bytes_len = len(image_bytes) + +        # Export the palette +        palette = [] +        pal = im.getpalette() +        for n in range(0, ncolors * 3, 3): +            palette.append((pal[n + 0], pal[n + 1], pal[n + 2])) + +        bytearray = [] +        for x in range(expected_byte_count): +            byte = 0 +            for n in range(pixels_per_byte): +                byte_offset = x * pixels_per_byte + n +                if byte_offset < image_bytes_len: +                    # If color, each input byte is the index into the color palette -- pack them together +                    byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter)) +            bytearray.append(byte) + +    if len(bytearray) != expected_byte_count: +        raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}") + +    return (palette, bytearray) + + +def compress_bytes_qmk_rle(bytearray): +    debug_dump = False +    output = [] +    temp = [] +    repeat = False + +    def append_byte(c): +        if debug_dump: +            print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c) +        output.append(c) + +    def append_range(r): +        append_byte(127 + len(r)) +        if debug_dump: +            print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']') +        output.extend(r) + +    for n in range(0, len(bytearray) + 1): +        end = True if n == len(bytearray) else False +        if not end: +            c = bytearray[n] +            temp.append(c) +            if len(temp) <= 1: +                continue + +        if debug_dump: +            print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']') + +        if repeat: +            if temp[-1] != temp[-2]: +                repeat = False +            if not repeat or len(temp) == 128 or end: +                append_byte(len(temp) if end else len(temp) - 1) +                append_byte(temp[0]) +                temp = [temp[-1]] +                repeat = False +        else: +            if len(temp) >= 2 and temp[-1] == temp[-2]: +                repeat = True +                if len(temp) > 2: +                    append_range(temp[0:(len(temp) - 2)]) +                    temp = [temp[-1], temp[-1]] +                continue +            if len(temp) == 128 or end: +                append_range(temp) +                temp = [] +                repeat = False +    return output | 
