From: W. Trevor King Date: Thu, 9 Dec 2010 17:03:09 +0000 (-0500) Subject: Add gallery.py post. X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=3734a2ef80bcefa07b0533ce1eef3b3a0005e95c;p=blog.git Add gallery.py post. --- diff --git a/posts/gallery.mdwn b/posts/gallery.mdwn new file mode 100644 index 0000000..cc94be6 --- /dev/null +++ b/posts/gallery.mdwn @@ -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 index 0000000..0db8ffa --- /dev/null +++ b/posts/gallery/gallery.py @@ -0,0 +1,314 @@ +#!/usr/bin/python +# +# Copyright (C) 2010 W. Trevor King +# +# 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 ```` as plain text in +``.txt``. + +The resulting gallery pages will be:: + + pics + |-- some_directory + | |-- a_picture.jpg + | |-- another_picture.jpg + | |-- ... + | |-- -> + | |-- + | |-- + | |-- + | |-- + | | |-- a_picture.png + | | |-- another_picture.png + | | |-- ... + | |-- + | | |-- a_picture.html + | | |-- another_picture.html + | | |-- ... + |-- ... + +So you'll probably want to symlink index.html to . +""" + +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([ + '', + '', '']) + +def page_footer(): + return '\n'.join([ + '', + '', '']) + +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('
\n') + if previous_pic != None: + p.write('previous\n' % pagename(previous_pic)) + p.write('all\n' % gallery) + if next_pic != None: + p.write('next\n' % pagename(next_pic)) + p.write('
\n') + p.write('\n' % pic) + if caption != None: + p.write('

%s

\n' % caption) + p.write('
\n') + p.write(page_footer()) + return os.path.join(pagedir, name) + +def gallery_header(gallery_page_index=None): + return '\n'.join([ + '', + '','']) + +def gallery_footer(gallery_page_index=None): + return '\n'.join([ + '', + '','']) + +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 = '%s\n' % up_link + else: + up_html = '' + if gallery_i > 1: + prev_url = gallery % (gallery_i-1) + prev_html = 'previous\n' % prev_url + else: + prev_html = '' + if i + rows*cols < len(pic_thumbs): + next_url = gallery % (gallery_i+1) + next_html = 'next\n' % next_url + else: + next_html = '' + g.write('
\n') + g.write('

%s%s%s

\n' % (prev_html, up_html, next_html)) + g.write('\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(' \n') + g.write(' \n') + column += 1 + if column == cols: + g.write(' \n') + column = 0 + row += 1 + if row == rows or i+1 == len(pic_thumbs): + g.write('
\n') + g.write(' \n' % page) + g.write(' %s\n') + g.write('
\n') + g.write('
\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 (%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)