mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-04 01:47:47 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#
 | 
						|
# 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
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import warnings
 | 
						|
from io import BytesIO
 | 
						|
from math import ceil, log
 | 
						|
from typing import IO, NamedTuple
 | 
						|
 | 
						|
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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
 | 
						|
    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,
 | 
						|
                    [ImageFile._Tile("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: bytes) -> bool:
 | 
						|
    return prefix.startswith(_MAGIC)
 | 
						|
 | 
						|
 | 
						|
class IconHeader(NamedTuple):
 | 
						|
    width: int
 | 
						|
    height: int
 | 
						|
    nb_color: int
 | 
						|
    reserved: int
 | 
						|
    planes: int
 | 
						|
    bpp: int
 | 
						|
    size: int
 | 
						|
    offset: int
 | 
						|
    dim: tuple[int, int]
 | 
						|
    square: int
 | 
						|
    color_depth: int
 | 
						|
 | 
						|
 | 
						|
class IcoFile:
 | 
						|
    def __init__(self, buf: IO[bytes]) -> None:
 | 
						|
        """
 | 
						|
        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)
 | 
						|
 | 
						|
            # See Wikipedia
 | 
						|
            width = s[0] or 256
 | 
						|
            height = s[1] or 256
 | 
						|
 | 
						|
            # No. of colors in image (0 if >=8bpp)
 | 
						|
            nb_color = s[2]
 | 
						|
            bpp = i16(s, 6)
 | 
						|
            icon_header = IconHeader(
 | 
						|
                width=width,
 | 
						|
                height=height,
 | 
						|
                nb_color=nb_color,
 | 
						|
                reserved=s[3],
 | 
						|
                planes=i16(s, 4),
 | 
						|
                bpp=i16(s, 6),
 | 
						|
                size=i32(s, 8),
 | 
						|
                offset=i32(s, 12),
 | 
						|
                dim=(width, height),
 | 
						|
                square=width * height,
 | 
						|
                # See Wikipedia notes about color depth.
 | 
						|
                # We need this just to differ images with equal sizes
 | 
						|
                color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
 | 
						|
            )
 | 
						|
 | 
						|
            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.square, reverse=True)
 | 
						|
 | 
						|
    def sizes(self) -> set[tuple[int, int]]:
 | 
						|
        """
 | 
						|
        Get a set of all available icon sizes and color depths.
 | 
						|
        """
 | 
						|
        return {(h.width, h.height) for h in self.entry}
 | 
						|
 | 
						|
    def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
 | 
						|
        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: tuple[int, int], bpp: int | bool = False) -> Image.Image:
 | 
						|
        """
 | 
						|
        Get an image from the icon
 | 
						|
        """
 | 
						|
        return self.frame(self.getentryindex(size, bpp))
 | 
						|
 | 
						|
    def frame(self, idx: int) -> Image.Image:
 | 
						|
        """
 | 
						|
        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)
 | 
						|
 | 
						|
        im: Image.Image
 | 
						|
        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] = ImageFile._Tile(d, (0, 0) + im.size, o, a)
 | 
						|
 | 
						|
            # figure out where AND mask image starts
 | 
						|
            if header.bpp == 32:
 | 
						|
                # 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
 | 
						|
                try:
 | 
						|
                    mask = Image.frombuffer(
 | 
						|
                        "L",  # 8bpp
 | 
						|
                        im.size,  # (w, h)
 | 
						|
                        alpha_bytes,  # source chars
 | 
						|
                        "raw",  # raw decoder
 | 
						|
                        ("L", 0, -1),  # 8bpp inverted, unpadded, reversed
 | 
						|
                    )
 | 
						|
                except ValueError:
 | 
						|
                    if ImageFile.LOAD_TRUNCATED_IMAGES:
 | 
						|
                        mask = None
 | 
						|
                    else:
 | 
						|
                        raise
 | 
						|
            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
 | 
						|
                try:
 | 
						|
                    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
 | 
						|
                    )
 | 
						|
                except ValueError:
 | 
						|
                    if ImageFile.LOAD_TRUNCATED_IMAGES:
 | 
						|
                        mask = None
 | 
						|
                    else:
 | 
						|
                        raise
 | 
						|
 | 
						|
                # now we have two images, im is XOR image and mask is AND image
 | 
						|
 | 
						|
            # apply mask image as alpha channel
 | 
						|
            if mask:
 | 
						|
                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) -> None:
 | 
						|
        self.ico = IcoFile(self.fp)
 | 
						|
        self.info["sizes"] = self.ico.sizes()
 | 
						|
        self.size = self.ico.entry[0].dim
 | 
						|
        self.load()
 | 
						|
 | 
						|
    @property
 | 
						|
    def size(self) -> tuple[int, int]:
 | 
						|
        return self._size
 | 
						|
 | 
						|
    @size.setter
 | 
						|
    def size(self, value: tuple[int, int]) -> None:
 | 
						|
        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) -> Image.core.PixelAccess | None:
 | 
						|
        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._mode = im.mode
 | 
						|
        if im.palette:
 | 
						|
            self.palette = im.palette
 | 
						|
        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
 | 
						|
        return Image.Image.load(self)
 | 
						|
 | 
						|
    def load_seek(self, pos: int) -> None:
 | 
						|
        # 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")
 |