9
\$\begingroup\$

I am writing a program in Python that takes a folder of images and animates them.

For example, going through a list of pictures like this and animating them one by one:

enter image description here

Relevant features include:

  1. Upon startup, ask user what directory they want to animate images from.

  2. If the chosen directory has no valid images, display an error message that describes the error, shows the user a list of valid file types, and then re-asks for a directory.

  3. Using up and down arrow keys speeds and slows the frame rate.

  4. The window starts out at default size, but the user can make the window bigger or smaller with his/her mouse. Then, when the user refreshes (presses Enter), the images will be scaled to fit the window size he/she has chosen. Images will always keep their aspect ratio.

  5. Image files are sorted numerically and alphabetically for the animation. i.e.

    ['hello.ico', '10.png', '2.jpg']
    

    becomes

    ['2.jpg', '10.png', 'hello.ico']
    

I know the code works in Python 2.7, but I haven't tried it in Python 3. It should work, I think!

Any comments, improvements, or suggestions?

# -*- coding: utf-8 -*-
"""
Flipbook Animation: Animate a folder of images.
All image names that are numbers will be sorted in
increasing order, the rest will be sorted alphabetically.
For example, files:
 "70.png", "image.jpg", "29.bmp", and "8.ico"
will be animated in the following order:
 "8.ico", "29.bmp", "70.png", "image.jpg". 
Image types must be in VALID_TYPES (defined below).
Up/Down arrow key increases/decreases the frame rate.
Enter (Return) key refreshes the animation.
Images will be scaled to fit the screen size while
keeping their original aspect ratios. Therefore, to
resize the images, simply resize the window with the
mouse and press Enter (Return) to refresh.
"""
import io, os
from PIL import Image, ImageTk
if os.sys.version_info.major > 2:
 from tkinter.filedialog import askdirectory
 import tkinter as tk
else:
 from tkFileDialog import askdirectory
 import Tkinter as tk
#Icon for main window.
FAVICON = "flipbook.ico"
## Image file extensions that PIL supports reading from.
## Must all be lowercase because that is how they are compared later.
## From "https://infohost.nmt.edu/tcc/help/pubs/pil/formats.html"
VALID_TYPES = (
 "bmp", "dib", "dcx", "gif", "im", "jpg", "jpe", "jpeg", "pcd", "pcx",
 "png", "pbm", "pgm", "ppm", "psd", "tif", "tiff", "xbm", "xpm"
)
class FolderError(Exception):
 """
 Raised if the directory selected by the user has no valid images to animate.
 directory: str, path of user selected folder.
 """
 message = "No valid file types %s found in '{}'." % str(VALID_TYPES)
 def __init__(self, directory):
 self.message = FolderError.message.format(directory)
 def __str__(self):
 return self.message
def sort_list(l):
 """
 Mutates l, returns None.
 l: list of filenames.
 Sorts l alphabetically. But fixes the fact that, 
 alphabetically, '20' comes before '9'.
 Example:
 if l = ['10.png', '2.jpg']:
 a simple l.sort() would make l = ['10.png', '2.jpg'],
 but this function instead makes l = ['2.jpg', '10.png']
 """
 l.sort()
 numbers, extensions = [], []
 i = 0
 while i < len(l):
 try:
 s = l[i].split(".")
 numbers.append(int(s[0]))
 extensions.append(s[1])
 l.pop(i)
 except ValueError:
 i += 1
 #Selection sort.
 for i in xrange(len(numbers)):
 for n in xrange(i, len(numbers)):
 if numbers[i] < numbers[n]:
 numbers[i], numbers[n] = numbers[n], numbers[i]
 extensions[i], extensions[n] = extensions[n], extensions[i]
 for i in xrange(len(numbers)):
 l.insert(0, "%d.%s" % (numbers[i], extensions[i]))
def get_files(directory):
 """
 directory: str, directory to search for files.
 Returns a sorted generator of all the files in directory
 that are a valid type (in VALID_TYPES).
 """
 files = [] 
 for f in os.listdir(directory):
 if len(f.split(".")) > 1 and f.split(".")[1].lower() in VALID_TYPES:
 files.append(f)
 sort_list(files)
 return (directory+"/"+f for f in files)
def get_images(directory, screen):
 """
 directory: str, path to look for files in.
 screen: tuple, (w, h), screen size to resize images to fit on.
 Sorted generator. Yields resized PIL.Image objects for each
 supported image file in directory.
 """
 for filename in get_files(directory):
 with open(filename, "rb") as f:
 fh = io.BytesIO(f.read())
 #Create a PIL image from the data
 img = Image.open(fh, mode="r")
 #Scale image to the screen size while keeping aspect ratio.
 w, h = img.size
 scale = min((screen[0]/float(w), screen[1]/float(h)))
 yield img.resize((int(w*scale), int(h*scale)), Image.ANTIALIAS)
