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

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

171 statements  

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 

23from __future__ import annotations 

24 

25import warnings 

26from io import BytesIO 

27from math import ceil, log 

28from typing import IO 

29 

30from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin 

31from ._binary import i16le as i16 

32from ._binary import i32le as i32 

33from ._binary import o8 

34from ._binary import o16le as o16 

35from ._binary import o32le as o32 

36 

37# 

38# -------------------------------------------------------------------- 

39 

40_MAGIC = b"\0\0\1\0" 

41 

42 

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

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

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

46 sizes = im.encoderinfo.get( 

47 "sizes", 

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

49 ) 

50 frames = [] 

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

52 width, height = im.size 

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

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

55 continue 

56 

57 for provided_im in provided_ims: 

58 if provided_im.size != size: 

59 continue 

60 frames.append(provided_im) 

61 if bmp: 

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

63 bits_used = [bits] 

64 for other_im in provided_ims: 

65 if other_im.size != size: 

66 continue 

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

68 if bits not in bits_used: 

69 # Another image has been supplied for this size 

70 # with a different bit depth 

71 frames.append(other_im) 

72 bits_used.append(bits) 

73 break 

74 else: 

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

76 frame = provided_im.copy() 

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

78 frames.append(frame) 

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

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

81 for frame in frames: 

82 width, height = frame.size 

83 # 0 means 256 

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

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

86 

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

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

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

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

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

92 

93 image_io = BytesIO() 

94 if bmp: 

95 frame.save(image_io, "dib") 

96 

97 if bits != 32: 

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

99 ImageFile._save( 

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

101 ) 

102 else: 

103 frame.save(image_io, "png") 

104 image_io.seek(0) 

105 image_bytes = image_io.read() 

106 if bmp: 

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

108 bytes_len = len(image_bytes) 

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

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

111 current = fp.tell() 

112 fp.seek(offset) 

113 fp.write(image_bytes) 

114 offset = offset + bytes_len 

115 fp.seek(current) 

116 

117 

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

119 return prefix[:4] == _MAGIC 

120 

121 

122class IcoFile: 

123 def __init__(self, buf): 

124 """ 

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

126 """ 

127 

128 # check magic 

129 s = buf.read(6) 

130 if not _accept(s): 

131 msg = "not an ICO file" 

132 raise SyntaxError(msg) 

133 

134 self.buf = buf 

135 self.entry = [] 

136 

137 # Number of items in file 

138 self.nb_items = i16(s, 4) 

139 

140 # Get headers for each item 

141 for i in range(self.nb_items): 

142 s = buf.read(16) 

143 

144 icon_header = { 

145 "width": s[0], 

146 "height": s[1], 

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

148 "reserved": s[3], 

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

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

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

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

153 } 

154 

155 # See Wikipedia 

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

157 if not icon_header[j]: 

158 icon_header[j] = 256 

159 

160 # See Wikipedia notes about color depth. 

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

162 icon_header["color_depth"] = ( 

163 icon_header["bpp"] 

164 or ( 

165 icon_header["nb_color"] != 0 

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

167 ) 

168 or 256 

169 ) 

170 

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

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

173 

174 self.entry.append(icon_header) 

175 

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

177 # ICO images are usually squares 

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

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: int) -> Image.Image: 

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 im: Image.Image 

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

211 # png frame 

212 im = PngImagePlugin.PngImageFile(self.buf) 

213 Image._decompression_bomb_check(im.size) 

214 else: 

215 # XOR + AND mask bmp frame 

216 im = BmpImagePlugin.DibImageFile(self.buf) 

217 Image._decompression_bomb_check(im.size) 

218 

219 # change tile dimension to only encompass XOR image 

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

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

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

223 

224 # figure out where AND mask image starts 

225 bpp = header["bpp"] 

226 if 32 == bpp: 

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

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

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

230 # channel. 

231 

232 # Back up to start of bmp data 

233 self.buf.seek(o) 

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

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

236 

237 # convert to an 8bpp grayscale image 

238 mask = Image.frombuffer( 

239 "L", # 8bpp 

240 im.size, # (w, h) 

241 alpha_bytes, # source chars 

242 "raw", # raw decoder 

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

244 ) 

245 else: 

246 # get AND image from end of bitmap 

247 w = im.size[0] 

248 if (w % 32) > 0: 

249 # bitmap row data is aligned to word boundaries 

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

251 

252 # the total mask data is 

253 # padded row size * height / bits per char 

254 

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

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

257 

258 self.buf.seek(and_mask_offset) 

259 mask_data = self.buf.read(total_bytes) 

260 

261 # convert raw data to image 

262 mask = Image.frombuffer( 

263 "1", # 1 bpp 

264 im.size, # (w, h) 

265 mask_data, # source chars 

266 "raw", # raw decoder 

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

268 ) 

269 

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

271 

272 # apply mask image as alpha channel 

273 im = im.convert("RGBA") 

274 im.putalpha(mask) 

275 

276 return im 

277 

278 

279## 

280# Image plugin for Windows Icon files. 

281 

282 

283class IcoImageFile(ImageFile.ImageFile): 

284 """ 

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

286 

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

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

289 

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

291 in the icon file. 

292 

293 Handles classic, XP and Vista icon formats. 

294 

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

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

297 image to "RGBA" mode before saving. 

298 

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

300 <casadebender@gmail.com>. 

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

302 """ 

303 

304 format = "ICO" 

305 format_description = "Windows Icon" 

306 

307 def _open(self) -> None: 

308 self.ico = IcoFile(self.fp) 

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

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

311 self.load() 

312 

313 @property 

314 def size(self): 

315 return self._size 

316 

317 @size.setter 

318 def size(self, value): 

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

320 msg = "This is not one of the allowed sizes of this image" 

321 raise ValueError(msg) 

322 self._size = value 

323 

324 def load(self): 

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

326 # Already loaded 

327 return Image.Image.load(self) 

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

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

330 im.load() 

331 self.im = im.im 

332 self.pyaccess = None 

333 self._mode = im.mode 

334 if im.palette: 

335 self.palette = im.palette 

336 if im.size != self.size: 

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

338 

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

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

341 sizes[index] = im.size 

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

343 

344 self.size = im.size 

345 

346 def load_seek(self, pos: int) -> None: 

347 # Flag the ImageFile.Parser so that it 

348 # just does all the decode at the end. 

349 pass 

350 

351 

352# 

353# -------------------------------------------------------------------- 

354 

355 

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

357Image.register_save(IcoImageFile.format, _save) 

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

359 

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