Add gallery.py post.
[blog.git] / posts / gallery / gallery.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 """
20 Generate HTML gallery pages for a picture directory organized along::
21
22   pics
23   |-- some_directory
24   |   |-- a_picture.jpg
25   |   |-- another_picture.jpg
26   |   |-- ...
27   |-- another_directory
28   |   |-- a_picture.jpg
29   |   |-- captioned_picture.jpg
30   |   |-- captioned_picture.jpg.txt
31   |-- ...
32
33 With::
34
35   pics$ gallery.py some_directory another_directory
36
37 Note that you can store a caption for ``<PICTURE>`` as plain text in
38 ``<PICTURE>.txt``.
39
40 The resulting gallery pages will be::
41
42   pics
43   |-- some_directory
44   |   |-- a_picture.jpg
45   |   |-- another_picture.jpg
46   |   |-- ...
47   |   |-- <INDEX> -> <GALLERY-1>
48   |   |-- <GALLERY-1>
49   |   |-- <GALLERY-2>
50   |   |-- <GALLERY-...>
51   |   |-- <THUMBDIR>
52   |   |   |-- a_picture.png
53   |   |   |-- another_picture.png
54   |   |   |-- ...
55   |   |-- <PAGEDIR>
56   |   |   |-- a_picture.html
57   |   |   |-- another_picture.html
58   |   |   |-- ...
59   |-- ...
60
61 So you'll probably want to symlink index.html to <GALLERY-1>.
62 """
63
64 import logging
65 import os
66 import os.path
67 from subprocess import Popen, PIPE
68 import sys
69
70
71 __version__ = '0.3'
72 LOG = logging
73
74
75 class CommandError(Exception):
76     def __init__(self, command, status, stdout=None, stderr=None):
77         strerror = ['Command failed (%d):\n  %s\n' % (status, stderr),
78                     'while executing\n  %s' % str(command)]
79         Exception.__init__(self, '\n'.join(strerror))
80         self.command = command
81         self.status = status
82         self.stdout = stdout
83         self.stderr = stderr
84
85 def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
86            cwd=None, encoding=None):
87     """
88     expect should be a tuple of allowed exit codes.  cwd should be
89     the directory from which the command will be executed.  When
90     unicode_output == True, convert stdout and stdin strings to
91     unicode before returing them.
92     """
93     if cwd == None:
94         cwd = '.'
95     LOG.debug('%s$ %s' % (cwd, ' '.join(args)))
96     try :
97         q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
98     except OSError, e:
99         raise CommandError(args, status=e.args[0], stderr=e)
100     stdout,stderr = q.communicate(input=stdin)
101     status = q.wait()
102     LOG.debug('%d\n%s%s' % (status, stdout, stderr))
103     if status not in expect:
104         raise CommandError(args, status, stdout, stderr)
105     return status, stdout, stderr
106
107 def is_picture(filename):
108     for extension in ['.jpg', '.jpeg', '.tif', '.tiff', '.png', '.gif']:
109         if filename.lower().endswith(extension):
110             return True
111     return False
112
113 def picture_base(filename):
114     parts = filename.rsplit('.', 1)
115     assert len(parts) == 2, parts
116     return parts[0]
117
118 def pictures(picdir):
119     return sorted([p for p in os.listdir(picdir) if is_picture(p)])
120
121 def make_thumbs(picdir, pictures, thumbdir, max_height, max_width,
122                 force=False):
123     fullthumbdir = os.path.join(picdir, thumbdir)
124     if not os.path.exists(fullthumbdir):
125         os.mkdir(fullthumbdir)
126     if force == False:
127         new_pictures = []
128         for p in pictures:
129             thumb_p = os.path.join(picdir, thumbdir, picture_base(p)+'.png')
130             if (not os.path.exists(thumb_p)
131                 or os.path.getmtime(p) > os.path.getmtime(thumb_p)):
132                 new_pictures.append(p)
133     if len(new_pictures) > 0:
134         log.info('  making %d thumbnails for %s' % (len(new_pictures), picdir))
135         invoke(['mogrify', '-format', 'png', '-strip', '-quality', '95',
136                 '-path', thumbdir,
137                 '-thumbnail', '%dx%d' % (max_width, max_height),
138                 ]+new_pictures, cwd=picdir)
139     return [os.path.join(thumbdir, picture_base(p)+'.png')
140             for p in pictures]
141
142 def page_header():
143     return '\n'.join([
144             '<html>',
145             '<body>', ''])
146
147 def page_footer():
148     return '\n'.join([
149             '</body>',
150             '</html>', ''])
151
152 def pagename(pic):
153     return picture_base(pic) + '.html'
154
155 def make_page(picdir, gallery, pagedir, pic,
156               previous_pic=None, next_pic=None, force=False):
157     fullpagedir = os.path.join(picdir, pagedir)
158     name = pagename(pic)
159     if not os.path.exists(fullpagedir):
160         os.mkdir(fullpagedir)
161     page_p = os.path.join(fullpagedir, name)
162     captionfile = os.path.join(picdir, pic+'.txt')
163     if (not force
164         and os.path.exists(page_p)
165         and ((not os.path.exists(captionfile))
166              or (os.path.exists(captionfile)
167                  and os.path.getmtime(captionfile) < os.path.getmtime(page_p)))):
168         LOG.info('  skip page for %s' % page_p)
169         return os.path.join(pagedir, name)
170     LOG.info('  make page for %s' % page_p)
171     p = open(page_p, 'w')
172     p.write(page_header())
173     if os.path.exists(captionfile):
174         caption = open(captionfile, 'r').read()
175         LOG.debug('    found caption %s' % captionfile)
176     else:
177         caption = None
178     p.write('<div style="align: center; text-align: center;">\n')
179     if previous_pic != None:
180         p.write('<a href="./%s">previous</a>\n' % pagename(previous_pic))
181     p.write('<a href="../%s">all</a>\n' % gallery)
182     if next_pic != None:
183         p.write('<a href="./%s">next</a>\n' % pagename(next_pic))
184     p.write('<br />\n')
185     p.write('<img width="600" src="../%s" />\n' % pic)
186     if caption != None:
187         p.write('<p>%s</p>\n' % caption)
188     p.write('</div>\n')
189     p.write(page_footer())
190     return os.path.join(pagedir, name)
191
192 def gallery_header(gallery_page_index=None):
193     return '\n'.join([
194             '<html>',
195             '<body>',''])
196
197 def gallery_footer(gallery_page_index=None):
198     return '\n'.join([
199             '</body>',
200             '</html>',''])
201
202 def make_gallery(picdir, index, gallery, pagedir, thumbdir,
203                  cols, rows, height, width,
204                  up_link=None, force=False):
205     LOG.info('make gallery for %s' % picdir)
206     pics = pictures(picdir)
207     thumbs = make_thumbs(picdir, pics, thumbdir, height, width, force=force)
208     pages = []
209     if os.path.exists(os.path.join(picdir, gallery)) and force == False:
210         return
211     pic_thumbs = zip(pics, thumbs)
212     i = 0
213     gallery_i = 1 # one-indexed
214     g = None
215     while i < len(pic_thumbs):
216         if g == None:
217             gallery_page = os.path.join(picdir, gallery % gallery_i))
218             LOG.info('  write gallery page %s' % gallery_page)
219             g = open(gallery_page, 'w')
220             g.write(gallery_header(gallery_i))
221             if up_link != None:
222                 up_html = '<a href="../">%s</a>\n' % up_link
223             else:
224                 up_html = ''
225             if gallery_i > 1:
226                 prev_url = gallery % (gallery_i-1)
227                 prev_html = '<a href="./%s">previous</a>\n' % prev_url
228             else:
229                 prev_html = ''
230             if i + rows*cols < len(pic_thumbs):
231                 next_url = gallery % (gallery_i+1)
232                 next_html = '<a href="./%s">next</a>\n' % next_url
233             else:
234                 next_html = ''
235             g.write('<div style="align: center; text-align: center;">\n')
236             g.write('<p>%s%s%s</p>\n' % (prev_html, up_html, next_html))
237             g.write('<table>\n')
238             column = 0
239             row = 0
240         LOG.info('placing picture %d of %d' % (i+1, len(pic_thumbs)))
241         pic,thumb = pic_thumbs[i]
242         prev = next = None
243         if i > 0:
244             prev = pics[i-1]
245         if i+1 < len(pics):
246             next = pics[i+1]
247         page = make_page(picdir, gallery % gallery_i, pagedir, pic, prev, next,
248                          force=force)
249         if column == 0:
250             g.write('  <tr>\n')
251         g.write('    <td style="text-align: center">\n')
252         g.write('      <a href="%s" style="border: 0">\n' % page)
253         g.write('        <img alt="%s" src="%s"\n' % (pic, thumb))
254         g.write('      </a>\n')
255         g.write('    </td>\n')
256         column += 1
257         if column == cols:
258             g.write('  </tr>\n')
259             column = 0
260             row += 1
261         if row == rows or i+1 == len(pic_thumbs):
262             g.write('</table>\n')
263             g.write('</div>\n')
264             g.write(gallery_footer(gallery_i))
265             g.close()
266             g = None
267             gallery_i += 1
268         i += 1
269     if i > 0 and not os.path.exists(index):
270         os.symlink(gallery % 1, index)
271
272
273 if __name__ == '__main__':
274     import optparse
275     parser = optparse.OptionParser(usage='%prog [options] PICTURE-DIR ...',
276                                    epilog=__doc__)
277     parser.format_epilog = lambda formatter : __doc__
278     parser.add_option('--index', default='index.html', dest='index',
279      help='Name of the index page (symlinked to <GALLERY-1> (%default)')
280     parser.add_option('--gallery', default='gallery-%d.html', dest='gallery',
281      help='Name of the gallery page, must include a %%d. (%default)')
282     parser.add_option('--up-link', default=None, dest='up_link',
283      help='Text for link to gallery parent')
284     parser.add_option('--pagedir', default='./pages', dest='pagedir',
285      help='Relative path from gallery page to page directory (%default)')
286     parser.add_option('--thumbdir', default='./thumbs', dest='thumbdir',
287      help='Relative path from gallery page to thumbnail directory (%default)')
288     parser.add_option('-r', '--rows', default=4, type='int', dest='cols',
289      help='Rows of thumbnails per page (%default)')
290     parser.add_option('-c', '--cols', default=10, type='int', dest='rows',
291      help='Columns of thumbnails per page (%default)')
292     parser.add_option('-H', '--height', default=100, type='int', dest='height',
293      help='Maximum thumbnail height in pixels (%default)')
294     parser.add_option('-W', '--width', default=300, type='int', dest='width',
295      help='Maximum thumbnail width in pixels (%default)')
296     parser.add_option('--force', default=False, dest='force',
297      help='Regenerate existing gallery files', action='store_true')
298     parser.add_option('-v', '--verbose', default=0, dest='verbose',
299      help='Increment verbosity', action='count')
300     options,args = parser.parse_args()
301
302     logging.basicConfig(level=level)
303     levels =
304     level = [
305         logging.WARNING,
306         logging.INFO,
307         logging.DEBUG][options.verbose]
308
309     for picdir in args:
310         kwargs = {}
311         for attr in ['index', 'gallery', 'up_link', 'pagedir', 'thumbdir',
312                      'cols', 'rows', 'height', 'width', 'force']:
313             kwargs[attr] = getattr(options, attr)
314         make_gallery(picdir, **kwargs)