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

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

87 statements  

1# 

2# The Python Imaging Library. 

3# 

4# MSP file handling 

5# 

6# This is the format used by the Paint program in Windows 1 and 2. 

7# 

8# History: 

9# 95-09-05 fl Created 

10# 97-01-03 fl Read/write MSP images 

11# 17-02-21 es Fixed RLE interpretation 

12# 

13# Copyright (c) Secret Labs AB 1997. 

14# Copyright (c) Fredrik Lundh 1995-97. 

15# Copyright (c) Eric Soroos 2017. 

16# 

17# See the README file for information on usage and redistribution. 

18# 

19# More info on this format: https://archive.org/details/gg243631 

20# Page 313: 

21# Figure 205. Windows Paint Version 1: "DanM" Format 

22# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 

23# 

24# See also: https://www.fileformat.info/format/mspaint/egff.htm 

25from __future__ import annotations 

26 

27import io 

28import struct 

29from typing import IO 

30 

31from . import Image, ImageFile 

32from ._binary import i16le as i16 

33from ._binary import o16le as o16 

34 

35# 

36# read MSP files 

37 

38 

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

40 return prefix[:4] in [b"DanM", b"LinS"] 

41 

42 

43## 

44# Image plugin for Windows MSP images. This plugin supports both 

45# uncompressed (Windows 1.0). 

46 

47 

48class MspImageFile(ImageFile.ImageFile): 

49 format = "MSP" 

50 format_description = "Windows Paint" 

51 

52 def _open(self) -> None: 

53 # Header 

54 assert self.fp is not None 

55 

56 s = self.fp.read(32) 

57 if not _accept(s): 

58 msg = "not an MSP file" 

59 raise SyntaxError(msg) 

60 

61 # Header checksum 

62 checksum = 0 

63 for i in range(0, 32, 2): 

64 checksum = checksum ^ i16(s, i) 

65 if checksum != 0: 

66 msg = "bad MSP checksum" 

67 raise SyntaxError(msg) 

68 

69 self._mode = "1" 

70 self._size = i16(s, 4), i16(s, 6) 

71 

72 if s[:4] == b"DanM": 

73 self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] 

74 else: 

75 self.tile = [("MSP", (0, 0) + self.size, 32, None)] 

76 

77 

78class MspDecoder(ImageFile.PyDecoder): 

79 # The algo for the MSP decoder is from 

80 # https://www.fileformat.info/format/mspaint/egff.htm 

81 # cc-by-attribution -- That page references is taken from the 

82 # Encyclopedia of Graphics File Formats and is licensed by 

83 # O'Reilly under the Creative Common/Attribution license 

84 # 

85 # For RLE encoded files, the 32byte header is followed by a scan 

86 # line map, encoded as one 16bit word of encoded byte length per 

87 # line. 

88 # 

89 # NOTE: the encoded length of the line can be 0. This was not 

90 # handled in the previous version of this encoder, and there's no 

91 # mention of how to handle it in the documentation. From the few 

92 # examples I've seen, I've assumed that it is a fill of the 

93 # background color, in this case, white. 

94 # 

95 # 

96 # Pseudocode of the decoder: 

97 # Read a BYTE value as the RunType 

98 # If the RunType value is zero 

99 # Read next byte as the RunCount 

100 # Read the next byte as the RunValue 

101 # Write the RunValue byte RunCount times 

102 # If the RunType value is non-zero 

103 # Use this value as the RunCount 

104 # Read and write the next RunCount bytes literally 

105 # 

106 # e.g.: 

107 # 0x00 03 ff 05 00 01 02 03 04 

108 # would yield the bytes: 

109 # 0xff ff ff 00 01 02 03 04 

110 # 

111 # which are then interpreted as a bit packed mode '1' image 

112 

113 _pulls_fd = True 

114 

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

116 assert self.fd is not None 

117 

118 img = io.BytesIO() 

119 blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) 

120 try: 

121 self.fd.seek(32) 

122 rowmap = struct.unpack_from( 

123 f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) 

124 ) 

125 except struct.error as e: 

126 msg = "Truncated MSP file in row map" 

127 raise OSError(msg) from e 

128 

129 for x, rowlen in enumerate(rowmap): 

130 try: 

131 if rowlen == 0: 

132 img.write(blank_line) 

133 continue 

134 row = self.fd.read(rowlen) 

135 if len(row) != rowlen: 

136 msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" 

137 raise OSError(msg) 

138 idx = 0 

139 while idx < rowlen: 

140 runtype = row[idx] 

141 idx += 1 

142 if runtype == 0: 

143 (runcount, runval) = struct.unpack_from("Bc", row, idx) 

144 img.write(runval * runcount) 

145 idx += 2 

146 else: 

147 runcount = runtype 

148 img.write(row[idx : idx + runcount]) 

149 idx += runcount 

150 

151 except struct.error as e: 

152 msg = f"Corrupted MSP file in row {x}" 

153 raise OSError(msg) from e 

154 

155 self.set_as_raw(img.getvalue(), ("1", 0, 1)) 

156 

157 return -1, 0 

158 

159 

160Image.register_decoder("MSP", MspDecoder) 

161 

162 

163# 

164# write MSP files (uncompressed only) 

165 

166 

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

168 if im.mode != "1": 

169 msg = f"cannot write mode {im.mode} as MSP" 

170 raise OSError(msg) 

171 

172 # create MSP header 

173 header = [0] * 16 

174 

175 header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 

176 header[2], header[3] = im.size 

177 header[4], header[5] = 1, 1 

178 header[6], header[7] = 1, 1 

179 header[8], header[9] = im.size 

180 

181 checksum = 0 

182 for h in header: 

183 checksum = checksum ^ h 

184 header[12] = checksum # FIXME: is this the right field? 

185 

186 # header 

187 for h in header: 

188 fp.write(o16(h)) 

189 

190 # image body 

191 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) 

192 

193 

194# 

195# registry 

196 

197Image.register_open(MspImageFile.format, MspImageFile, _accept) 

198Image.register_save(MspImageFile.format, _save) 

199 

200Image.register_extension(MspImageFile.format, ".msp")