| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- #
- # The Python Imaging Library.
- #
- # QOI support for PIL
- #
- # See the README file for information on usage and redistribution.
- #
- from __future__ import annotations
- import os
- from typing import IO
- from . import Image, ImageFile
- from ._binary import i32be as i32
- from ._binary import o8
- from ._binary import o32be as o32
- def _accept(prefix: bytes) -> bool:
- return prefix.startswith(b"qoif")
- class QoiImageFile(ImageFile.ImageFile):
- format = "QOI"
- format_description = "Quite OK Image"
- def _open(self) -> None:
- if not _accept(self.fp.read(4)):
- msg = "not a QOI file"
- raise SyntaxError(msg)
- self._size = i32(self.fp.read(4)), i32(self.fp.read(4))
- channels = self.fp.read(1)[0]
- self._mode = "RGB" if channels == 3 else "RGBA"
- self.fp.seek(1, os.SEEK_CUR) # colorspace
- self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())]
- class QoiDecoder(ImageFile.PyDecoder):
- _pulls_fd = True
- _previous_pixel: bytes | bytearray | None = None
- _previously_seen_pixels: dict[int, bytes | bytearray] = {}
- def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
- 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
- def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
- assert self.fd is not None
- self._previously_seen_pixels = {}
- self._previous_pixel = bytearray((0, 0, 0, 255))
- data = bytearray()
- bands = Image.getmodebands(self.mode)
- dest_length = self.state.xsize * self.state.ysize * bands
- while len(data) < dest_length:
- byte = self.fd.read(1)[0]
- value: bytes | bytearray
- if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
- value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
- 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))
- )
- 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],
- )
- )
- elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
- 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))
- )
- )
- value += self._previous_pixel[3:]
- elif op == 3 and self._previous_pixel: # QOI_OP_RUN
- 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
- self.set_as_raw(data)
- return -1, 0
- def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
- if im.mode == "RGB":
- channels = 3
- elif im.mode == "RGBA":
- channels = 4
- else:
- msg = "Unsupported QOI image mode"
- raise ValueError(msg)
- colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
- fp.write(b"qoif")
- fp.write(o32(im.size[0]))
- fp.write(o32(im.size[1]))
- fp.write(o8(channels))
- fp.write(o8(colorspace))
- ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
- class QoiEncoder(ImageFile.PyEncoder):
- _pushes_fd = True
- _previous_pixel: tuple[int, int, int, int] | None = None
- _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
- _run = 0
- def _write_run(self) -> bytes:
- data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
- self._run = 0
- return data
- def _delta(self, left: int, right: int) -> int:
- result = (left - right) & 255
- if result >= 128:
- result -= 256
- return result
- def encode(self, bufsize: int) -> tuple[int, int, bytes]:
- assert self.im is not None
- self._previously_seen_pixels = {0: (0, 0, 0, 0)}
- self._previous_pixel = (0, 0, 0, 255)
- data = bytearray()
- w, h = self.im.size
- bands = Image.getmodebands(self.mode)
- for y in range(h):
- for x in range(w):
- pixel = self.im.getpixel((x, y))
- if bands == 3:
- pixel = (*pixel, 255)
- if pixel == self._previous_pixel:
- self._run += 1
- if self._run == 62:
- data += self._write_run()
- else:
- if self._run:
- data += self._write_run()
- r, g, b, a = pixel
- hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
- if self._previously_seen_pixels.get(hash_value) == pixel:
- data += o8(hash_value) # QOI_OP_INDEX
- elif self._previous_pixel:
- self._previously_seen_pixels[hash_value] = pixel
- prev_r, prev_g, prev_b, prev_a = self._previous_pixel
- if prev_a == a:
- delta_r = self._delta(r, prev_r)
- delta_g = self._delta(g, prev_g)
- delta_b = self._delta(b, prev_b)
- if (
- -2 <= delta_r < 2
- and -2 <= delta_g < 2
- and -2 <= delta_b < 2
- ):
- data += o8(
- 0b01000000
- | (delta_r + 2) << 4
- | (delta_g + 2) << 2
- | (delta_b + 2)
- ) # QOI_OP_DIFF
- else:
- delta_gr = self._delta(delta_r, delta_g)
- delta_gb = self._delta(delta_b, delta_g)
- if (
- -8 <= delta_gr < 8
- and -32 <= delta_g < 32
- and -8 <= delta_gb < 8
- ):
- data += o8(
- 0b10000000 | (delta_g + 32)
- ) # QOI_OP_LUMA
- data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
- else:
- data += o8(0b11111110) # QOI_OP_RGB
- data += bytes(pixel[:3])
- else:
- data += o8(0b11111111) # QOI_OP_RGBA
- data += bytes(pixel)
- self._previous_pixel = pixel
- if self._run:
- data += self._write_run()
- data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
- return len(data), 0, data
- Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
- Image.register_decoder("qoi", QoiDecoder)
- Image.register_extension(QoiImageFile.format, ".qoi")
- Image.register_save(QoiImageFile.format, _save)
- Image.register_encoder("qoi", QoiEncoder)
|