# -*- coding: iso-8859-1 -*- """ Gallery.py Version 0.82 This macro creates dynamic tabulated displays based on attachment contents @copyright: 2004 by Simon Ryan smartblackbox.com> http://smartblackbox.com/simon @license: GPL Special thanks go to: My beautiful wife Jenny: For keeping the kids at bay long enough for me to code it :-) Adam Shand: For his GallerySoftware feature wish list, support, ideas and suggestions. Usage: [[Gallery(key1=value1,key2=value2....)]] where the following keys are valid: thumbnailwidth = no of pixels wide to make the thumbnails webnailwidth = width in pixels of the web sized images numberofcolumns = no of columns used in the thumbnail table Bugs: All attachments are expected to be images Continued rotation will degrade the tmp images (but they can be forced to regen) Features: Simple usage, just put [[Gallery]] on any page and upload some pictures as attachments Rotate buttons Annotation Not yet implemented, but in the works: Comment on this Pic button (to create a moinmoin subpage) Handling of video formats Support for Python Imaging Library Speed up: # When you get really sick of how slow the moinmoin image system is, # you can set the following variables in your moin_config.py gallerytempdir (the path to a writable directory) gallerytempurl (the path in your webservers url space where this directory can be read from) eg: gallerytempdir='/var/www/html/nails' gallerytempurl='/nails' or maybe: gallerytempurl=url_prefix+'/nails' # There are other ways of getting speedups for attachment, but this method is the safest (IMHO) """ __author__ = "Simon D. Ryan" __version__ = "0.82" from MoinMoin import config, wikiutil import string, cStringIO, os import commands, shutil import moin_config class Globs: # A quick place to plonk those shared variables thumbnailwidth='200' webnailwidth='600' numberofcolumns=4 adminmsg='' debuglevel=0 originals={} convertbin='' annotated={} attachmentdir='' gallerytempdirroot='' gallerytempdir='' gallerytempurl='' pagename='' admin='' bcomp='' baseurl='' timeout=40 def message(astring,level=1): if level<=Globs.debuglevel: Globs.adminmsg=Globs.adminmsg+'Gallery:  '+astring+'
\n' def version(): return(' version '+Globs.version+' by Simon D. Ryan.'+\ '
Copyright 2004 Simon D. Ryan
Gallery is a MoinMoin macro and is released under the '+\ 'GPL\n'+\ '

Upload some images as attachments to '+Globs.pagename+' and I will generate a gallery for you.') # Thanks to dennyece.arizona.edu # This can be replaced with a static translation table to speed things up (later) def mktrans(): # Allow only letters and digits and a few other valid file characters alphanumeric=string.letters+string.digits+'.,-_\'!"' source_string="" destination_string="" for i in range(256): source_string=source_string+chr(i) if chr(i) in alphanumeric: destination_string=destination_string+chr(i) else: destination_string=destination_string+' ' return string.maketrans(source_string,destination_string) def qlink(pagename, querystring, query, description=''): # Returns a hyperlink constructed as a form query on pagename if not description: description=query return ''+description+'' def navibar(target,querystring): # Returns a navigational bar with PREV,THUMBS,NEXT positions=Globs.originals.keys() positions.sort() thumbs='THUMBS' index=positions.index(target) back,forward='','' if not index==0: # We are not the first so we can provide a back link back=qlink(Globs.pagename, querystring, positions[index-1], 'PREV') if not index==len(positions)-1: # We are not the last so we can provide a forward link forward=qlink(Globs.pagename, querystring, positions[index+1], 'NEXT') return '
'+back+''+thumbs+''+forward+'
' def toolbar(target,naillevel): if Globs.admin: rotateleft='' rotateright='' htarget='' compat='' return '

'+rotateleft+''+rotateright+'
\n'+htarget+compat+'
' else: return '' def buildnails(items): # For now we use commands.getoutput to do our dirty work # Later we can build a batch job and fork it off. # Make sure our temp directory is writable and generate a message if it isn't try: if not os.path.isfile(Globs.gallerytempdir+'/tmp.writetest'): # There is probably a less ugly was to do this using stat (later) open(Globs.gallerytempdir+'/tmp.writetest','w').close() except IOError: message('I had some trouble writing to the temp directory. Is it owned by me and writable?',0) # Don't go further if there is a lock in place if os.path.isfile(Globs.attachmentdir+'/tmp.lock'): message("I'm currently busy generating thumbnails and webnails, please try again later.",0) return '' # Find the convert binary in standard locations if not os.path.isfile('/usr/bin/convert'): if not os.path.isfile('/usr/X11R6/bin/convert'): message('Please install ImageMagick so I can build thumbnails and webnails

