QoiImagePlugin.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  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 . import Image, ImageFile
  11. from ._binary import i32be as i32
  12. from ._binary import o8
  13. def _accept(prefix):
  14. return prefix[:4] == b"qoif"
  15. class QoiImageFile(ImageFile.ImageFile):
  16. format = "QOI"
  17. format_description = "Quite OK Image"
  18. def _open(self):
  19. if not _accept(self.fp.read(4)):
  20. msg = "not a QOI file"
  21. raise SyntaxError(msg)
  22. self._size = tuple(i32(self.fp.read(4)) for i in range(2))
  23. channels = self.fp.read(1)[0]
  24. self._mode = "RGB" if channels == 3 else "RGBA"
  25. self.fp.seek(1, os.SEEK_CUR) # colorspace
  26. self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)]
  27. class QoiDecoder(ImageFile.PyDecoder):
  28. _pulls_fd = True
  29. def _add_to_previous_pixels(self, value):
  30. self._previous_pixel = value
  31. r, g, b, a = value
  32. hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
  33. self._previously_seen_pixels[hash_value] = value
  34. def decode(self, buffer):
  35. self._previously_seen_pixels = {}
  36. self._previous_pixel = None
  37. self._add_to_previous_pixels(b"".join(o8(i) for i in (0, 0, 0, 255)))
  38. data = bytearray()
  39. bands = Image.getmodebands(self.mode)
  40. while len(data) < self.state.xsize * self.state.ysize * bands:
  41. byte = self.fd.read(1)[0]
  42. if byte == 0b11111110: # QOI_OP_RGB
  43. value = self.fd.read(3) + self._previous_pixel[3:]
  44. elif byte == 0b11111111: # QOI_OP_RGBA
  45. value = self.fd.read(4)
  46. else:
  47. op = byte >> 6
  48. if op == 0: # QOI_OP_INDEX
  49. op_index = byte & 0b00111111
  50. value = self._previously_seen_pixels.get(op_index, (0, 0, 0, 0))
  51. elif op == 1: # QOI_OP_DIFF
  52. value = (
  53. (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
  54. % 256,
  55. (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
  56. % 256,
  57. (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
  58. )
  59. value += (self._previous_pixel[3],)
  60. elif op == 2: # QOI_OP_LUMA
  61. second_byte = self.fd.read(1)[0]
  62. diff_green = (byte & 0b00111111) - 32
  63. diff_red = ((second_byte & 0b11110000) >> 4) - 8
  64. diff_blue = (second_byte & 0b00001111) - 8
  65. value = tuple(
  66. (self._previous_pixel[i] + diff_green + diff) % 256
  67. for i, diff in enumerate((diff_red, 0, diff_blue))
  68. )
  69. value += (self._previous_pixel[3],)
  70. elif op == 3: # QOI_OP_RUN
  71. run_length = (byte & 0b00111111) + 1
  72. value = self._previous_pixel
  73. if bands == 3:
  74. value = value[:3]
  75. data += value * run_length
  76. continue
  77. value = b"".join(o8(i) for i in value)
  78. self._add_to_previous_pixels(value)
  79. if bands == 3:
  80. value = value[:3]
  81. data += value
  82. self.set_as_raw(bytes(data))
  83. return -1, 0
  84. Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
  85. Image.register_decoder("qoi", QoiDecoder)
  86. Image.register_extension(QoiImageFile.format, ".qoi")