Quick Image Sorting

From CodeCodex

This Python script, using PyGTK, takes the names of a bunch of image files or directories containing image files, and presents the images to the user one at a time, using the right- and left-arrow keys to move quickly through the images, and F11 and F12 to rotate the image view in 90-degree increments. It is also possible to bind custom actions (in the form of Shell commands) to keystrokes. For instance, the command

SortPictures --act="b:mv %s bad/" --act="g:mv %s good/" new/

presents all the image files in turn in the new subdirectory, such that pressing the "b" key moves the currently-displayed image into the bad subdirectory, while pressing the "g" key moves the current image into the good subdirectory.

This script allows the user to quickly sort through several hundred images in just a few minutes.

#!/usr/bin/python
#+
# This script displays picture files in turn from specified directories,
# and allows the user to hit keystrokes to apply commands to them.
#
# Invoke this script as follows:
#
#     SortPictures [options] item [item ...]
#
# where each "item" is either the name of an image file or of a
# directory containg image files to be shown. Valid options are:
#
#     --act k:cmd
#         defines a key binding, where k is a single ASCII character
#         which, when typed by the user, invokes cmd. This option can be
#         specified multiple times with different k values, to define multiple
#         key bindings. When cmd is invoked, occurrences of %s are substituted
#         with the full name of the image file.
#    --random
#         equivalent to --sort=random
#    --sort=how
#         displays the images in order according to how:
#             none (default) -- no special sorting
#             mod -- sort by last-mod date
#             random -- display in random order
#
# Standard keystrokes are:
#    right or down arrow -- go to next picture
#    left or up arrow -- go to previous picture
#    F11 -- rotate picture anticlockwise
#    F12 -- rotate picture clockwise
#
# Created 2006 March 30 by Lawrence D'Oliveiro <ldo@geek-central.gen.nz>.
# Add --sort 2007 February 10.
# Scale down large images to fit window and add rotation functions 2007 May 6.
#-

import sys
import os
import random
import getopt
import gobject
import gtk

#+
# Useful stuff
#-

def ForEachFile(ArgList, Action, ActionArg) :
    """invokes Action(FileName, ActionArg) for each non-directory item
    found in ArgList. If an item is not a directory, passes it directly
    to action; otherwise, passes each file directly contained within it,
    unless the name ends with "...", in which case all file descendants
    of the directory are passed."""

    def ForEach(Item, Recurse) :
        if os.path.isdir(Item) :
            for Child in os.listdir(Item) :
                Child = os.path.join(Item, Child)
                if os.path.isdir(Child) :
                    if Recurse :
                        ForEach(Child, True)
                    #end if
                else :
                    Action(Child, ActionArg)
                #end if
            #end for
        else :
            Action(Item, ActionArg)
        #end if
    #end ForEach

    for Arg in ArgList :
        if Arg.endswith("...") :
            Recurse = True
            Arg = Arg[: -3]
        else :
            Recurse = False
        #end if
        ForEach(Arg, Recurse)
    #end for
#end ForEachFile

#+
# GUI callbacks
#-

# globals:
# TheImage -- GDK pixbuf object containing image being displayed
# ImageDisplay -- GTK image object for showing an image
# ImageLabel -- GTK label object for showing image name
#
# Act -- mapping of actions to perform by keystroke
# Files -- list of image files to show
# FileIndex -- index into Files of image being shown

def DestroyWindow(TheWindow) :
    # called when main window's close box is clicked.
    gtk.main_quit()
#end DestroyWindow

def LoadImage() :
    # loads the image from the currently selected file.
    global TheImage
    ImageName = Files[FileIndex]
    try :
        TheImage = gtk.gdk.pixbuf_new_from_file(ImageName)
    except gobject.GError :
        TheImage = None
    #end try
    ImageLabel.set_text("%u/%u: %s" % (FileIndex + 1, len(Files), ImageName))
#end LoadImage

def RotateImage(Clockwise) :
    # rotates the displayed image by 90 degrees.
    global TheImage
    if Clockwise :
        Direction = gtk.gdk.PIXBUF_ROTATE_CLOCKWISE
    else :
        Direction = gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE
    #end if
    if TheImage != None :
        TheImage = TheImage.rotate_simple(Direction)
    #end if
#end RotateImage

