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

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

192 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, NamedTuple 

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, 

101 image_io, 

102 [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], 

103 ) 

104 else: 

105 frame.save(image_io, "png") 

106 image_io.seek(0) 

107 image_bytes = image_io.read() 

108 if bmp: 

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

110 bytes_len = len(image_bytes) 

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

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

113 current = fp.tell() 

114 fp.seek(offset) 

115 fp.write(image_bytes) 

116 offset = offset + bytes_len 

117 fp.seek(current) 

118 

119 

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

121 return prefix.startswith(_MAGIC) 

122 

123 

124class IconHeader(NamedTuple): 

125 width: int 

126 height: int 

127 nb_color: int 

128 reserved: int 

129 planes: int 

130 bpp: int 

131 size: int 

132 offset: int 

133 dim: tuple[int, int] 

134 square: int 

135 color_depth: int 

136 

137 

138class IcoFile: 

139 def __init__(self, buf: IO[bytes]) -> None: 

140 """ 

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

142 """ 

143 

144 # check magic 

145 s = buf.read(6) 

146 if not _accept(s): 

147 msg = "not an ICO file" 

148 raise SyntaxError(msg) 

149 

150 self.buf = buf 

151 self.entry = [] 

152 

153 # Number of items in file 

154 self.nb_items = i16(s, 4) 

155 

156 # Get headers for each item 

157 for i in range(self.nb_items): 

158 s = buf.read(16) 

159 

160 # See Wikipedia 

161 width = s[0] or 256 

162 height = s[1] or 256 

163 

164 # No. of colors in image (0 if >=8bpp) 

165 nb_color = s[2] 

166 bpp = i16(s, 6) 

167 icon_header = IconHeader( 

168 width=width, 

169 height=height, 

170 nb_color=nb_color, 

171 reserved=s[3], 

172 planes=i16(s, 4), 

173 bpp=i16(s, 6), 

174 size=i32(s, 8), 

175 offset=i32(s, 12), 

176 dim=(width, height), 

177 square=width * height, 

178 # See Wikipedia notes about color depth. 

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

180 color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, 

181 ) 

182 

183 self.entry.append(icon_header) 

184 

185 self.entry = sorted(self.entry, key=lambda x: x.color_depth) 

186 # ICO images are usually squares 

187 self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) 

188 

189 def sizes(self) -> set[tuple[int, int]]: 

190 """ 

191 Get a set of all available icon sizes and color depths. 

192 """ 

193 return {(h.width, h.height) for h in self.entry} 

194 

195 def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: 

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

197 if size == h.dim and (bpp is False or bpp == h.color_depth): 

198 return i 

199 return 0 

200 

201 def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image: 

202 """ 

203 Get an image from the icon 

204 """ 

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

206 

207 def frame(self, idx: int) -> Image.Image: 

208 """ 

209 Get an image from frame idx 

210 """ 

211 

212 header = self.entry[idx] 

213 

214 self.buf.seek(header.offset) 

215 data = self.buf.read(8) 

216 self.buf.seek(header.offset) 

217 

218 im: Image.Image 

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

220 # png frame 

221 im = PngImagePlugin.PngImageFile(self.buf) 

222 Image._decompression_bomb_check(im.size) 

223 else: 

224 # XOR + AND mask bmp frame 

225 im = BmpImagePlugin.DibImageFile(self.buf) 

226 Image._decompression_bomb_check(im.size) 

227 

228 # change tile dimension to only encompass XOR image 

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

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

231 im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a) 

232 

233 # figure out where AND mask image starts 

234 if header.bpp == 32: 

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

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

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

238 # channel. 

239 

240 # Back up to start of bmp data 

241 self.buf.seek(o) 

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

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

244 

245 # convert to an 8bpp grayscale image 

246 try: 

247 mask = Image.frombuffer( 

248 "L", # 8bpp 

249 im.size, # (w, h) 

250 alpha_bytes, # source chars 

251 "raw", # raw decoder 

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

253 ) 

254 except ValueError: 

255 if ImageFile.LOAD_TRUNCATED_IMAGES: 

256 mask = None 

257 else: 

258 raise 

259 else: 

260 # get AND image from end of bitmap 

261 w = im.size[0] 

262 if (w % 32) > 0: 

263 # bitmap row data is aligned to word boundaries 

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

265 

266 # the total mask data is 

267 # padded row size * height / bits per char 

268 

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

270 and_mask_offset = header.offset + header.size - total_bytes 

271 

272 self.buf.seek(and_mask_offset) 

273 mask_data = self.buf.read(total_bytes) 

274 

275 # convert raw data to image 

276 try: 

277 mask = Image.frombuffer( 

278 "1", # 1 bpp 

279 im.size, # (w, h) 

280 mask_data, # source chars 

281 "raw", # raw decoder 

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

283 ) 

284 except ValueError: 

285 if ImageFile.LOAD_TRUNCATED_IMAGES: 

286 mask = None 

287 else: 

288 raise 

289 

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

291 

292 # apply mask image as alpha channel 

293 if mask: 

294 im = im.convert("RGBA") 

295 im.putalpha(mask) 

296 

297 return im 

298 

299 

300## 

301# Image plugin for Windows Icon files. 

302 

303 

304class IcoImageFile(ImageFile.ImageFile): 

305 """ 

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

307 

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

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

310 

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

312 in the icon file. 

313 

314 Handles classic, XP and Vista icon formats. 

315 

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

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

318 image to "RGBA" mode before saving. 

319 

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

321 <casadebender@gmail.com>. 

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

323 """ 

324 

325 format = "ICO" 

326 format_description = "Windows Icon" 

327 

328 def _open(self) -> None: 

329 self.ico = IcoFile(self.fp) 

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

331 self.size = self.ico.entry[0].dim 

332 self.load() 

333 

334 @property 

335 def size(self) -> tuple[int, int]: 

336 return self._size 

337 

338 @size.setter 

339 def size(self, value: tuple[int, int]) -> None: 

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

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

342 raise ValueError(msg) 

343 self._size = value 

344 

345 def load(self) -> Image.core.PixelAccess | None: 

346 if self._im is not None and self.im.size == self.size: 

347 # Already loaded 

348 return Image.Image.load(self) 

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

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

351 im.load() 

352 self.im = im.im 

353 self._mode = im.mode 

354 if im.palette: 

355 self.palette = im.palette 

356 if im.size != self.size: 

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

358 

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

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

361 sizes[index] = im.size 

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

363 

364 self.size = im.size 

365 return None 

366 

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

368 # Flag the ImageFile.Parser so that it 

369 # just does all the decode at the end. 

370 pass 

371 

372 

373# 

374# -------------------------------------------------------------------- 

375 

376 

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

378Image.register_save(IcoImageFile.format, _save) 

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

380 

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