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