Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/QoiImagePlugin.py: 48%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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#
8from __future__ import annotations
10import os
11from typing import IO
13from . import Image, ImageFile
14from ._binary import i32be as i32
15from ._binary import o8
16from ._binary import o32be as o32
19def _accept(prefix: bytes) -> bool:
20 return prefix.startswith(b"qoif")
23class QoiImageFile(ImageFile.ImageFile):
24 format = "QOI"
25 format_description = "Quite OK Image"
27 def _open(self) -> None:
28 assert self.fp is not None
29 if not _accept(self.fp.read(4)):
30 msg = "not a QOI file"
31 raise SyntaxError(msg)
33 self._size = i32(self.fp.read(4)), i32(self.fp.read(4))
35 channels = self.fp.read(1)[0]
36 self._mode = "RGB" if channels == 3 else "RGBA"
38 self.fp.seek(1, os.SEEK_CUR) # colorspace
39 self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())]
42class QoiDecoder(ImageFile.PyDecoder):
43 _pulls_fd = True
44 _previous_pixel: bytes | bytearray | None = None
45 _previously_seen_pixels: dict[int, bytes | bytearray] = {}
47 def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
48 self._previous_pixel = value
50 r, g, b, a = value
51 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
52 self._previously_seen_pixels[hash_value] = value
54 def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
55 assert self.fd is not None
57 self._previously_seen_pixels = {}
58 self._previous_pixel = bytearray((0, 0, 0, 255))
60 data = bytearray()
61 bands = Image.getmodebands(self.mode)
62 dest_length = self.state.xsize * self.state.ysize * bands
63 while len(data) < dest_length:
64 byte = self.fd.read(1)[0]
65 value: bytes | bytearray
66 if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
67 value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
68 elif byte == 0b11111111: # QOI_OP_RGBA
69 value = self.fd.read(4)
70 else:
71 op = byte >> 6
72 if op == 0: # QOI_OP_INDEX
73 op_index = byte & 0b00111111
74 value = self._previously_seen_pixels.get(
75 op_index, bytearray((0, 0, 0, 0))
76 )
77 elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
78 value = bytearray(
79 (
80 (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
81 % 256,
82 (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
83 % 256,
84 (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
85 self._previous_pixel[3],
86 )
87 )
88 elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
89 second_byte = self.fd.read(1)[0]
90 diff_green = (byte & 0b00111111) - 32
91 diff_red = ((second_byte & 0b11110000) >> 4) - 8
92 diff_blue = (second_byte & 0b00001111) - 8
94 value = bytearray(
95 tuple(
96 (self._previous_pixel[i] + diff_green + diff) % 256
97 for i, diff in enumerate((diff_red, 0, diff_blue))
98 )
99 )
100 value += self._previous_pixel[3:]
101 elif op == 3 and self._previous_pixel: # QOI_OP_RUN
102 run_length = (byte & 0b00111111) + 1
103 value = self._previous_pixel
104 if bands == 3:
105 value = value[:3]
106 data += value * run_length
107 continue
108 self._add_to_previous_pixels(value)
110 if bands == 3:
111 value = value[:3]
112 data += value
113 self.set_as_raw(data)
114 return -1, 0
117def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
118 if im.mode == "RGB":
119 channels = 3
120 elif im.mode == "RGBA":
121 channels = 4
122 else:
123 msg = "Unsupported QOI image mode"
124 raise ValueError(msg)
126 colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
128 fp.write(b"qoif")
129 fp.write(o32(im.size[0]))
130 fp.write(o32(im.size[1]))
131 fp.write(o8(channels))
132 fp.write(o8(colorspace))
134 ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
137class QoiEncoder(ImageFile.PyEncoder):
138 _pushes_fd = True
139 _previous_pixel: tuple[int, int, int, int] | None = None
140 _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
141 _run = 0
143 def _write_run(self) -> bytes:
144 data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
145 self._run = 0
146 return data
148 def _delta(self, left: int, right: int) -> int:
149 result = (left - right) & 255
150 if result >= 128:
151 result -= 256
152 return result
154 def encode(self, bufsize: int) -> tuple[int, int, bytes]:
155 assert self.im is not None
157 self._previously_seen_pixels = {0: (0, 0, 0, 0)}
158 self._previous_pixel = (0, 0, 0, 255)
160 data = bytearray()
161 w, h = self.im.size
162 bands = Image.getmodebands(self.mode)
164 for y in range(h):
165 for x in range(w):
166 pixel = self.im.getpixel((x, y))
167 if bands == 3:
168 pixel = (*pixel, 255)
170 if pixel == self._previous_pixel:
171 self._run += 1
172 if self._run == 62:
173 data += self._write_run()
174 else:
175 if self._run:
176 data += self._write_run()
178 r, g, b, a = pixel
179 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
180 if self._previously_seen_pixels.get(hash_value) == pixel:
181 data += o8(hash_value) # QOI_OP_INDEX
182 elif self._previous_pixel:
183 self._previously_seen_pixels[hash_value] = pixel
185 prev_r, prev_g, prev_b, prev_a = self._previous_pixel
186 if prev_a == a:
187 delta_r = self._delta(r, prev_r)
188 delta_g = self._delta(g, prev_g)
189 delta_b = self._delta(b, prev_b)
191 if (
192 -2 <= delta_r < 2
193 and -2 <= delta_g < 2
194 and -2 <= delta_b < 2
195 ):
196 data += o8(
197 0b01000000
198 | (delta_r + 2) << 4
199 | (delta_g + 2) << 2
200 | (delta_b + 2)
201 ) # QOI_OP_DIFF
202 else:
203 delta_gr = self._delta(delta_r, delta_g)
204 delta_gb = self._delta(delta_b, delta_g)
205 if (
206 -8 <= delta_gr < 8
207 and -32 <= delta_g < 32
208 and -8 <= delta_gb < 8
209 ):
210 data += o8(
211 0b10000000 | (delta_g + 32)
212 ) # QOI_OP_LUMA
213 data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
214 else:
215 data += o8(0b11111110) # QOI_OP_RGB
216 data += bytes(pixel[:3])
217 else:
218 data += o8(0b11111111) # QOI_OP_RGBA
219 data += bytes(pixel)
221 self._previous_pixel = pixel
223 if self._run:
224 data += self._write_run()
225 data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
227 return len(data), 0, data
230Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
231Image.register_decoder("qoi", QoiDecoder)
232Image.register_extension(QoiImageFile.format, ".qoi")
234Image.register_save(QoiImageFile.format, _save)
235Image.register_encoder("qoi", QoiEncoder)