--- /dev/null
+#!/usr/bin/python
+#
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Generate HTML gallery pages for a picture directory organized along::
+
+ pics
+ |-- some_directory
+ | |-- a_picture.jpg
+ | |-- another_picture.jpg
+ | |-- ...
+ |-- another_directory
+ | |-- a_picture.jpg
+ | |-- captioned_picture.jpg
+ | |-- captioned_picture.jpg.txt
+ |-- ...
+
+With::
+
+ pics$ gallery.py some_directory another_directory
+
+Note that you can store a caption for ``<PICTURE>`` as plain text in
+``<PICTURE>.txt``.
+
+The resulting gallery pages will be::
+
+ pics
+ |-- some_directory
+ | |-- a_picture.jpg
+ | |-- another_picture.jpg
+ | |-- ...
+ | |-- <INDEX> -> <GALLERY-1>
+ | |-- <GALLERY-1>
+ | |-- <GALLERY-2>
+ | |-- <GALLERY-...>
+ | |-- <THUMBDIR>
+ | | |-- a_picture.png
+ | | |-- another_picture.png
+ | | |-- ...
+ | |-- <PAGEDIR>
+ | | |-- a_picture.html
+ | | |-- another_picture.html
+ | | |-- ...
+ |-- ...
+
+So you'll probably want to symlink index.html to <GALLERY-1>.
+"""
+
+import logging
+import os
+import os.path
+from subprocess import Popen, PIPE
+import sys
+
+
+__version__ = '0.3'
+LOG = logging
+
+
+class CommandError(Exception):
+ def __init__(self, command, status, stdout=None, stderr=None):
+ strerror = ['Command failed (%d):\n %s\n' % (status, stderr),
+ 'while executing\n %s' % str(command)]
+ Exception.__init__(self, '\n'.join(strerror))
+ self.command = command
+ self.status = status
+ self.stdout = stdout
+ self.stderr = stderr
+
+def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
+ cwd=None, encoding=None):
+ """
+ expect should be a tuple of allowed exit codes. cwd should be
+ the directory from which the command will be executed. When
+ unicode_output == True, convert stdout and stdin strings to
+ unicode before returing them.
+ """
+ if cwd == None:
+ cwd = '.'
+ LOG.debug('%s$ %s' % (cwd, ' '.join(args)))
+ try :
+ q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
+ except OSError, e:
+ raise CommandError(args, status=e.args[0], stderr=e)
+ stdout,stderr = q.communicate(input=stdin)
+ status = q.wait()
+ LOG.debug('%d\n%s%s' % (status, stdout, stderr))
+ if status not in expect:
+ raise CommandError(args, status, stdout, stderr)
+ return status, stdout, stderr
+
+def is_picture(filename):
+ for extension in ['.jpg', '.jpeg', '.tif', '.tiff', '.png', '.gif']:
+ if filename.lower().endswith(extension):
+ return True
+ return False
+
+def picture_base(filename):
+ parts = filename.rsplit('.', 1)
+ assert len(parts) == 2, parts
+ return parts[0]
+
+def pictures(picdir):
+ return sorted([p for p in os.listdir(picdir) if is_picture(p)])
+
+def make_thumbs(picdir, pictures, thumbdir, max_height, max_width,
+ force=False):
+ fullthumbdir = os.path.join(picdir, thumbdir)
+ if not os.path.exists(fullthumbdir):
+ os.mkdir(fullthumbdir)
+ if force == False:
+ new_pictures = []
+ for p in pictures:
+ thumb_p = os.path.join(picdir, thumbdir, picture_base(p)+'.png')
+ if (not os.path.exists(thumb_p)
+ or os.path.getmtime(p) > os.path.getmtime(thumb_p)):
+ new_pictures.append(p)
+ if len(new_pictures) > 0:
+ log.info(' making %d thumbnails for %s' % (len(new_pictures), picdir))
+ invoke(['mogrify', '-format', 'png', '-strip', '-quality', '95',
+ '-path', thumbdir,
+ '-thumbnail', '%dx%d' % (max_width, max_height),
+ ]+new_pictures, cwd=picdir)
+ return [os.path.join(thumbdir, picture_base(p)+'.png')
+ for p in pictures]
+
+def page_header():
+ return '\n'.join([
+ '<html>',
+ '<body>', ''])
+
+def page_footer():
+ return '\n'.join([
+ '</body>',
+ '</html>', ''])
+
+def pagename(pic):
+ return picture_base(pic) + '.html'
+
+def make_page(picdir, gallery, pagedir, pic,
+ previous_pic=None, next_pic=None, force=False):
+ fullpagedir = os.path.join(picdir, pagedir)
+ name = pagename(pic)
+ if not os.path.exists(fullpagedir):
+ os.mkdir(fullpagedir)
+ page_p = os.path.join(fullpagedir, name)
+ captionfile = os.path.join(picdir, pic+'.txt')
+ if (not force
+ and os.path.exists(page_p)
+ and ((not os.path.exists(captionfile))
+ or (os.path.exists(captionfile)
+ and os.path.getmtime(captionfile) < os.path.getmtime(page_p)))):
+ LOG.info(' skip page for %s' % page_p)
+ return os.path.join(pagedir, name)
+ LOG.info(' make page for %s' % page_p)
+ p = open(page_p, 'w')
+ p.write(page_header())
+ if os.path.exists(captionfile):
+ caption = open(captionfile, 'r').read()
+ LOG.debug(' found caption %s' % captionfile)
+ else:
+ caption = None
+ p.write('<div style="align: center; text-align: center;">\n')
+ if previous_pic != None:
+ p.write('<a href="./%s">previous</a>\n' % pagename(previous_pic))
+ p.write('<a href="../%s">all</a>\n' % gallery)
+ if next_pic != None:
+ p.write('<a href="./%s">next</a>\n' % pagename(next_pic))
+ p.write('<br />\n')
+ p.write('<img width="600" src="../%s" />\n' % pic)
+ if caption != None:
+ p.write('<p>%s</p>\n' % caption)
+ p.write('</div>\n')
+ p.write(page_footer())
+ return os.path.join(pagedir, name)
+
+def gallery_header(gallery_page_index=None):
+ return '\n'.join([
+ '<html>',
+ '<body>',''])
+
+def gallery_footer(gallery_page_index=None):
+ return '\n'.join([
+ '</body>',
+ '</html>',''])
+
+def make_gallery(picdir, index, gallery, pagedir, thumbdir,
+ cols, rows, height, width,
+ up_link=None, force=False):
+ LOG.info('make gallery for %s' % picdir)
+ pics = pictures(picdir)
+ thumbs = make_thumbs(picdir, pics, thumbdir, height, width, force=force)
+ pages = []
+ if os.path.exists(os.path.join(picdir, gallery)) and force == False:
+ return
+ pic_thumbs = zip(pics, thumbs)
+ i = 0
+ gallery_i = 1 # one-indexed
+ g = None
+ while i < len(pic_thumbs):
+ if g == None:
+ gallery_page = os.path.join(picdir, gallery % gallery_i))
+ LOG.info(' write gallery page %s' % gallery_page)
+ g = open(gallery_page, 'w')
+ g.write(gallery_header(gallery_i))
+ if up_link != None:
+ up_html = '<a href="../">%s</a>\n' % up_link
+ else:
+ up_html = ''
+ if gallery_i > 1:
+ prev_url = gallery % (gallery_i-1)
+ prev_html = '<a href="./%s">previous</a>\n' % prev_url
+ else:
+ prev_html = ''
+ if i + rows*cols < len(pic_thumbs):
+ next_url = gallery % (gallery_i+1)
+ next_html = '<a href="./%s">next</a>\n' % next_url
+ else:
+ next_html = ''
+ g.write('<div style="align: center; text-align: center;">\n')
+ g.write('<p>%s%s%s</p>\n' % (prev_html, up_html, next_html))
+ g.write('<table>\n')
+ column = 0
+ row = 0
+ LOG.info('placing picture %d of %d' % (i+1, len(pic_thumbs)))
+ pic,thumb = pic_thumbs[i]
+ prev = next = None
+ if i > 0:
+ prev = pics[i-1]
+ if i+1 < len(pics):
+ next = pics[i+1]
+ page = make_page(picdir, gallery % gallery_i, pagedir, pic, prev, next,
+ force=force)
+ if column == 0:
+ g.write(' <tr>\n')
+ g.write(' <td style="text-align: center">\n')
+ g.write(' <a href="%s" style="border: 0">\n' % page)
+ g.write(' <img alt="%s" src="%s"\n' % (pic, thumb))
+ g.write(' </a>\n')
+ g.write(' </td>\n')
+ column += 1
+ if column == cols:
+ g.write(' </tr>\n')
+ column = 0
+ row += 1
+ if row == rows or i+1 == len(pic_thumbs):
+ g.write('</table>\n')
+ g.write('</div>\n')
+ g.write(gallery_footer(gallery_i))
+ g.close()
+ g = None
+ gallery_i += 1
+ i += 1
+ if i > 0 and not os.path.exists(index):
+ os.symlink(gallery % 1, index)
+
+
+if __name__ == '__main__':
+ import optparse
+ parser = optparse.OptionParser(usage='%prog [options] PICTURE-DIR ...',
+ epilog=__doc__)
+ parser.format_epilog = lambda formatter : __doc__
+ parser.add_option('--index', default='index.html', dest='index',
+ help='Name of the index page (symlinked to <GALLERY-1> (%default)')
+ parser.add_option('--gallery', default='gallery-%d.html', dest='gallery',
+ help='Name of the gallery page, must include a %%d. (%default)')
+ parser.add_option('--up-link', default=None, dest='up_link',
+ help='Text for link to gallery parent')
+ parser.add_option('--pagedir', default='./pages', dest='pagedir',
+ help='Relative path from gallery page to page directory (%default)')
+ parser.add_option('--thumbdir', default='./thumbs', dest='thumbdir',
+ help='Relative path from gallery page to thumbnail directory (%default)')
+ parser.add_option('-r', '--rows', default=4, type='int', dest='cols',
+ help='Rows of thumbnails per page (%default)')
+ parser.add_option('-c', '--cols', default=10, type='int', dest='rows',
+ help='Columns of thumbnails per page (%default)')
+ parser.add_option('-H', '--height', default=100, type='int', dest='height',
+ help='Maximum thumbnail height in pixels (%default)')
+ parser.add_option('-W', '--width', default=300, type='int', dest='width',
+ help='Maximum thumbnail width in pixels (%default)')
+ parser.add_option('--force', default=False, dest='force',
+ help='Regenerate existing gallery files', action='store_true')
+ parser.add_option('-v', '--verbose', default=0, dest='verbose',
+ help='Increment verbosity', action='count')
+ options,args = parser.parse_args()
+
+ logging.basicConfig(level=level)
+ levels =
+ level = [
+ logging.WARNING,
+ logging.INFO,
+ logging.DEBUG][options.verbose]
+
+ for picdir in args:
+ kwargs = {}
+ for attr in ['index', 'gallery', 'up_link', 'pagedir', 'thumbdir',
+ 'cols', 'rows', 'height', 'width', 'force']:
+ kwargs[attr] = getattr(options, attr)
+ make_gallery(picdir, **kwargs)