mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-11 12:17:14 +03:00
507 lines
12 KiB
Python
507 lines
12 KiB
Python
#
|
|
# THIS IS WORK IN PROGRESS
|
|
#
|
|
# The Python Imaging Library.
|
|
# $Id$
|
|
#
|
|
# ARG animation support code
|
|
#
|
|
# history:
|
|
# 1996-12-30 fl Created
|
|
# 1996-01-06 fl Added safe scripting environment
|
|
# 1996-01-10 fl Added JHDR, UHDR and sYNC support
|
|
# 2005-03-02 fl Removed AAPP and ARUN support
|
|
#
|
|
# Copyright (c) Secret Labs AB 1997.
|
|
# Copyright (c) Fredrik Lundh 1996-97.
|
|
#
|
|
# See the README file for information on usage and redistribution.
|
|
#
|
|
|
|
from __future__ import print_function
|
|
|
|
__version__ = "0.4"
|
|
|
|
from PIL import Image, ImageFile, ImagePalette
|
|
|
|
from PIL.PngImagePlugin import i8, i16, i32, ChunkStream, _MODES
|
|
|
|
MAGIC = b"\212ARG\r\n\032\n"
|
|
|
|
# --------------------------------------------------------------------
|
|
# ARG parser
|
|
|
|
class ArgStream(ChunkStream):
|
|
"Parser callbacks for ARG data"
|
|
|
|
def __init__(self, fp):
|
|
|
|
ChunkStream.__init__(self, fp)
|
|
|
|
self.eof = 0
|
|
|
|
self.im = None
|
|
self.palette = None
|
|
|
|
self.__reset()
|
|
|
|
def __reset(self):
|
|
|
|
# reset decoder state (called on init and sync)
|
|
|
|
self.count = 0
|
|
self.id = None
|
|
self.action = ("NONE",)
|
|
|
|
self.images = {}
|
|
self.names = {}
|
|
|
|
|
|
def chunk_AHDR(self, offset, bytes):
|
|
"AHDR -- animation header"
|
|
|
|
# assertions
|
|
if self.count != 0:
|
|
raise SyntaxError("misplaced AHDR chunk")
|
|
|
|
s = self.fp.read(bytes)
|
|
self.size = i32(s), i32(s[4:])
|
|
try:
|
|
self.mode, self.rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
|
except:
|
|
raise SyntaxError("unknown ARG mode")
|
|
|
|
if Image.DEBUG:
|
|
print("AHDR size", self.size)
|
|
print("AHDR mode", self.mode, self.rawmode)
|
|
|
|
return s
|
|
|
|
def chunk_AFRM(self, offset, bytes):
|
|
"AFRM -- next frame follows"
|
|
|
|
# assertions
|
|
if self.count != 0:
|
|
raise SyntaxError("misplaced AFRM chunk")
|
|
|
|
self.show = 1
|
|
self.id = 0
|
|
self.count = 1
|
|
self.repair = None
|
|
|
|
s = self.fp.read(bytes)
|
|
if len(s) >= 2:
|
|
self.id = i16(s)
|
|
if len(s) >= 4:
|
|
self.count = i16(s[2:4])
|
|
if len(s) >= 6:
|
|
self.repair = i16(s[4:6])
|
|
else:
|
|
self.repair = None
|
|
|
|
if Image.DEBUG:
|
|
print("AFRM", self.id, self.count)
|
|
|
|
return s
|
|
|
|
def chunk_ADEF(self, offset, bytes):
|
|
"ADEF -- store image"
|
|
|
|
# assertions
|
|
if self.count != 0:
|
|
raise SyntaxError("misplaced ADEF chunk")
|
|
|
|
self.show = 0
|
|
self.id = 0
|
|
self.count = 1
|
|
self.repair = None
|
|
|
|
s = self.fp.read(bytes)
|
|
if len(s) >= 2:
|
|
self.id = i16(s)
|
|
if len(s) >= 4:
|
|
self.count = i16(s[2:4])
|
|
|
|
if Image.DEBUG:
|
|
print("ADEF", self.id, self.count)
|
|
|
|
return s
|
|
|
|
def chunk_NAME(self, offset, bytes):
|
|
"NAME -- name the current image"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced NAME chunk")
|
|
|
|
name = self.fp.read(bytes)
|
|
self.names[self.id] = name
|
|
|
|
return name
|
|
|
|
def chunk_AEND(self, offset, bytes):
|
|
"AEND -- end of animation"
|
|
|
|
if Image.DEBUG:
|
|
print("AEND")
|
|
|
|
self.eof = 1
|
|
|
|
raise EOFError("end of ARG file")
|
|
|
|
def __getmodesize(self, s, full=1):
|
|
|
|
size = i32(s), i32(s[4:])
|
|
|
|
try:
|
|
mode, rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
|
except:
|
|
raise SyntaxError("unknown image mode")
|
|
|
|
if full:
|
|
if i8(s[12]):
|
|
pass # interlace not yet supported
|
|
if i8(s[11]):
|
|
raise SyntaxError("unknown filter category")
|
|
|
|
return size, mode, rawmode
|
|
|
|
def chunk_PAST(self, offset, bytes):
|
|
"PAST -- paste one image into another"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced PAST chunk")
|
|
|
|
if self.repair is not None:
|
|
# we must repair the target image before we
|
|
# start pasting
|
|
|
|
# brute force; a better solution would be to
|
|
# update only the dirty rectangles in images[id].
|
|
# note that if images[id] doesn't exist, it must
|
|
# be created
|
|
|
|
self.images[self.id] = self.images[self.repair].copy()
|
|
self.repair = None
|
|
|
|
s = self.fp.read(bytes)
|
|
im = self.images[i16(s)]
|
|
x, y = i32(s[2:6]), i32(s[6:10])
|
|
bbox = x, y, im.size[0]+x, im.size[1]+y
|
|
|
|
if im.mode in ["RGBA"]:
|
|
# paste with transparency
|
|
# FIXME: should handle P+transparency as well
|
|
self.images[self.id].paste(im, bbox, im)
|
|
else:
|
|
# paste without transparency
|
|
self.images[self.id].paste(im, bbox)
|
|
|
|
self.action = ("PAST",)
|
|
self.__store()
|
|
|
|
return s
|
|
|
|
def chunk_BLNK(self, offset, bytes):
|
|
"BLNK -- create blank image"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced BLNK chunk")
|
|
|
|
s = self.fp.read(bytes)
|
|
size, mode, rawmode = self.__getmodesize(s, 0)
|
|
|
|
# store image (FIXME: handle colour)
|
|
self.action = ("BLNK",)
|
|
self.im = Image.core.fill(mode, size, 0)
|
|
self.__store()
|
|
|
|
return s
|
|
|
|
def chunk_IHDR(self, offset, bytes):
|
|
"IHDR -- full image follows"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced IHDR chunk")
|
|
|
|
# image header
|
|
s = self.fp.read(bytes)
|
|
size, mode, rawmode = self.__getmodesize(s)
|
|
|
|
# decode and store image
|
|
self.action = ("IHDR",)
|
|
self.im = Image.core.new(mode, size)
|
|
self.decoder = Image.core.zip_decoder(rawmode)
|
|
self.decoder.setimage(self.im, (0,0) + size)
|
|
self.data = b""
|
|
|
|
return s
|
|
|
|
def chunk_DHDR(self, offset, bytes):
|
|
"DHDR -- delta image follows"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced DHDR chunk")
|
|
|
|
s = self.fp.read(bytes)
|
|
|
|
size, mode, rawmode = self.__getmodesize(s)
|
|
|
|
# delta header
|
|
diff = i8(s[13])
|
|
offs = i32(s[14:18]), i32(s[18:22])
|
|
|
|
bbox = offs + (offs[0]+size[0], offs[1]+size[1])
|
|
|
|
if Image.DEBUG:
|
|
print("DHDR", diff, bbox)
|
|
|
|
# FIXME: decode and apply image
|
|
self.action = ("DHDR", diff, bbox)
|
|
|
|
# setup decoder
|
|
self.im = Image.core.new(mode, size)
|
|
|
|
self.decoder = Image.core.zip_decoder(rawmode)
|
|
self.decoder.setimage(self.im, (0,0) + size)
|
|
|
|
self.data = b""
|
|
|
|
return s
|
|
|
|
def chunk_JHDR(self, offset, bytes):
|
|
"JHDR -- JPEG image follows"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced JHDR chunk")
|
|
|
|
# image header
|
|
s = self.fp.read(bytes)
|
|
size, mode, rawmode = self.__getmodesize(s, 0)
|
|
|
|
# decode and store image
|
|
self.action = ("JHDR",)
|
|
self.im = Image.core.new(mode, size)
|
|
self.decoder = Image.core.jpeg_decoder(rawmode)
|
|
self.decoder.setimage(self.im, (0,0) + size)
|
|
self.data = b""
|
|
|
|
return s
|
|
|
|
def chunk_UHDR(self, offset, bytes):
|
|
"UHDR -- uncompressed image data follows (EXPERIMENTAL)"
|
|
|
|
# assertions
|
|
if self.count == 0:
|
|
raise SyntaxError("misplaced UHDR chunk")
|
|
|
|
# image header
|
|
s = self.fp.read(bytes)
|
|
size, mode, rawmode = self.__getmodesize(s, 0)
|
|
|
|
# decode and store image
|
|
self.action = ("UHDR",)
|
|
self.im = Image.core.new(mode, size)
|
|
self.decoder = Image.core.raw_decoder(rawmode)
|
|
self.decoder.setimage(self.im, (0,0) + size)
|
|
self.data = b""
|
|
|
|
return s
|
|
|
|
def chunk_IDAT(self, offset, bytes):
|
|
"IDAT -- image data block"
|
|
|
|
# pass compressed chunks through the decoder
|
|
s = self.fp.read(bytes)
|
|
self.data = self.data + s
|
|
n, e = self.decoder.decode(self.data)
|
|
if n < 0:
|
|
# end of image
|
|
if e < 0:
|
|
raise IOError("decoder error %d" % e)
|
|
else:
|
|
self.data = self.data[n:]
|
|
|
|
return s
|
|
|
|
def chunk_DEND(self, offset, bytes):
|
|
return self.chunk_IEND(offset, bytes)
|
|
|
|
def chunk_JEND(self, offset, bytes):
|
|
return self.chunk_IEND(offset, bytes)
|
|
|
|
def chunk_UEND(self, offset, bytes):
|
|
return self.chunk_IEND(offset, bytes)
|
|
|
|
def chunk_IEND(self, offset, bytes):
|
|
"IEND -- end of image"
|
|
|
|
# we now have a new image. carry out the operation
|
|
# defined by the image header.
|
|
|
|
# won't need these anymore
|
|
del self.decoder
|
|
del self.data
|
|
|
|
self.__store()
|
|
|
|
return self.fp.read(bytes)
|
|
|
|
def __store(self):
|
|
|
|
# apply operation
|
|
cid = self.action[0]
|
|
|
|
if cid in ["BLNK", "IHDR", "JHDR", "UHDR"]:
|
|
# store
|
|
self.images[self.id] = self.im
|
|
|
|
elif cid == "DHDR":
|
|
# paste
|
|
cid, mode, bbox = self.action
|
|
im0 = self.images[self.id]
|
|
im1 = self.im
|
|
if mode == 0:
|
|
im1 = im1.chop_add_modulo(im0.crop(bbox))
|
|
im0.paste(im1, bbox)
|
|
|
|
self.count = self.count - 1
|
|
|
|
if self.count == 0 and self.show:
|
|
self.im = self.images[self.id]
|
|
raise EOFError # end of this frame
|
|
|
|
def chunk_PLTE(self, offset, bytes):
|
|
"PLTE -- palette data"
|
|
|
|
s = self.fp.read(bytes)
|
|
if self.mode == "P":
|
|
self.palette = ImagePalette.raw("RGB", s)
|
|
return s
|
|
|
|
def chunk_sYNC(self, offset, bytes):
|
|
"SYNC -- reset decoder"
|
|
|
|
if self.count != 0:
|
|
raise SyntaxError("misplaced sYNC chunk")
|
|
|
|
s = self.fp.read(bytes)
|
|
self.__reset()
|
|
return s
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
# ARG reader
|
|
|
|
def _accept(prefix):
|
|
return prefix[:8] == MAGIC
|
|
|
|
##
|
|
# Image plugin for the experimental Animated Raster Graphics format.
|
|
|
|
class ArgImageFile(ImageFile.ImageFile):
|
|
|
|
format = "ARG"
|
|
format_description = "Animated raster graphics"
|
|
|
|
def _open(self):
|
|
|
|
if Image.warnings:
|
|
Image.warnings.warn(
|
|
"The ArgImagePlugin driver is obsolete, and will be removed "
|
|
"from a future release of PIL. If you rely on this module, "
|
|
"please contact the PIL authors.",
|
|
RuntimeWarning
|
|
)
|
|
|
|
if self.fp.read(8) != MAGIC:
|
|
raise SyntaxError("not an ARG file")
|
|
|
|
self.arg = ArgStream(self.fp)
|
|
|
|
# read and process the first chunk (AHDR)
|
|
|
|
cid, offset, bytes = self.arg.read()
|
|
|
|
if cid != "AHDR":
|
|
raise SyntaxError("expected an AHDR chunk")
|
|
|
|
s = self.arg.call(cid, offset, bytes)
|
|
|
|
self.arg.crc(cid, s)
|
|
|
|
# image characteristics
|
|
self.mode = self.arg.mode
|
|
self.size = self.arg.size
|
|
|
|
def load(self):
|
|
|
|
if self.arg.im is None:
|
|
self.seek(0)
|
|
|
|
# image data
|
|
self.im = self.arg.im
|
|
self.palette = self.arg.palette
|
|
|
|
# set things up for further processing
|
|
Image.Image.load(self)
|
|
|
|
def seek(self, frame):
|
|
|
|
if self.arg.eof:
|
|
raise EOFError("end of animation")
|
|
|
|
self.fp = self.arg.fp
|
|
|
|
while True:
|
|
|
|
#
|
|
# process chunks
|
|
|
|
cid, offset, bytes = self.arg.read()
|
|
|
|
if self.arg.eof:
|
|
raise EOFError("end of animation")
|
|
|
|
try:
|
|
s = self.arg.call(cid, offset, bytes)
|
|
except EOFError:
|
|
break
|
|
|
|
except "glurk": # AttributeError
|
|
if Image.DEBUG:
|
|
print(cid, bytes, "(unknown)")
|
|
s = self.fp.read(bytes)
|
|
|
|
self.arg.crc(cid, s)
|
|
|
|
self.fp.read(4) # ship extra CRC
|
|
|
|
def tell(self):
|
|
return 0
|
|
|
|
def verify(self):
|
|
"Verify ARG file"
|
|
|
|
# back up to first chunk
|
|
self.fp.seek(8)
|
|
|
|
self.arg.verify(self)
|
|
self.arg.close()
|
|
|
|
self.fp = None
|
|
|
|
#
|
|
# --------------------------------------------------------------------
|
|
|
|
Image.register_open("ARG", ArgImageFile, _accept)
|
|
|
|
Image.register_extension("ARG", ".arg")
|
|
|
|
Image.register_mime("ARG", "video/x-arg")
|