Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/IcoImagePlugin.py: 16%
165 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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# Icon format references:
21# * https://en.wikipedia.org/wiki/ICO_(file_format)
22# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
25import warnings
26from io import BytesIO
27from math import ceil, log
29from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
30from ._binary import i16le as i16
31from ._binary import i32le as i32
32from ._binary import o8
33from ._binary import o16le as o16
34from ._binary import o32le as o32
36#
37# --------------------------------------------------------------------
39_MAGIC = b"\0\0\1\0"
42def _save(im, fp, filename):
43 fp.write(_MAGIC) # (2+2)
44 bmp = im.encoderinfo.get("bitmap_format") == "bmp"
45 sizes = im.encoderinfo.get(
46 "sizes",
47 [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
48 )
49 frames = []
50 provided_ims = [im] + im.encoderinfo.get("append_images", [])
51 width, height = im.size
52 for size in sorted(set(sizes)):
53 if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
54 continue
56 for provided_im in provided_ims:
57 if provided_im.size != size:
58 continue
59 frames.append(provided_im)
60 if bmp:
61 bits = BmpImagePlugin.SAVE[provided_im.mode][1]
62 bits_used = [bits]
63 for other_im in provided_ims:
64 if other_im.size != size:
65 continue
66 bits = BmpImagePlugin.SAVE[other_im.mode][1]
67 if bits not in bits_used:
68 # Another image has been supplied for this size
69 # with a different bit depth
70 frames.append(other_im)
71 bits_used.append(bits)
72 break
73 else:
74 # TODO: invent a more convenient method for proportional scalings
75 frame = provided_im.copy()
76 frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
77 frames.append(frame)
78 fp.write(o16(len(frames))) # idCount(2)
79 offset = fp.tell() + len(frames) * 16
80 for frame in frames:
81 width, height = frame.size
82 # 0 means 256
83 fp.write(o8(width if width < 256 else 0)) # bWidth(1)
84 fp.write(o8(height if height < 256 else 0)) # bHeight(1)
86 bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
87 fp.write(o8(colors)) # bColorCount(1)
88 fp.write(b"\0") # bReserved(1)
89 fp.write(b"\0\0") # wPlanes(2)
90 fp.write(o16(bits)) # wBitCount(2)
92 image_io = BytesIO()
93 if bmp:
94 frame.save(image_io, "dib")
96 if bits != 32:
97 and_mask = Image.new("1", size)
98 ImageFile._save(
99 and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
100 )
101 else:
102 frame.save(image_io, "png")
103 image_io.seek(0)
104 image_bytes = image_io.read()
105 if bmp:
106 image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
107 bytes_len = len(image_bytes)
108 fp.write(o32(bytes_len)) # dwBytesInRes(4)
109 fp.write(o32(offset)) # dwImageOffset(4)
110 current = fp.tell()
111 fp.seek(offset)
112 fp.write(image_bytes)
113 offset = offset + bytes_len
114 fp.seek(current)
117def _accept(prefix):
118 return prefix[:4] == _MAGIC
121class IcoFile:
122 def __init__(self, buf):
123 """
124 Parse image from file-like object containing ico file data
125 """
127 # check magic
128 s = buf.read(6)
129 if not _accept(s):
130 raise SyntaxError("not an ICO file")
132 self.buf = buf
133 self.entry = []
135 # Number of items in file
136 self.nb_items = i16(s, 4)
138 # Get headers for each item
139 for i in range(self.nb_items):
140 s = buf.read(16)
142 icon_header = {
143 "width": s[0],
144 "height": s[1],
145 "nb_color": s[2], # No. of colors in image (0 if >=8bpp)
146 "reserved": s[3],
147 "planes": i16(s, 4),
148 "bpp": i16(s, 6),
149 "size": i32(s, 8),
150 "offset": i32(s, 12),
151 }
153 # See Wikipedia
154 for j in ("width", "height"):
155 if not icon_header[j]:
156 icon_header[j] = 256
158 # See Wikipedia notes about color depth.
159 # We need this just to differ images with equal sizes
160 icon_header["color_depth"] = (
161 icon_header["bpp"]
162 or (
163 icon_header["nb_color"] != 0
164 and ceil(log(icon_header["nb_color"], 2))
165 )
166 or 256
167 )
169 icon_header["dim"] = (icon_header["width"], icon_header["height"])
170 icon_header["square"] = icon_header["width"] * icon_header["height"]
172 self.entry.append(icon_header)
174 self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
175 # ICO images are usually squares
176 # self.entry = sorted(self.entry, key=lambda x: x['width'])
177 self.entry = sorted(self.entry, key=lambda x: x["square"])
178 self.entry.reverse()
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}
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
192 def getimage(self, size, bpp=False):
193 """
194 Get an image from the icon
195 """
196 return self.frame(self.getentryindex(size, bpp))
198 def frame(self, idx):
199 """
200 Get an image from frame idx
201 """
203 header = self.entry[idx]
205 self.buf.seek(header["offset"])
206 data = self.buf.read(8)
207 self.buf.seek(header["offset"])
209 if data[:8] == PngImagePlugin._MAGIC:
210 # png frame
211 im = PngImagePlugin.PngImageFile(self.buf)
212 Image._decompression_bomb_check(im.size)
213 else:
214 # XOR + AND mask bmp frame
215 im = BmpImagePlugin.DibImageFile(self.buf)
216 Image._decompression_bomb_check(im.size)
218 # change tile dimension to only encompass XOR image
219 im._size = (im.size[0], int(im.size[1] / 2))
220 d, e, o, a = im.tile[0]
221 im.tile[0] = d, (0, 0) + im.size, o, a
223 # figure out where AND mask image starts
224 bpp = header["bpp"]
225 if 32 == bpp:
226 # 32-bit color depth icon image allows semitransparent areas
227 # PIL's DIB format ignores transparency bits, recover them.
228 # The DIB is packed in BGRX byte order where X is the alpha
229 # channel.
231 # Back up to start of bmp data
232 self.buf.seek(o)
233 # extract every 4th byte (eg. 3,7,11,15,...)
234 alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
236 # convert to an 8bpp grayscale image
237 mask = Image.frombuffer(
238 "L", # 8bpp
239 im.size, # (w, h)
240 alpha_bytes, # source chars
241 "raw", # raw decoder
242 ("L", 0, -1), # 8bpp inverted, unpadded, reversed
243 )
244 else:
245 # get AND image from end of bitmap
246 w = im.size[0]
247 if (w % 32) > 0:
248 # bitmap row data is aligned to word boundaries
249 w += 32 - (im.size[0] % 32)
251 # the total mask data is
252 # padded row size * height / bits per char
254 total_bytes = int((w * im.size[1]) / 8)
255 and_mask_offset = header["offset"] + header["size"] - total_bytes
257 self.buf.seek(and_mask_offset)
258 mask_data = self.buf.read(total_bytes)
260 # convert raw data to image
261 mask = Image.frombuffer(
262 "1", # 1 bpp
263 im.size, # (w, h)
264 mask_data, # source chars
265 "raw", # raw decoder
266 ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
267 )
269 # now we have two images, im is XOR image and mask is AND image
271 # apply mask image as alpha channel
272 im = im.convert("RGBA")
273 im.putalpha(mask)
275 return im
278##
279# Image plugin for Windows Icon files.
282class IcoImageFile(ImageFile.ImageFile):
283 """
284 PIL read-only image support for Microsoft Windows .ico files.
286 By default the largest resolution image in the file will be loaded. This
287 can be changed by altering the 'size' attribute before calling 'load'.
289 The info dictionary has a key 'sizes' that is a list of the sizes available
290 in the icon file.
292 Handles classic, XP and Vista icon formats.
294 When saving, PNG compression is used. Support for this was only added in
295 Windows Vista. If you are unable to view the icon in Windows, convert the
296 image to "RGBA" mode before saving.
298 This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
299 <casadebender@gmail.com>.
300 https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
301 """
303 format = "ICO"
304 format_description = "Windows Icon"
306 def _open(self):
307 self.ico = IcoFile(self.fp)
308 self.info["sizes"] = self.ico.sizes()
309 self.size = self.ico.entry[0]["dim"]
310 self.load()
312 @property
313 def size(self):
314 return self._size
316 @size.setter
317 def size(self, value):
318 if value not in self.info["sizes"]:
319 raise ValueError("This is not one of the allowed sizes of this image")
320 self._size = value
322 def load(self):
323 if self.im is not None and self.im.size == self.size:
324 # Already loaded
325 return Image.Image.load(self)
326 im = self.ico.getimage(self.size)
327 # if tile is PNG, it won't really be loaded yet
328 im.load()
329 self.im = im.im
330 self.mode = im.mode
331 if im.size != self.size:
332 warnings.warn("Image was not the expected size")
334 index = self.ico.getentryindex(self.size)
335 sizes = list(self.info["sizes"])
336 sizes[index] = im.size
337 self.info["sizes"] = set(sizes)
339 self.size = im.size
341 def load_seek(self):
342 # Flag the ImageFile.Parser so that it
343 # just does all the decode at the end.
344 pass
347#
348# --------------------------------------------------------------------
351Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
352Image.register_save(IcoImageFile.format, _save)
353Image.register_extension(IcoImageFile.format, ".ico")
355Image.register_mime(IcoImageFile.format, "image/x-icon")