4

I'm trying to make a program to display an animated GIF using Tkinter. Here is the code that I originally used:

from __future__ import division # Just because division doesn't work right in 2.7.4 from Tkinter import * from PIL import Image,ImageTk import threading from time import sleep def anim_gif(name): ## Returns { 'frames', 'delay', 'loc', 'len' } im = Image.open(name) gif = { 'frames': [], 'delay': 100, 'loc' : 0, 'len' : 0 } pics = [] try: while True: pics.append(im.copy()) im.seek(len(pics)) except EOFError: pass temp = pics[0].convert('RGBA') gif['frames'] = [ImageTk.PhotoImage(temp)] temp = pics[0] for item in pics[1:]: temp.paste(item) gif['frames'].append(ImageTk.PhotoImage(temp.convert('RGBA'))) try: gif['delay'] = im.info['duration'] except: pass gif['len'] = len(gif['frames']) return gif def ratio(a,b): if b < a: d,c = a,b else: c,d = a,b if b == a: return 1,1 for i in reversed(xrange(2,int(round(a / 2)))): if a % i == 0 and b % i == 0: a /= i b /= i return (int(a),int(b)) class App(Frame): def show(self,image=None,event=None): self.display.create_image((0,0),anchor=NW,image=image) def animate(self,event=None): self.show(image=self.gif['frames'][self.gif['loc']]) self.gif['loc'] += 1 if self.gif['loc'] == self.gif['len']: self.gif['loc'] = 0 if self.cont: threading.Timer((self.gif['delay'] / 1000),self.animate).start() def kill(self,event=None): self.cont = False sleep(0.1) self.quit() def __init__(self,master): Frame.__init__(self,master) self.grid(row=0,sticky=N+E+S+W) self.rowconfigure(1,weight=2) self.rowconfigure(3,weight=1) self.columnconfigure(0,weight=1) self.title = Label(self,text='No title') self.title.grid(row=0,sticky=E+W) self.display = Canvas(self) self.display.grid(row=1,sticky=N+E+S+W) self.user = Label(self,text='Posted by No Username') self.user.grid(row=2,sticky=E+W) self.comment = Text(self,height=4,width=40,state=DISABLED) self.comment.grid(row=3,sticky=N+E+S+W) self.cont = True self.gif = anim_gif('test.gif') self.animate() root.protocol("WM_DELETE_WINDOW",self.kill) root = Tk() root.rowconfigure(0,weight=1) root.columnconfigure(0,weight=1) app = App(root) app.mainloop() try: root.destroy() except: pass 

test.gif is the following GIF:

enter image description here

This works fine, but the GIF quality is terrible. I tried changing it to what follows:

def anim_gif(name): ## Returns { 'frames', 'delay', 'loc', 'len' } im = Image.open(name) gif = { 'frames': [], 'delay': 100, 'loc' : 0, 'len' : 0 } pics = [] try: while True: gif['frames'].append(im.copy()) im.seek(len(gif['frames'])) except EOFError: pass try: gif['delay'] = im.info['duration'] except: pass gif['len'] = len(gif['frames']) return gif class App(Frame): def show(self,image=None,event=None): can_w = self.display['width'] can_h = self.display['height'] pic_w,pic_h = image.size rat_w,rat_h = ratio(pic_w,pic_h) while pic_w > int(can_w) or pic_h > int(can_h): pic_w -= rat_w pic_h -= rat_h resized = image.resize((pic_w,pic_h)) resized = ImageTk.PhotoImage(resized) self.display.create_image((0,0),anchor=NW,image=resized) 

However, this will occasionally flash a picture. While the picture looks good, it's pretty useless as a program. What am I doing wrong?

3
  • The first thing to do is figure out where the problem is. Try saving temp to a new .PNG file each time through the loop. Are they already screwed up? If so, Tkinter has nothing to do with the problem, which means you can write a much smaller SSCCE. Commented Jun 20, 2013 at 21:32
  • Having looked at the code more carefully, and having done the suggested test for you: The problem is entirely in your PIL code; trying to fix the Tkinter stuff is chasing a wild goose up the wrong tree. Commented Jun 20, 2013 at 22:12
  • Related: Play an Animated GIF with tkinter Commented Dec 16, 2017 at 16:58

2 Answers 2

10

For one, you are creating a new canvas object for every frame. Eventually you will have thousands of images stacked on top of one another. This is highly inefficient; the canvas widget has performance issues when you start to have thousands of objects.

Instead of creating new image objects on the canvas, just reconfigure the existing object with the itemconfig method of the canvas.

Second, you don't need the complexities of threading for such a simple task. There is a well known pattern in tkinter for doing animations: draw a frame, then have that function use after to call itself in the future.

Something like this:

def animate(self): if self._image_id is None: self._image_id = self.display.create_image(...) else: self.itemconfig(self._image_id, image= the_new_image) self.display.after(self.gif["delay"], self.animate) 

Finally, unless there's a strict reason to use a canvas, you can lower the complexity a little more by using a Label widget.

Sign up to request clarification or add additional context in comments.

Comments

5

Your problem has nothing to do with Tkinter. (For all I know, you may also have Tk problems, but your images are already bad before you get to Tk.)

The way I tested this was to modify your anim_gif function to write out the frames as separate image file, by changing the for item in pics[1:] loop like this:

 for i, item in enumerate(pics[1:]): temp.paste(item) temp.save('temp{}.png'.format(i)) gif['frames'].append(ImageTk.PhotoImage(temp.convert('RGBA'))) 

The very first file, temp0.png, is already screwed up, with no Tk-related code being called.

In fact, you can test the same thing even more easily:

from PIL import Image im = Image.open('test.gif') temp = im.copy() im.seek(1) temp.paste(im.copy()) temp.save('test.png') 

The problem is that you're pasting the pixels from frame #1 over top of the pixels from frame #0, but leaving the color palette from frame #0.

There are two easy ways to solve this.

First, use the RGBA-converted frames instead of the palette-color frames:

temp = pics[0].convert('RGBA') gif['frames'] = [ImageTk.PhotoImage(temp)] for item in pics[1:]: frame = item.convert('RGBA') temp.paste(frame) gif['frames'].append(ImageTk.PhotoImage(temp)) 

Second, don't use copy and paste at all; just copy over each frame as an independent image:

gif['frames'] = [ImageTk.PhotoImage(frame.convert('RGBA')) for frame in pics] 

1 Comment

Whoever downvoted, care to explain why? This explains why the OP's code doesn't work, and how to fix it. Of course rewriting the whole thing in a smarter way while coincidentally not making the same mistake also solves the problem, but it doesn't explain what the OP was doing wrong.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.