3 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
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.
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.
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.
20 Generate HTML gallery pages for a picture directory organized along::
25 | |-- another_picture.jpg
29 | |-- captioned_picture.jpg
30 | |-- captioned_picture.jpg.txt
35 pics$ gallery.py some_directory another_directory
37 Note that you can store a caption for ``<PICTURE>`` as plain text in
40 The resulting gallery pages will be::
45 | |-- another_picture.jpg
47 | |-- <INDEX> -> <GALLERY-1>
53 | | |-- another_picture.png
56 | | |-- a_picture.html
57 | | |-- another_picture.html
61 So you'll probably want to symlink index.html to <GALLERY-1>.
67 from subprocess import Popen, PIPE
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
85 def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
86 cwd=None, encoding=None):
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.
95 LOG.debug('%s$ %s' % (cwd, ' '.join(args)))
97 q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
99 raise CommandError(args, status=e.args[0], stderr=e)
100 stdout,stderr = q.communicate(input=stdin)
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
107 def is_picture(filename):
108 for extension in ['.jpg', '.jpeg', '.tif', '.tiff', '.png', '.gif']:
109 if filename.lower().endswith(extension):
113 def picture_base(filename):
114 parts = filename.rsplit('.', 1)
115 assert len(parts) == 2, parts
118 def pictures(picdir):
119 return sorted([p for p in os.listdir(picdir) if is_picture(p)])
121 def make_thumbs(picdir, pictures, thumbdir, max_height, max_width,
123 fullthumbdir = os.path.join(picdir, thumbdir)
124 if not os.path.exists(fullthumbdir):
125 os.mkdir(fullthumbdir)
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',
137 '-thumbnail', '%dx%d' % (max_width, max_height),
138 ]+new_pictures, cwd=picdir)
139 return [os.path.join(thumbdir, picture_base(p)+'.png')
153 return picture_base(pic) + '.html'
155 def make_page(picdir, gallery, pagedir, pic,
156 previous_pic=None, next_pic=None, force=False):
157 fullpagedir = os.path.join(picdir, pagedir)
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')
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)
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)
183 p.write('<a href="./%s">next</a>\n' % pagename(next_pic))
185 p.write('<img width="600" src="../%s" />\n' % pic)
187 p.write('<p>%s</p>\n' % caption)
189 p.write(page_footer())
190 return os.path.join(pagedir, name)
192 def gallery_header(gallery_page_index=None):
197 def gallery_footer(gallery_page_index=None):
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)
209 if os.path.exists(os.path.join(picdir, gallery)) and force == False:
211 pic_thumbs = zip(pics, thumbs)
213 gallery_i = 1 # one-indexed
215 while i < len(pic_thumbs):
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))
222 up_html = '<a href="../">%s</a>\n' % up_link
226 prev_url = gallery % (gallery_i-1)
227 prev_html = '<a href="./%s">previous</a>\n' % prev_url
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
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))
240 LOG.info('placing picture %d of %d' % (i+1, len(pic_thumbs)))
241 pic,thumb = pic_thumbs[i]
247 page = make_page(picdir, gallery % gallery_i, pagedir, pic, prev, next,
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))
261 if row == rows or i+1 == len(pic_thumbs):
262 g.write('</table>\n')
264 g.write(gallery_footer(gallery_i))
269 if i > 0 and not os.path.exists(index):
270 os.symlink(gallery % 1, index)
273 if __name__ == '__main__':
275 parser = optparse.OptionParser(usage='%prog [options] PICTURE-DIR ...',
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()
302 logging.basicConfig(level=level)
307 logging.DEBUG][options.verbose]
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)