QoiImagePlugin.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #
  2. # The Python Imaging Library.
  3. #
  4. # QOI support for PIL
  5. #
  6. # See the README file for information on usage and redistribution.
  7. #
  8. from __future__ import annotations
  9. import os
  10. from typing import IO
  11. from . import Image, ImageFile
  12. from ._binary import i32be as i32
  13. from ._binary import o8
  14. from ._binary import o32be as o32
  15. def _accept(prefix: bytes) -> bool:
  16. return prefix.startswith(b"qoif")
  17. class QoiImageFile(ImageFile.ImageFile):
  18. format = "QOI"
  19. format_description = "Quite OK Image"
  20. def _open(self) -> None:
  21. assert self.fp is not None
  22. if not _accept(self.fp.read(4)):
  23. msg = "not a QOI file"
  24. raise SyntaxError(msg)
  25. self._size = i32(self.fp.read(4)), i32(self.fp.read(4))
  26. channels = self.fp.read(1)[0]
  27. self._mode = "RGB" if channels == 3 else "RGBA"
  28. self.fp.seek(1, os.SEEK_CUR) # colorspace
  29. self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())]
  30. class QoiDecoder(ImageFile.PyDecoder):
  31. _pulls_fd = True
  32. _previous_pixel: bytes | bytearray | None = None
  33. _previously_seen_pixels: dict[int, bytes | bytearray] = {}
  34. def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
  35. self._previous_pixel = value
  36. r, g, b, a = value
  37. hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
  38. self._previously_seen_pixels[hash_value] = value
  39. def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
  40. assert self.fd is not None
  41. self._previously_seen_pixels = {}
  42. self._previous_pixel = bytearray((0, 0, 0, 255))
  43. data = bytearray()
  44. bands = Image.getmodebands(self.mode)
  45. dest_length = self.state.xsize * self.state.ysize * bands
  46. while len(data) < dest_length:
  47. byte = self.fd.read(1)[0]
  48. value: bytes | bytearray
  49. if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
  50. value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
  51. elif byte == 0b11111111: # QOI_OP_RGBA
  52. value = self.fd.read(4)
  53. else:
  54. op = byte >> 6
  55. if op == 0: # QOI_OP_INDEX
  56. op_index = byte & 0b00111111
  57. value = self._previously_seen_pixels.get(
  58. op_index, bytearray((0, 0, 0, 0))
  59. )
  60. elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
  61. value = bytearray(
  62. (
  63. (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
  64. % 256,
  65. (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
  66. % 256,
  67. (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
  68. self._previous_pixel[3],
  69. )
  70. )
  71. elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
  72. second_byte = self.fd.read(1)[0]
  73. diff_green = (byte & 0b00111111) - 32
  74. diff_red = ((second_byte & 0b11110000) >> 4) - 8
  75. diff_blue = (second_byte & 0b00001111) - 8
  76. value = bytearray(
  77. tuple(
  78. (self._previous_pixel[i] + diff_green + diff) % 256
  79. for i, diff in enumerate((diff_red, 0, diff_blue))
  80. )
  81. )
  82. value += self._previous_pixel[3:]
  83. elif op == 3 and self._previous_pixel: # QOI_OP_RUN
  84. run_length = (byte & 0b00111111) + 1
  85. value = self._previous_pixel
  86. if bands == 3:
  87. value = value[:3]
  88. data += value * run_length
  89. continue
  90. self._add_to_previous_pixels(value)
  91. if bands == 3:
  92. value = value[:3]
  93. data += value
  94. self.set_as_raw(data)
  95. return -1, 0
  96. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  97. if im.mode == "RGB":
  98. channels = 3
  99. elif im.mode == "RGBA":
  100. channels = 4
  101. else:
  102. msg = "Unsupported QOI image mode"
  103. raise ValueError(msg)
  104. colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
  105. fp.write(b"qoif")
  106. fp.write(o32(im.size[0]))
  107. fp.write(o32(im.size[1]))
  108. fp.write(o8(channels))
  109. fp.write(o8(colorspace))
  110. ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
  111. class QoiEncoder(ImageFile.PyEncoder):
  112. _pushes_fd = True
  113. _previous_pixel: tuple[int, int, int, int] | None = None
  114. _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
  115. _run = 0
  116. def _write_run(self) -> bytes:
  117. data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
  118. self._run = 0
  119. return data
  120. def _delta(self, left: int, right: int) -> int:
  121. result = (left - right) & 255
  122. if result >= 128:
  123. result -= 256
  124. return result
  125. def encode(self, bufsize: int) -> tuple[int, int, bytes]:
  126. assert self.im is not None
  127. self._previously_seen_pixels = {0: (0, 0, 0, 0)}
  128. self._previous_pixel = (0, 0, 0, 255)
  129. data = bytearray()
  130. w, h = self.im.size
  131. bands = Image.getmodebands(self.mode)
  132. for y in range(h):
  133. for x in range(w):
  134. pixel = self.im.getpixel((x, y))
  135. if bands == 3:
  136. pixel = (*pixel, 255)
  137. if pixel == self._previous_pixel:
  138. self._run += 1
  139. if self._run == 62:
  140. data += self._write_run()
  141. else:
  142. if self._run:
  143. data += self._write_run()
  144. r, g, b, a = pixel
  145. hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
  146. if self._previously_seen_pixels.get(hash_value) == pixel:
  147. data += o8(hash_value) # QOI_OP_INDEX
  148. elif self._previous_pixel:
  149. self._previously_seen_pixels[hash_value] = pixel
  150. prev_r, prev_g, prev_b, prev_a = self._previous_pixel
  151. if prev_a == a:
  152. delta_r = self._delta(r, prev_r)
  153. delta_g = self._delta(g, prev_g)
  154. delta_b = self._delta(b, prev_b)
  155. if (
  156. -2 <= delta_r < 2
  157. and -2 <= delta_g < 2
  158. and -2 <= delta_b < 2
  159. ):
  160. data += o8(
  161. 0b01000000
  162. | (delta_r + 2) << 4
  163. | (delta_g + 2) << 2
  164. | (delta_b + 2)
  165. ) # QOI_OP_DIFF
  166. else:
  167. delta_gr = self._delta(delta_r, delta_g)
  168. delta_gb = self._delta(delta_b, delta_g)
  169. if (
  170. -8 <= delta_gr < 8
  171. and -32 <= delta_g < 32
  172. and -8 <= delta_gb < 8
  173. ):
  174. data += o8(
  175. 0b10000000 | (delta_g + 32)
  176. ) # QOI_OP_LUMA
  177. data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
  178. else:
  179. data += o8(0b11111110) # QOI_OP_RGB
  180. data += bytes(pixel[:3])
  181. else:
  182. data += o8(0b11111111) # QOI_OP_RGBA
  183. data += bytes(pixel)
  184. self._previous_pixel = pixel
  185. if self._run:
  186. data += self._write_run()
  187. data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
  188. return len(data), 0, data
  189. Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
  190. Image.register_decoder("qoi", QoiDecoder)
  191. Image.register_extension(QoiImageFile.format, ".qoi")
  192. Image.register_save(QoiImageFile.format, _save)
  193. Image.register_encoder("qoi", QoiEncoder)