emin

Wed, 24 Apr 2013 10:38:33 +0200

author
Andy Buckley <andy@insectnation.org>
date
Wed, 24 Apr 2013 10:38:33 +0200
changeset 25
fb778c6b6d4b
parent 24
02f21810d43d
child 26
79167c58c3a5
permissions
-rwxr-xr-x

Adding -1/--one-pageand --exclude options, and various tweaks

     1 #! /usr/bin/env python
     2 # -*- python -*-
     4 """%prog [opts] dir [outdir]
     6 emin - a static web gallery builder
     8 emin makes static Web pages for presenting lots of imagey things: photos, PDFs,
     9 graphs with thumbnails as well as links to the image/doc file proper.
    11 It's primarily intended for making Web photo galleries for the sorts of people
    12 who don't want to install some PHP monstrosity just to put their photos
    13 online. On the assumption that most people will want to tweak their gallery's
    14 appearance, the output is fully customisable using the Cheetah templating
    15 engine.
    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.
    22 As for the name, this is a program to make pretty simple galleries, so it's
    23 named after a pretty crappy artist. And, thankfully, e-m-i-n is not many
    24 characters to type.
    26 TODO:
    27  * Make Cheetah templating optional, or use Genshi/Jinja/Mako?
    28  * Try to validate the HTML output
    29  * Add all on one page option
    30  * Resize option
    31  * Rename option
    32  * Crop-to-thumb option
    33  * Auto-rotate by EXIF orientation
    34  * Copy Lightbox stuff into place
    35  * Allow complete rollback if any failure (or on demand?)
    37 Author: Andy Buckley, http://www.insectnation.org\
    38 """
    40 __author__ = "Andy Buckley <andy@insectnation.org>"
    41 __version__ = "0.3.4"
    44 import logging
    45 from optparse import OptionParser, OptionGroup
    46 parser = OptionParser(usage=__doc__, version=__version__)
    47 parser.add_option("-t", "--title", dest="TITLE", default="",
    48                   help="title of this gallery")
    49 parser.add_option("--template", dest="TEMPLATE", default=None,
    50                   help="specify the template file to be used for the index pages")
    51 parser.add_option("--zipfile", dest="ZIPFILE", default=None,
    52                   help="name of zip archive file. Default is based on the title.")
    53 parser.add_option("--no-zipfile", action="store_false",
    54                   dest="WRITE_ZIPFILE", default=True,
    55                   help="disable writing out of a zipped archive of photos from this gallery")
    56 parser.add_option("-1", "--one-page", dest="ONE_PAGE", action="store_true", default=False,
    57                   help="put all thumbnails on one page (default: %default)")
    58 parser.add_option("-c", "--num-cols", "--cols", dest="NUM_COLS", default=5, type=int,
    59                   help="max number of thumbnail columns on one page (default: %default)")
    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)")
    62 parser.add_option("--thumb-height", dest="THUMB_HEIGHT", default=150, type=int,
    63                   help="thumbnail height, in pixels (default: %default)")
    64 parser.add_option("--max-imgsize", dest="MAX_IMGSIZE", default=800, type=int,
    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)")
    68 parser.add_option("--no-js", dest="USE_JS", action="store_false", default=True,
    69                   help="disable use of funky JavaScript display stuff")
    70 parser.add_option("--force", dest="FORCE", action="store_true", default=False,
    71                   help="force creation of gallery: regen thumbnails etc.")
    72 parser.add_option("--no-table", dest="USE_TABLE", action="store_false", default=True,
    73                   help="don't use an HTML table for thumbnail presentation: just let the thumbs flow into the browser window")
    74 verbgroup = OptionGroup(parser, "Verbosity control")
    75 verbgroup.add_option("-v", "--verbose", action="store_const", const=logging.DEBUG, dest="LOGLEVEL",
    76                      default=logging.INFO, help="print debug (very verbose) messages")
    77 verbgroup.add_option("-q", "--quiet", action="store_const", const=logging.WARNING, dest="LOGLEVEL",
    78                      default=logging.INFO, help="be very quiet")
    79 parser.add_option_group(verbgroup)
    80 opts, args = parser.parse_args()
    81 logging.basicConfig(level=opts.LOGLEVEL, format="%(message)s")
    84 ## More imports
    85 import sys, os, glob, re, math, shutil, fnmatch
    86 try:
    87     from Cheetah.Template import Template
    88 except Exception, e:
    89     logging.error("Couldn't import required Cheetah package")
    90     exit(1)
    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)
    98 ## Set processing/output dir
    99 if len(args) < 1 or len(args) > 2:
   100     parser.print_usage()
   101     exit(1)
   102 if len(args) >= 1:
   103     opts.SRCDIR = os.path.normpath(args[0])
   104     opts.OUTDIR = os.path.normpath(args[0])
   105 if len(args) == 2:
   106     opts.OUTDIR = os.path.normpath(args[1])
   109 ## Deal with consequences of interacting optional settings
   110 if opts.ONE_PAGE:
   111     opts.NUM_ROWS = -1
   112 # TODO: These should be options...
   113 opts.RENAME = False
   114 opts.CONVERT = False
   117 def safeencode(s):
   118     """Encode a string for use as a filename."""
   119     newstr = s.replace(" ", "-").replace(",", "").replace("/", "").replace(".", "")
   120     return newstr
   123 logging.debug("Title: %s" % opts.TITLE)
   124 logging.debug("Thumb height: %d" % opts.THUMB_HEIGHT)
   127 class ImageInfo(object):
   128     def __init__(self):
   129         self.name = None
   130         self.path = None
   131         self.thumbname = None
   132         self.thumbpath = None
   133         self.thumbx = None
   134         self.thumby = None
   136     def setsize(self, sizetuple):
   137         self.thumbx = sizetuple[0]
   138         self.thumby = sizetuple[1]
   140     def _getsize(self):
   141         return self.thumbx, self.thumby
   143     thumbsize = property(_getsize, setsize)
   146 ## Go to the gallery directory and test if it's writeable
   147 if not os.access(opts.OUTDIR, os.W_OK):
   148     try:
   149         logging.info("Making output dir in %s" % opts.OUTDIR)
   150         os.makedirs(opts.OUTDIR)
   151     except Exception, e:
   152         logging.error("Problem when making output dir %s... exiting" % opts.OUTDIR)
   153         exit(1)
   156 ## Make thumbnail directory if needed
   157 opts.THUMBDIR = "thumbs"
   158 THUMBDIR = os.path.join(opts.OUTDIR, opts.THUMBDIR)
   159 try:
   160     if not os.path.isdir(THUMBDIR):
   161         logging.info("Making thumbs dir in %s" % THUMBDIR)
   162         os.makedirs(THUMBDIR)
   163 except Exception, e:
   164     logging.error("Problem when making thumbnails dir... exiting")
   165     exit(1)
   168 ## Build the list of pictures to display
   169 # TODO: types: PNG/GIF, JPEG, PDF
   170 # TODO: match formats & store thumb filenames
   171 EXTENSIONS = \
   172     ["*.jpg", "*.jpeg"] + \
   173     ["*.png", "*.gif"]  + \
   174     ["*.tif", "*.tiff"] + \
   175     ["*.eps", "*.pdf"]
   176 imgs = []
   177 for img in os.listdir(opts.SRCDIR):
   178     for e in EXTENSIONS:
   179         if not fnmatch.fnmatch(img.lower(), e):
   180             continue
   181         if opts.EXCLUDE and re.search(opts.EXCLUDE, img):
   182             continue
   183         imgpath = os.path.join(opts.SRCDIR, img)
   184         imgs.append(imgpath)
   185         break
   187 ## Count the pictures
   188 logging.debug("Number of pictures = %d" % len(imgs))
   189 if len(imgs) == 0:
   190     logging.debug("No pictures from which to build a gallery...")
   191     exit(2)
   192 logging.debug("Images: " + str(sorted(imgs)))
   195 ## Rename/move if needed
   196 outimgs = []
   197 for n, imgpath in enumerate(imgs):
   198     imgname = os.path.basename(imgpath)
   199     imgnameparts = os.path.splitext(imgname)
   200     targetname = imgname
   201     if opts.RENAME:
   202         targetname = "%s-%03d%s" % (safename(opts.OUTDIR), n, imgnameparts[1])
   203     targetpath = os.path.join(opts.OUTDIR, targetname)
   204     outimgs.append(targetpath)
   205     if imgpath != targetpath:
   206         logging.debug("Copying %s -> %s" % (imgpath, targetpath))
   207         import shutil
   208         shutil.copy(imgpath, targetpath)
   211 ## Store some image info
   212 imgsinfo = {}
   213 for picpath in outimgs:
   214     picname = os.path.basename(picpath)
   215     picbase = os.path.splitext(picname)[0]
   217     ## Convert EPS, PDF, TIFF to Web-viewable formats
   218     picversions = {}
   219     picpathparts = os.path.splitext(picpath)
   220     picnameparts = os.path.splitext(picname)
   221     extn = picnameparts[1].lower()
   222     convcmd = None
   223     if extn in [".tif", ".tiff"]:
   224         picversions["TIFF"] = picname
   225         newpicname = picname + ".jpg"
   226         newpicpath = picpath + ".jpg"
   227         picversions["JPG"] = newpicname
   228         convcmd = ["convert", picpath, newpicpath]
   229         picname = newpicname
   230         picpath = newpicpath
   231     elif extn in [".eps", ".pdf"]:
   232         picversions[extn[1:].upper()] = picname
   233         newpicname = picname + ".png"
   234         newpicpath = picpath + ".png"
   235         picversions["PNG"] = newpicname
   236         convcmd = ["convert", "-density", "200", "-resize", "800x700", picpath, newpicpath]
   237         picname = newpicname
   238         picpath = newpicpath
   239     else:
   240         picversions[extn[1:].upper()] = picname
   242     ## Do the conversion
   243     if convcmd:
   244         try:
   245             # TODO: threading / multiprocessing for speed-up?
   246             import subprocess
   247             subprocess.check_call(convcmd)
   248         except:
   249             raise
   251     ## Main pic info
   252     info = ImageInfo()
   253     info.name = picname
   254     info.path = picpath
   255     info.versions = picversions
   257     ## Thumb info
   258     ## TODO: Un-hard-code PNG thumb format
   259     thumbname = picname + ".png"
   260     thumbpath = os.path.join(THUMBDIR, thumbname)
   261     info.thumbname = thumbname
   262     info.thumbpath = thumbpath
   264     ## Make thumbnail
   265     ## TODO: Be lazy!
   266     #if opts.FORCE or not os.access(thumbpath, os.R_OK) or os.stat(thumbpath).st_mtime > os.stat(pic).st_mtime:
   267     try:
   268         logging.debug("Making new thumbnail %s for %s (max height %d)" % \
   269                           (thumbpath, picname, opts.THUMB_HEIGHT))
   270         thumbimg = PILI.open(picpath, "r")
   271         thumbimg.thumbnail((100000000, opts.THUMB_HEIGHT), resample=PILI.ANTIALIAS)
   272         thumbimg.save(thumbpath)
   273         info.thumbsize = thumbimg.size
   274     except Exception, e:
   275         logging.warning("Problem when making thumbnail from %s... exiting" % picpath)
   276         exit(1)
   278     ## Store info
   279     imgsinfo[picname] = info
   282 #####################
   285 ## Calculate how many pages will be needed
   286 if opts.NUM_ROWS >= 1:
   287     NUM_PER_PAGE = opts.NUM_ROWS * opts.NUM_COLS
   288     NUM_PAGES = int(math.ceil( len(imgs)/float(NUM_PER_PAGE) ))
   289 else:
   290     NUM_PER_PAGE = len(imgs)
   291     NUM_PAGES = 1
   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" % NUM_PAGES)
   298 ## TODO: Move HTML extension-setting to option parser
   299 ## (or take from template name, e.g. page.html.template -> html)
   300 opts.EXTN = "html"
   303 def getPageFilename(pagenum):
   304     if pagenum == 1:
   305         pagefile = "index.%s" % opts.EXTN
   306     else:
   307         pagefile = "index%02d.%s" % (pagenum, opts.EXTN)
   308     return pagefile
   311 def mkPageLinkStr(pagenum):
   312     "Write the linked page list"
   313     global NUM_PAGES
   314     out = ''
   315     if NUM_PAGES > 1:
   316         out += ""
   317         ## Previous
   318         prev = pagenum - 1
   319         if prev > 0:
   320             out += '<a href="%s">prev</a>' % getPageFilename(prev)
   321         else:
   322             out += 'prev'
   323         out += '&nbsp;'
   324         ## Numbers
   325         for n in range(1, NUM_PAGES+1):
   326             if n != pagenum:
   327                 out += '<a href="%s">%d</a>' % (getPageFilename(n), n)
   328             else:
   329                 out += "%d" % n
   330             out += '&nbsp;'
   331         ## Next
   332         next = pagenum + 1
   333         if next <= NUM_PAGES:
   334             out += '<a href="%s">next</a>' % getPageFilename(next)
   335         else:
   336             out += 'next'
   337     return out
   340 ## Make a zip archive
   341 ZIPFILE = "photo-album.zip"
   342 if True: #opts.WRITE_ZIPFILE:
   343     logging.debug("Making zipped picture archive")
   344     if opts.ZIPFILE is not None:
   345         ZIPFILE = opts.ZIPFILE
   346     elif opts.TITLE is not None or len(opts.TITLE) > 0:
   347         ZIPFILE = safeencode(opts.TITLE)
   348         #ZIPFILE = safename(opts.OUTDIR)
   349     if not "." in ZIPFILE or os.path.splitextn(ZIPFILE)[1] != ".zip":
   350         ZIPFILE += ".zip"
   351     ## Do the zipping
   352     if ZIPFILE:
   353         from zipfile import ZipFile
   354         zf = ZipFile(os.path.join(opts.OUTDIR, ZIPFILE), "w")
   355         for img in imgs:
   356             zf.write(img, os.path.basename(img))
   357         zf.close()
   358     else:
   359         logging.warning("No zip file made because zip filename is empty")
   362 ## Copy Lightbox stuff into ZIP
   363 #if opts.USE_JS:
   364 #    from zipfile import ZipFile
   365 #    zf = ZipFile(os.path.join(opts.OUTDIR, "lightbox.zip"), "r")
   366 #    for img in imgs:
   367 #        zf.write(img, os.path.basename(img))
   368 #    zf.close()
   371 ## Default template
   372 tmplstr = \
   373 '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   374         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
   375 <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
   376   <head>
   377     #set title = $PAGETITLE
   378     #if $NUM_PAGES > 1
   379     #set title = $title + " (page %s)" % $PAGENUM
   380     #end if
   381     <title>$title</title>
   382     <style>
   383       img { border:0; padding:10 10 0 0; }
   384       body { padding:1em; background:white; font-family:sans-serif; }
   385       h1 { font-family:sans-serif; }
   386       a.format { text-decoration:none; font-variant:small-caps; color:grey; font-size:small; }
   387       a.format:hover { color:deeppink; }
   388       a.format:active { color:deeppink; }
   389       .pagelinks { text-decoration:none; font-variant:small-caps; color:grey; margin-top:1em; margin-bottom:1em; }
   390       .pagelinks a:link { color:#22c; text-decoration:none; }
   391       .pagelinks a:hover { color:#55c; text-decoration:none; }
   392       .pagelinks a:active { color:#55c; text-decoration:none; }
   393     </style>
   394     #if $OPTS.USE_JS:
   395     <link rel="stylesheet" href="lightbox/css/lightbox.css" type="text/css" media="screen" />
   396     <script src="lightbox/js/prototype.js" type="text/javascript"></script>
   397     <script src="lightbox/js/scriptaculous.js?load=effects,builder" type="text/javascript"></script>
   398     <script src="lightbox/js/lightbox.js" type="text/javascript"></script>
   399     #end if
   400   </head>
   401   <body>
   402     <h1>$PAGETITLE</h1>
   403     #if $NUM_PAGES > 1
   404     <div class="pagelinks">Pages: $LINKSTR</div>
   405     #end if
   407     <table>
   408     <tr>
   409     #set jsrel = ''
   410     #if $OPTS.USE_JS:
   411     #set jsrel = 'rel="lightbox[emin]"'
   412     #end if
   413     #for n, thumb in enumerate($PAGEPICS)
   414       #if $n % $NUM_COLS == 0 and $n not in (0, len($PAGEPICS)-1)
   415       <tr/><tr>
   416       #end if
   417       #set info = $PICINFO[$thumb]
   418       <td>
   419         <a href="$info.relpath" $jsrel><img alt="$thumb" src="$info.relthumbpath" style="border:0;" width="$info.thumbx" height="$info.thumby" /></a><br/>
   420         #for fmt, name in $info.versions.iteritems()
   421         <a class="format" href="$name">$fmt.lower()</a>
   422         #end for
   423       </td>
   424     #end for
   425     </tr>
   426     </table>
   428     #if $NUM_PAGES > 1
   429     <div class="pagelinks">Pages: $LINKSTR</div>
   430     #end if
   432     #if $OPTS.WRITE_ZIPFILE and $ZIPFILE:
   433     <p>All zipped up: <a href="$ZIPFILE">$ZIPFILE</a></p>
   434     #end if
   435   </body>
   436 </html>
   437 '''
   440 ## Override default template with a template file
   441 if opts.TEMPLATE is not None:
   442     logging.info("Using index template file %s" % opts.TEMPLATE)
   443     tf = open(opts.TEMPLATE, "r")
   444     tmplstr = tf.read()
   445     tf.close()
   448 ## Make each index page
   449 for n in range(NUM_PAGES):
   450     PAGENUM = n + 1
   452     ## Choose and open page file
   453     PAGEFILE = getPageFilename(PAGENUM)
   454     PAGEPATH = os.path.join(opts.OUTDIR, PAGEFILE)
   456     ## Write the title
   457     PAGETITLE = opts.TITLE or os.path.basename(opts.SRCDIR)
   459     ## Write the linked page list
   460     LINKSTR = mkPageLinkStr(PAGENUM)
   462     ## Work out the picture offsets for this page
   463     pics_start = n * NUM_PER_PAGE
   464     pics_end = (n+1) * NUM_PER_PAGE - 1
   465     if pics_end >= len(imgs):
   466         pics_end = len(imgs) - 1
   468     PAGEPICS = sorted(imgsinfo.keys())[pics_start: pics_end+1]
   469     PAGEPICNUMS = range(len(PAGEPICS))
   470     relthumbdir = opts.THUMBDIR
   471     #relthumbdir = os.path.relpath(THUMBDIR, OUTDIR)
   472     reloutdir = "."
   473     for k in imgsinfo.keys():
   474         imgsinfo[k].relthumbpath = os.path.normpath(os.path.join(relthumbdir, imgsinfo[k].thumbname))
   475         imgsinfo[k].relpath = os.path.normpath(os.path.join(reloutdir, imgsinfo[k].name))
   476     PICINFO = imgsinfo
   477 #     THUMBNAMES = [t.name for t in thumbsinfo.values()[pics_start : pics_end]]
   478 #     print THUMBNAMES
   479 #     THUMBPATHS = [os.path.join(relthumbdir, name) for name in THUMBNAMES]
   480 #     print THUMBPATHS
   481 #     THUMBDIMS = [t.size for t in thumbsinfo.values()[pics_start : pics_end]]
   483     logging.info("Writing to index file %s" % PAGEPATH)
   484     f = open(PAGEPATH, "w")
   485     logging.debug("Images on page: %s" % PAGEPICS)
   486     tdict = {}
   487     tdict["NUM_PAGES"] = NUM_PAGES
   488     tdict["NUM_PER_PAGE"] = NUM_PER_PAGE
   489     tdict["NUM_COLS"] = opts.NUM_COLS
   490     tdict["PAGEPICS"] = PAGEPICS
   491     tdict["PAGENUM"] = PAGENUM
   492     tdict["PAGETITLE"] = PAGETITLE
   493     tdict["LINKSTR"] = LINKSTR
   494     tdict["PAGEPICNUMS"] = PAGEPICNUMS
   495     tdict["PICINFO"] = imgsinfo
   496 #     tdict["THUMBS"] = thumbsinfo
   497 #     tdict["THUMBNAMES"] = THUMBNAMES
   498 #     tdict["THUMBPATHS"] = THUMBPATHS
   499 #     tdict["THUMBDIMS"] = THUMBDIMS
   500     tdict["ZIPFILE"] = ZIPFILE
   501     tdict["OPTS"] = opts
   502     indexstr = Template(tmplstr, searchList=[tdict])
   503     #print indexstr
   504     f.write(str(indexstr))
   505     f.close()
   507 ## It's over. Nothing to see here.
   508 logging.debug("All done!")

mercurial