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

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

93 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 

27__lazy_modules__ = {"PIL._binary", "struct"} 

28 

29import struct 

30 

31from . import Image, ImageFile 

32from ._binary import i16le as i16 

33from ._binary import o16le as o16 

34 

35TYPE_CHECKING = False 

36if TYPE_CHECKING: 

37 from typing import IO 

38# 

39# read MSP files 

40 

41 

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

43 return prefix.startswith((b"DanM", b"LinS")) 

44 

45 

46## 

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

48# uncompressed (Windows 1.0). 

49 

50 

51class MspImageFile(ImageFile.ImageFile): 

52 format = "MSP" 

53 format_description = "Windows Paint" 

54 

55 def _open(self) -> None: 

56 # Header 

57 assert self.fp is not None 

58 

59 s = self.fp.read(32) 

60 if not _accept(s): 

61 msg = "not an MSP file" 

62 raise SyntaxError(msg) 

63 

64 # Header checksum 

65 checksum = 0 

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

67 checksum = checksum ^ i16(s, i) 

68 if checksum != 0: 

69 msg = "bad MSP checksum" 

70 raise SyntaxError(msg) 

71 

72 self._mode = "1" 

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

74 

75 if s.startswith(b"DanM"): 

76 self.tile = [ImageFile._Tile("raw", None, 32, "1")] 

77 else: 

78 self.tile = [ImageFile._Tile("MSP", None, 32)] 

79 

80 

81class MspDecoder(ImageFile.PyDecoder): 

82 # The algo for the MSP decoder is from 

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

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

85 # Encyclopedia of Graphics File Formats and is licensed by 

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

87 # 

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

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

90 # line. 

91 # 

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

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

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

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

96 # background color, in this case, white. 

97 # 

98 # 

99 # Pseudocode of the decoder: 

100 # Read a BYTE value as the RunType 

101 # If the RunType value is zero 

102 # Read next byte as the RunCount 

103 # Read the next byte as the RunValue 

104 # Write the RunValue byte RunCount times 

105 # If the RunType value is non-zero 

106 # Use this value as the RunCount 

107 # Read and write the next RunCount bytes literally 

108 # 

109 # e.g.: 

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

111 # would yield the bytes: 

112 # 0xff ff ff 00 01 02 03 04 

113 # 

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

115 

116 _pulls_fd = True 

117 

118 def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]: 

119 assert self.fd is not None 

120 

121 data = bytearray() 

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

123 try: 

124 self.fd.seek(32) 

125 rowmap = struct.unpack_from( 

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

127 ) 

128 except struct.error as e: 

129 msg = "Truncated MSP file in row map" 

130 raise OSError(msg) from e 

131 

132 for y, rowlen in enumerate(rowmap): 

133 try: 

134 if rowlen == 0: 

135 data += blank_line 

136 continue 

137 row = self.fd.read(rowlen) 

138 if len(row) != rowlen: 

139 msg = f"Truncated MSP file, expected {rowlen} bytes on row {y}" 

140 raise OSError(msg) 

141 idx = 0 

142 row_data = bytearray() 

143 while idx < rowlen: 

144 runtype = row[idx] 

145 idx += 1 

146 if runtype == 0: 

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

148 runcount = min(runcount, self.state.xsize - len(row_data)) 

149 row_data += runval * runcount 

150 idx += 2 

151 else: 

152 runcount = min(runtype, self.state.xsize - len(row_data)) 

153 row_data += row[idx : idx + runcount] 

154 idx += runcount 

155 data += row_data 

156 

157 except struct.error as e: 

158 msg = f"Corrupted MSP file in row {y}" 

159 raise OSError(msg) from e 

160 

161 self.set_as_raw(bytes(data), "1") 

162 

163 return -1, 0 

164 

165 

166Image.register_decoder("MSP", MspDecoder) 

167 

168 

169# 

170# write MSP files (uncompressed only) 

171 

172 

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

174 if im.mode != "1": 

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

176 raise OSError(msg) 

177 

178 # create MSP header 

179 header = [0] * 16 

180 

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

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

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

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

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

186 

187 checksum = 0 

188 for h in header: 

189 checksum = checksum ^ h 

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

191 

192 # header 

193 for h in header: 

194 fp.write(o16(h)) 

195 

196 # image body 

197 ImageFile._save(im, fp, [ImageFile._Tile("raw", None, 32, "1")]) 

198 

199 

200# 

201# registry 

202 

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

204Image.register_save(MspImageFile.format, _save) 

205 

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