Skip to content

Bad image grab from GIF file by tkinter.PhotoImage with option format 'gif -index indexValue' #93510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jason990420 opened this issue Jun 5, 2022 · 5 comments
Labels
topic-tkinter type-feature A feature request or enhancement

Comments

@jason990420
Copy link

Bug report

Bad image grab from the GIF file by tkinter PhotoImage with option format 'gif -index indexValue'.
This issue happened at lot of GIF image files, not only for this special GIF image.

In following image, there are 52 frames, it is enough, only first two frames to show this issue.

  • Top-two images show the result grabbed by PhotoImage with option format 'gif -index indexValue'
  • Bottom-two images show the result grabbed by PIL.Image

image

Script to demo

from io import BytesIO
from PIL import Image
import tkinter as tk

def image_to_data(im):
    with BytesIO() as output:
        im.save(output, format="PNG")
        data = output.getvalue()
    return data

filename = '1ac.gif'    # https://cdn2.thecatapi.com/images/1ac.gif
n_frames = 2            # 52 but only 2 to show to difference

root = tk.Tk()

frame1 = tk.Frame(root)
frame1.pack()

images1 = [tk.PhotoImage(file=filename, format=f'gif -index {i}') for i in range(n_frames)]
for i in range(n_frames):
    label = tk.Label(frame1, image=images1[i])
    label.pack(side='left')

im = Image.open(filename)

frame2 = tk.Frame(root)
frame2.pack()

images2 = []
for i in range(n_frames):
    im.seek(i)
    images2.append(tk.PhotoImage(data=image_to_data(im)))
    label = tk.Label(frame2, image=images2[i])
    label.pack(side='left')

root.mainloop()

Environment

WIN10 / Pillow 9.1.1
python 3.7.9 / tkinter 8.6.9
python 3.8.10 / tkinter 8.6.9
python 3.9.9 / tkinter 8.6.12
python 3.10.1 / tkinter 8.6.12

@jason990420 jason990420 added the type-bug An unexpected behavior, bug, or error label Jun 5, 2022
@arhadthedev
Copy link
Member

It looks like PhotoImage returns raw GIF frames (with transparency) that are designed to be painted over the previous ones, while PIL.Image converts them into full-fledged still images.

@jason990420
Copy link
Author

jason990420 commented Jun 5, 2022

Following code show each frame and it looks that

  • Only first frame full shown
  • All other frames maybe only show the differeces
import tkinter as tk

def next_one():
    global index, image

    index = (index+1) % n_frames
    image = tk.PhotoImage(file=filename, format=f'gif -index {index}')
    label.configure(image=image)
    text.configure(text=f'Frame {index}')

filename = '1ac.gif'    # https://cdn2.thecatapi.com/images/1ac.gif
n_frames = 52

root = tk.Tk()

index = 0
image = tk.PhotoImage(file=filename, format=f'gif -index {index}')
label = tk.Label(root, image=image)
label.pack()

text = tk.Label(text='Frame 0')
text.pack()

button = tk.Button(root, text='Next', command=next_one)
button.pack()

root.mainloop()

image
image
image

If so, how it can be painted to widget ?

  • For tk.Label widget, it will be updated by new image frame, no overlay.
  • For tk.Canvas, following code show it work, but to be painted over the previous ones will get items in Canvas widget get more and more, then maybe eventually out of memory ?
import tkinter as tk

def next_one():
    global index, image

    index = (index+1) % n_frames
    image = tk.PhotoImage(file=filename, format=f'gif -index {index}')
    canvas.create_image(0, 0, anchor=tk.NW, image=image)
    text.configure(text=f'Frame {index}')

filename = '1ac.gif'    # https://cdn2.thecatapi.com/images/1ac.gif
n_frames = 52

root = tk.Tk()

index = 0
image = tk.PhotoImage(file=filename, format=f'gif -index {index}')
canvas = tk.Canvas(root, width=500, height=211)
canvas.pack()

canvas.create_image(0, 0, anchor=tk.NW, image=image)

text = tk.Label(text='Frame 0')
text.pack()

button = tk.Button(root, text='Next', command=next_one)
button.pack()

root.mainloop()

@serhiy-storchaka
Copy link
Member

Tk supports the copy subcommand for photoimages which allows in-place modifications. It supports multiple options. But this is one of few cases in which Tkinter provides an interface significantly different from Tk. PhotoImage.copy() returns a new copy instead of modifying image in-place (perhaps for compatibility with dict.copy() etc), there are also methods subsample() and zoom() for some of Tk copy options, but there are no methods for other options, and you cannot combine two different images and several options in one command.

For now, your only options either to use third-party libraries like PIL, or call Tk directly from Python:

accumImage.tk.call(accumImage, 'copy', deltaImage)

(note that attributes tk and call are not documented and not officially supported, so this code can stop working in some distant future).

I think that we can add a new method for this. But we should also think about more general solution.

@serhiy-storchaka serhiy-storchaka added type-feature A feature request or enhancement and removed type-bug An unexpected behavior, bug, or error labels Oct 14, 2022
@jason990420
Copy link
Author

It work fine by using the method accumImage.tk.call(accumImage, 'copy', deltaImage).

import tkinter as tk

def next_one():
    global index, accumImage

    index = (index+1) % n_frames
    deltaImage = tk.PhotoImage(file=filename, format=f'gif -index {index}')
    accumImage.tk.call(accumImage, 'copy', deltaImage)
    canvas.create_image(0, 0, anchor=tk.NW, image=accumImage)
    text.configure(text=f'Frame {index}')

filename = '1ac.gif'    # https://cdn2.thecatapi.com/images/1ac.gif
n_frames = 52

root = tk.Tk()

index = 0
accumImage = tk.PhotoImage(file=filename, format=f'gif -index {index}')
canvas = tk.Canvas(root, width=500, height=211)
canvas.pack()

canvas.create_image(0, 0, anchor=tk.NW, image=accumImage)

text = tk.Label(text='Frame 0')
text.pack()

button = tk.Button(root, text='Next', command=next_one)
button.pack()

root.mainloop()

image

@serhiy-storchaka
Copy link
Member

A new method for copying from image to image will be added in #118225.

A new method for reading from a file into existing image will be added in #118271. (Not sure it will help here.)

Closing this issue, as it is not a bug, and there are other issues for particular features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-tkinter type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

4 participants