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
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
39import warnings
40from io import BytesIO
41from math import ceil, log
42from typing import IO, NamedTuple
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
51#
52# --------------------------------------------------------------------
54_MAGIC = b"\0\0\1\0"
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
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)
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)
107 image_io = BytesIO()
108 if bmp:
109 frame.save(image_io, "dib")
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)
134def _accept(prefix: bytes) -> bool:
135 return prefix.startswith(_MAGIC)
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
152class IcoFile:
153 def __init__(self, buf: IO[bytes]) -> None:
154 """
155 Parse image from file-like object containing ico file data
156 """
158 # check magic
159 s = buf.read(6)
160 if not _accept(s):
161 msg = "not an ICO file"
162 raise SyntaxError(msg)
164 self.buf = buf
165 self.entry = []
167 # Number of items in file
168 self.nb_items = i16(s, 4)
170 # Get headers for each item
171 for i in range(self.nb_items):
172 s = buf.read(16)
174 # See Wikipedia
175 width = s[0] or 256
176 height = s[1] or 256
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 )
197 self.entry.append(icon_header)
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)
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}
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
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))
221 def frame(self, idx: int) -> Image.Image:
222 """
223 Get an image from frame idx
224 """
226 header = self.entry[idx]
228 self.buf.seek(header.offset)
229 data = self.buf.read(8)
230 self.buf.seek(header.offset)
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)
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)
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.
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]
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)
280 # the total mask data is
281 # padded row size * height / bits per char
283 total_bytes = int((w * im.size[1]) / 8)
284 and_mask_offset = header.offset + header.size - total_bytes
286 self.buf.seek(and_mask_offset)
287 mask_data = self.buf.read(total_bytes)
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
304 # now we have two images, im is XOR image and mask is AND image
306 # apply mask image as alpha channel
307 if mask:
308 im = im.convert("RGBA")
309 im.putalpha(mask)
311 return im
314##
315# Image plugin for Windows Icon files.
318class IcoImageFile(ImageFile.ImageFile):
319 """
320 PIL read-only image support for Microsoft Windows .ico files.
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'.
325 The info dictionary has a key 'sizes' that is a list of the sizes available
326 in the icon file.
328 Handles classic, XP and Vista icon formats.
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.
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 """
339 format = "ICO"
340 format_description = "Windows Icon"
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()
348 @property
349 def size(self) -> tuple[int, int]:
350 return self._size
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
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")
373 index = self.ico.getentryindex(self.size)
374 sizes = list(self.info["sizes"])
375 sizes[index] = im.size
376 self.info["sizes"] = set(sizes)
378 self.size = im.size
379 return Image.Image.load(self)
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
387#
388# --------------------------------------------------------------------
391Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
392Image.register_save(IcoImageFile.format, _save)
393Image.register_extension(IcoImageFile.format, ".ico")
395Image.register_mime(IcoImageFile.format, "image/x-icon")