Add gallery.py post.
authorW. Trevor King <wking@drexel.edu>
Thu, 9 Dec 2010 17:03:09 +0000 (12:03 -0500)
committerW. Trevor King <wking@drexel.edu>
Thu, 9 Dec 2010 17:03:26 +0000 (12:03 -0500)
posts/gallery.mdwn [new file with mode: 0644]
posts/gallery/gallery.py [new file with mode: 0755]

diff --git a/posts/gallery.mdwn b/posts/gallery.mdwn
new file mode 100644 (file)
index 0000000..cc94be6
--- /dev/null
@@ -0,0 +1,18 @@
+I've written up a little script ([[gallery.py]]) to generate HTML
+galleries of my pictures.  There are tons of scripts to do this, but
+this one's mine ;).  It uses [ImageMagick][]'s [mogrify][] to generate
+thumbnails and supports per-picture captions via similarly named
+caption files (e.g. `some-pic.jpg.txt` contains the caption for
+`some-pic.jpg`).
+
+You'll probably want to tweak the script to create appropriate header
+or footer code for your particular site.  Perhaps I'll convert the
+script to use [jinja2][] in the future.
+
+[ImageMagick]: http://www.imagemagick.org/
+[mogrify]: http://www.imagemagick.org/script/mogrify.php
+[jinja2]: http://jinja.pocoo.org/
+
+[[!tag tags/fun]]
+[[!tag tags/python]]
+[[!tag tags/tools]]
diff --git a/posts/gallery/gallery.py b/posts/gallery/gallery.py
new file mode 100755 (executable)
index 0000000..0db8ffa
--- /dev/null
@@ -0,0 +1,314 @@
+#!/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)