# -*- coding: iso-8859-1 -*-
"""
    Gallery.py  Version 0.82
                                                                                                           
    This macro creates dynamic tabulated displays based on attachment contents
                                                                                                           
    @copyright: 2004 by Simon Ryan <simon<at>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+'<font color="#FF0000"><strong>Gallery</strong></font>:&nbsp;&nbsp;'+astring+'<br>\n'

def version():
        return(' version <b>'+Globs.version+'</b> by Simon D. Ryan.'+\
	'<br>Copyright 2004 Simon D. Ryan<br>Gallery is a MoinMoin macro and is released under the '+\
	'<a href="http://www.gnu.org/licenses/gpl.txt">GPL</a>\n'+\
	'<p>Upload some images as attachments to <a href="'+Globs.baseurl+Globs.pagename+'?action=AttachFile"><b>'+Globs.pagename+'</b></a> and I will generate a gallery for you.')

# Thanks to denny<at>ece.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 '<a href="'+Globs.baseurl+pagename+'?'+querystring+'='+query+Globs.bcomp+'">'+description+'</a>'

def navibar(target,querystring):
    # Returns a navigational bar with PREV,THUMBS,NEXT
    positions=Globs.originals.keys()
    positions.sort()
    thumbs='<a href="'+Globs.pagename+'">THUMBS</a>'
    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 '<table><tr><td>'+back+'</td><td>'+thumbs+'</td><td>'+forward+'</td></tr></table>'

def toolbar(target,naillevel):
    if Globs.admin:
	rotateleft='<input type="submit" name="rotate" value="rotate left">'
	rotateright='<input type="submit" name="rotate" value="rotate right">'
	htarget='<input type=hidden value="'+target+'" name="'+naillevel+'">'
	compat='<input type=hidden value="show" name="action">'
	return '<form METHOD=POST><table><tr><td>'+rotateleft+'</td><td>'+rotateright+'</td></tr></table>\n'+htarget+compat+'</form>'
    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('<b>Please install ImageMagick so I can build thumbnails and webnails</b><p>',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('<b>Please install ImageMagick so I can build thumbnails and webnails</b><p>',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,'"','&quot;')

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&amp;do=get&amp;target='
    if args:
        args=macro.request.getText(args)


    # HTML Constants
    tleft='<table><tr><td><center>'
    tmidd='</center></td><td><center>'
    trigh='</center></td></tr></table>\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 = <admin|delete|any>
        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+'<p>')

	# 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'],'"','&quot;')
	    target=formvals['target']
	    ouf=open(Globs.attachmentdir+'/tmp.annotation.'+target+'.txt','w')
	    ouf.write(atext)
	    ouf.close()
	    message('Annotation updated to <i>'+atext+'</i>',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('<a href="'+Globs.baseurl+Globs.pagename+'?original='+target+'&action=content"><img src="'+Globs.gallerytempurl+'tmp.webnail.'+target+'.jpg"></a>\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('<form action='+Globs.pagename+' name=annotate METHOD=POST>')
		out.write('<input maxLength=256 size=55 name=annotate value="'+atext+'">')
		out.write('<input type=hidden value="'+target+'" name="target">')
		out.write('<input type=hidden value="show" name="action">')
		out.write('<input type=hidden value="'+target+'" name="webnail">')
		out.write('</form>')
	    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('<a href="'+Globs.baseurl+Globs.pagename+'?webnail='+target+Globs.bcomp+'"><img src="'+Globs.gallerytempurl+'tmp.rotated.'+target+'.jpg"></a>\n')
	    else:
		out.write('<a href="'+Globs.baseurl+Globs.pagename+'?webnail='+target+Globs.bcomp+'"><img src="'+Globs.pagename+'?action=AttachFile&amp;do=get&amp;target='+originalfilename+'"></a>\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('<form action='+Globs.pagename+' name=annotate METHOD=POST>')
		out.write('<input maxLength=256 size=55 name=annotate value="'+atext+'">')
		out.write('<input type=hidden value="'+target+'" name="target">')
		out.write('<input type=hidden value="show" name="action">')
		out.write('<input type=hidden value="'+target+'" name="original">')
		out.write('</form>')
	    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('<a href="'+Globs.baseurl+Globs.pagename+'?webnail='+target+Globs.bcomp+'"><img src="'+Globs.gallerytempurl+'tmp.webnail.'+target+'.jpg"></a>\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<table>')
	cease=''
	rollover=''
	while 1:
	    out.write('<tr>')
	    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('<td><a href="'+Globs.baseurl+Globs.pagename+'?webnail='+item+Globs.bcomp+'"><center><img src="'+Globs.gallerytempurl+'tmp.thumbnail.'+item+'.jpg" '+rollover+'></a></center></td>')
	    out.write('</tr>\n')
	    if cease:
		out.write('</table>')
		break
	
    out.seek(0)
    # Finally output any administrative messages at the top followed by any generated content
    return macro.formatter.rawHTML(
        Globs.adminmsg+'<p>'
        +out.read()	
    )

