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

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

201 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# Copyright 2008 Bryan Davis 

21# 

22# Licensed under the Apache License, Version 2.0 (the "License"); you may 

23# not use this file except in compliance with the License. You may obtain 

24# a copy of the License at 

25# 

26# https://www.apache.org/licenses/LICENSE-2.0 

27# 

28# Unless required by applicable law or agreed to in writing, software 

29# distributed under the License is distributed on an "AS IS" BASIS, 

30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

31# See the License for the specific language governing permissions and 

32# limitations under the License. 

33 

34# Icon format references: 

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

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

37from __future__ import annotations 

38 

39__lazy_modules__ = {"PIL._binary", "io", "math", "warnings"} 

40 

41import warnings 

42from io import BytesIO 

43from math import ceil, log 

44from typing import NamedTuple 

45 

46from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin 

47from ._binary import i16le as i16 

48from ._binary import i32le as i32 

49from ._binary import o8 

50from ._binary import o16le as o16 

51from ._binary import o32le as o32 

52 

53TYPE_CHECKING = False 

54if TYPE_CHECKING: 

55 from typing import IO 

56 

57# 

58# -------------------------------------------------------------------- 

59 

60_MAGIC = b"\0\0\1\0" 

61 

62 

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

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

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

66 if "sizes" in im.encoderinfo: 

67 sizes = sorted(set(im.encoderinfo["sizes"])) 

68 else: 

69 sizes = ( 

70 [im.size] 

71 if min(im.size) < 16 

72 else [(d, d) for d in (16, 24, 32, 48, 64, 128, 256)] 

73 ) 

74 frames = [] 

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

76 for size in sizes: 

77 if size[0] > min(256, im.width) or size[1] > min(256, im.height): 

78 continue 

79 

80 for provided_im in provided_ims: 

81 if provided_im.size != size: 

82 continue 

83 frames.append(provided_im) 

84 if bmp: 

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

86 bits_used = [bits] 

87 for other_im in provided_ims: 

88 if other_im.size != size: 

89 continue 

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

91 if bits not in bits_used: 

92 # Another image has been supplied for this size 

93 # with a different bit depth 

94 frames.append(other_im) 

95 bits_used.append(bits) 

96 break 

97 else: 

98 # Scale down from the smallest provided image that still covers this size 

99 source = min( 

100 ( 

101 provided_im 

102 for provided_im in provided_ims 

103 if provided_im.width >= size[0] and provided_im.height >= size[1] 

104 ), 

105 key=lambda provided_im: provided_im.width * provided_im.height, 

106 default=im, 

107 ) 

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

109 frame = source.copy() 

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

111 frames.append(frame) 

112 if not frames: 

113 msg = "All sizes too large for image" 

114 raise ValueError(msg) 

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

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

117 for frame in frames: 

118 width, height = frame.size 

119 # 0 means 256 

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

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

122 

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

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

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

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

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

128 

129 image_io = BytesIO() 

130 if bmp: 

131 frame.save(image_io, "dib") 

132 

133 if bits != 32: 

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

135 ImageFile._save( 

136 and_mask, 

137 image_io, 

138 [ImageFile._Tile("raw", args=("1", 0, -1))], 

139 ) 

140 else: 

141 frame.save(image_io, "png") 

142 image_io.seek(0) 

143 image_bytes = image_io.read() 

144 if bmp: 

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

146 bytes_len = len(image_bytes) 

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

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

149 current = fp.tell() 

150 fp.seek(offset) 

151 fp.write(image_bytes) 

152 offset = offset + bytes_len 

153 fp.seek(current) 

154 

155 

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

157 return prefix.startswith(_MAGIC) 

158 

159 

160class IconHeader(NamedTuple): 

161 width: int 

162 height: int 

163 nb_color: int 

164 reserved: int 

165 planes: int 

166 bpp: int 

167 size: int 

168 offset: int 

169 dim: tuple[int, int] 

170 square: int 

171 color_depth: int 

172 

173 

174class IcoFile: 

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

176 """ 

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

178 """ 

179 

180 # check magic 

181 s = buf.read(6) 

182 if not _accept(s): 

183 msg = "not an ICO file" 

184 raise SyntaxError(msg) 

185 

186 self.buf = buf 

187 self.entry = [] 

188 

189 # Number of items in file 

190 self.nb_items = i16(s, 4) 

191 

192 # Get headers for each item 

193 for i in range(self.nb_items): 

194 s = buf.read(16) 

195 

196 # See Wikipedia 

197 width = s[0] or 256 

198 height = s[1] or 256 

199 

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

201 nb_color = s[2] 