',0) return else: Globs.convertbin='/usr/X11R6/bin/convert' else: Globs.convertbin='/usr/bin/convert' # Create a lock file in the attachments dir so we can always remotely remove it if there is a problem open(Globs.attachmentdir+'/tmp.lock','w').close() import time tstart=time.time() pid,pid2='','' # For each original file, check for the existance of a nail for item in items: basename,prefix,width=item # Check to see if we tarry too long on the road if tstart and (time.time()-tstart) > Globs.timeout: # This is taking waaay too long let us fork and detach else the browser will time out or worse, the webserver may kill us pid = os.fork() if pid != 0: # We are in the parent so we break out message('The thumbnail generation process was taking too long so it has been backgrounded. Please try again later to see the full set of thumbnails',0) break else: # Once we are forked we want to ignore the time tstart='' # Break away from the controlling terminal, so that the web server cannot kill us by killing our parent os.setsid() # Fork again so we can get away without a controlling terminal pid2 = os.fork() if (pid2 != 0): os._exit(0) else: # Close all open file descriptors try: max_fd = os.sysconf("SC_OPEN_MAX") except (AttributeError, ValueError): max_fd = 256 for fd in range(0, max_fd): try: os.close(fd) except OSError: pass # Redirect the standard file descriptors to /dev/null os.open("/dev/null", os.O_RDONLY) # stdin os.open("/dev/null", os.O_RDWR) # stdout os.open("/dev/null", os.O_RDWR) # stderr # Now we are finally free to continue the conversions as a daemon # If you would like to know more about the above, see: # Advanced Programming in the Unix Environment: W. Richard Stevens # It is also explained in: # Unix Network Programming (Volume 1): W. Richard Stevens pathtooriginal='"'+Globs.attachmentdir+'/'+Globs.originals[basename]+'"' # Warning: # Take care if modifying the following line, # you may inadvertantly overwrite your original images! convout=commands.getoutput('%s -geometry %s %s "%s/%s.%s.jpg"' % (Globs.convertbin,width+'x'+width,pathtooriginal,Globs.gallerytempdir,prefix,basename)) convout=string.strip(convout) if convout: message(convout) if (not pid) and (not pid2): # Release the lock file when finished os.unlink(Globs.attachmentdir+'/tmp.lock') # We have built thumbnails so we can deposit an indicator file to prevent rebuilding next time if not os.path.isfile(Globs.attachmentdir+'/delete.me.to.regenerate.thumbnails.and.webnails'): open(Globs.attachmentdir+'/delete.me.to.regenerate.thumbnails.and.webnails','w').close() def rotate(target,direction): # Rotate the images # Don't go further if there is a lock in place if os.path.isfile(Globs.attachmentdir+'/tmp.lock'): message("I'm currently busy generating thumbnails and webnails. Please try your rotate request again later.",0) return '' # Find the correct binary if not os.path.isfile('/usr/bin/mogrify'): if not os.path.isfile('/usr/X11R6/bin/mogrify'): message('Please install ImageMagick so I can build thumbnails and webnails

