Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/QoiImagePlugin.py: 28%

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

69 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 

11 

12from . import Image, ImageFile 

13from ._binary import i32be as i32 

14 

15 

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

17 return prefix[:4] == b"qoif" 

18 

19 

20class QoiImageFile(ImageFile.ImageFile): 

21 format = "QOI" 

22 format_description = "Quite OK Image" 

23 

24 def _open(self) -> None: 

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 

38class QoiDecoder(ImageFile.PyDecoder): 

39 _pulls_fd = True 

40 _previous_pixel: bytes | bytearray | None = None 

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

42 

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

44 self._previous_pixel = value 

45 

46 r, g, b, a = value 

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

48 self._previously_seen_pixels[hash_value] = value 

49 

50 def decode(self, buffer: bytes) -> tuple[int, int]: 

51 assert self.fd is not None 

52 

53 self._previously_seen_pixels = {} 

54 self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) 

55 

56 data = bytearray() 

57 bands = Image.getmodebands(self.mode) 

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

59 while len(data) < dest_length: 

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

61 value: bytes | bytearray 

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

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

64 elif byte == 0b11111111: # QOI_OP_RGBA 

65 value = self.fd.read(4) 

66 else: 

67 op = byte >> 6 

68 if op == 0: # QOI_OP_INDEX 

69 op_index = byte & 0b00111111 

70 value = self._previously_seen_pixels.get( 

71 op_index, bytearray((0, 0, 0, 0)) 

72 ) 

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

74 value = bytearray( 

75 ( 

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

77 % 256, 

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

79 % 256, 

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

81 self._previous_pixel[3], 

82 ) 

83 ) 

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

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

86 diff_green = (byte & 0b00111111) - 32 

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

88 diff_blue = (second_byte & 0b00001111) - 8 

89 

90 value = bytearray( 

91 tuple( 

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

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

94 ) 

95 ) 

96 value += self._previous_pixel[3:] 

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

98 run_length = (byte & 0b00111111) + 1 

99 value = self._previous_pixel 

100 if bands == 3: 

101 value = value[:3] 

102 data += value * run_length 

103 continue 

104 self._add_to_previous_pixels(value) 

105 

106 if bands == 3: 

107 value = value[:3] 

108 data += value 

109 self.set_as_raw(data) 

110 return -1, 0 

111 

112 

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

114Image.register_decoder("qoi", QoiDecoder) 

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