class Home(object):
 """
 Creates a tk.Frame and packs it onto the main window (master).
 Creates a tk.Canvas and packs it onto the frame.
 master: tk.Tk window.
 """
 def __init__(self, master, directory):
 self.frame = tk.Frame(master)
 self.frame.pack(expand=tk.TRUE, fill=tk.BOTH)
 self.can = tk.Canvas(self.frame)
 self.can.pack(expand=tk.TRUE, fill=tk.BOTH)
 #User can update screen size as he/she wishes.
 master.update()
 screen = master.winfo_width(), master.winfo_height()
 #ImageTk.PhotoImages must be made after main Tk window has been opened.
 self.photos = [
 ImageTk.PhotoImage(image=img) for img in get_images(directory, screen)
 ]
 if not self.photos: #len(self.photos) must be > 0.
 raise FolderError(directory)
 self.index = 0 #Index of next photo to draw in self.photos.
 self.current_image = False #Start with no image drawn on screen.
 #center position of photos on screen.
 self.center = screen[0]//2, screen[1]//2
 #Milliseconds between screen redraw.
 self.speed = 100
 self.master, self.directory = master, directory
 def animate(self):
 """
 Destroys old image (if there is one) and shows the next image 
 of the sequence in self.photos. If at end of sequence, restart.
 Re-calls itself every self.screen milliseconds.
 """
 #Remove old image.
 if self.current_image:
 self.can.delete(self.current_image)
 #Continue to next image if possible; otherwise, go back to beginning. 
 self.index = self.index + 1 if self.index + 1 < len(self.photos) else 0
 #Draw image.
 self.current_image = self.can.create_image(self.center, 
 image=self.photos[self.index])
 #Recall self.animate after self.speed milliseconds.
 self.frame.after(self.speed, self.animate)
 def refresh(self):
 """ Destroy current frame. Re-initialize animation. """
 self.frame.destroy()
 self.__init__(self.master, self.directory)
 self.animate()
 def increase_fps(self):
 """ Decrease time between screen redraws. """
 self.speed = self.speed - 1 if self.speed > 1 else 1
 def decrease_fps(self):
 """ Increase time between screen redraws. """
 self.speed += 1
def display_error(name, message):
 """
 name: str, type of error to display as title.
 message: str, error message to display.
 Create new window, display error message.
 """
 root = tk.Tk()
 root.resizable(0, 0)
 root.bell()
 color = "#ffe6e6"
 root.config(bg=color)
 root.title(name)
 root.attributes("-toolwindow", 1)
 tk.Message(root, text=message, bg=color, width=400).pack()
 tk.Button(root, text="Okay", command=root.destroy, width=7).pack()
 root.eval('tk::PlaceWindow %s center'%root.winfo_pathname(root.winfo_id()))
 root.bind("<Return>", lambda event: root.destroy())
 root.mainloop()
def animate(root):
 """
 root: tk.Tk window.
 Ask for a directory of images to animate.
 If directory does not contain at least one valid image:
 display Error window and recall main.
 Bind keys:
 arrow keys increase/decrease frame rate.
 enter/return refreshed animation.
 Begin animation. 
 """
 directory = askdirectory(parent=root, title="Select folder to animate.")
 if not directory: #If user selected cancel or exited.
 root.destroy()
 else:
 try:
 home = Home(root, directory)
 #Key bindings
 root.bind("<Return>", lambda event: home.refresh())
 root.bind("<Up>", lambda event: home.increase_fps())
 root.bind("<Down>", lambda event: home.decrease_fps())
 #Place window in the center of the screen.
 root.eval(
 'tk::PlaceWindow %s center'%root.winfo_pathname(root.winfo_id())
 )
 home.animate()
 #Display any error that was encountered (should only be FolderError).
 except Exception as e:
 root.destroy()
 display_error(e.__class__.__name__, e) #Display error window.
 main() #Restart.
 ## My intention was to re-call animate(root) instead of destroying
 ## root and then calling main. That way I didn't have to keep
 ## making new main windows. This didn't work. The askdirectory
 ## function didn't comply!
 ## i.e.
 ## display_error(e.__class__.__name__, e)
 ## animate(root)
def main():
 """ Open new window. Try to set icon. Set title. Call animate. """
 root = tk.Tk()
 try: root.wm_iconbitmap(FAVICON)
 except tk.TclError: pass #In case file has been moved, etc.
 root.title("Flipbook Animation")
 animate(root)
 root.mainloop()
if __name__ == "__main__": 
 main()
Toby Speight
87.4k14 gold badges104 silver badges322 bronze badges
asked Aug 11, 2016 at 2:09
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Your sorting is overcomplicated as you re-invent a sorting algoritmh in your code while Python has it built-in.

You should use the standard list.sort function and give it an appropriate key (credit to Mathias for suggesting this as it works in-place). "If the name starts with a digit sort by the integer value of the starting number else sort alphabetically".

answered Aug 11, 2016 at 9:27
\$\endgroup\$
3
  • \$\begingroup\$ Note that the sorting is done inplace in OP's function. You could suggest list.sort for the same functionality. \$\endgroup\$ Commented Aug 11, 2016 at 12:53
  • 1
    \$\begingroup\$ That's cool! I didn't realize sort had a key option. \$\endgroup\$ Commented Aug 11, 2016 at 15:35
  • \$\begingroup\$ @MathiasEttinger Answer amended to suggest .sort \$\endgroup\$ Commented Aug 11, 2016 at 16:01

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.