| 1 | Overview | ||
| 1.1 | The Main Body | ||
| 2 | Definitions | ||
| 2.1 | Define Imports | ||
| 2.2 | Define Global Variables | ||
| 3 | Set Up | ||
| 3.1 | Handle Options | ||
| 3.2 | Initialization | ||
| 3.3 | Start the Main Loop | ||
| 3.4 | Display Image | ||
| 3.5 | End Main Loop | ||
| 4 | Threads | ||
| 4.1 | Define the Keyboard Thread | ||
| 4.2 | Define the Update Database Thread | ||
| 5 | Subroutines | ||
| 5.1 | Compute next image | ||
| 6 | Macros | ||
| 7 | The Image Class | ||
| 7.1 | |||
| 8 | TO DO | ||
| 9 | Indices | ||
| 9.1 | Identifier Index | ||
| 9.2 | Chunk Index | ||
| 9.3 | File Index | ||
| 9.4 | Define Version Information | ||
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.
#!/usr/bin/python <define version information 9.3> <define imports 2.1> <define global variables 2.2> <define subroutines 5.1> <define the keyboard thread 4.1> <define the update database thread 4.5> if __name__ == '__main__': <run the main code 1.2> pass # end of if main pass # end of program
Define everything, then execute the main body of code.
<run the main code 1.2> =xmlfilen='/home/ajh/etc/LSL06Images.xml'{Note 1.2.1} start=-1 <handle options 3.1> <do initialization 3.2> while running and not shutdown: <start main loop 3.6> <determine pause time 3.7> <display image and pause 3.8> <end main loop 3.9> pass # end of while if debug: print "main quitting ..."
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.
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.
debug{Note 2.2.1}=0 running{Note 2.2.2}=1 ; shutdown=0 userReq='~'; userScore=-1; userLock=threading.Lock() modified{Note 2.2.3}=0 modifyLock{Note 2.2.4}=threading.Lock() inputQueue{Note 2.2.5}=Queue.Queue(80) timerCV=threading.Condition() dom=None ignoreScored{Note 2.2.6}=0 timeOut=1 imageNo=0 hold=0 next=0 noSkip=0{Note 2.2.7} randomImages=0{Note 2.2.8}
(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
The following options are available:
<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))')
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={}
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)
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)
<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.
<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: print "skipped %d" % (imageNo) 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=10.0 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)
<debug 6.1>(msg='"main displaying image %s" % image') if inputQueue.empty() and sleeptime>0: 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] 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 (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).
<debug 6.1>(msg='"main updates nextToDisplay"') rootElem.setAttribute('nextToDisplay',"%d" % (imageNo)) <set main loop modification flag 3.10>
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> =if debug: print "main waits to acquire modifyLock" modifyLock.acquire() if debug: print "main acquires modifyLock" modified=1 modifyLock.release() if debug: print "main releases modifyLock"
Flag the fact that database has been modified, and use the modifyLock lock variable to avoid mutual update.
def readKB(): global debug,\ running,shutdown,inputQueue,timeOut,imageNo,\ modified,hold,noSkip # loop forever (while running), reading keyboard input line="" while running: 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
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': # toggle debug mode debug = not debug 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=='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.
def updateDB(xmlfilen,dom): global running,shutdown,modified,modifyLock while running: if modified: modifyLock.acquire() xmlout=open(xmlfilen+'.new','w') PrettyPrint(dom,xmlout) xmlout.close() os.rename(xmlfilen+'.new',xmlfilen) modified=0 modifyLock.release() if shutdown: <debug 6.1>(msg='"updateDB quitting ..."') running=0 return else: time.sleep(2.0)
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.)
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.
Define some useful macros
<debug 6.1> = if debug: print msgHandling of the image database is done in this class, defined as a separate module.
"image.py" 7.1 =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 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 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)
This module defines two key components: the dom and the imagelist.
| Identifier | Defined in | Used in |
|---|---|---|
| buildImageList | 7.1 | |
| 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 |
| modified | 2.2 | 3.8, 3.10, 4.1, 4.3, 4.5, 4.5, 4.5 |
| modifyLock | 2.2 | 3.8, 3.8, 3.10, 3.10, 4.3, 4.3, 4.5, 4.5, 4.5 |
| path | 3.1 | |
| randomImages | 2.2 | 5.2 |
| timerCV | 2.2 | 3.8, 3.8, 3.8, 4.2, 4.2, 4.2 |
| xmlfilen | 1.2 | 3.1, 3.2, 3.3, 4.5, 4.5, 4.5, 4.5 |
| Chunk Name | Defined in | Used in |
|---|---|---|
| debug | 6.1 | 3.2, 3.6, 3.7, 3.8, 3.9, 4.5 |
| define global variables | 2.2 | 1.1 |
| define imports | 2.1 | 1.1 |
| define subroutines | 5.1 | 1.1 |
| define the compute next image routine | 5.2 | 5.1 |
| define the keyboard thread | 4.1 | 1.1 |
| define the update database thread | 4.5 | 1.1 |
| define version information | 9.3 | 1.1 |
| determine pause time | 3.7 | 1.2 |
| display image and pause | 3.8 | 1.2 |
| do initialization | 3.2 | 1.2 |
| end main loop | 3.9 | 1.2 |
| handle an input command | 4.4 | 4.2 |
| handle an input score | 4.3 | 4.2 |
| handle input queue | 4.2 | 4.1 |
| handle options | 3.1 | 1.2 |
| initialize the dom tree | 3.3, 3.4 | 3.2 |
| make image element from image name | 3.5 | 3.4 |
| run the main code | 1.2 | 1.1 |
| set main loop modification flag | 3.10 | 3.9 |
| start main loop | 3.6 | 1.2 |
| version | 9.1 | 9.3 |
| version date | 9.2 | 9.3 |
| File Name | Defined in |
|---|---|
| image.py | 7.1 |
| rootImage.py | 1.1 |
version='<version 9.1>' versioninfo='fixed bug in generating new image list' versiondate='<version date 9.2>'
| 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 |