mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-06-11 00:23:21 +03:00
238 lines
7.2 KiB
Python
238 lines
7.2 KiB
Python
#
|
|
# The Python Imaging Library.
|
|
# $Id$
|
|
#
|
|
# Windows Cursor support for PIL
|
|
#
|
|
# notes:
|
|
# uses BmpImagePlugin.py to read the bitmap data.
|
|
#
|
|
# 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.
|
|
#
|
|
from __future__ import annotations
|
|
|
|
from io import BytesIO
|
|
from typing import IO, NamedTuple
|
|
|
|
from . import BmpImagePlugin, IcoImagePlugin, Image, ImageFile
|
|
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"\x00\x00\x02\x00"
|
|
|
|
|
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|
fp.write(_MAGIC)
|
|
bmp = im.encoderinfo.get("bitmap_format", "") == "bmp"
|
|
s = im.encoderinfo.get(
|
|
"sizes",
|
|
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
|
)
|
|
h = im.encoderinfo.get("hotspots", [(0, 0) for i in range(len(s))])
|
|
|
|
if len(h) != len(s):
|
|
msg = "Number of hotspots must be equal to number of cursor sizes"
|
|
raise ValueError(msg)
|
|
|
|
# sort and remove duplicate sizes
|
|
sizes, hotspots = [], []
|
|
for size, hotspot in sorted(zip(s, h), key=lambda x: x[0]):
|
|
if size not in sizes:
|
|
sizes.append(size)
|
|
hotspots.append(hotspot)
|
|
|
|
frames = []
|
|
width, height = im.size
|
|
for size in sizes:
|
|
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
|
|
continue
|
|
|
|
# TODO: invent a more convenient method for proportional scalings
|
|
frame = 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 hotspot, frame in zip(hotspots, 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(o16(hotspot[0])) # x_hotspot(2)
|
|
fp.write(o16(hotspot[1])) # y_hotspot(2)
|
|
|
|
image_io = BytesIO()
|
|
if bmp:
|
|
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.alpha = True
|
|
|
|
frame.save(image_io, "dib")
|
|
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
|
|
bpp: int
|
|
x_hotspot: int
|
|
y_hotspot: int
|
|
size: int
|
|
offset: int
|
|
dim: tuple[int, int]
|
|
square: int
|
|
|
|
|
|
##
|
|
# Image plugin for Windows Cursor files.
|
|
class CurFile(IcoImagePlugin.IcoFile):
|
|
def __init__(self, buf: IO[bytes]) -> None:
|
|
"""
|
|
Parse image from file-like object containing cur file data
|
|
"""
|
|
|
|
# check if CUR
|
|
s = buf.read(6)
|
|
if not _accept(s):
|
|
msg = "not a CUR 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 _ in range(self.nb_items):
|
|
s = buf.read(16)
|
|
|
|
# See Wikipedia
|
|
width = s[0] or 256
|
|
height = s[1] or 256
|
|
|
|
size = i32(s, 8)
|
|
square = width * height
|
|
|
|
# TODO: This needs further investigation. Cursor files do not really
|
|
# specify their bpp like ICO's as those bits are used for the y_hotspot.
|
|
# For now, bpp is calculated by subtracting the AND mask (equal to number
|
|
# of pixels * 1bpp) and dividing by the number of pixels. This seems
|
|
# to work well so far.
|
|
BITMAP_INFO_HEADER_SIZE = 40
|
|
bpp_without_and = ((size - BITMAP_INFO_HEADER_SIZE) * 8) // square
|
|
if bpp_without_and != 32:
|
|
bpp = ((size - BITMAP_INFO_HEADER_SIZE) * 8 - square) // square
|
|
else:
|
|
bpp = bpp_without_and
|
|
|
|
icon_header = IconHeader(
|
|
width=width,
|
|
height=height,
|
|
nb_color=s[2], # No. of colors in image (0 if >=8bpp)
|
|
reserved=s[3],
|
|
bpp=bpp,
|
|
x_hotspot=i16(s, 4),
|
|
y_hotspot=i16(s, 6),
|
|
size=size,
|
|
offset=i32(s, 12),
|
|
dim=(width, height),
|
|
square=square,
|
|
)
|
|
|
|
self.entry.append(icon_header)
|
|
|
|
self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)
|
|
|
|
def hotspots(self) -> list[tuple[int, int]]:
|
|
return [(h.x_hotspot, h.y_hotspot) for h in self.entry]
|
|
|
|
|
|
class CurImageFile(IcoImagePlugin.IcoImageFile):
|
|
"""
|
|
PIL read-only image support for Microsoft Windows .cur 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. It also contains key 'hotspots' that is a list of the
|
|
cursor hotspots.
|
|
|
|
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 is an extension of the IcoImagePlugin.
|
|
|
|
Raises:
|
|
ValueError: The number of sizes and hotspots do not match.
|
|
SyntaxError: The file is not a cursor file.
|
|
TypeError: There are no cursors contained withing the file.
|
|
"""
|
|
|
|
format = "CUR"
|
|
format_description = "Windows Cursor"
|
|
|
|
def _open(self) -> None:
|
|
self.ico = CurFile(self.fp)
|
|
self.info["sizes"] = self.ico.sizes()
|
|
self.info["hotspots"] = self.ico.hotspots()
|
|
if len(self.ico.entry) > 0:
|
|
self.size = self.ico.entry[0].dim
|
|
else:
|
|
msg = "No cursors were found"
|
|
raise TypeError(msg)
|
|
self.load()
|
|
|
|
|
|
#
|
|
# --------------------------------------------------------------------
|
|
|
|
Image.register_open(CurImageFile.format, CurImageFile, _accept)
|
|
Image.register_save(CurImageFile.format, _save)
|
|
Image.register_extension(CurImageFile.format, ".cur")
|