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

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# 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 

39import warnings 

40from io import BytesIO 

41from math import ceil, log 

42from typing import IO, NamedTuple 

43 

44from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin 

45from ._binary import i16le as i16 

46from ._binary import i32le as i32 

47from ._binary import o8 

48from ._binary import o16le as o16 

49from ._binary import o32le as o32 

50 

51# 

52# -------------------------------------------------------------------- 

53 

54_MAGIC = b"\0\0\1\0" 

55 

56 

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

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

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

60 sizes = im.encoderinfo.get( 

61 "sizes", 

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

63 ) 

64 frames = [] 

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

66 width, height = im.size 

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

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

69 continue 

70 

71 for provided_im in provided_ims: 

72 if provided_im.size != size: 

73 continue 

74 frames.append(provided_im) 

75 if bmp: 

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

77 bits_used = [bits] 

78 for other_im in provided_ims: 

79 if other_im.size != size: 

80 continue 

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

82 if bits not in bits_used: 

83 # Another image has been supplied for this size 

84 # with a different bit depth 

85 frames.append(other_im) 

86 bits_used.append(bits) 

87 break 

88 else: 

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

90 frame = provided_im.copy() 

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

92 frames.append(frame) 

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

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

95 for frame in frames: 

96 width, height = frame.size 

97 # 0 means 256 

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

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

100 

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

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

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

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

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

106 

107 image_io = BytesIO() 

108 if bmp: 

109 frame.save(image_io, "dib") 

110 

111 if bits != 32: 

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

113 ImageFile._save( 

114 and_mask, 

115 image_io, 

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

117 ) 

118 else: 

119 frame.save(image_io, "png") 

120 image_io.seek(0) 

121 image_bytes = image_io.read() 

122 if bmp: 

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

124 bytes_len = len(image_bytes) 

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

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

127 current = fp.tell() 

128 fp.seek(offset) 

129 fp.write(image_bytes) 

130 offset = offset + bytes_len 

131 fp.seek(current) 

132 

133 

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

135 return prefix.startswith(_MAGIC) 

136 

137 

138class IconHeader(NamedTuple): 

139 width: int 

140 height: int 

141 nb_color: int 

142 reserved: int 

143 planes: int 

144 bpp: int 

145 size: int 

146 offset: int 

147 dim: tuple[int, int] 

148 square: int 

149 color_depth: int 

150 

151 

152class IcoFile: 

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

154 """ 

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

156 """ 

157 

158 # check magic 

159 s = buf.read(6) 

160 if not _accept(s): 

161 msg = "not an ICO file" 

162 raise SyntaxError(msg) 

163 

164 self.buf = buf 

165 self.entry = [] 

166 

167 # Number of items in file 

168 self.nb_items = i16(s, 4) 

169 

170 # Get headers for each item 

171 for i in range(self.nb_items): 

172 s = buf.read(16) 

173 

174 # See Wikipedia 

175 width = s[0] or 256 

176 height = s[1] or 256 

177 

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

179 nb_color = s[2] 

180 bpp = i16(s, 6) 

181 icon_header = IconHeader( 

182 width=width, 

183 height=height, 

184 nb_color=nb_color, 

185 reserved=s[3], 

186 planes=i16(s, 4), 

187 bpp=i16(s, 6), 

188 size=i32(s, 8), 

189 offset=i32(s, 12), 

190 dim=(width, height), 

191 square=width * height, 

192 # See Wikipedia notes about color depth. 

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

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

195 ) 

196 

197 self.entry.append(icon_header) 

198 

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

200 # ICO images are usually squares 

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

202 

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

204 """ 

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

206 """ 

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

208 

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

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

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

212 return i 

213 return 0 

214 

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

216 """ 

217 Get an image from the icon 

218 """ 

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

220 

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

222 """ 

223 Get an image from frame idx 

224 """ 

225 

226 header = self.entry[idx] 

227 

228 self.buf.seek(header.offset) 

229 data = self.buf.read(8) 

230 self.buf.seek(header.offset) 

231 

232 im: Image.Image 

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

234 # png frame 

235 im = PngImagePlugin.PngImageFile(self.buf) 

236 Image._decompression_bomb_check(im.size) 

237 else: 

238 # XOR + AND mask bmp frame 

239 im = BmpImagePlugin.DibImageFile(self.buf) 

240 Image._decompression_bomb_check(im.size) 

241 

242 # change tile dimension to only encompass XOR image 

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

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

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

246 

247 # figure out where AND mask image starts 

248 if header.bpp == 32: 

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

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

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

252 # channel. 

253 

254 # Back up to start of bmp data 

255 self.buf.seek(o) 

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

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

258 

259 # convert to an 8bpp grayscale image 

260 try: 

261 mask = Image.frombuffer( 

262 "L", # 8bpp 

263 im.size, # (w, h) 

264 alpha_bytes, # source chars 

265 "raw", # raw decoder 

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

267 ) 

268 except ValueError: 

269 if ImageFile.LOAD_TRUNCATED_IMAGES: 

270 mask = None 

271 else: 

272 raise 

273 else: 

274 # get AND image from end of bitmap 

275 w = im.size[0] 

276 if (w % 32) > 0: 

277 # bitmap row data is aligned to word boundaries 

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

279 

280 # the total mask data is 

281 # padded row size * height / bits per char 

282 

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

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

285 

286 self.buf.seek(and_mask_offset) 

287 mask_data = self.buf.read(total_bytes) 

288 

289 # convert raw data to image 

290 try: 

291 mask = Image.frombuffer( 

292 "1", # 1 bpp 

293 im.size, # (w, h) 

294 mask_data, # source chars 

295 "raw", # raw decoder 

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

297 ) 

298 except ValueError: 

299 if ImageFile.LOAD_TRUNCATED_IMAGES: 

300 mask = None 

301 else: 

302 raise 

303 

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

305 

306 # apply mask image as alpha channel 

307 if mask: 

308 im = im.convert("RGBA") 

309 im.putalpha(mask) 

310 

311 return im 

312 

313 

314## 

315# Image plugin for Windows Icon files. 

316 

317 

318class IcoImageFile(ImageFile.ImageFile): 

319 """ 

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

321 

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

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

324 

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

326 in the icon file. 

327 

328 Handles classic, XP and Vista icon formats. 

329 

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

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

332 image to "RGBA" mode before saving. 

333 

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

335 <casadebender@gmail.com>. 

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

337 """ 

338 

339 format = "ICO" 

340 format_description = "Windows Icon" 

341 

342 def _open(self) -> None: 

343 self.ico = IcoFile(self.fp) 

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

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

346 self.load() 

347 

348 @property 

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

350 return self._size 

351 

352 @size.setter 

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

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

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

356 raise ValueError(msg) 

357 self._size = value 

358 

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

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

361 # Already loaded 

362 return Image.Image.load(self) 

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

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

365 im.load() 

366 self.im = im.im 

367 self._mode = im.mode 

368 if im.palette: 

369 self.palette = im.palette 

370 if im.size != self.size: 

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

372 

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

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

375 sizes[index] = im.size 

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

377 

378 self.size = im.size 

379 return Image.Image.load(self) 

380 

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

382 # Flag the ImageFile.Parser so that it 

383 # just does all the decode at the end. 

384 pass 

385 

386 

387# 

388# -------------------------------------------------------------------- 

389 

390 

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

392Image.register_save(IcoImageFile.format, _save) 

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

394 

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