logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

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