Multithreaded Image Viewer
John Hurst
Version 2.6.0
20071229:082507
Table of Contents
1. Overview
This document describes a python program to display background
images on the Mac OS X desktop. It accesses an XML database
containing a list of images for display, along with `scoring'
data on the images themselves. The score of an image can be
recorded by the program, and previously recorded scores can be
used to determine the length of time an image is displayed.
The display operation is handled separately through a system
call to an external script setBackground.sh.
1.1 The Main Body
"rootImage.py" 1.1 =
Define everything, then execute the main body of code.
<run the main code 1.2> =
- {Note 1.2.1}
- The file containing the image database
Execute the main body of code. The flag running is
true while we wish to display images, and the flag
shutdown is set when the program enters its shutdown
routine. The second flag is necessary, as each thread must be
terminated in the shutdown routine before the program stops
completely.
2. Definitions
2.1 Define Imports
<define imports 2.1> =import getopt
import image
import math
import os
import Queue
import random
import re
from subprocess import PIPE,Popen
import sys
import threading
import time
import xml.dom
from xml.dom.minidom import parse
from xml.dom.ext import PrettyPrint
from image import *
Grab all the imported modules here.
- getopt
- is used to perform option handling
<handle options 3.1>;
- image
- is a private mode to handle `image' elements in the
XML database;
- math
- is used for exponentiation in
<determine pause time 3.7>;
- os
- for file operations;
- Queue
- for the input queue handling <handle input queue 4.2>;
- random
- for selecting images at random
<define the compute next image routine 5.2>;
- re
- for various regular expression handling;
- subprocess
- for handling the desktop display
<display image and pause 3.8>;
- sys
- threading
- time
- xml.dom
- xml.dom.minidom
- xml.dom.ext
- for the PrettyPrint routine. This is used to output
the updated XML database
<define the update database thread 4.6>
2.2 Define Global Variables
<define global variables 2.2> =
- {Note 2.2.1}
- flag to indicate that debugging output should be generated.
- {Note 2.2.2}
- set while we want images to be displayed
- {Note 2.2.3}
- flag to indicate that database has
been modified, and not yet updated.
- {Note 2.2.4}
- A lock variable to control
thread access to global variables.
- {Note 2.2.5}
- queue to handle input requests
- {Note 2.2.6}
- flag to indicate that scored entries are to be skipped in displaying
- {Note 2.2.7}
- set when we do not want to skip images due to
explicit navigation to the image.
- {Note 2.2.8}
- count of contiguous images skipped: if this gets past the number of images in the file, quit.
- {Note 2.2.9}
- set to indicate random choice of image
- {Note 2.2.10}
- default length of time image is displayed if no count data
3. Set Up
3.1 Handle Options
<handle options 3.1> =(vals,
path{Note 3.1.1})=getopt.getopt(sys.argv[1:],'dfinrs:vVx',
['debug','file=','ignoreScored','next','random',\
'start=','verbose','version','images='])
for (opt,val) in vals:
if opt=='-d' or opt=='--debug':
debug=1
elif opt=='-i' or opt=='--ignoreScored':
ignoreScored=1
elif opt=='--images':
xmlfilen=val
elif opt=='-n' or opt=='--next':
next=1
elif opt=='-r' or opt=='--random':
randomImages=1
elif opt=='-s' or opt=='--start':
if val:
start=int(val)
else:
print "no value for starting image number"
sys.exit(1)
elif opt=='-v' or opt=='--verbose':
verbose=1
elif opt=='-V' or opt=='--version':
print versiondate,version,versioninfo
sys.exit(0)
else:
pass
- {Note 3.1.1}
-
path is used later to add images to the XML database
The following options are available:
- -d/--debug
- turn on loads of debugging output
- -i/--ignoreScored
- Do not display images that have a score associated with
them. (This is to facilitate processing of large numbers of
images.)
- --images=file
- Use the file file for the image database
- -n/--next
- Display the next image and quit.
- -r/--random
- choose images randomly. If used in conjunction with
-n, choose one image randomly, display it, and exit.
- --start=number
- Start the display at image number number.
- -V/--version
- Display the version information and quit.
3.2 Initialization
<do initialization 3.2> =
<initialize the dom tree 3.3,3.4>
# start the input thread, but not if single shotting (next==1)
if not next:
inputThread=threading.Thread(target=readKB,args=())
inputThread.start()
# start the output thread
outputThread=threading.Thread(target=updateDB,args=(
xmlfilen,dom))
outputThread.start()
images=dom.getElementsByTagName('image')
rootElem=dom.getElementsByTagName('images')[0]
workingdir=rootElem.getAttribute('directory')
if
workingdir:
if
workingdir[-1]!='/':
workingdir+='/'
nextTD=rootElem.getAttribute('nextToDisplay')
if start>-1:
imageNo=start
else:
imageNo=int(nextTD)
if next:
imageNo+=1
if
debug:
print "imageNo incremented to %d" % (imageNo)
if imageNo>=len(images):
imageNo=0
<debug 6.1>(msg='"imageNo=%d (image list size = %d)" % (imageNo,len(images))')
<initialize the dom tree 3.3> =if os.path.isfile(xmlfilen):
# Parse the input XML file
xmlfile=open(
xmlfilen,'r')
dom=parse(xmlfile)
xmlfile.close()
imagelist=buildImageList(dom)
else:
# Build an (empty) XML dom tree for subsequent population
domimp=xml.dom.getDOMImplementation()
doctype=domimp.createDocumentType('images',None,None)
dom=domimp.createDocument(None,'images',doctype)
if debug: PrettyPrint(dom,sys.stdout)
images=dom.documentElement
images.setAttribute('nextToDisplay','0')
imagelist={}
Chunk referenced in 3.2Chunk defined in 3.3,
3.4
If the input file exists and is an XML file, we parse it for
the list of files to display. Otherwise create an empty dom
tree, which will be filled later.
<initialize the dom tree 3.4> =if len(path)>0:
# Add images from the file list to the DOM tree
images=dom.documentElement
for image in path:
<make image element from image name 3.5>
if debug: PrettyPrint(dom,sys.stdout)
# build image list from the new dom tree
imagelist=buildImageList(dom)
Chunk referenced in 3.2Chunk defined in 3.3,
3.4
If we are given a command line list of files, add these to the
DOM tree. Then scan it for the list of images to process.
<make image element from image name 3.5> =# 'image' is the image name, 'imageElement' is the constructed element
if not imagelist.has_key(image):
imageElement=dom.createElement('image')
imageElement.setAttribute('name',image)
images.appendChild(imageElement)
else:
print "%s is already in the database, not added" % (image)
3.3 Start the Main Loop
<start main loop 3.6> =
<debug 6.1>(msg='"main running=%d, shutdown=%d" % (running,shutdown)')
threadList=threading.enumerate()
if
debug:
for th in threadList:
print "thread %s" % (th)
Start the main loop by issuing a debug message if required,
identifying the facts that a) the loop has started, and b) the
threads that are running.
3.4 Display Image
<determine pause time 3.7> =
<debug 6.1>(msg='"main determining pause time"')
power=0.0
count=images[imageNo].getAttribute('count')
if
debug: print "noSkip=%d at entry to ignore loop" % (noSkip)
if ignoreScored and not noSkip:
while count and not shutdown:
print "skipped %d" % (imageNo)
skipped+=1
lenImages=len(images)
if
skipped>lenImages:
shutdown=1
{Note 3.7.1}
print "Nothing to display!"
imageNo=
nextImage(imageNo)
count=images[imageNo].getAttribute('count')
if count: count=int(count)
image=images[imageNo].getAttribute('name')
score=images[imageNo].getAttribute('score')
if count:
count=int(count)
score=int(score)
if count:
power=float(score)/float(count)
else:
power=2.0 # arbitrary value
else:
count=0;score=0; power=0.0
sleeptime=defaultTime
if noSkip:
if
debug: print "noSkip causes count check branch avoidance"
hold=1
elif count:
if score>0:
sleeptime=math.pow(2,power-1)
else:
sleeptime=0.0
if hold:
sleeptime=128.0
msg='holding'
else:
msg='background'
noSkip=0
if
debug: print "noSkip reset"
print "%s %d: %s for %3.1f (score:%3.1f)" % (msg,imageNo,image,sleeptime,power)
- {Note 3.7.1}
- If we have skipped lenImages in a row, then stop running
<display image and pause 3.8> =
<debug 6.1>(msg='"main displaying image %s" % image')
if inputQueue.empty() and sleeptime>0:
if image[0]=='/':
filename=image
else:
filename="%s%s" % (
workingdir,image)
(root,ext)=os.path.splitext(filename)
if ext=='':
filename=filename+'.JPG'
if os.path.isfile(filename):
if debug: print "main displaying image %s" % filename
args=["/home/ajh/bin/setBackground.sh", filename]
output=Popen(args,stdout=PIPE).communicate()[0]
skipped=0
if debug: print output
if next:
shutdown=1
else:
timerCV.acquire()
timeOut=1
timerCV.wait(sleeptime)
timerCV.release()
if timeOut:
imageNo=
nextImage(imageNo)
else:
print "No such image: %s" % (image)
images[imageNo].setAttribute('score',"%d" % 0)
images[imageNo].setAttribute('count',"%d" % 1)
modifyLock.acquire()
modified=1
modifyLock.release()
imageNo=
nextImage(imageNo)
if
debug:
print "finish display: (imageNo)=(%d)" % (imageNo)
else:
imageNo=
nextImage(imageNo)
hold=0
Collect the filename of the image to be displayed, and check
that it is OK. Run a system command pipe to set the display
image via the command setBackground.sh (so that we can
check its output if necessary), and start the timer (if this is
not a single shot display).
If there is no image, update the datebase to give it a score of
zero (which means don't attempt to display it again).
3.5 End Main Loop
<end main loop 3.9> =
Issue a debug message to indicate that we have reached the
end of the main loop. As we have also updated some variables,
indicate this by setting the nextToDisplay attribute in
the database, and the modified flag. Since this is a
shared variable between the threads, modified must be
protected by a lock.
<set main loop modification flag 3.10> =
Flag the fact that database has been modified, and use the
modifyLock lock variable to avoid mutual update.
4. Threads
4.1 Define the Keyboard Thread
<define the keyboard thread 4.1> =def readKB():
global
debug,\
running,shutdown,inputQueue,timeOut,imageNo,\
modified,hold,noSkip
# loop forever (while running), reading keyboard input
line=""
while running and not shutdown:
if
debug:
print "readKB: start read"
line=sys.stdin.readline()
line.strip()
if
debug:
print "readKB: end read, got '%s'" % (line)
while line:
{Note 4.1.1}
# check for score/command input
res=re.match('(([0-9]+)|([a-z])|.)(.*)$',line)
if res:
if
debug:
print "readKB: match a command: %s" % (line)
command=res.group(1)
inputQueue.put(command)
line=res.group(4)
pass # end of while line
<handle input queue 4.2>
pass
- {Note 4.1.1}
- put commands on line into input queue (see below)
The keyboard thread is responsible for reading characters
from the keyboard and assembling them into commands. It loops
continuously while images are being displayed, waiting for
input, and using a regular expression to recognize command
strings. Each command (there may be more than one per line) is
extracted on each cycle of the while line command. The
queue of commands is then processed by chunk
<handle input queue 4.2>
<handle input queue 4.2> =while not inputQueue.empty():
# we have a command, update data and abort timer in the main program
if
debug:
print "while not inputEmpty starts with (imageNo)=(%d)" % (imageNo)
command=inputQueue.get()
if
debug:
print "handling command %s" % (command)
res=re.match('([0-9]+)',command)
if res:
<handle an input score 4.3>
res=re.match('([a-z])',command)
if res:
<handle an input command 4.4>
if
debug:
print "readKB computes next image as %d" % (imageNo)
print "readKB: cancel time out on display"
timerCV.acquire()
timeOut=0
timerCV.notify()
timerCV.release()
if
debug:
print "readKB: timer done"
if shutdown:
if
debug: print "readKB returns"
return
pass # end of while not inputQueue.empty()
While there are input commands on the queue, extract one,
decompose it (some commands have parameters), and handle it
appropriately. Notice the use of a Condition variable
from the Threading module to handle a forced
termination of the display of the current image. In
particular, the notify call signals the wait in
chunk <display image and pause 3.8>, thus terminating the
timer for the (current) image that is being displayed.
<handle an input score 4.3> =userScore=int(res.group(1))
if
debug:
print "got a score '%d'" % (userScore)
userLock.acquire()
count=images[imageNo].getAttribute('count')
score=images[imageNo].getAttribute('score')
if count:
count=int(count)
score=int(score)
count+=1
score+=userScore
else:
count=1
score=userScore
images[imageNo].setAttribute('score',"%d" % score)
images[imageNo].setAttribute('count',"%d" % count)
if
debug:
image=images[imageNo].getAttribute('name')
print "score updated (%d,%d) for %s" % (count,score,image)
userScore=-1
userLock.release()
modifyLock.acquire()
modified=1
modifyLock.release()
imageNo=
nextImage(imageNo)
hold=0
User has entered a score for this image. Update the (local
copy of the) database, and mark it modified. Release any
hold.
<handle an input command 4.4> =userReq=res.group(1)
if
debug:
print "got a command '%s'" % (userReq)
userLock.acquire()
hold=0
if userReq=='d': # delete image from db
<delete this image 4.5>
elif userReq=='g': # go to a particular image
pass # yet to figure this out
elif userReq=='h': # hold this image
hold=1
elif userReq=='n':
imageNo=(imageNo+1) % len(images)
elif userReq=='p':
imageNo=(imageNo-1) % len(images)
elif userReq=='v': # toggle debug mode
debug = not
debug
elif userReq=='q':
threadList=threading.enumerate()
if
debug:
for th in threadList:
print "thread %s" % (th)
print "waiting to update database ..."
shutdown=1
if
debug: print "readKB quitting ..."
noSkip=1
if
debug: print "noSkip set"
userReq='~'
userLock.release()
Parse a single character command.
<delete this image 4.5> =delel=images[imageNo]
parent=delel.parentNode
parent.removeChild(delel)
del images[imageNo]
4.2 Define the Update Database Thread
<define the update database thread 4.6> =
This thread is quite simple. It runs a main loop, as long as
running is required, and on each iteration of the loop, updates
the main database if data has been modified. If a shutdown is
imminent, turn the running variable off only when it is safe to
do so (DB has been updated and modified is off).
Otherwise, pause for 2 seconds to give other things a chance to
run. (Note: this could be a bit smarter, by making
modified a signal.)
5. Subroutines
<define subroutines 5.1> =
5.1 Compute next image
<define the compute next image routine 5.2> =def
nextImage(n):
global randomImages
lenImages=len(images)
if
randomImages:
return random.randrange(0,lenImages,1)
else:
n = (n+1) % lenImages
return n
Compute the index of the next image. If choosing randomly,
choose a value between 0 and len(images)-1. Otherwise
increment the current index modulo the number of images.
6. Macros
Define some useful macros
<debug 6.1> = if debug: print msg
7. The Image Class
Handling of the image database is done in this class, defined
as a separate module.
"image.py" 7.1 =
This module defines two key components: the dom and the imagelist.
7.1 class Image
<classImage 7.2> =class Image:
def __init__(self,name='',elem=None):
self.name=name
self.orgname=''
self.albumpath=''
self.totalvotes=0
self.votescast=0
self.score=0.0
self.elem=elem
def setOrgName(self,name):
self.orgname=name
def setAlbumPath(self,path):
self.albumpath=path
def setTotalVotes(self,votes):
self.totalvotes=votes
def incTotalVotes(self,votes):
self.totalvotes+=votes
self.votescast+=1
self.score=self.totalvotes/self.votescast
def resetTotalVotes(self):
self.totalvotes=0
def str(self):
out='<image name="%s" ' % (self.name)
out+= 'orgname="%s" ' % (self.orgname)
out+= 'albumpath="%s" ' % (self.albumpath)
out+= 'totalvotes="%d" ' % (self.totalvotes)
out+= 'votescast="%d" ' % (self.votescast)
out+= 'score="%5.2f" ' % (self.score)
out+= '/>\n'
return out
7.2 class ImageList
<classImageList 7.3> =class ImageList:
def __init__(self):
self.list={}
def addImage(self,image):
name=image.name
if self.list.has_key(name):
print "already have image %s" % (name)
else:
self.list[name]=image
7.3 image class utility routines
<image class utility routines 7.4> =def
buildImageList(dom):
imagelist={}
images=dom.getElementsByTagName('image')
for image in images:
name=image.getAttribute('name')
#print "got image name %s" % (name)
imagelist[name]=image
return imagelist
def setImageAttr(dom,imagelist,name,attr,value):
if imagelist.has_key(name):
image=imagelist[name]
thisname=image.getAttribute('name')
#print "checking image name %s" % (thisname)
if thisname==name:
image.setAttribute(attr,value)
return
# no element found, must add a new one
image=dom.createElement('image')
image.setAttribute('name',name)
image.setAttribute(attr,value)
images=dom.getElementsByTagName('images')[0]
imagelist[name]=image
images.appendChild(image)
def getImageAttr(dom,imagelist,name,attr):
if imagelist.has_key(name):
image=imagelist[name]
thisname=image.getAttribute('name')
#print "checking image name %s" % (thisname)
if thisname==name:
return image.getAttribute(attr)
# no element found, return empty
return ''
def sortImages(dom,imagelist):
keys=imagelist.keys()
keys.sort()
images=dom.getElementsByTagName('images')[0]
for key in keys:
print "removing key %s" % (key)
imageElem=imagelist[key]
images.removeChild(imageElem)
for key in keys:
print "adding key %s" % (key)
imageElem=imagelist[key]
images.appendChild(imageElem)
8. TO DO
- when using the 'n' and 'p' commands, the time is always 128secs. Perhaps better to use the actual score-time?
- add a 'delete' command that removes the image from further
consideration. This is not that same as scoring 0
(zero) for the image.
- add an XPath parser to determine according to an XPath
expression which images are to be shown.
9. Indices
9.1 Identifier Index
Identifier |
Defined in |
Used in |
buildImageList |
7.4 |
|
debug |
2.2 |
1.2, 3.1, 3.2, 3.6, 3.7, 3.7, 3.7, 3.8, 3.10, 3.10, 3.10, 4.1, 4.1, 4.1, 4.1, 4.2, 4.2, 4.2, 4.2, 4.2, 4.3, 4.3, 4.4, 4.4, 4.4, 4.4, 4.4, 4.4, 6.1
|
defaultTime |
2.2 |
|
modified |
2.2 |
3.8, 3.10, 4.1, 4.3, 4.6, 4.6, 4.6
|
modifyLock |
2.2 |
3.8, 3.8, 3.10, 3.10, 4.3, 4.3, 4.6, 4.6, 4.6
|
nextImage |
5.2 |
3.7, 3.8, 3.8, 3.8, 4.3
|
path |
3.1 |
|
randomImages |
2.2 |
5.2 |
skipped |
2.2 |
3.7, 3.7, 3.8
|
timerCV |
2.2 |
3.8, 3.8, 3.8, 4.2, 4.2, 4.2
|
workingdir |
3.2 |
3.2, 3.2, 3.2, 3.8
|
xmlfilen |
1.2 |
3.1, 3.2, 3.3, 4.6, 4.6, 4.6, 4.6
|
9.2 Chunk Index
9.3 File Index
File Name |
Defined in |
image.py |
7.1 |
rootImage.py |
1.1 |
9.4 Define Version Information
<version 9.1> = 2.6.0
<version date 9.2> = 20071229:082507
<define version information 9.3> =
Document History
20071229:082507 |
ajh |
2.6.0 |
added "d" delete command (debug renamed to "v") |
20071229:080505 |
ajh |
2.5.2 |
allow absolute or relative file names in image list |
20070912:112557 |
2.5.1 |
ajh |
fixed bug in generating new image list |
20070630:171701 |
ajh |
2.5.0 |
include the image module in this literate program |
20070528:141128 |
ajh |
2.4.1 |
literate program amplification |
20070524:154309 |
ajh |
2.4.0 |
added build of XML file form file list |
20070506:114936 |
ajh |
2.3.0 |
added random choice of image |
20070418:161100 |
ajh |
2.2.2 |
toggle debug mode command added |
20070417:121047 |
ajh |
2.2.1 |
added database recovery |
20070413:112145 |
ajh |
2.2.0 |
added 'next' option |