qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

painter_qgf.py (17071B)


      1 # Copyright 2021 Nick Brassel (@tzarc)
      2 # Copyright 2023 Pablo Martinez (@elpekenin) <elpekenin@elpekenin.dev>
      3 # SPDX-License-Identifier: GPL-2.0-or-later
      4 
      5 # Quantum Graphics File "QGF" Image File Format.
      6 # See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
      7 
      8 import functools
      9 from colorsys import rgb_to_hsv
     10 from types import FunctionType
     11 from PIL import Image, ImageFile, ImageChops
     12 from PIL._binary import o8, o16le as o16, o32le as o32
     13 import qmk.painter
     14 
     15 
     16 def o24(i):
     17     return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
     18 
     19 
     20 # Helper to convert from RGB888 to the QMK "dialect" of HSV888
     21 def rgb888_to_qmk_hsv888(e):
     22     hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
     23     return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
     24 
     25 
     26 ########################################################################################################################
     27 
     28 
     29 class QGFBlockHeader:
     30     block_size = 5
     31 
     32     def write(self, fp):
     33         fp.write(b''  # start off with empty bytes...
     34                  + o8(self.type_id)  # block type id
     35                  + o8((~self.type_id) & 0xFF)  # negated block type id
     36                  + o24(self.length)  # blob length
     37                  )
     38 
     39 
     40 ########################################################################################################################
     41 
     42 
     43 class QGFGraphicsDescriptor:
     44     type_id = 0x00
     45     length = 18
     46     magic = 0x464751
     47 
     48     def __init__(self):
     49         self.header = QGFBlockHeader()
     50         self.header.type_id = QGFGraphicsDescriptor.type_id
     51         self.header.length = QGFGraphicsDescriptor.length
     52         self.version = 1
     53         self.total_file_size = 0
     54         self.image_width = 0
     55         self.image_height = 0
     56         self.frame_count = 0
     57 
     58     def write(self, fp):
     59         self.header.write(fp)
     60         fp.write(
     61             b''  # start off with empty bytes...
     62             + o24(QGFGraphicsDescriptor.magic)  # magic
     63             + o8(self.version)  # version
     64             + o32(self.total_file_size)  # file size
     65             + o32((~self.total_file_size) & 0xFFFFFFFF)  # negated file size
     66             + o16(self.image_width)  # width
     67             + o16(self.image_height)  # height
     68             + o16(self.frame_count)  # frame count
     69         )
     70 
     71     @property
     72     def image_size(self):
     73         return self.image_width, self.image_height
     74 
     75     @image_size.setter
     76     def image_size(self, size):
     77         self.image_width, self.image_height = size
     78 
     79 
     80 ########################################################################################################################
     81 
     82 
     83 class QGFFrameOffsetDescriptorV1:
     84     type_id = 0x01
     85 
     86     def __init__(self, frame_count):
     87         self.header = QGFBlockHeader()
     88         self.header.type_id = QGFFrameOffsetDescriptorV1.type_id
     89         self.frame_offsets = [0xFFFFFFFF] * frame_count
     90         self.frame_count = frame_count
     91 
     92     def write(self, fp):
     93         self.header.length = len(self.frame_offsets) * 4
     94         self.header.write(fp)
     95         for offset in self.frame_offsets:
     96             fp.write(b''  # start off with empty bytes...
     97                      + o32(offset)  # offset
     98                      )
     99 
    100 
    101 ########################################################################################################################
    102 
    103 
    104 class QGFFrameDescriptorV1:
    105     type_id = 0x02
    106     length = 6
    107 
    108     def __init__(self):
    109         self.header = QGFBlockHeader()
    110         self.header.type_id = QGFFrameDescriptorV1.type_id
    111         self.header.length = QGFFrameDescriptorV1.length
    112         self.format = 0xFF
    113         self.flags = 0
    114         self.compression = 0xFF
    115         self.transparency_index = 0xFF  # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
    116         self.delay = 1000  # Placeholder until it gets read from the animation
    117 
    118     def write(self, fp):
    119         self.header.write(fp)
    120         fp.write(b''  # start off with empty bytes...
    121                  + o8(self.format)  # format
    122                  + o8(self.flags)  # flags
    123                  + o8(self.compression)  # compression
    124                  + o8(self.transparency_index)  # transparency index
    125                  + o16(self.delay)  # delay
    126                  )
    127 
    128     @property
    129     def is_transparent(self):
    130         return (self.flags & 0x01) == 0x01
    131 
    132     @is_transparent.setter
    133     def is_transparent(self, val):
    134         if val:
    135             self.flags |= 0x01
    136         else:
    137             self.flags &= ~0x01
    138 
    139     @property
    140     def is_delta(self):
    141         return (self.flags & 0x02) == 0x02
    142 
    143     @is_delta.setter
    144     def is_delta(self, val):
    145         if val:
    146             self.flags |= 0x02
    147         else:
    148             self.flags &= ~0x02
    149 
    150 
    151 ########################################################################################################################
    152 
    153 
    154 class QGFFramePaletteDescriptorV1:
    155     type_id = 0x03
    156 
    157     def __init__(self):
    158         self.header = QGFBlockHeader()
    159         self.header.type_id = QGFFramePaletteDescriptorV1.type_id
    160         self.header.length = 0
    161         self.palette_entries = [(0xFF, 0xFF, 0xFF)] * 4
    162 
    163     def write(self, fp):
    164         self.header.length = len(self.palette_entries) * 3
    165         self.header.write(fp)
    166         for entry in self.palette_entries:
    167             fp.write(b''  # start off with empty bytes...
    168                      + o8(entry[0])  # h
    169                      + o8(entry[1])  # s
    170                      + o8(entry[2])  # v
    171                      )
    172 
    173 
    174 ########################################################################################################################
    175 
    176 
    177 class QGFFrameDeltaDescriptorV1:
    178     type_id = 0x04
    179     length = 8
    180 
    181     def __init__(self):
    182         self.header = QGFBlockHeader()
    183         self.header.type_id = QGFFrameDeltaDescriptorV1.type_id
    184         self.header.length = QGFFrameDeltaDescriptorV1.length
    185         self.left = 0
    186         self.top = 0
    187         self.right = 0
    188         self.bottom = 0
    189 
    190     def write(self, fp):
    191         self.header.write(fp)
    192         fp.write(b''  # start off with empty bytes...
    193                  + o16(self.left)  # left
    194                  + o16(self.top)  # top
    195                  + o16(self.right)  # right
    196                  + o16(self.bottom)  # bottom
    197                  )
    198 
    199     @property
    200     def bbox(self):
    201         return self.left, self.top, self.right, self.bottom
    202 
    203     @bbox.setter
    204     def bbox(self, bbox):
    205         self.left, self.top, self.right, self.bottom = bbox
    206 
    207 
    208 ########################################################################################################################
    209 
    210 
    211 class QGFFrameDataDescriptorV1:
    212     type_id = 0x05
    213 
    214     def __init__(self):
    215         self.header = QGFBlockHeader()
    216         self.header.type_id = QGFFrameDataDescriptorV1.type_id
    217         self.data = []
    218 
    219     def write(self, fp):
    220         self.header.length = len(self.data)
    221         self.header.write(fp)
    222         fp.write(bytes(self.data))
    223 
    224 
    225 ########################################################################################################################
    226 
    227 
    228 class QGFImageFile(ImageFile.ImageFile):
    229 
    230     format = "QGF"
    231     format_description = "Quantum Graphics File Format"
    232 
    233     def _open(self):
    234         raise NotImplementedError("Reading QGF files is not supported")
    235 
    236 
    237 ########################################################################################################################
    238 
    239 
    240 def _accept(prefix):
    241     """Helper method used by PIL to work out if it can parse an input file.
    242 
    243     Currently unimplemented.
    244     """
    245     return False
    246 
    247 
    248 def _for_all_frames(x: FunctionType, /, images):
    249     frame_num = 0
    250     last_frame = None
    251     for frame in images:
    252         # Get number of of frames in this image
    253         nfr = getattr(frame, "n_frames", 1)
    254         for idx in range(nfr):
    255             frame.seek(idx)
    256             frame.load()
    257             copy = frame.copy().convert("RGB")
    258             x(frame_num, copy, last_frame)
    259             last_frame = copy
    260             frame_num += 1
    261 
    262 
    263 def _compress_image(frame, last_frame, *, use_rle, use_deltas, format_, **_kwargs):
    264     # Convert the original frame so we can do comparisons
    265     converted = qmk.painter.convert_requested_format(frame, format_)
    266     graphic_data = qmk.painter.convert_image_bytes(converted, format_)
    267 
    268     # Convert the raw data to RLE-encoded if requested
    269     raw_data = graphic_data[1]
    270     if use_rle:
    271         rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1])
    272     use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data)
    273     image_data = raw_data if use_raw_this_frame else rle_data
    274 
    275     # Work out if a delta frame is smaller than injecting it directly
    276     use_delta_this_frame = False
    277     bbox = None
    278     if use_deltas and last_frame is not None:
    279         # If we want to use deltas, then find the difference
    280         diff = ImageChops.difference(frame, last_frame)
    281 
    282         # Get the bounding box of those differences
    283         bbox = diff.getbbox()
    284 
    285         # If we have a valid bounding box...
    286         if bbox:
    287             # ...create the delta frame by cropping the original.
    288             delta_frame = frame.crop(bbox)
    289 
    290             # Convert the delta frame to the requested format
    291             delta_converted = qmk.painter.convert_requested_format(delta_frame, format_)
    292             delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format_)
    293 
    294             # Work out how large the delta frame is going to be with compression etc.
    295             delta_raw_data = delta_graphic_data[1]
    296             if use_rle:
    297                 delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1])
    298             delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data)
    299             delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data
    300 
    301             # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead
    302             # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash
    303             # sizing constraints.
    304             if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data):
    305                 # Copy across all the delta equivalents so that the rest of the processing acts on those
    306                 graphic_data = delta_graphic_data
    307                 raw_data = delta_raw_data
    308                 rle_data = delta_rle_data
    309                 use_raw_this_frame = delta_use_raw_this_frame
    310                 image_data = delta_image_data
    311                 use_delta_this_frame = True
    312 
    313         # Default to whole image
    314         bbox = bbox or [0, 0, *frame.size]
    315         # Fix sze (as per #20296), we need to cast first as tuples are inmutable
    316         bbox = list(bbox)
    317         bbox[2] -= 1
    318         bbox[3] -= 1
    319 
    320     return {
    321         "bbox": bbox,
    322         "graphic_data": graphic_data,
    323         "image_data": image_data,
    324         "use_delta_this_frame": use_delta_this_frame,
    325         "use_raw_this_frame": use_raw_this_frame,
    326     }
    327 
    328 
    329 # Helper function to save each frame to the output file
    330 def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, metadata, **kwargs):
    331     # Not an argument of the function as it would then not be part of kwargs
    332     # This would cause an issue with `_compress_image(**kwargs)` missing an argument
    333     format_ = kwargs["format_"]
    334 
    335     # (potentially) Apply RLE and/or delta, and work out output image's information
    336     outputs = _compress_image(frame, last_frame, **kwargs)
    337     bbox = outputs["bbox"]
    338     graphic_data = outputs["graphic_data"]
    339     image_data = outputs["image_data"]
    340     use_delta_this_frame = outputs["use_delta_this_frame"]
    341     use_raw_this_frame = outputs["use_raw_this_frame"]
    342 
    343     # Write out the frame descriptor
    344     frame_offsets.frame_offsets[idx] = fp.tell()
    345     vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h')
    346     frame_descriptor = QGFFrameDescriptorV1()
    347     frame_descriptor.is_delta = use_delta_this_frame
    348     frame_descriptor.is_transparent = False
    349     frame_descriptor.format = format_['image_format_byte']
    350     frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01  # See qp.h, painter_compression_t
    351     frame_descriptor.delay = frame.info.get('duration', 1000)  # If we're not an animation, just pretend we're delaying for 1000ms
    352     frame_descriptor.write(fp)
    353 
    354     # Write out the palette if required
    355     if format_['has_palette']:
    356         palette = graphic_data[0]
    357         palette_descriptor = QGFFramePaletteDescriptorV1()
    358 
    359         # Convert all palette entries to HSV888 and write to the output
    360         palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette))
    361         vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h')
    362         palette_descriptor.write(fp)
    363 
    364     # Write out the delta info if required
    365     if use_delta_this_frame:
    366         # Set up the rendering location of where the delta frame should be situated
    367         delta_descriptor = QGFFrameDeltaDescriptorV1()
    368         delta_descriptor.bbox = bbox
    369 
    370         # Write the delta frame to the output
    371         vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
    372         delta_descriptor.write(fp)
    373 
    374     # Store metadata, showed later in a comment in the generated file
    375     frame_metadata = {
    376         "compression": frame_descriptor.compression,
    377         "delta": frame_descriptor.is_delta,
    378         "delay": frame_descriptor.delay,
    379     }
    380     if frame_metadata["delta"]:
    381         frame_metadata.update({"delta_rect": [
    382             delta_descriptor.left,
    383             delta_descriptor.top,
    384             delta_descriptor.right,
    385             delta_descriptor.bottom,
    386         ]})
    387     metadata.append(frame_metadata)
    388 
    389     # Write out the data for this frame to the output
    390     data_descriptor = QGFFrameDataDescriptorV1()
    391     data_descriptor.data = image_data
    392     vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h')
    393     data_descriptor.write(fp)
    394 
    395 
    396 def _save(im, fp, _filename):
    397     """Helper method used by PIL to write to an output file.
    398     """
    399     # Work out from the parameters if we need to do anything special
    400     encoderinfo = im.encoderinfo.copy()
    401 
    402     # Store image file in metadata structure
    403     metadata = encoderinfo.get("metadata", [])
    404     metadata.append({"width": im.width, "height": im.height})
    405 
    406     # Helper for prints, noop taking any args if not verbose
    407     global vprint
    408     verbose = encoderinfo.get("verbose", False)
    409     vprint = print if verbose else lambda *_args, **_kwargs: None
    410 
    411     # Helper to iterate through all frames in the input image
    412     append_images = list(encoderinfo.get("append_images", []))
    413     for_all_frames = functools.partial(_for_all_frames, images=[im, *append_images])
    414 
    415     # Collect all the frame sizes
    416     frame_sizes = []
    417     for_all_frames(lambda _idx, frame, _last_frame: frame_sizes.append(frame.size))
    418 
    419     # Make sure all frames are the same size
    420     if len(set(frame_sizes)) != 1:
    421         raise ValueError("Mismatching sizes on frames")
    422 
    423     # Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the
    424     # correct values once we've written all the frames to the output
    425     graphics_descriptor_location = fp.tell()
    426     graphics_descriptor = QGFGraphicsDescriptor()
    427     graphics_descriptor.frame_count = len(frame_sizes)
    428     graphics_descriptor.image_size = frame_sizes[0]
    429     vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
    430     graphics_descriptor.write(fp)
    431 
    432     # Work out the frame offset descriptor location (and write a dummy value), so that we can come back and fill in the
    433     # correct offsets once we've written all the frames to the output
    434     frame_offset_location = fp.tell()
    435     frame_offsets = QGFFrameOffsetDescriptorV1(graphics_descriptor.frame_count)
    436     vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
    437     frame_offsets.write(fp)
    438 
    439     # Iterate over each if the input frames, writing it to the output in the process
    440     write_frame = functools.partial(_write_frame, format_=encoderinfo["qmk_format"], fp=fp, use_deltas=encoderinfo.get("use_deltas", True), use_rle=encoderinfo.get("use_rle", True), frame_offsets=frame_offsets, metadata=metadata)
    441     for_all_frames(write_frame)
    442 
    443     # Go back and update the graphics descriptor now that we can determine the final file size
    444     graphics_descriptor.total_file_size = fp.tell()
    445     fp.seek(graphics_descriptor_location, 0)
    446     graphics_descriptor.write(fp)
    447 
    448     # Go back and update the frame offsets now that they're written to the file
    449     fp.seek(frame_offset_location, 0)
    450     frame_offsets.write(fp)
    451 
    452 
    453 ########################################################################################################################
    454 
    455 # Register with PIL so that it knows about the QGF format
    456 Image.register_open(QGFImageFile.format, QGFImageFile, _accept)
    457 Image.register_save(QGFImageFile.format, _save)
    458 Image.register_save_all(QGFImageFile.format, _save)
    459 Image.register_extension(QGFImageFile.format, f".{QGFImageFile.format.lower()}")
    460 Image.register_mime(QGFImageFile.format, f"image/{QGFImageFile.format.lower()}")