* Install dependencies before executing unit tests. * Split out UTF-8 decoder. * Fixup python formatting rules. * Add documentation for QGF/QFF and the RLE format used. * Add CLI commands for converting images and fonts. * Add stub rules.mk for QP. * Add stream type. * Add base driver and comms interfaces. * Add support for SPI, SPI+D/C comms drivers. * Include <qp.h> when enabled. * Add base support for SPI+D/C+RST panels, as well as concrete implementation of ST7789. * Add support for GC9A01. * Add support for ILI9341. * Add support for ILI9163. * Add support for SSD1351. * Implement qp_setpixel, including pixdata buffer management. * Implement qp_line. * Implement qp_rect. * Implement qp_circle. * Implement qp_ellipse. * Implement palette interpolation. * Allow for streams to work with either flash or RAM. * Image loading. * Font loading. * QGF palette loading. * Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp monochrome and palette-based images. * Image drawing. * Animations. * Font rendering. * Check against 256 colours, dump out the loaded palette if debugging enabled. * Fix build. * AVR is not the intended audience. * `qmk format-c` * Generation fix. * First batch of docs. * More docs and examples. * Review comments. * Public API documentation.
		
			
				
	
	
		
			269 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""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
 |