123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- #
- # The Python Imaging Library.
- # $Id$
- #
- # Windows Icon support for PIL
- #
- # History:
- # 96-05-27 fl Created
- #
- # Copyright (c) Secret Labs AB 1997.
- # Copyright (c) Fredrik Lundh 1996.
- #
- # See the README file for information on usage and redistribution.
- #
- # This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
- # <casadebender@gmail.com>.
- # https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
- #
- # Icon format references:
- # * https://en.wikipedia.org/wiki/ICO_(file_format)
- # * https://msdn.microsoft.com/en-us/library/ms997538.aspx
- import warnings
- from io import BytesIO
- from math import ceil, log
- from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
- from ._binary import i16le as i16
- from ._binary import i32le as i32
- from ._binary import o8
- from ._binary import o16le as o16
- from ._binary import o32le as o32
- #
- # --------------------------------------------------------------------
- _MAGIC = b"\0\0\1\0"
- def _save(im, fp, filename):
- fp.write(_MAGIC) # (2+2)
- bmp = im.encoderinfo.get("bitmap_format") == "bmp"
- sizes = im.encoderinfo.get(
- "sizes",
- [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
- )
- frames = []
- provided_ims = [im] + im.encoderinfo.get("append_images", [])
- width, height = im.size
- for size in sorted(set(sizes)):
- if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
- continue
- for provided_im in provided_ims:
- if provided_im.size != size:
- continue
- frames.append(provided_im)
- if bmp:
- bits = BmpImagePlugin.SAVE[provided_im.mode][1]
- bits_used = [bits]
- for other_im in provided_ims:
- if other_im.size != size:
- continue
- bits = BmpImagePlugin.SAVE[other_im.mode][1]
- if bits not in bits_used:
- # Another image has been supplied for this size
- # with a different bit depth
- frames.append(other_im)
- bits_used.append(bits)
- break
- else:
- # TODO: invent a more convenient method for proportional scalings
- frame = provided_im.copy()
- frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
- frames.append(frame)
- fp.write(o16(len(frames))) # idCount(2)
- offset = fp.tell() + len(frames) * 16
- for frame in frames:
- width, height = frame.size
- # 0 means 256
- fp.write(o8(width if width < 256 else 0)) # bWidth(1)
- fp.write(o8(height if height < 256 else 0)) # bHeight(1)
- bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
- fp.write(o8(colors)) # bColorCount(1)
- fp.write(b"\0") # bReserved(1)
- fp.write(b"\0\0") # wPlanes(2)
- fp.write(o16(bits)) # wBitCount(2)
- image_io = BytesIO()
- if bmp:
- frame.save(image_io, "dib")
- if bits != 32:
- and_mask = Image.new("1", size)
- ImageFile._save(
- and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
- )
- else:
- frame.save(image_io, "png")
- image_io.seek(0)
- image_bytes = image_io.read()
- if bmp:
- image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
- bytes_len = len(image_bytes)
- fp.write(o32(bytes_len)) # dwBytesInRes(4)
- fp.write(o32(offset)) # dwImageOffset(4)
- current = fp.tell()
- fp.seek(offset)
- fp.write(image_bytes)
- offset = offset + bytes_len
- fp.seek(current)
- def _accept(prefix):
- return prefix[:4] == _MAGIC
- class IcoFile:
- def __init__(self, buf):
- """
- Parse image from file-like object containing ico file data
- """
- # check magic
- s = buf.read(6)
- if not _accept(s):
- msg = "not an ICO file"
- raise SyntaxError(msg)
- self.buf = buf
- self.entry = []
- # Number of items in file
- self.nb_items = i16(s, 4)
- # Get headers for each item
- for i in range(self.nb_items):
- s = buf.read(16)
- icon_header = {
- "width": s[0],
- "height": s[1],
- "nb_color": s[2], # No. of colors in image (0 if >=8bpp)
- "reserved": s[3],
- "planes": i16(s, 4),
- "bpp": i16(s, 6),
- "size": i32(s, 8),
- "offset": i32(s, 12),
- }
- # See Wikipedia
- for j in ("width", "height"):
- if not icon_header[j]:
- icon_header[j] = 256
- # See Wikipedia notes about color depth.
- # We need this just to differ images with equal sizes
- icon_header["color_depth"] = (
- icon_header["bpp"]
- or (
- icon_header["nb_color"] != 0
- and ceil(log(icon_header["nb_color"], 2))
- )
- or 256
- )
- icon_header["dim"] = (icon_header["width"], icon_header["height"])
- icon_header["square"] = icon_header["width"] * icon_header["height"]
- self.entry.append(icon_header)
- self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
- # ICO images are usually squares
- # self.entry = sorted(self.entry, key=lambda x: x['width'])
- self.entry = sorted(self.entry, key=lambda x: x["square"])
- self.entry.reverse()
- def sizes(self):
- """
- Get a list of all available icon sizes and color depths.
- """
- return {(h["width"], h["height"]) for h in self.entry}
- def getentryindex(self, size, bpp=False):
- for i, h in enumerate(self.entry):
- if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
- return i
- return 0
- def getimage(self, size, bpp=False):
- """
- Get an image from the icon
- """
- return self.frame(self.getentryindex(size, bpp))
- def frame(self, idx):
- """
- Get an image from frame idx
- """
- header = self.entry[idx]
- self.buf.seek(header["offset"])
- data = self.buf.read(8)
- self.buf.seek(header["offset"])
- if data[:8] == PngImagePlugin._MAGIC:
- # png frame
- im = PngImagePlugin.PngImageFile(self.buf)
- Image._decompression_bomb_check(im.size)
- else:
- # XOR + AND mask bmp frame
- im = BmpImagePlugin.DibImageFile(self.buf)
- Image._decompression_bomb_check(im.size)
- # change tile dimension to only encompass XOR image
- im._size = (im.size[0], int(im.size[1] / 2))
- d, e, o, a = im.tile[0]
- im.tile[0] = d, (0, 0) + im.size, o, a
- # figure out where AND mask image starts
- bpp = header["bpp"]
- if 32 == bpp:
- # 32-bit color depth icon image allows semitransparent areas
- # PIL's DIB format ignores transparency bits, recover them.
- # The DIB is packed in BGRX byte order where X is the alpha
- # channel.
- # Back up to start of bmp data
- self.buf.seek(o)
- # extract every 4th byte (eg. 3,7,11,15,...)
- alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
- # convert to an 8bpp grayscale image
- mask = Image.frombuffer(
- "L", # 8bpp
- im.size, # (w, h)
- alpha_bytes, # source chars
- "raw", # raw decoder
- ("L", 0, -1), # 8bpp inverted, unpadded, reversed
- )
- else:
- # get AND image from end of bitmap
- w = im.size[0]
- if (w % 32) > 0:
- # bitmap row data is aligned to word boundaries
- w += 32 - (im.size[0] % 32)
- # the total mask data is
- # padded row size * height / bits per char
- total_bytes = int((w * im.size[1]) / 8)
- and_mask_offset = header["offset"] + header["size"] - total_bytes
- self.buf.seek(and_mask_offset)
- mask_data = self.buf.read(total_bytes)
- # convert raw data to image
- mask = Image.frombuffer(
- "1", # 1 bpp
- im.size, # (w, h)
- mask_data, # source chars
- "raw", # raw decoder
- ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
- )
- # now we have two images, im is XOR image and mask is AND image
- # apply mask image as alpha channel
- im = im.convert("RGBA")
- im.putalpha(mask)
- return im
- ##
- # Image plugin for Windows Icon files.
- class IcoImageFile(ImageFile.ImageFile):
- """
- PIL read-only image support for Microsoft Windows .ico files.
- By default the largest resolution image in the file will be loaded. This
- can be changed by altering the 'size' attribute before calling 'load'.
- The info dictionary has a key 'sizes' that is a list of the sizes available
- in the icon file.
- Handles classic, XP and Vista icon formats.
- When saving, PNG compression is used. Support for this was only added in
- Windows Vista. If you are unable to view the icon in Windows, convert the
- image to "RGBA" mode before saving.
- This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
- <casadebender@gmail.com>.
- https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
- """
- format = "ICO"
- format_description = "Windows Icon"
- def _open(self):
- self.ico = IcoFile(self.fp)
- self.info["sizes"] = self.ico.sizes()
- self.size = self.ico.entry[0]["dim"]
- self.load()
- @property
- def size(self):
- return self._size
- @size.setter
- def size(self, value):
- if value not in self.info["sizes"]:
- msg = "This is not one of the allowed sizes of this image"
- raise ValueError(msg)
- self._size = value
- def load(self):
- if self.im is not None and self.im.size == self.size:
- # Already loaded
- return Image.Image.load(self)
- im = self.ico.getimage(self.size)
- # if tile is PNG, it won't really be loaded yet
- im.load()
- self.im = im.im
- self.pyaccess = None
- self._mode = im.mode
- if im.size != self.size:
- warnings.warn("Image was not the expected size")
- index = self.ico.getentryindex(self.size)
- sizes = list(self.info["sizes"])
- sizes[index] = im.size
- self.info["sizes"] = set(sizes)
- self.size = im.size
- def load_seek(self):
- # Flag the ImageFile.Parser so that it
- # just does all the decode at the end.
- pass
- #
- # --------------------------------------------------------------------
- Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
- Image.register_save(IcoImageFile.format, _save)
- Image.register_extension(IcoImageFile.format, ".ico")
- Image.register_mime(IcoImageFile.format, "image/x-icon")
|