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
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
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#
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.
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
39__lazy_modules__ = {"PIL._binary", "io", "math", "warnings"}
41import warnings
42from io import BytesIO
43from math import ceil, log
44from typing import NamedTuple
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
53TYPE_CHECKING = False
54if TYPE_CHECKING:
55 from typing import IO
57#
58# --------------------------------------------------------------------
60_MAGIC = b"\0\0\1\0"
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
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)
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)
129 image_io = BytesIO()
130 if bmp:
131 frame.save(image_io, "dib")
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)
156def _accept(prefix: bytes) -> bool:
157 return prefix.startswith(_MAGIC)
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
174class IcoFile:
175 def __init__(self, buf: IO[bytes]) -> None:
176 """
177 Parse image from file-like object containing ico file data
178 """
180 # check magic
181 s = buf.read(6)
182 if not _accept(s):
183 msg = "not an ICO file"
184 raise SyntaxError(msg)
186 self.buf = buf
187 self.entry = []
189 # Number of items in file
190 self.nb_items = i16(s, 4)
192 # Get headers for each item
193 for i in range(self.nb_items):
194 s = buf.read(16)
196 # See Wikipedia
197 width = s[0] or 256
198 height = s[1] or 256
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 )
219 self.entry.append(icon_header)
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)
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}
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
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))
243 def frame(self, idx: int) -> Image.Image:
244 """
245 Get an image from frame idx
246 """
248 header = self.entry[idx]
250 self.buf.seek(header.offset)
251 data = self.buf.read(8)
252 self.buf.seek(header.offset)
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)
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)]
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.
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]
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)
301 # the total mask data is
302 # padded row size * height / bits per char
304 total_bytes = int((w * im.size[1]) / 8)
305 and_mask_offset = header.offset + header.size - total_bytes
307 self.buf.seek(and_mask_offset)
308 mask_data = self.buf.read(total_bytes)
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
325 # now we have two images, im is XOR image and mask is AND image
327 # apply mask image as alpha channel
328 if mask:
329 im = im.convert("RGBA")
330 im.putalpha(mask)
332 return im
335##
336# Image plugin for Windows Icon files.
339class IcoImageFile(ImageFile.ImageFile):
340 """
341 PIL read-only image support for Microsoft Windows .ico files.
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'.
346 The info dictionary has a key 'sizes' that is a list of the sizes available
347 in the icon file.
349 Handles classic, XP and Vista icon formats.
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.
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 """
360 format = "ICO"
361 format_description = "Windows Icon"
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()
370 @property
371 def size(self) -> tuple[int, int]:
372 return self._size
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
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")
395 index = self.ico.getentryindex(self.size)
396 sizes = list(self.info["sizes"])
397 sizes[index] = im.size
398 self.info["sizes"] = set(sizes)
400 self.size = im.size
401 return Image.Image.load(self)
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
409#
410# --------------------------------------------------------------------
413Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
414Image.register_save(IcoImageFile.format, _save)
415Image.register_extension(IcoImageFile.format, ".ico")
417Image.register_mime(IcoImageFile.format, "image/x-icon")