[Image-SIG] Updated GIF animations for Grail

Fred L. Drake Fred L. Drake, Jr." <fdrake@acm.org
Thu, 4 Jun 1998 12:07:32 -0400 (EDT)


  In April, 1997, I posted an image_gif.py for Grail that used PIL to
load GIFs and support animation if present in the GIF.  This version
uses PIL only if available and Tk if not, so it will replace the old
image_gif.py in the next Grail.  In the meanwhile, it can be dropped
in place of the old one.
  To use PIL, it requires the pil_interface.py module from PIL; see
the docstring at the top of the module.
  It also supports looping of animations, which the older version did
not.
  (Fredrik:  feel free to include this in the Grail/ directory of
future PIL distributions!)


  -Fred

--
Fred L. Drake, Jr.	     <fdrake@acm.org>
Corporation for National Research Initiatives
1895 Preston White Dr.	    Reston, VA  20191


"""image/gif document handling for Grail.

This supports both plain Tk support and PIL-enhanced support.  When PIL is
available and the line

	browser--enable-pil: 1

is located in the ~/.grail/grail-preferences file, PIL will be used and can
support animation of GIF89a files as well as single-frame display.  We still
need a way for the user to STOP the animation!

The files Grail/*.py from the PIL distribution should be installed in the
same directory as this file.
"""
import AsyncImage
import grailutil
import os
import string
import sys
import tempfile
import Tkinter

from formatter import AS_IS

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO


ERROR_FILE = os.path.join("icons", "sadsmiley.gif")


class pil_interface:
    """Dummy class to keep us from having to define PILGifParser within a
    try/except construct."""
    pass

try:
    import Image
    import ImageTk
    from pil_interface import pil_interface
except ImportError:
    _use_pil = 0
else:
    _use_pil = 1


class PILGifParser(pil_interface):
    im = None
    currentpos = 0
    duration = 0
    loop = 0

    def close(self):
	if self.buf:
	    try:
		self.label.config(text="<decoding>")
		self.label.update_idletasks()
		data = string.joinfields(self.buf, "")
                self.buf = None
		self.im = im = Image.open(StringIO(data))
		im.load()
		self.tkim = tkim = ImageTk.PhotoImage(im.mode, im.size)
		tkim.paste(im)
		self.label.image = tkim.image
		self.label.config(image=self.label.image)
		if im.info.has_key("duration"):
		    self.duration = im.info["duration"]
		if im.info.has_key("loop"):
		    self.duration = self.duration or 100
		    self.loop = im.info["loop"]
                    self.data = data
		if self.duration or self.loop:
		    self.viewer.register_reset_interest(self.cancel_loop)
		    self.after_id = self.label.after(self.duration,
						     self.next_image)
	    except:
		self.broken = 1
		stdout = sys.stdout
		try:
		    sys.stdout = sys.stderr
		    print "Error decoding image:"
		    print sys.exc_type + ":", sys.exc_value
		finally:
		    sys.stdout = stdout
	if self.broken:
	    self.label.image = Tkinter.PhotoImage(
		file=grailutil.which(ERROR_FILE))
	    self.label.config(image = self.label.image)
	    self.viewer.text.insert(Tkinter.END, '\nBroken Image!')

    def next_image(self):
	newpos = self.currentpos + 1
	try:
	    self.im.seek(newpos)
	except (ValueError, EOFError):
            if self.loop:
                self.reset_loop()
            else:
                self.viewer.unregister_reset_interest(self.cancel_loop)
                return
        else:
            self.currentpos = newpos
            self.tkim.paste(self.im)
        self.after_id = self.label.after(self.duration, self.next_image)

    def reset_loop(self):
        im = Image.open(StringIO(self.data))
        im.load()
        self.tkim.paste(im)
        self.im = im
        self.currentpos = 0

    def cancel_loop(self, *args):
	self.viewer.unregister_reset_interest(self.cancel_loop)
	self.label.after_cancel(self.after_id)


class TkGifParser:
    """Parser for image/gif files.

    Collect all the data on a temp file and then create an in-line
    image from it.
    """

    def __init__(self, viewer, reload=0):
	self.tf = self.tfname = None
	self.viewer = viewer
	self.viewer.new_font((AS_IS, AS_IS, AS_IS, 1))
	self.tfname = tempfile.mktemp()
	self.tf = open(self.tfname, 'wb')
	self.label = Tkinter.Label(self.viewer.text, text=self.tfname,
                                   highlightthickness=0, borderwidth=0)
	self.viewer.add_subwindow(self.label)

    def feed(self, data):
	self.tf.write(data)

    def close(self):
	if self.tf:
	    self.tf.close()
	    self.tf = None
	    self.label.image = Tkinter.PhotoImage(file=self.tfname)
	    self.label.config(image=self.label.image)
	if self.tfname:
	    try:
		os.unlink(self.tfname)
	    except os.error:
		pass


def parse_image_gif(*args, **kw):
    """Create the appropriate image handler, and replace this function with
    the handler for future references (to skip the determination step)."""

    global parse_image_gif
    if _use_pil and AsyncImage.isPILAllowed():
        parse_image_gif = PILGifParser
    else:
        parse_image_gif = TkGifParser
    return apply(parse_image_gif, args, kw)