emin

changeset 24
02f21810d43d
parent 23
4e1d30c61d0e
child 25
fb778c6b6d4b
equal deleted inserted replaced
23:4e1d30c61d0e 24:02f21810d43d
3 3
4 """%prog [opts] dir [outdir] 4 """%prog [opts] dir [outdir]
5 5
6 emin - a static web gallery builder 6 emin - a static web gallery builder
7 7
8 by Andy Buckley 8 emin makes static Web pages for presenting lots of imagey things: photos, PDFs,
9 http://www.insectnation.org 9 graphs with thumbnails as well as links to the image/doc file proper.
10 10
11 This is a weeny script for making static sets of Web pages for presenting lots 11 It's primarily intended for making Web photo galleries for the sorts of people
12 of imagey things: photos, PDFs, graphs... 12 who don't want to install some PHP monstrosity just to put their photos
13 13 online. On the assumption that most people will want to tweak their gallery's
14 As for the name, this is a program to make pretty crappy galleries, so it's 14 appearance, the output is fully customisable using the Cheetah templating
15 engine.
16
17 Supported image formats are JPEG, PNG, GIF, TIFF, PDF and EPS, with the latter
18 two being converted to PNG for Web display. Image resizing, renaming and
19 thumbnailing is supported, as is building a zip file to download the whole
20 set. Large image sets can be split over several pages.
21
22 As for the name, this is a program to make pretty simple galleries, so it's
15 named after a pretty crappy artist. And, thankfully, e-m-i-n is not many 23 named after a pretty crappy artist. And, thankfully, e-m-i-n is not many
16 characters to type (and they're all close together on the Colemak keyboard 24 characters to type.
17 layout) --- trebles all round!
18 25
19 TODO: 26 TODO:
20 * Make Cheetah templating optional 27 * Make Cheetah templating optional, or use Genshi/Jinja/Mako?
21 * Try to import BeautifulSoup for validating/pretty-printing the output 28 * Try to validate the HTML output
22 * Add all on one page option 29 * Add all on one page option
23 * Resize option 30 * Resize option
24 * Rename option 31 * Rename option
25 * Crop-to-thumb option 32 * Crop-to-thumb option
26 * Auto-rotate by EXIF orientation 33 * Auto-rotate by EXIF orientation
27 * Copy Lightbox stuff into place 34 * Copy Lightbox stuff into place
28 * Allow complete rollback if any failure (or on demand?) 35 * Allow complete rollback if any failure (or on demand?)
36
37 Author: Andy Buckley, http://www.insectnation.org\
29 """ 38 """
30 39
31 __version__ = "0.3.3" 40 __author__ = "Andy Buckley <andy@insectnation.org>"
41 __version__ = "0.3.4"
32 42
33 43
34 import logging 44 import logging
35 from optparse import OptionParser, OptionGroup 45 from optparse import OptionParser, OptionGroup
36 parser = OptionParser(usage=__doc__, version="%prog " + __version__) 46 parser = OptionParser(usage=__doc__, version=__version__)
37 parser.add_option("-t", "--title", dest="TITLE", default="", 47 parser.add_option("-t", "--title", dest="TITLE", default="",
38 help="title of this gallery") 48 help="title of this gallery")
39 parser.add_option("--template", dest="TEMPLATE", default=None, 49 parser.add_option("--template", dest="TEMPLATE", default=None,
40 help="specify the template file to be used for the index pages") 50 help="specify the template file to be used for the index pages")
41 parser.add_option("--zipfile", dest="ZIPFILE", default=None, 51 parser.add_option("--zipfile", dest="ZIPFILE", default=None,
42 help="name of zip archive file. Default is based on the title.") 52 help="name of zip archive file. Default is based on the title.")
43 parser.add_option("--no-zipfile", action="store_false", 53 parser.add_option("--no-zipfile", action="store_false",
44 dest="WRITE_ZIPFILE", default=True, 54 dest="WRITE_ZIPFILE", default=True,
45 help="disable writing out of a zipped archive of photos from this gallery") 55 help="disable writing out of a zipped archive of photos from this gallery")
46 ## TODO: Add all on one page option 56 parser.add_option("-1", "--one-page", dest="ONE_PAGE", action="store_true", default=False,
47 parser.add_option("-c", "--num-cols", dest="NUM_COLS", default=5, type=int, 57 help="put all thumbnails on one page (default: %default)")
48 help="max number of thumbnail columns on one page (default: 5)") 58 parser.add_option("-c", "--num-cols", "--cols", dest="NUM_COLS", default=5, type=int,
49 parser.add_option("-r", "--num-rows", dest="NUM_ROWS", default=6, type=int, 59 help="max number of thumbnail columns on one page (default: %default)")
50 help="max number of thumbnail rows on one page (default: 6). Set < 1 for unlimited (i.e. all on one page)") 60 parser.add_option("-r", "--num-rows", "--rows", dest="NUM_ROWS", default=6, type=int,
61 help="max number of thumbnail rows on one page (default: %default). Set < 1 for unlimited (i.e. all on one page)")
51 parser.add_option("--thumb-height", dest="THUMB_HEIGHT", default=150, type=int, 62 parser.add_option("--thumb-height", dest="THUMB_HEIGHT", default=150, type=int,
52 help="thumbnail height, in pixels (default: 100)") 63 help="thumbnail height, in pixels (default: %default)")
53 parser.add_option("--max-imgsize", dest="MAX_IMGSIZE", default=800, type=int, 64 parser.add_option("--max-imgsize", dest="MAX_IMGSIZE", default=800, type=int,
54 help="max large image dimension in pixels (default: 800)") 65 help="max large image dimension in pixels (default: %default)")
66 parser.add_option("--exclude", dest="EXCLUDE", default=None,
67 help="a regex pattern specifying image files to be excludes (default: %default)")
55 parser.add_option("--no-js", dest="USE_JS", action="store_false", default=True, 68 parser.add_option("--no-js", dest="USE_JS", action="store_false", default=True,
56 help="disable use of funky JavaScript display stuff") 69 help="disable use of funky JavaScript display stuff")
57 parser.add_option("--force", dest="FORCE", action="store_true", default=False, 70 parser.add_option("--force", dest="FORCE", action="store_true", default=False,
58 help="force creation of gallery: regen thumbnails etc.") 71 help="force creation of gallery: regen thumbnails etc.")
59 parser.add_option("--no-table", dest="USE_TABLE", action="store_false", default=True, 72 parser.add_option("--no-table", dest="USE_TABLE", action="store_false", default=True,
66 parser.add_option_group(verbgroup) 79 parser.add_option_group(verbgroup)
67 opts, args = parser.parse_args() 80 opts, args = parser.parse_args()
68 logging.basicConfig(level=opts.LOGLEVEL, format="%(message)s") 81 logging.basicConfig(level=opts.LOGLEVEL, format="%(message)s")
69 82
70 83
71 ## More stdlib imports 84 ## More imports
72 import sys, os, glob, re, commands, math, shutil 85 import sys, os, glob, re, math, shutil, fnmatch
73 import traceback 86 try:
74 87 from Cheetah.Template import Template
75 88 except Exception, e:
76 ## TODO: These should be options... 89 logging.error("Couldn't import required Cheetah package")
77 opts.RENAME = False 90 exit(1)
78 opts.CONVERT = False 91 try:
92 import PIL.Image as PILI
93 except Exception, e:
94 logging.error("Couldn't import required Python Imaging Library package")
95 exit(1)
79 96
80 97
81 ## Set processing/output dir 98 ## Set processing/output dir
82 if len(args) < 1 or len(args) > 2: 99 if len(args) < 1 or len(args) > 2:
83 print parser.show_usage() 100 parser.print_usage()
84 exit(1) 101 exit(1)
85 if len(args) >= 1: 102 if len(args) >= 1:
86 opts.SRCDIR = os.path.normpath(args[0]) 103 opts.SRCDIR = os.path.normpath(args[0])
87 opts.OUTDIR = os.path.normpath(args[0]) 104 opts.OUTDIR = os.path.normpath(args[0])
88 if len(args) == 2: 105 if len(args) == 2:
89 opts.OUTDIR = os.path.normpath(args[1]) 106 opts.OUTDIR = os.path.normpath(args[1])
90 107
91 108
92 ## Try to import Cheetah templating 109 ## Deal with consequences of interacting optional settings
93 try: 110 if opts.ONE_PAGE:
94 from Cheetah.Template import Template 111 opts.NUM_ROWS = -1
95 except Exception, e: 112 # TODO: These should be options...
96 logging.error("Couldn't import required Cheetah package") 113 opts.RENAME = False
97 exit(1) 114 opts.CONVERT = False
98
99 ## Try to import Python Imaging Library
100 try:
101 import PIL.Image as PILI
102 except Exception, e:
103 logging.error("Couldn't import required Python Imaging Library package")
104 exit(1)
105 115
106 116
107 def safeencode(s): 117 def safeencode(s):
108 """Encode a string for use as a filename.""" 118 """Encode a string for use as a filename."""
109 newstr = s.replace(" ", "-").replace(",", "").replace("/", "").replace(".", "") 119 newstr = s.replace(" ", "-").replace(",", "").replace("/", "").replace(".", "")
110 return newstr 120 return newstr
121
111 122
112 logging.debug("Title: %s" % opts.TITLE) 123 logging.debug("Title: %s" % opts.TITLE)
113 logging.debug("Thumb height: %d" % opts.THUMB_HEIGHT) 124 logging.debug("Thumb height: %d" % opts.THUMB_HEIGHT)
114 125
115 126
137 try: 148 try:
138 logging.info("Making output dir in %s" % opts.OUTDIR) 149 logging.info("Making output dir in %s" % opts.OUTDIR)
139 os.makedirs(opts.OUTDIR) 150 os.makedirs(opts.OUTDIR)
140 except Exception, e: 151 except Exception, e:
141 logging.error("Problem when making output dir %s... exiting" % opts.OUTDIR) 152 logging.error("Problem when making output dir %s... exiting" % opts.OUTDIR)
142 #traceback.print_exc()
143 exit(1) 153 exit(1)
144 154
145 155
146 ## Make thumbnail directory if needed 156 ## Make thumbnail directory if needed
147 opts.THUMBDIR = "thumbs" 157 opts.THUMBDIR = "thumbs"
150 if not os.path.isdir(THUMBDIR): 160 if not os.path.isdir(THUMBDIR):
151 logging.info("Making thumbs dir in %s" % THUMBDIR) 161 logging.info("Making thumbs dir in %s" % THUMBDIR)
152 os.makedirs(THUMBDIR) 162 os.makedirs(THUMBDIR)
153 except Exception, e: 163 except Exception, e:
154 logging.error("Problem when making thumbnails dir... exiting") 164 logging.error("Problem when making thumbnails dir... exiting")
155 #traceback.print_exc()
156 exit(1) 165 exit(1)
157 166
158 167
159 ## Build the list of pictures to display 168 ## Build the list of pictures to display
160 ## TODO: types: PNG/GIF, JPEG, PDF 169 # TODO: types: PNG/GIF, JPEG, PDF
161 ## TODO: match formats & store thumb filenames 170 # TODO: match formats & store thumb filenames
162 EXTENSIONS = \ 171 EXTENSIONS = \
163 ["*.jpg", "*.jpeg"] + \ 172 ["*.jpg", "*.jpeg"] + \
164 ["*.png", "*.gif"] + \ 173 ["*.png", "*.gif"] + \
165 ["*.tif", "*.tiff"] + \ 174 ["*.tif", "*.tiff"] + \
166 ["*.eps", "*.pdf"] 175 ["*.eps", "*.pdf"]
167 imgs = [] 176 imgs = []
168 import fnmatch
169 for img in os.listdir(opts.SRCDIR): 177 for img in os.listdir(opts.SRCDIR):
170 for e in EXTENSIONS: 178 for e in EXTENSIONS:
171 if fnmatch.fnmatch(img.lower(), e): 179 if not fnmatch.fnmatch(img.lower(), e):
172 imgpath = os.path.join(opts.SRCDIR, img) 180 continue
173 imgs.append(imgpath) 181 if opts.EXCLUDE and re.search(opts.EXCLUDE, img):
174 break 182 continue
183 imgpath = os.path.join(opts.SRCDIR, img)
184 imgs.append(imgpath)
185 break
175 186
176 ## Count the pictures 187 ## Count the pictures
177 logging.debug("Number of pictures = %d" % len(imgs)) 188 logging.debug("Number of pictures = %d" % len(imgs))
178 if len(imgs) == 0: 189 if len(imgs) == 0:
179 logging.debug("No pictures from which to build a gallery...") 190 logging.debug("No pictures from which to build a gallery...")
229 picversions[extn[1:].upper()] = picname 240 picversions[extn[1:].upper()] = picname
230 241
231 ## Do the conversion 242 ## Do the conversion
232 if convcmd: 243 if convcmd:
233 try: 244 try:
245 # TODO: threading / multiprocessing for speed-up?
234 import subprocess 246 import subprocess
235 subprocess.check_call(convcmd) 247 subprocess.check_call(convcmd)
236 except: 248 except:
237 raise 249 raise
238 250
259 thumbimg.thumbnail((100000000, opts.THUMB_HEIGHT), resample=PILI.ANTIALIAS) 271 thumbimg.thumbnail((100000000, opts.THUMB_HEIGHT), resample=PILI.ANTIALIAS)
260 thumbimg.save(thumbpath) 272 thumbimg.save(thumbpath)
261 info.thumbsize = thumbimg.size 273 info.thumbsize = thumbimg.size
262 except Exception, e: 274 except Exception, e:
263 logging.warning("Problem when making thumbnail from %s... exiting" % picpath) 275 logging.warning("Problem when making thumbnail from %s... exiting" % picpath)
264 #traceback.print_exc()
265 exit(1) 276 exit(1)
266 277
267 ## Store info 278 ## Store info
268 imgsinfo[picname] = info 279 imgsinfo[picname] = info
269 280
270 281
271 ##################### 282 #####################
272 283
273 284
274 ## Calculate how many pages will be needed 285 ## Calculate how many pages will be needed
275 ## TODO: allow all on one page 286 if opts.NUM_ROWS >= 1:
276 NUM_PER_PAGE = opts.NUM_ROWS * opts.NUM_COLS 287 NUM_PER_PAGE = opts.NUM_ROWS * opts.NUM_COLS
277 NUM_PAGES = int(math.ceil( len(imgs)/float(NUM_PER_PAGE) )) 288 NUM_PAGES = int(math.ceil( len(imgs)/float(NUM_PER_PAGE) ))
289 else:
290 NUM_PER_PAGE = len(imgs)
291 NUM_PAGES = 1
292
293
294 if NUM_PAGES > 1:
295 logging.warn("%d gallery pages will be made. If you just want one page, use the -1 or --one-page option")
278 296
279 297
280 ## TODO: Move HTML extension-setting to option parser 298 ## TODO: Move HTML extension-setting to option parser
281 ## (or take from template name, e.g. page.html.template -> html) 299 ## (or take from template name, e.g. page.html.template -> html)
282 opts.EXTN = "html" 300 opts.EXTN = "html"
339 zf.close() 357 zf.close()
340 else: 358 else:
341 logging.warning("No zip file made because zip filename is empty") 359 logging.warning("No zip file made because zip filename is empty")
342 360
343 361
344 ## Copy Lightbox stuff into place 362 ## Copy Lightbox stuff into ZIP
345 #if opts.USE_JS: 363 #if opts.USE_JS:
346 # from zipfile import ZipFile 364 # from zipfile import ZipFile
347 # zf = ZipFile(os.path.join(opts.OUTDIR, "lightbox.zip"), "r") 365 # zf = ZipFile(os.path.join(opts.OUTDIR, "lightbox.zip"), "r")
348 # for img in imgs: 366 # for img in imgs:
349 # zf.write(img, os.path.basename(img)) 367 # zf.write(img, os.path.basename(img))
350 # zf.close() 368 # zf.close()
351 369
352 370
353 ## Default template 371 ## Default template
354 tmplstr = \ 372 tmplstr = \
355 """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 373 '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
356 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 374 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
357 <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> 375 <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
358 <head> 376 <head>
359 #set title = $PAGETITLE 377 #set title = $PAGETITLE
360 #if $NUM_PAGES > 1 378 #if $NUM_PAGES > 1
414 #if $OPTS.WRITE_ZIPFILE and $ZIPFILE: 432 #if $OPTS.WRITE_ZIPFILE and $ZIPFILE:
415 <p>All zipped up: <a href="$ZIPFILE">$ZIPFILE</a></p> 433 <p>All zipped up: <a href="$ZIPFILE">$ZIPFILE</a></p>
416 #end if 434 #end if
417 </body> 435 </body>
418 </html> 436 </html>
419 """ 437 '''
420 438
421 439
422 ## Override default template with a template file 440 ## Override default template with a template file
423 if opts.TEMPLATE is not None: 441 if opts.TEMPLATE is not None:
424 logging.info("Using index template file %s" % opts.TEMPLATE) 442 logging.info("Using index template file %s" % opts.TEMPLATE)
466 f = open(PAGEPATH, "w") 484 f = open(PAGEPATH, "w")
467 logging.debug("Images on page: %s" % PAGEPICS) 485 logging.debug("Images on page: %s" % PAGEPICS)
468 tdict = {} 486 tdict = {}
469 tdict["NUM_PAGES"] = NUM_PAGES 487 tdict["NUM_PAGES"] = NUM_PAGES
470 tdict["NUM_PER_PAGE"] = NUM_PER_PAGE 488 tdict["NUM_PER_PAGE"] = NUM_PER_PAGE
471 tdict["NUM_ROWS"] = opts.NUM_ROWS
472 tdict["NUM_COLS"] = opts.NUM_COLS 489 tdict["NUM_COLS"] = opts.NUM_COLS
473 tdict["PAGEPICS"] = PAGEPICS 490 tdict["PAGEPICS"] = PAGEPICS
474 tdict["PAGENUM"] = PAGENUM 491 tdict["PAGENUM"] = PAGENUM
475 tdict["PAGETITLE"] = PAGETITLE 492 tdict["PAGETITLE"] = PAGETITLE
476 tdict["LINKSTR"] = LINKSTR 493 tdict["LINKSTR"] = LINKSTR

mercurial