Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/IcoImagePlugin.py: 16%

165 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# Windows Icon support for PIL 

6# 

7# History: 

8# 96-05-27 fl Created 

9# 

10# Copyright (c) Secret Labs AB 1997. 

11# Copyright (c) Fredrik Lundh 1996. 

12# 

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

14# 

15 

16# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis 

17# <casadebender@gmail.com>. 

18# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki 

19# 

20# Icon format references: 

21# * https://en.wikipedia.org/wiki/ICO_(file_format) 

22# * https://msdn.microsoft.com/en-us/library/ms997538.aspx 

23 

24 

25import warnings 

26from io import BytesIO 

27from math import ceil, log 

28 

29from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin 

30from ._binary import i16le as i16 

31from ._binary import i32le as i32 

32from ._binary import o8 

33from ._binary import o16le as o16 

34from ._binary import o32le as o32 

35 

36# 

37# -------------------------------------------------------------------- 

38 

39_MAGIC = b"\0\0\1\0" 

40 

41 

42def _save(im, fp, filename): 

43 fp.write(_MAGIC) # (2+2) 

44 bmp = im.encoderinfo.get("bitmap_format") == "bmp" 

45 sizes = im.encoderinfo.get( 

46 "sizes", 

47 [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], 

48 ) 

49 frames = [] 

50 provided_ims = [im] + im.encoderinfo.get("append_images", []) 

51 width, height = im.size 

52 for size in sorted(set(sizes)): 

53 if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: 

54 continue 

55 

56 for provided_im in provided_ims: 

57 if provided_im.size != size: 

58 continue 

59 frames.append(provided_im) 

60 if bmp: 

61 bits = BmpImagePlugin.SAVE[provided_im.mode][1] 

62 bits_used = [bits] 

63 for other_im in provided_ims: 

64 if other_im.size != size: 

65 continue 

66 bits = BmpImagePlugin.SAVE[other_im.mode][1] 

67 if bits not in bits_used: 

68 # Another image has been supplied for this size 

69 # with a different bit depth 

70 frames.append(other_im) 

71 bits_used.append(bits) 

72 break 

73 else: 

74 # TODO: invent a more convenient method for proportional scalings 

75 frame = provided_im.copy() 

76 frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) 

77 frames.append(frame) 

78 fp.write(o16(len(frames))) # idCount(2) 

79 offset = fp.tell() + len(frames) * 16 

80 for frame in frames: 

81 width, height = frame.size 

82 # 0 means 256 

83 fp.write(o8(width if width < 256 else 0)) # bWidth(1) 

84 fp.write(o8(height if height < 256 else 0)) # bHeight(1) 

85 

86 bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) 

87 fp.write(o8(colors)) # bColorCount(1) 

88 fp.write(b"\0") # bReserved(1) 

89 fp.write(b"\0\0") # wPlanes(2) 

90 fp.write(o16(bits)) # wBitCount(2) 

91 

92 image_io = BytesIO() 

93 if bmp: 

94 frame.save(image_io, "dib") 

95 

96 if bits != 32: 

97 and_mask = Image.new("1", size) 

98 ImageFile._save( 

99 and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] 

100 ) 

101 else: 

102 frame.save(image_io, "png") 

103 image_io.seek(0) 

104 image_bytes = image_io.read() 

105 if bmp: 

106 image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] 

107 bytes_len = len(image_bytes) 

108 fp.write(o32(bytes_len)) # dwBytesInRes(4) 

109 fp.write(o32(offset)) # dwImageOffset(4) 

110 current = fp.tell() 

111 fp.seek(offset) 

112 fp.write(image_bytes) 

113 offset = offset + bytes_len 

114 fp.seek(current) 

115 

116 

117def _accept(prefix): 

118 return prefix[:4] == _MAGIC 

119 

120 

121class IcoFile: 

122 def __init__(self, buf): 

123 """ 

124 Parse image from file-like object containing ico file data 

125 """ 

126 

127 # check magic 

128 s = buf.read(6) 

129 if not _accept(s): 

130 raise SyntaxError("not an ICO file") 

131 

132 self.buf = buf 

133 self.entry = [] 

134 

135 # Number of items in file 

136 self.nb_items = i16(s, 4) 

137 

138 # Get headers for each item 

139 for i in range(self.nb_items): 

140 s = buf.read(16) 

141 

142 icon_header = { 

143 "width": s[0], 

144 "height": s[1], 

145 "nb_color": s[2], # No. of colors in image (0 if >=8bpp) 

146 "reserved": s[3], 

147 "planes": i16(s, 4), 

148 "bpp": i16(s, 6), 

149 "size": i32(s, 8), 

150 "offset": i32(s, 12), 

151 } 

152 

153 # See Wikipedia 

154 for j in ("width", "height"): 

155 if not icon_header[j]: 

156 icon_header[j] = 256 

157 

158 # See Wikipedia notes about color depth. 

159 # We need this just to differ images with equal sizes 

160 icon_header["color_depth"] = ( 

161 icon_header["bpp"] 

162 or ( 

163 icon_header["nb_color"] != 0 

164 and ceil(log(icon_header["nb_color"], 2)) 

165 ) 

166 or 256 

167 ) 

168 

169 icon_header["dim"] = (icon_header["width"], icon_header["height"]) 

170 icon_header["square"] = icon_header["width"] * icon_header["height"] 

171 

172 self.entry.append(icon_header) 

173 

174 self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) 

175 # ICO images are usually squares 

176 # self.entry = sorted(self.entry, key=lambda x: x['width']) 

177 self.entry = sorted(self.entry, key=lambda x: x["square"]) 

178 self.entry.reverse() 

179 

180 def sizes(self): 

181 """ 

182 Get a list of all available icon sizes and color depths. 

183 """ 

