|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 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:
|
|
|
|
|
| frames.append(other_im)
|
| bits_used.append(bits)
|
| break
|
| else:
|
|
|
| frame = provided_im.copy()
|
| frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
| frames.append(frame)
|
| fp.write(o16(len(frames)))
|
| offset = fp.tell() + len(frames) * 16
|
| for frame in frames:
|
| width, height = frame.size
|
|
|
| fp.write(o8(width if width < 256 else 0))
|
| fp.write(o8(height if height < 256 else 0))
|
|
|
| bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
|
| fp.write(o8(colors))
|
| fp.write(b"\0")
|
| fp.write(b"\0\0")
|
| fp.write(o16(bits))
|
|
|
| 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))
|
| fp.write(o32(offset))
|
| 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
|
| """
|
|
|
|
|
| s = buf.read(6)
|
| if not _accept(s):
|
| msg = "not an ICO file"
|
| raise SyntaxError(msg)
|
|
|
| self.buf = buf
|
| self.entry = []
|
|
|
|
|
| self.nb_items = i16(s, 4)
|
|
|
|
|
| for i in range(self.nb_items):
|
| s = buf.read(16)
|
|
|
|
|
| width = s[0] or 256
|
| height = s[1] or 256
|
|
|
|
|
| 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,
|
|
|
|
|
| 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)
|
|
|
| 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:
|
|
|
| im = PngImagePlugin.PngImageFile(self.buf)
|
| Image._decompression_bomb_check(im.size)
|
| else:
|
|
|
| im = BmpImagePlugin.DibImageFile(self.buf)
|
| Image._decompression_bomb_check(im.size)
|
|
|
|
|
| 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)
|
|
|
|
|
| if header.bpp == 32:
|
|
|
|
|
|
|
|
|
|
|
|
|
| self.buf.seek(o)
|
|
|
| alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
|
|
|
|
|
| try:
|
| mask = Image.frombuffer(
|
| "L",
|
| im.size,
|
| alpha_bytes,
|
| "raw",
|
| ("L", 0, -1),
|
| )
|
| except ValueError:
|
| if ImageFile.LOAD_TRUNCATED_IMAGES:
|
| mask = None
|
| else:
|
| raise
|
| else:
|
|
|
| w = im.size[0]
|
| if (w % 32) > 0:
|
|
|
| w += 32 - (im.size[0] % 32)
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
| try:
|
| mask = Image.frombuffer(
|
| "1",
|
| im.size,
|
| mask_data,
|
| "raw",
|
| ("1;I", int(w / 8), -1),
|
| )
|
| except ValueError:
|
| if ImageFile.LOAD_TRUNCATED_IMAGES:
|
| mask = None
|
| else:
|
| raise
|
|
|
|
|
|
|
|
|
| if mask:
|
| im = im.convert("RGBA")
|
| im.putalpha(mask)
|
|
|
| return im
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
| return Image.Image.load(self)
|
| im = self.ico.getimage(self.size)
|
|
|
| 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:
|
|
|
|
|
| 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")
|
|
|