',0) return else: Globs.convertbin='/usr/X11R6/bin/mogrify' else: Globs.convertbin='/usr/bin/mogrify' # Do the actual rotations if direction=='rotate right': degs='90' else: degs='270' convout=commands.getoutput(Globs.convertbin+' -rotate '+degs+' '+Globs.gallerytempdir+'/tmp.webnail.'+target+'.jpg') convout=commands.getoutput(Globs.convertbin+' -rotate '+degs+' '+Globs.gallerytempdir+'/tmp.thumbnail.'+target+'.jpg') if not os.path.isfile(Globs.gallerytempdir+'/tmp.rotated.'+target+'.jpg'): # Generate from original pathtooriginal=Globs.attachmentdir+'/'+Globs.originals[target] shutil.copy(pathtooriginal,Globs.gallerytempdir+'/tmp.rotated.'+target+'.jpg') convout=commands.getoutput(Globs.convertbin+' -rotate '+degs+' '+Globs.gallerytempdir+'/tmp.rotated.'+target+'.jpg') def getannotation(target): # Annotations are stored as a file for now (later to be stored in images) atext='' if Globs.annotated.has_key(target): atext=open(Globs.attachmentdir+'/tmp.annotation.'+target+'.txt').readline() message('was annotated') else: message('was not annotated') # replace double quotes with the html escape so quoted annotations appear return string.replace(atext,'"','"') def execute(macro, args): Globs.version=__version__ # Containers formvals={} thumbnails={} webnails={} rotated={} # Class variables need to be specifically set # (except for the case where a value is to be shared with another Gallery macro on the same wiki page) Globs.originals={} Globs.annotated={} Globs.attachmentdir='' Globs.admin='' Globs.adminmsg='' Globs.pagename='' # process arguments if args: # Arguments are comma delimited key=value pairs sargs=string.split(args,',') for item in sargs: sitem=string.split(item,'=') if len(sitem)==2: key,value=sitem[0],sitem[1] if key=='thumbnailwidth': Globs.thumbnailwidth=value elif key=='webnailwidth': Globs.webnailwidth=value elif key=='numberofcolumns': try: Globs.numberofcolumns=string.atoi(value) except TypeError: pass # Experimental, uncomment at own risk #elif key=='pagename': # Globs.pagename=value transtable=mktrans() # Useful variables dontregen='' annotationmessage='' textdir=config.text_dir Globs.baseurl=macro.request.getBaseURL()+'/' if not Globs.pagename: Globs.pagename = string.replace(macro.formatter.page.page_name,'/','_2f') # Hmmm. A bug in moinmoin? underscores are getting escaped. These doubly escaped pagenames are even appearing in data/pages pagepath = string.replace(wikiutil.getPagePath(Globs.pagename),'_5f','_') Globs.attachmentdir = pagepath+'/attachments' if hasattr(moin_config,'gallerytempdir') and hasattr(moin_config,'gallerytempurl'): message('gallerytempdir and gallerytempurl found') Globs.gallerytempdirroot=moin_config.gallerytempdir Globs.gallerytempdir=moin_config.gallerytempdir+'/'+Globs.pagename+'/' Globs.gallerytempurl=moin_config.gallerytempurl+'/'+Globs.pagename+'/' elif hasattr(moin_config,'attachments'): Globs.gallerytempdirroot=moin_config.attachments['dir'] Globs.gallerytempdir=moin_config.attachments['dir']+'/'+Globs.pagename+'/attachments/' Globs.gallerytempurl=moin_config.attachments['url']+'/'+Globs.pagename+'/attachments/' Globs.attachmentdir = Globs.gallerytempdir else: Globs.gallerytempdir=Globs.attachmentdir Globs.gallerytempurl=Globs.pagename+'?action=AttachFile&do=get&target=' if args: args=macro.request.getText(args) # HTML Constants tleft='
' tmidd='
' trigh='
\n' # Add this to the end of each URL to keep some versions of moinmoin happy Globs.bcomp='&action=show' # Process any form items into a dictionary (values become unique) for item in macro.form.items(): if not formvals.has_key(item[0]): # Here is where we clean the untrusted web input # (sometimes we get foreign keys from moinmoin when the page is edited) try: formvals[item[0]]=string.translate(item[1][0],transtable) except AttributeError: pass # Figure out if we have delete privs try: # If a user can delete the page containing the Gallery, then they are considered a Gallery administrator # This probably should be configurable via a moin_config variable eg: galleryadminreq = if macro.request.user.may.delete(macro.formatter.page.page_name): Globs.admin='true' except AttributeError: pass out=cStringIO.StringIO() # Grab a list of the files in the attachment directory if os.path.isdir(Globs.attachmentdir): if Globs.gallerytempdir==Globs.attachmentdir: afiles=os.listdir(Globs.attachmentdir) else: if not os.path.isdir(Globs.gallerytempdir): # Try to create it if it is absent spagename=string.split(Globs.pagename,'/') compbit='' for component in spagename: compbit=compbit+'/'+component os.mkdir(Globs.gallerytempdirroot+compbit) #os.mkdir(Globs.gallerytempdir) if os.path.isdir(Globs.gallerytempdir): afiles=os.listdir(Globs.attachmentdir)+os.listdir(Globs.gallerytempdir) else: message('You need to create the temp dir first:'+Globs.gallerytempdir,0) return macro.formatter.rawHTML( Globs.adminmsg+'