184 return {(h["width"], h["height"]) for h in self.entry} 

185 

186 def getentryindex(self, size, bpp=False): 

187 for (i, h) in enumerate(self.entry): 

188 if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): 

189 return i 

190 return 0 

191 

192 def getimage(self, size, bpp=False): 

193 """ 

194 Get an image from the icon 

195 """ 

196 return self.frame(self.getentryindex(size, bpp)) 

197 

198 def frame(self, idx): 

199 """ 

200 Get an image from frame idx 

201 """ 

202 

203 header = self.entry[idx] 

204 

205 self.buf.seek(header["offset"]) 

206 data = self.buf.read(8) 

207 self.buf.seek(header["offset"]) 

208 

209 if data[:8] == PngImagePlugin._MAGIC: 

210 # png frame 

211 im = PngImagePlugin.PngImageFile(self.buf) 

212 Image._decompression_bomb_check(im.size) 

213 else: 

214 # XOR + AND mask bmp frame 

215 im = BmpImagePlugin.DibImageFile(self.buf) 

216 Image._decompression_bomb_check(im.size) 

217 

218 # change tile dimension to only encompass XOR image 

219 im._size = (im.size[0], int(im.size[1] / 2)) 

220 d, e, o, a = im.tile[0] 

221 im.tile[0] = d, (0, 0) + im.size, o, a 

222 

223 # figure out where AND mask image starts 

224 bpp = header["bpp"] 

225 if 32 == bpp: 

226 # 32-bit color depth icon image allows semitransparent areas 

227 # PIL's DIB format ignores transparency bits, recover them. 

228 # The DIB is packed in BGRX byte order where X is the alpha 

229 # channel. 

230 

231 # Back up to start of bmp data 

232 self.buf.seek(o) 

233 # extract every 4th byte (eg. 3,7,11,15,...) 

234 alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] 

235 

236 # convert to an 8bpp grayscale image 

237 mask = Image.frombuffer( 

238 "L", # 8bpp 

239 im.size, # (w, h) 

240 alpha_bytes, # source chars 

241 "raw", # raw decoder 

242 ("L", 0, -1), # 8bpp inverted, unpadded, reversed 

243 ) 

244 else: 

245 # get AND image from end of bitmap 

246 w = im.size[0] 

247 if (w % 32) > 0: 

248 # bitmap row data is aligned to word boundaries 

249 w += 32 - (im.size[0] % 32) 

250 

251 # the total mask data is 

252 # padded row size * height / bits per char 

253 

254 total_bytes = int((w * im.size[1]) / 8) 

255 and_mask_offset = header["offset"] + header["size"] - total_bytes 

256 

257 self.buf.seek(and_mask_offset) 

258 mask_data = self.buf.read(total_bytes) 

259 

260 # convert raw data to image 

261 mask = Image.frombuffer( 

262 "1", # 1 bpp 

263 im.size, # (w, h) 

264 mask_data, # source chars 

265 "raw", # raw decoder 

266 ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed 

267 ) 

268 

269 # now we have two images, im is XOR image and mask is AND image 

270 

271 # apply mask image as alpha channel 

272 im = im.convert("RGBA") 

273 im.putalpha(mask) 

274 

275 return im 

276 

277 

278## 

279# Image plugin for Windows Icon files. 

280 

281 

282class IcoImageFile(ImageFile.ImageFile): 

283 """ 

284 PIL read-only image support for Microsoft Windows .ico files. 

285 

286 By default the largest resolution image in the file will be loaded. This 

287 can be changed by altering the 'size' attribute before calling 'load'. 

288 

289 The info dictionary has a key 'sizes' that is a list of the sizes available 

290 in the icon file. 

291 

292 Handles classic, XP and Vista icon formats. 

293 

294 When saving, PNG compression is used. Support for this was only added in 

295 Windows Vista. If you are unable to view the icon in Windows, convert the 

296 image to "RGBA" mode before saving. 

297 

298 This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis 

299 <casadebender@gmail.com>. 

300 https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki 

301 """ 

302 

303 format = "ICO" 

304 format_description = "Windows Icon" 

305 

306 def _open(self): 

307 self.ico = IcoFile(self.fp) 

308 self.info["sizes"] = self.ico.sizes() 

309 self.size = self.ico.entry[0]["dim"] 

310 self.load() 

311 

312 @property 

313 def size(self): 

314 return self._size 

315 

316 @size.setter 

317 def size(self, value): 

318 if value not in self.info["sizes"]: 

319 raise ValueError("This is not one of the allowed sizes of this image") 

320 self._size = value 

321 

322 def load(self): 

323 if self.im is not None and self.im.size == self.size: 

324 # Already loaded 

325 return Image.Image.load(self) 

326 im = self.ico.getimage(self.size) 

327 # if tile is PNG, it won't really be loaded yet 

328 im.load() 

329 self.im = im.im 

330 self.mode = im.mode 

331 if im.size != self.size: 

332 warnings.warn("Image was not the expected size") 

333 

334 index = self.ico.getentryindex(self.size) 

335 sizes = list(self.info["sizes"]) 

336 sizes[index] = im.size 

337 self.info["sizes"] = set(sizes) 

338 

339 self.size = im.size 

340 

341 def load_seek(self): 

342 # Flag the ImageFile.Parser so that it 

343 # just does all the decode at the end. 

344 pass 

345 

346 

347# 

348# -------------------------------------------------------------------- 

349 

350 

351Image.register_open(IcoImageFile.format, IcoImageFile, _accept) 

352Image.register_save(IcoImageFile.format, _save) 

353Image.register_extension(IcoImageFile.format, ".ico") 

354 

355Image.register_mime(IcoImageFile.format, "image/x-icon")