Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/QoiImagePlugin.py: 42%

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

148 statements  

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 

9 

10import os 

11from typing import IO 

12 

13from . import Image, ImageFile 

14from ._binary import i32be as i32 

15from ._binary import o8 

16from ._binary import o32be as o32 

17 

18 

19def _accept(prefix: bytes) -> bool: 

20 return prefix.startswith(b"qoif") 

21 

22 

23class QoiImageFile(ImageFile.ImageFile): 

24 format = "QOI" 

25 format_description = "Quite OK Image" 

26 

27 def _open(self) -> None: 

28 if not _accept(self.fp.read(4)): 

29 msg = "not a QOI file" 

30 raise SyntaxError(msg) 

31 

32 self._size = i32(self.fp.read(4)), i32(self.fp.read(4)) 

33 

34 channels = self.fp.read(1)[0] 

35 self._mode = "RGB" if channels == 3 else "RGBA" 

36 

37 self.fp.seek(1, os.SEEK_CUR) # colorspace 

38 self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] 

39 

40 

41class QoiDecoder(ImageFile.PyDecoder): 

42 _pulls_fd = True 

43 _previous_pixel: bytes | bytearray | None = None 

44 _previously_seen_pixels: dict[int, bytes | bytearray] = {} 

45 

46 def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: 

47 self._previous_pixel = value 

48 

49 r, g, b, a = value 

50 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 

51 self._previously_seen_pixels[hash_value] = value 

52 

53 def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: 

54 assert self.fd is not None 

55 

56 self._previously_seen_pixels = {} 

57 self._previous_pixel = bytearray((0, 0, 0, 255)) 

58 

59 data = bytearray() 

60 bands = Image.getmodebands(self.mode) 

61 dest_length = self.state.xsize * self.state.ysize * bands 

62 while len(data) < dest_length: 

63 byte = self.fd.read(1)[0] 

64 value: bytes | bytearray 

65 if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB 

66 value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] 

67 elif byte == 0b11111111: # QOI_OP_RGBA 

68 value = self.fd.read(4) 

69 else: 

70 op = byte >> 6 

71 if op == 0: # QOI_OP_INDEX 

72 op_index = byte & 0b00111111 

73 value = self._previously_seen_pixels.get( 

74 op_index, bytearray((0, 0, 0, 0)) 

75 ) 

76 elif op == 1 and self._previous_pixel: # QOI_OP_DIFF 

77 value = bytearray( 

78 ( 

79 (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) 

80 % 256, 

81 (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) 

82 % 256, 

83 (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, 

84 self._previous_pixel[3], 

85 ) 

86 ) 

87 elif op == 2 and self._previous_pixel: # QOI_OP_LUMA 

88 second_byte = self.fd.read(1)[0] 

89 diff_green = (byte & 0b00111111) - 32 

90 diff_red = ((second_byte & 0b11110000) >> 4) - 8 

91 diff_blue = (second_byte & 0b00001111) - 8 

92 

93 value = bytearray( 

94 tuple( 

95 (self._previous_pixel[i] + diff_green + diff) % 256 

96 for i, diff in enumerate((diff_red, 0, diff_blue)) 

97 ) 

98 ) 

99 value += self._previous_pixel[3:] 

100 elif op == 3 and self._previous_pixel: # QOI_OP_RUN 

101 run_length = (byte & 0b00111111) + 1 

102 value = self._previous_pixel 

103 if bands == 3: 

104 value = value[:3] 

105 data += value * run_length 

106 continue 

107 self._add_to_previous_pixels(value) 

108 

109 if bands == 3: 

110 value = value[:3] 

111 data += value 

112 self.set_as_raw(data) 

113 return -1, 0 

114 

115 

116def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

117 if im.mode == "RGB": 

118 channels = 3 

119 elif im.mode == "RGBA": 

120 channels = 4 

121 else: 

122 msg = "Unsupported QOI image mode" 

123 raise ValueError(msg) 

124 

125 colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 

126 

127 fp.write(b"qoif") 

128 fp.write(o32(im.size[0])) 

129 fp.write(o32(im.size[1])) 

130 fp.write(o8(channels)) 

131 fp.write(o8(colorspace)) 

132 

133 ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) 

134 

135 

136class QoiEncoder(ImageFile.PyEncoder): 

137 _pushes_fd = True 

138 _previous_pixel: tuple[int, int, int, int] | None = None 

139 _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} 

140 _run = 0 

141 

142 def _write_run(self) -> bytes: 

143 data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN 

144 self._run = 0 

145 return data 

146 

147 def _delta(self, left: int, right: int) -> int: 

148 result = (left - right) & 255 

149 if result >= 128: 

150 result -= 256 

151 return result 

152 

153 def encode(self, bufsize: int) -> tuple[int, int, bytes]: 

154 assert self.im is not None 

155 

156 self._previously_seen_pixels = {0: (0, 0, 0, 0)} 

157 self._previous_pixel = (0, 0, 0, 255) 

158 

159 data = bytearray() 

160 w, h = self.im.size 

161 bands = Image.getmodebands(self.mode) 

162 

163 for y in range(h): 

164 for x in range(w): 

165 pixel = self.im.getpixel((x, y)) 

166 if bands == 3: 

167 pixel = (*pixel, 255) 

168 

169 if pixel == self._previous_pixel: 

170 self._run += 1 

171 if self._run == 62: 

172 data += self._write_run() 

173 else: 

174 if self._run: 

175 data += self._write_run() 

176 

177 r, g, b, a = pixel 

178 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 

179 if self._previously_seen_pixels.get(hash_value) == pixel: 

180 data += o8(hash_value) # QOI_OP_INDEX 

181 elif self._previous_pixel: 

182 self._previously_seen_pixels[hash_value] = pixel 

183 

184 prev_r, prev_g, prev_b, prev_a = self._previous_pixel 

185 if prev_a == a: 

186 delta_r = self._delta(r, prev_r) 

187 delta_g = self._delta(g, prev_g) 

188 delta_b = self._delta(b, prev_b) 

189 

190 if ( 

191 -2 <= delta_r < 2 

192 and -2 <= delta_g < 2 

193 and -2 <= delta_b < 2 

194 ): 

195 data += o8( 

196 0b01000000 

197 | (delta_r + 2) << 4 

198 | (delta_g + 2) << 2 

199 | (delta_b + 2) 

200 ) # QOI_OP_DIFF 

201 else: 

202 delta_gr = self._delta(delta_r, delta_g) 

203 delta_gb = self._delta(delta_b, delta_g) 

204 if ( 

205 -8 <= delta_gr < 8 

206 and -32 <= delta_g < 32 

207 and -8 <= delta_gb < 8 

208 ): 

209 data += o8( 

210 0b10000000 | (delta_g + 32) 

211 ) # QOI_OP_LUMA 

212 data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) 

213 else: 

214 data += o8(0b11111110) # QOI_OP_RGB 

215 data += bytes(pixel[:3]) 

216 else: 

217 data += o8(0b11111111) # QOI_OP_RGBA 

218 data += bytes(pixel) 

219 

220 self._previous_pixel = pixel 

221 

222 if self._run: 

223 data += self._write_run() 

224 data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding 

225 

226 return len(data), 0, data 

227 

228 

229Image.register_open(QoiImageFile.format, QoiImageFile, _accept) 

230Image.register_decoder("qoi", QoiDecoder) 

231Image.register_extension(QoiImageFile.format, ".qoi") 

232 

233Image.register_save(QoiImageFile.format, _save) 

234Image.register_encoder("qoi", QoiEncoder)