') # Split out the thumbnails and webnails for item in afiles: if item.startswith('tmp.thumbnail.'): origname=item[14:-4] thumbnails[origname]='' elif item.startswith('tmp.webnail.'): origname=item[12:-4] webnails[origname]='' elif item.startswith('tmp.rotated.'): origname=item[12:-4] rotated[origname]='' elif item.startswith('tmp.annotation.'): origname=item[15:-4] Globs.annotated[origname]='' elif item == 'delete.me.to.regenerate.thumbnails.and.webnails': dontregen='true' elif item == 'tmp.writetest' or item == 'tmp.lock': pass else: # This must be one of the original images lastdot=string.rfind(item,'.') origname=item[:lastdot] Globs.originals[origname]=item else: message(version(),0) return macro.formatter.rawHTML( Globs.adminmsg ) if not Globs.gallerytempdir==Globs.attachmentdir and os.path.isfile(Globs.attachmentdir+'/tmp.writetest'): # If we are using the new gallerytempdir and we were using the old system then make sure there are no # remnant files from the old system in the attachment dir to confuse us message('You have changed to using a gallerytempdir so I am cleaning old tmp files from your attachment dir.',0) for item in webnails.keys(): try: os.unlink(Globs.attachmentdir+'/tmp.webnail.'+item+'.jpg') except: pass # Try deleting any old thumbnails which may be in the attachment directory for item in thumbnails.keys(): try: os.unlink(Globs.attachmentdir+'/tmp.thumbnail.'+item+'.jpg') except: pass # Try deleting any old rotated originals which may be in the attachment directory for item in rotated.keys(): try: os.unlink(Globs.attachmentdir+'/tmp.rotated.'+item+'.jpg') except: pass os.unlink(Globs.attachmentdir+'/tmp.writetest') newnails=[] # Any thumbnails need to be built? for key in Globs.originals.keys(): if (not thumbnails.has_key(key)) or (not dontregen): # Create a thumbnail for this original newnails.append((key,'tmp.thumbnail',Globs.thumbnailwidth)) # Any webnails need to be built? for key in Globs.originals.keys(): if (not webnails.has_key(key)) or (not dontregen): # Create a webnail for this original newnails.append((key,'tmp.webnail',Globs.webnailwidth)) # Ok, lets build them all at once if not len(newnails)==0: buildnails(newnails) # If a regen of thumbnails and webnails has occurred, then we should also delete any tmp.rotated files. if not dontregen: for key in rotated.keys(): # Wrapped in a try except since child processes may try to unlink a second time try: os.unlink(Globs.gallerytempdir+'/tmp.rotated.'+key+'.jpg') except: pass if formvals.has_key('annotate'): if Globs.admin and formvals.has_key('target'): target=formvals['target'] # Write an annotation file atext=string.replace(formvals['annotate'],'"','"') target=formvals['target'] ouf=open(Globs.attachmentdir+'/tmp.annotation.'+target+'.txt','w') ouf.write(atext) ouf.close() message('Annotation updated to '+atext+'',0) # Now update the annotated dictionary if not Globs.annotated.has_key(target): Globs.annotated[target]='' if formvals.has_key('webnail'): # Does the webnail exist? message('webnail requested') target=formvals['webnail'] if Globs.originals.has_key(target): out.write(navibar(target,'webnail')) if formvals.has_key('rotate'): direction=formvals['rotate'] message(direction) rotate(target,direction) # Put things in a table out.write(tleft) # Lets build up an image tag out.write('\n') out.write(trigh) out.write(tleft) atext=getannotation(target) # Are we an administrator? if Globs.admin: # We always provide an annotation text field out.write('

') out.write('') out.write('') out.write('') out.write('') out.write('
') else: out.write(atext) out.write(trigh) out.write(toolbar(target,'webnail')) else: message('I do not have file: '+target,0) elif formvals.has_key('original'): # Now we just construct a single item rather than a table # Does the webnail exist? message('original requested') target=formvals['original'] if not Globs.originals.has_key(target): message('I do not have file: '+target,0) else: if formvals.has_key('rotate'): direction=formvals['rotate'] message(direction) rotate(target,direction) # Lets build up an image tag out.write(navibar(target,'original')) out.write(tleft) originalfilename=Globs.originals[target] # If there is a rotated version, show that instead if rotated.has_key(target): out.write('\n') else: out.write('\n') out.write(trigh) out.write(tleft) atext=getannotation(target) # Are we an administrator? if Globs.admin: # We always provide an annotation text field out.write('
') out.write('') out.write('') out.write('') out.write('') out.write('
') else: out.write(atext) out.write(trigh) out.write(toolbar(target,'original')) elif formvals.has_key('rotate'): # We rotate all sizes of this image to the left or right message('rotate requested') target=formvals['target'] direction=formvals['rotate'] if not Globs.originals.has_key(target): message('I do not have file: '+target,0) else: # Do the rotation rotate(target,direction) # Display the new image in webnail mode # We may need a way of forcing the browser to reload the newly rotated image here (later) out.write(tleft) out.write('\n') out.write(trigh) else: # Finally lets build a table of thumbnails thumbs=Globs.originals.keys() thumbs.sort() thumbs.reverse() # If version number is requested (append a ?version=tellme&action=show to the page request) # or if there are no original images, just give help message and return if formvals.has_key('version') or len(thumbs)==0: message(version(),0) return macro.formatter.rawHTML( Globs.adminmsg ) out.write('\n') cease='' rollover='' while 1: out.write('') for i in range(Globs.numberofcolumns): try: item=thumbs.pop() except IndexError: cease='true' break # Alt text atext=getannotation(item) rollover='alt="'+atext+'" title="'+atext+'"' # Table entry for thumbnail image out.write('') out.write('\n') if cease: out.write('
') break out.seek(0) # Finally output any administrative messages at the top followed by any generated content return macro.formatter.rawHTML( Globs.adminmsg+'

' +out.read() )