202 bpp = i16(s, 6) 

203 icon_header = IconHeader( 

204 width=width, 

205 height=height, 

206 nb_color=nb_color, 

207 reserved=s[3], 

208 planes=i16(s, 4), 

209 bpp=i16(s, 6), 

210 size=i32(s, 8), 

211 offset=i32(s, 12), 

212 dim=(width, height), 

213 square=width * height, 

214 # See Wikipedia notes about color depth. 

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

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

217 ) 

218 

219 self.entry.append(icon_header) 

220 

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

222 # ICO images are usually squares 

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

224 

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

226 """ 

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

228 """ 

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

230 

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

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

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

234 return i 

235 return 0 

236 

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

238 """ 

239 Get an image from the icon 

240 """ 

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

242 

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

244 """ 

245 Get an image from frame idx 

246 """ 

247 

248 header = self.entry[idx] 

249 

250 self.buf.seek(header.offset) 

251 data = self.buf.read(8) 

252 self.buf.seek(header.offset) 

253 

254 im: Image.Image 

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

256 # png frame 

257 im = PngImagePlugin.PngImageFile(self.buf) 

258 Image._decompression_bomb_check(im.size) 

259 else: 

260 # XOR + AND mask bmp frame 

261 im = BmpImagePlugin.DibImageFile(self.buf) 

262 Image._decompression_bomb_check(im.size) 

263 

264 # change tile dimension to only encompass XOR image 

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

266 im.tile = [im.tile[0]._replace(extents=None)] 

267 

268 # figure out where AND mask image starts 

269 if header.bpp == 32: 

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

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

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

273 # channel. 

274 

275 # Back up to start of bmp data 

276 self.buf.seek(im.tile[0].offset) 

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

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

279 

280 # convert to an 8bpp grayscale image 

281 try: 

282 mask = Image.frombuffer( 

283 "L", # 8bpp 

284 im.size, # (w, h) 

285 alpha_bytes, # source chars 

286 "raw", # raw decoder 

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

288 ) 

289 except ValueError: 

290 if ImageFile.LOAD_TRUNCATED_IMAGES: 

291 mask = None 

292 else: 

293 raise 

294 else: 

295 # get AND image from end of bitmap 

296 w = im.size[0] 

297 if (w % 32) > 0: 

298 # bitmap row data is aligned to word boundaries 

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

300 

301 # the total mask data is 

302 # padded row size * height / bits per char 

303 

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

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

306 

307 self.buf.seek(and_mask_offset) 

308 mask_data = self.buf.read(total_bytes) 

309 

310 # convert raw data to image 

311 try: 

312 mask = Image.frombuffer( 

313 "1", # 1 bpp 

314 im.size, # (w, h) 

315 mask_data, # source chars 

316 "raw", # raw decoder 

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

318 ) 

319 except ValueError: 

320 if ImageFile.LOAD_TRUNCATED_IMAGES: 

321 mask = None 

322 else: 

323 raise 

324 

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

326 

327 # apply mask image as alpha channel 

328 if mask: 

329 im = im.convert("RGBA") 

330 im.putalpha(mask) 

331 

332 return im 

333 

334 

335## 

336# Image plugin for Windows Icon files. 

337 

338 

339class IcoImageFile(ImageFile.ImageFile): 

340 """ 

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

342 

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

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

345 

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

347 in the icon file. 

348 

349 Handles classic, XP and Vista icon formats. 

350 

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

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

353 image to "RGBA" mode before saving. 

354 

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

356 <casadebender@gmail.com>. 

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

358 """ 

359 

360 format = "ICO" 

361 format_description = "Windows Icon" 

362 

363 def _open(self) -> None: 

364 assert self.fp is not None 

365 self.ico = IcoFile(self.fp) 

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

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

368 self.load() 

369 

370 @property 

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

372 return self._size 

373 

374 @size.setter 

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

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

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

378 raise ValueError(msg) 

379 self._size = value 

380 

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

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

383 # Already loaded 

384 return Image.Image.load(self) 

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

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

387 im.load() 

388 self.im = im.im 

389 self._mode = im.mode 

390 if im.palette: 

391 self.palette = im.palette 

392 if im.size != self.size: 

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

394 

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

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

397 sizes[index] = im.size 

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

399 

400 self.size = im.size 

401 return Image.Image.load(self) 

402 

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

404 # Flag the ImageFile.Parser so that it 

405 # just does all the decode at the end. 

406 pass 

407 

408 

409# 

410# -------------------------------------------------------------------- 

411 

412 

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

414Image.register_save(IcoImageFile.format, _save) 

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

416 

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