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:
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?
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.
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With