Pillow/src/PIL/QoiImagePlugin.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

116 lines
4.1 KiB
Python
Raw Normal View History

2023-03-09 05:34:44 +03:00
#
# The Python Imaging Library.
#
# QOI support for PIL
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations
2023-03-09 05:34:44 +03:00
import os
from . import Image, ImageFile
from ._binary import i32be as i32
2024-04-06 05:58:53 +03:00
def _accept(prefix: bytes) -> bool:
2023-03-09 05:34:44 +03:00
return prefix[:4] == b"qoif"
class QoiImageFile(ImageFile.ImageFile):
format = "QOI"
format_description = "Quite OK Image"
2024-05-11 03:48:09 +03:00
def _open(self) -> None:
2023-03-09 05:34:44 +03:00
if not _accept(self.fp.read(4)):
msg = "not a QOI file"
raise SyntaxError(msg)
2024-08-02 16:30:27 +03:00
self._size = i32(self.fp.read(4)), i32(self.fp.read(4))
2023-03-09 05:34:44 +03:00
channels = self.fp.read(1)[0]
self._mode = "RGB" if channels == 3 else "RGBA"
2023-03-09 05:34:44 +03:00
self.fp.seek(1, os.SEEK_CUR) # colorspace
2024-08-29 15:51:15 +03:00
self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)]
2023-03-09 05:34:44 +03:00
class QoiDecoder(ImageFile.PyDecoder):
_pulls_fd = True
2024-06-10 07:15:28 +03:00
_previous_pixel: bytes | bytearray | None = None
_previously_seen_pixels: dict[int, bytes | bytearray] = {}
2023-03-09 05:34:44 +03:00
2024-06-04 13:37:09 +03:00
def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
2023-03-09 05:34:44 +03:00
self._previous_pixel = value
r, g, b, a = value
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value
2024-09-06 08:16:59 +03:00
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
2024-06-10 07:15:28 +03:00
assert self.fd is not None
2023-03-09 05:34:44 +03:00
self._previously_seen_pixels = {}
self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
2023-03-09 05:34:44 +03:00
data = bytearray()
bands = Image.getmodebands(self.mode)
dest_length = self.state.xsize * self.state.ysize * bands
while len(data) < dest_length:
2023-03-09 05:34:44 +03:00
byte = self.fd.read(1)[0]
2024-06-10 07:15:28 +03:00
value: bytes | bytearray
if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
2023-03-09 05:34:44 +03:00
elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4)
else:
op = byte >> 6
if op == 0: # QOI_OP_INDEX
op_index = byte & 0b00111111
value = self._previously_seen_pixels.get(
op_index, bytearray((0, 0, 0, 0))
)
2024-06-10 07:15:28 +03:00
elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
value = bytearray(
(
(self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
% 256,
(self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
% 256,
(self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
self._previous_pixel[3],
)
2023-03-09 05:34:44 +03:00
)
2024-06-10 07:15:28 +03:00
elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
2023-03-09 05:34:44 +03:00
second_byte = self.fd.read(1)[0]
diff_green = (byte & 0b00111111) - 32
diff_red = ((second_byte & 0b11110000) >> 4) - 8
diff_blue = (second_byte & 0b00001111) - 8
value = bytearray(
tuple(
(self._previous_pixel[i] + diff_green + diff) % 256
for i, diff in enumerate((diff_red, 0, diff_blue))
)
2023-03-09 05:34:44 +03:00
)
value += self._previous_pixel[3:]
2024-06-10 07:15:28 +03:00
elif op == 3 and self._previous_pixel: # QOI_OP_RUN
2023-03-09 05:34:44 +03:00
run_length = (byte & 0b00111111) + 1
value = self._previous_pixel
if bands == 3:
value = value[:3]
data += value * run_length
continue
self._add_to_previous_pixels(value)
if bands == 3:
value = value[:3]
data += value
2024-04-01 07:24:40 +03:00
self.set_as_raw(data)
2023-03-09 05:34:44 +03:00
return -1, 0
Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
Image.register_decoder("qoi", QoiDecoder)
Image.register_extension(QoiImageFile.format, ".qoi")