def ShowImage() :
    # displays the currently-loaded image in the main window.
    if TheImage != None :
        ImageWidth = TheImage.get_property("width")
        ImageHeight = TheImage.get_property("height")
        if ImageWidth > MaxImageDisplay.x or ImageHeight > MaxImageDisplay.y :
            ScaleFactor = min \
              (
                float(MaxImageDisplay.x) / ImageWidth,
                float(MaxImageDisplay.y) / ImageHeight
              )
            UseImage = TheImage.scale_simple \
              (
                dest_width = int(round(ScaleFactor * ImageWidth)),
                dest_height = int(round(ScaleFactor * ImageHeight)),
                interp_type = gtk.gdk.INTERP_BILINEAR
              )
        else :
            UseImage = TheImage
        #end if
        ImageDisplay.set_from_pixbuf(UseImage)
    else :
        ImageDisplay.set_from_stock \
          (
            gtk.STOCK_MISSING_IMAGE,
            gtk.ICON_SIZE_LARGE_TOOLBAR
          )
    #end if
#end ShowImage

def KeyPressEvent(TheWindow, TheEvent) :
    # called in response to a keystroke when the main window has the focus.
    global Files, FileIndex
    # print "Keypress type %d val %d" % (TheEvent.type, TheEvent.keyval) # debug
    Key = TheEvent.keyval
    if Key == gtk.keysyms.Down or Key == gtk.keysyms.Right :
        if FileIndex + 1 < len(Files) :
            FileIndex += 1
            LoadImage()
            ShowImage()
        #end if
    elif Key == gtk.keysyms.Up or Key == gtk.keysyms.Left :
        if FileIndex > 0 :
            FileIndex -= 1
            LoadImage()
            ShowImage()
        #end if
    elif Key == gtk.keysyms.F11 :
        RotateImage(False)
        ShowImage()
    elif Key == gtk.keysyms.F12 :
        RotateImage(True)
        ShowImage()
    elif Key in Act :
        Cmd = Act[Key] % Files[FileIndex]
        print Cmd
        os.system(Cmd)
    #end if
    return True
#end KeyPressEvent

#+
# Mainline
#-

def AddFile(Item, Files) :
    # ForEachFile action to collect names of all image files.
    Files.append(Item)
#end AddFile

ModDate = {} # cache of mod dates to avoid repeated lookups
def ModDateKey(File) :
    """sort key callback which orders files by their last-mod date."""
    return ModDate.setdefault(File, os.path.getmtime(File))
#end ModDateOrder

def Order(Files) :
    # sorts Files according to the function defined by Key.
    Files.sort(key=Key)
#end Order

(Opts, Args) = getopt.getopt \
  (
    sys.argv[1:],
    "",
    ["act=", "random", "sort="]
  )

Files = []
ForEachFile(Args, AddFile, Files)

Act = {}
Sort = None
for Keyword, Value in Opts :
    if Keyword == "--act" :
        if len(Value) > 2 and Value[1] == ":" :
            Act[ord(Value[0])] = Value[2:]
            # print "act %d => \"%s\"" % (ord(Value[0]), Value[2:]) # debug
        else :
            raise getopt.error("Invalid --act syntax: \"%s\"" % Value)
        #end if
    elif Keyword == "--random" :
        Sort = random.shuffle
    elif Keyword == "--sort" :
        if Value == "random" :
            Sort = random.shuffle
        elif Value == "mod" :
            Key = ModDateKey
            Sort = Order
        elif Value == "none" :
            Sort = None
        else :
            raise getopt.error("Invalid sort option %s" % Value)
        #end if
    #end if
#end for
if len(Files) == 0 :
    raise getopt.error("Nothing to do")
#end if
if Sort != None :
    Sort(Files)
#end if

TheImage = None # to begin with
FileIndex = 0
# DefaultScreen = gtk.gdk.display_get_default().get_default_screen()
class MaxImageDisplay :
    """maximum bounds of image display"""
    x = gtk.gdk.screen_width() - 32
    y = gtk.gdk.screen_height() - 96
#end MaxImageDisplay
MainWindow = gtk.Window()
MainWindow.connect("destroy", DestroyWindow)
MainWindow.connect("key_press_event", KeyPressEvent)
MainWindow.set_border_width(10)
MainVBox = gtk.VBox(False, 8)
ImageDisplay = gtk.Image()
MainVBox.pack_start(ImageDisplay, False, False, 0)
ImageLabel = gtk.Label()
MainVBox.pack_start(ImageLabel, False, False, 0)
MainWindow.add(MainVBox)
MainWindow.show_all()
MainWindow.show()
LoadImage()
ShowImage()
gtk.main()