painter_qgf.py 15 KB

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