mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-04-15 06:41:59 +03:00
added .ani plugin
This commit is contained in:
parent
0b3bc198fb
commit
c11dea98b6
BIN
Tests/images/ani/aero_busy.ani
Normal file
BIN
Tests/images/ani/aero_busy.ani
Normal file
Binary file not shown.
BIN
Tests/images/ani/posy_busy.ani
Normal file
BIN
Tests/images/ani/posy_busy.ani
Normal file
Binary file not shown.
BIN
Tests/images/ani/stopwtch.ani
Normal file
BIN
Tests/images/ani/stopwtch.ani
Normal file
Binary file not shown.
378
src/PIL/AniImagePlugin.py
Normal file
378
src/PIL/AniImagePlugin.py
Normal file
|
@ -0,0 +1,378 @@
|
|||
from io import BytesIO
|
||||
import struct
|
||||
from PIL import Image, CurImagePlugin, ImageFile, BmpImagePlugin
|
||||
from PIL._binary import i16le as i16
|
||||
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
|
||||
sizes = info.get(
|
||||
"sizes",
|
||||
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||
)
|
||||
hotspots = info.get("hotspots", [(0, 0) for i in range(len(sizes))])
|
||||
if len(hotspots) != len(sizes):
|
||||
raise ValueError("Number of hotspots must be equal to number of cursor sizes")
|
||||
|
||||
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:
|
||||
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 _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] + 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):
|
||||
raise ValueError("Sequence index out of animation frame bounds")
|
||||
|
||||
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):
|
||||
raise ValueError("Length of rate must match rate of sequence")
|
||||
else:
|
||||
if len(rate) != len(frames):
|
||||
raise ValueError("Length of rate must match number of frames")
|
||||
|
||||
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)
|
||||
|
||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||
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):
|
||||
raise TypeError("'inam' argument must be either a string or bytes")
|
||||
|
||||
if isinstance(iart, str):
|
||||
iart = iart.encode()
|
||||
if not isinstance(iart, bytes):
|
||||
raise TypeError("'iart' argument must be either a string or bytes")
|
||||
|
||||
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:
|
||||
raise SyntaxError("not an ANI file")
|
||||
|
||||
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"]:
|
||||
raise ValueError("Frame index out of animation bounds")
|
||||
|
||||
offset, size = self.image_data[frame].values()
|
||||
self.buf.seek(offset)
|
||||
data = self.buf.read(size)
|
||||
|
||||
im = CurImagePlugin.CurImageFile(BytesIO(data))
|
||||
return im
|
||||
|
||||
|
||||
|
||||
class AniImageFile(ImageFile.ImageFile):
|
||||
format = "ANI"
|
||||
format_description = "Windows Animated Cursor"
|
||||
|
||||
def _open(self):
|
||||
self.ani = AniFile(self.fp)
|
||||
self.frame = 0
|
||||
self.seek(0)
|
||||
self.size = self.im.size
|
||||
self.info["seq"] = self.ani.seq
|
||||
self.info["rate"] = self.ani.rate
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, value):
|
||||
if value not in self.info['sizes']:
|
||||
raise ValueError("This is not one of the allowed sizes of this image")
|
||||
self._size = value
|
||||
|
||||
|
||||
def load(self):
|
||||
im = self.ani.frame(self.frame)
|
||||
self.info['sizes'] = im.info['sizes']
|
||||
self.im = im.im
|
||||
self.mode = im.mode
|
||||
|
||||
def seek(self, frame):
|
||||
if frame > self.ani.anih["nFrames"]:
|
||||
raise ValueError("Frame index out of animation bounds")
|
||||
|
||||
self.frame = frame
|
||||
self.load()
|
||||
|
||||
|
||||
Image.register_open(AniImageFile.format, AniImageFile, _accept)
|
||||
Image.register_extension(AniImageFile.format, ".ani")
|
||||
Image.register_save(AniImageFile.format, _save)
|
|
@ -23,6 +23,7 @@ del _version
|
|||
|
||||
|
||||
_plugins = [
|
||||
"AniImagePlugin",
|
||||
"BlpImagePlugin",
|
||||
"BmpImagePlugin",
|
||||
"BufrStubImagePlugin",
|
||||
|
|
Loading…
Reference in New Issue
Block a user