Pillow/src/PIL/AniImagePlugin.py
2023-09-02 19:24:33 +10:00

440 lines
13 KiB
Python

import struct
from io import BytesIO
from PIL import BmpImagePlugin, CurImagePlugin, Image, ImageFile
from PIL._binary import i32le as i32
from PIL._binary import o8
from PIL._binary import o16le as o16
from PIL._binary import o32le as o32
def _accept(s):
return s[:4] == b"RIFF"
def _save_frame(im: Image.Image, fp: BytesIO, filename: str, info: dict):
fp.write(b"\0\0\2\0")
bmp = True
s = info.get(
"sizes",
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
h = info.get("hotspots", [(0, 0) for _ 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 sorted(set(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, [("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 _write_single_frame(im: Image.Image, fp: BytesIO, filename: str):
fp.write(b"anih")
anih = o32(36) + o32(36) + (o32(1) * 2) + (o32(0) * 4) + o32(60) + o32(1)
fp.write(anih)
fp.write(b"LIST" + o32(0))
list_offset = fp.tell()
fp.write(b"fram")
fp.write(b"icon" + o32(0))
icon_offset = fp.tell()
with BytesIO(b"") as icon_fp:
_save_frame(im, icon_fp, filename, im.encoderinfo)
icon_fp.seek(0)
icon_data = icon_fp.read()
fp.write(icon_data)
fram_end = fp.tell()
fp.seek(icon_offset - 4)
icon_size = fram_end - icon_offset
fp.write(o32(icon_size))
fp.seek(list_offset - 4)
list_size = fram_end - list_offset
fp.write(o32(list_size))
fp.seek(fram_end)
def _write_multiple_frames(im: Image.Image, fp: BytesIO, filename: str):
anih_offset = fp.tell()
fp.write(b"anih" + o32(36))
fp.write(o32(0) * 9)
fp.write(b"LIST" + o32(0))
list_offset = fp.tell()
fp.write(b"fram")
frames = [im]
frames.extend(im.encoderinfo.get("append_images", []))
for frame in frames:
fp.write(b"icon" + o32(0))
icon_offset = fp.tell()
with BytesIO(b"") as icon_fp:
_save_frame(frame, icon_fp, filename, im.encoderinfo)
icon_fp.seek(0)
icon_data = icon_fp.read()
fp.write(icon_data)
fram_end = fp.tell()
fp.seek(icon_offset - 4)
icon_size = fram_end - icon_offset
fp.write(o32(icon_size))
fp.seek(fram_end)
fp.seek(list_offset - 4)
list_size = fram_end - list_offset
fp.write(o32(list_size))
fp.seek(fram_end)
seq = im.encoderinfo.get("seq", [])
if seq:
fp.write(b"seq " + o32(0))
seq_offset = fp.tell()
for i in seq:
if i >= len(frames):
msg = "Sequence index out of animation frame bounds"
raise ValueError(msg)
fp.write(o32(i))
fram_end = fp.tell()
fp.seek(seq_offset - 4)
seq_size = fram_end - seq_offset
fp.write(o32(seq_size))
fp.seek(fram_end)
rate = im.encoderinfo.get("rate", [])
if rate:
fp.write(b"rate" + o32(0))
rate_offset = fp.tell()
if seq:
if len(rate) != len(seq):
msg = "Length of rate must match length of sequence"
raise ValueError(msg)
else:
if len(rate) != len(frames):
msg = "Length of rate must match number of frames"
raise ValueError(msg)
for r in rate:
fp.write(o32(r))
fram_end = fp.tell()
fp.seek(rate_offset - 4)
rate_size = fram_end - rate_offset
fp.write(o32(rate_size))
fp.seek(fram_end)
display_rate = im.encoderinfo.get("display_rate", 2)
n_frames = len(frames)
n_steps = len(seq) if seq else n_frames
flag = 1 if not seq else 3
fram_end = fp.tell()
fp.seek(anih_offset)
fp.write(b"anih")
anih = (
o32(36)
+ o32(36)
+ o32(n_frames)
+ o32(n_steps)
+ (o32(0) * 4)
+ o32(display_rate)
+ o32(flag)
)
fp.write(anih)
fp.seek(fram_end)
def _write_info(im: Image.Image, fp: BytesIO, filename: str):
fp.write(b"LIST" + o32(0))
list_offset = fp.tell()
inam = im.encoderinfo.get("inam", filename)
iart = im.encoderinfo.get("iart", "Pillow")
if isinstance(inam, str):
inam = inam.encode()
if not isinstance(inam, bytes):
msg = "'inam' argument must be either a string or bytes"
raise TypeError(msg)
if isinstance(iart, str):
iart = iart.encode()
if not isinstance(iart, bytes):
msg = "'iart' argument must be either a string or bytes"
raise TypeError(msg)
fp.write(b"INFO")
fp.write(b"INAM" + o32(0))
inam_offset = fp.tell()
fp.write(inam + b"\x00")
inam_size = fp.tell() - inam_offset
fp.write(b"IART" + o32(0))
iart_offset = fp.tell()
fp.write(iart + b"\x00")
iart_size = fp.tell() - iart_offset
info_end = fp.tell()
fp.seek(iart_offset - 4)
fp.write(o32(iart_size))
fp.seek(inam_offset - 4)
fp.write(o32(inam_size))
fp.seek(list_offset - 4)
list_size = info_end - list_offset
fp.write(o32(list_size))
fp.seek(info_end)
def _save(im: Image.Image, fp: BytesIO, filename: str):
fp.write(b"RIFF\x00\x00\x00\x00")
riff_offset = fp.tell()
fp.write(b"ACON")
_write_info(im, fp, filename)
frames = im.encoderinfo.get("append_images", [])
if frames:
_write_multiple_frames(im, fp, filename)
else:
_write_single_frame(im, fp, filename)
pass
riff_end = fp.tell()
fp.seek(riff_offset - 4)
riff_size = riff_end - riff_offset
fp.write(o32(riff_size))
fp.seek(riff_end)
class AniFile:
def __init__(self, buf: BytesIO) -> None:
self.image_data = []
self.buf = buf
self.rate = None
self.seq = None
self.anih = None
riff, size, fformat = struct.unpack("<4sI4s", buf.read(12))
if riff != b"RIFF":
SyntaxError("Not an ANI file")
self.riff = {"size": size, "fformat": fformat}
chunkOffset = buf.tell()
while chunkOffset < self.riff["size"]:
buf.seek(chunkOffset)
chunk, size = struct.unpack("<4sI", buf.read(8))
chunkOffset = chunkOffset + size + 8
if chunk == b"anih":
s = buf.read(36)
self.anih = {
"size": i32(s), # Data structure size (in bytes)
"nFrames": i32(s, 4), # Number of frames
"nSteps": i32(s, 8), # Number of frames before repeat
"iWidth": i32(s, 12), # Width of frame (in pixels)
"iHeight": i32(s, 16), # Height of frame (in pixels)
"iBitCount": i32(s, 20), # Number of bits per pixel
"nPlanes": i32(s, 24), # Number of color planes
# Default frame display rate (1/60th sec)
"iDispRate": i32(s, 28),
"bfAttributes": i32(s, 32), # ANI attribute bit flags
}
if chunk == b"seq ":
s = buf.read(size)
self.seq = [i32(s, i * 4) for i in range(size // 4)]
if chunk == b"rate":
s = buf.read(size)
self.rate = [i32(s, i * 4) for i in range(size // 4)]
if chunk == b"LIST":
listtype = struct.unpack("<4s", buf.read(4))[0]
if listtype != b"fram":
continue
listOffset = 0
while listOffset < size - 8:
_, lSize = struct.unpack("<4sI", buf.read(8))
self.image_data.append({"offset": buf.tell(), "size": lSize})
buf.read(lSize)
listOffset = listOffset + lSize + 8
if self.anih is None:
msg = "not an ANI file"
raise SyntaxError(msg)
if self.seq is None:
self.seq = [i for i in range(self.anih["nFrames"])]
if self.rate is None:
self.rate = [self.anih["iDispRate"] for i in range(self.anih["nFrames"])]
def frame(self, frame):
if frame > self.anih["nFrames"]:
msg = "Frame index out of animation bounds"
raise EOFError(msg)
offset, size = self.image_data[frame].values()
self.buf.seek(offset)
data = self.buf.read(size)
im = CurImagePlugin.CurImageFile(BytesIO(data))
return im
def sizes(self):
return [data["size"] for data in self.image_data]
def hotspots(self):
pass
class AniImageFile(ImageFile.ImageFile):
"""
PIL read-only image support for Microsoft Windows .ani files.
By default the largest resolution image and first frame in the file will
be loaded.
The info dictionary has four keys:
'seq': the sequence of the frames used for animation.
'rate': the rate (in 1/60th of a second) for each frame in the sequence.
'frames': the number of frames in the file.
'sizes': a list of the sizes available for the current frame.
'hotspots': a list of the cursor hotspots for a given frame.
Saving is similar to GIF. Arguments for encoding are:
'sizes': The sizes of the cursor (used for scaling by windows).
'hotspots': The hotspot for each size, with (0, 0) being the top left.
'append_images': The frames for animation. Please note that the sizes and
hotspots are shared across each frame.
'seq': The sequence of frames, zero indexed.
'rate': The rate for each frame in the seq. Must be the same length as seq or
equal to the number of frames if seq is not passed.
"""
format = "ANI"
format_description = "Windows Animated Cursor"
def _open(self):
self.ani = AniFile(self.fp)
self.info["seq"] = self.ani.seq
self.info["rate"] = self.ani.rate
self.info["frames"] = self.ani.anih["nFrames"]
self.frame = 0
self._min_frame = 0
self.seek(0)
self.size = self.im.size
@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):
im = self.ani.frame(self.frame)
self.info["sizes"] = im.info["sizes"]
self.info["hotspots"] = im.info["hotspots"]
self.im = im.im
self._mode = im.mode
def seek(self, frame):
if frame > self.info["frames"] - 1 or frame < 0:
msg = "Frame index out of animation bounds"
raise EOFError(msg)
self.frame = frame
self.load()
Image.register_open(AniImageFile.format, AniImageFile, _accept)
Image.register_extension(AniImageFile.format, ".ani")
Image.register_save(AniImageFile.format, _save)