emin

Mon, 09 May 2011 15:15:59 +0100

author
Andy Buckley <andy@insectnation.org>
date
Mon, 09 May 2011 15:15:59 +0100
changeset 20
1c024e55880c
parent 17
d1377fc46e5d
child 21
71e18d3b1685
permissions
-rwxr-xr-x

Added tag emin-0.3.2 for changeset 14161f1492a4

     1 #! /usr/bin/env python
     2 # -*- python -*-
     4 """%prog [opts] dir [outdir]
     6 emin - a static web gallery builder
     8 by Andy Buckley
     9 http://www.insectnation.org
    11 This is a weeny script for making static sets of Web pages for presenting lots
    12 of imagey things: photos, PDFs, graphs...
    14 As for the name, this is a program to make pretty crappy galleries, so it's
    15 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
    17 layout) --- trebles all round!
    19 TODO:
    20  * Try to import BeautifulSoup for validating/pretty-printing the output
    21  * Add all on one page option
    22  * Resize option
    23  * Rename option
    24  * Crop-to-thumb option
    25  * Auto-rotate by EXIF orientation
    26  * Copy Lightbox stuff into place
    27  * Allow complete rollback if any failure (or on demand?)
    28 """
    30 __version__ = "0.3.1"
    33 import logging
    34 from optparse import OptionParser, OptionGroup
    35 parser = OptionParser(usage=__doc__, version="%prog " + __version__)
    36 parser.add_option("-t", "--title", dest="TITLE", default="",
    37                   help="title of this gallery")
    38 parser.add_option("--template", dest="TEMPLATE", default=None,
    39                   help="specify the template file to be used for the index pages")
    40 parser.add_option("--zipfile", dest="ZIPFILE", default=None,
    41                   help="name of zip archive file. Default is based on the title.")
    42 parser.add_option("--no-zipfile", action="store_false",
    43                   dest="WRITE_ZIPFILE", default=True,
    44                   help="disable writing out of a zipped archive of photos from this gallery")
    45 ## TODO: Add all on one page option
    46 parser.add_option("-c", "--num-cols", dest="NUM_COLS", default=5, type=int,
    47                   help="max number of thumbnail columns on one page (default: 5)")
    48 parser.add_option("-r", "--num-rows", dest="NUM_ROWS", default=6, type=int,
    49                   help="max number of thumbnail rows on one page (default: 6). Set < 1 for unlimited (i.e. all on one page)")
    50 parser.add_option("--thumb-height", dest="THUMB_HEIGHT", default=150, type=int,
    51                   help="thumbnail height, in pixels (default: 100)")
    52 parser.add_option("--max-imgsize", dest="MAX_IMGSIZE", default=800, type=int,
    53                   help="max large image dimension in pixels (default: 800)")
    54 parser.add_option("--no-js", dest="USE_JS", action="store_false", default=True,
    55                   help="disable use of funky JavaScript display stuff")
    56 parser.add_option("--force", dest="FORCE", action="store_true", default=False,
    57                   help="force creation of gallery: regen thumbnails etc.")
    58 parser.add_option("--no-table", dest="USE_TABLE", action="store_false", default=True,
    59                   help="don't use an HTML table for thumbnail presentation: just let the thumbs flow into the browser window")
    60 verbgroup = OptionGroup(parser, "Verbosity control")
    61 verbgroup.add_option("-v", "--verbose", action="store_const", const=logging.DEBUG, dest="LOGLEVEL",
    62                      default=logging.INFO, help="print debug (very verbose) messages")
    63 verbgroup.add_option("-q", "--quiet", action="store_const", const=logging.WARNING, dest="LOGLEVEL",
    64                      default=logging.INFO, help="be very quiet")
    65 parser.add_option_group(verbgroup)
    66 opts, args = parser.parse_args()
    67 logging.basicConfig(level=opts.LOGLEVEL, format="%(message)s")
    70 ## More stdlib imports
    71 import sys, os, glob, re, commands, math, shutil
    72 import traceback
    75 ## TODO: These should be options...
    76 opts.RENAME = False
    77 opts.CONVERT = False
    80 ## Set processing/output dir
    81 if len(args) < 1 or len(args) > 2:
    82     print parser.show_usage()
    83     exit(1)
    84 if len(args) >= 1:
    85     opts.SRCDIR = os.path.normpath(args[0])
    86     opts.OUTDIR = os.path.normpath(args[0])
    87 if len(args) == 2:
    88     opts.OUTDIR = os.path.normpath(args[1])
    91 ## Try to import Cheetah templating
    92 try:
    93     from Cheetah.Template import Template
    94 except Exception, e:
    95     logging.error("Couldn't import required Cheetah package")
    96     exit(1)
    98 ## Try to import Python Imaging Library
    99 try:
   100     import PIL.Image as PILI
   101 except Exception, e:
   102     logging.error("Couldn't import required Python Imaging Library package")
   103     exit(1)
   106 def safeencode(s):
   107     """Encode a string for use as a filename."""
   108     newstr = s.replace(" ", "-").replace(",", "").replace("/", "").replace(".", "")
   109     return newstr
   111 logging.debug("Title: %s" % opts.TITLE)
   112 logging.debug("Thumb height: %d" % opts.THUMB_HEIGHT)
   115 class ImageInfo(object):
   116     def __init__(self):
   117         self.name = None
   118         self.path = None
   119         self.thumbname = None
   120         self.thumbpath = None
   121         self.thumbx = None
   122         self.thumby = None
   124     def setsize(self, sizetuple):
   125         self.thumbx = sizetuple[0]
   126         self.thumby = sizetuple[1]
   128     def _getsize(self):
   129         return self.thumbx, self.thumby
   131     thumbsize = property(_getsize, setsize)
   134 ## Go to the gallery directory and test if it's writeable
   135 if not os.access(opts.OUTDIR, os.W_OK):
   136     try:
   137         logging.info("Making output dir in %s" % opts.OUTDIR)
   138         os.makedirs(opts.OUTDIR)
   139     except Exception, e:
   140         logging.error("Problem when making output dir %s... exiting" % opts.OUTDIR)
   141         #traceback.print_exc()
   142         exit(1)
   145 ## Make thumbnail directory if needed
   146 opts.THUMBDIR = "thumbs"
   147 THUMBDIR = os.path.join(opts.OUTDIR, opts.THUMBDIR)
   148 try:
   149     if not os.path.isdir(THUMBDIR):
   150         logging.info("Making thumbs dir in %s" % THUMBDIR)
   151         os.makedirs(THUMBDIR)
   152 except Exception, e:
   153     logging.error("Problem when making thumbnails dir... exiting")
   154     #traceback.print_exc()
   155     exit(1)
   158 ## Build the list of pictures to display
   159 ## TODO: types: PNG/GIF, JPEG, PDF
   160 ## TODO: match formats & store thumb filenames
   161 EXTENSIONS = \
   162     ["*.jpg", "*.jpeg"] + \
   163     ["*.png", "*.gif"]  + \
   164     ["*.tif", "*.tiff"] + \
   165     ["*.eps", "*.pdf"]
   166 imgs = []
   167 import fnmatch
   168 for img in os.listdir(opts.SRCDIR):
   169     for e in EXTENSIONS:
   170         if fnmatch.fnmatch(img.lower(), e):
   171             imgpath = os.path.join(opts.SRCDIR, img)
   172             imgs.append(imgpath)
   173             break
   175 ## Count the pictures
   176 logging.debug("Number of pictures = %d" % len(imgs))
   177 if len(imgs) == 0:
   178     logging.debug("No pictures from which to build a gallery...")
   179     exit(2)
   180 logging.debug("Images: " + str(sorted(imgs)))
   183 ## Rename/move if needed
   184 outimgs = []
   185 for n, imgpath in enumerate(imgs):
   186     imgname = os.path.basename(imgpath)
   187     imgnameparts = os.path.splitext(imgname)
   188     targetname = imgname
   189     if opts.RENAME:
   190         targetname = "%s-%03d%s" % (safename(opts.OUTDIR), n, imgnameparts[1])
   191     targetpath = os.path.join(opts.OUTDIR, targetname)
   192     outimgs.append(targetpath)
   193     if imgpath != targetpath:
   194         logging.debug("Copying %s -> %s" % (imgpath, targetpath))
   195         import shutil
   196         shutil.copy(imgpath, targetpath)
   199 ## Store some image info
   200 imgsinfo = {}
   201 for picpath in outimgs:
   202     picname = os.path.basename(picpath)
   203     picbase = os.path.splitext(picname)[0]
   205     ## Convert EPS, PDF, TIFF to Web-viewable formats
   206     picversions = {}
   207     picpathparts = os.path.splitext(picpath)
   208     picnameparts = os.path.splitext(picname)
   209     extn = picnameparts[1].lower()
   210     convcmd = None
   211     if extn in [".tif", ".tiff"]:
   212         picversions["TIFF"] = picname
   213         newpicname = picname + ".jpg"
   214         newpicpath = picpath + ".jpg"
   215         picversions["JPG"] = newpicname
   216         convcmd = ["convert", picpath, newpicpath]
   217         picname = newpicname
   218         picpath = newpicpath
   219     elif extn in [".eps", ".pdf"]:
   220         picversions[extn[1:].upper()] = picname
   221         newpicname = picname + ".png"
   222         newpicpath = picpath + ".png"
   223         picversions["PNG"] = newpicname
   224         convcmd = ["convert", "-density", "200", "-resize", "800x700", picpath, newpicpath]
   225         picname = newpicname
   226         picpath = newpicpath
   227     else:
   228         picversions[extn[1:].upper()] = picname
   230     ## Do the conversion
   231     if convcmd:
   232         try:
   233             import subprocess
   234             subprocess.check_call(convcmd)
   235         except:
   236             raise
   238     ## Main pic info
   239     info = ImageInfo()
   240     info.name = picname
   241     info.path = picpath
   242     info.versions = picversions
   244     ## Thumb info
   245     ## TODO: Un-hard-code PNG thumb format
   246     thumbname = picname + ".png"
   247     thumbpath = os.path.join(THUMBDIR, thumbname)
   248     info.thumbname = thumbname
   249     info.thumbpath = thumbpath
   251     ## Make thumbnail
   252     ## TODO: Be lazy!
   253     #if opts.FORCE or not os.access(thumbpath, os.R_OK) or os.stat(thumbpath).st_mtime > os.stat(pic).st_mtime:
   254     try:
   255         logging.debug("Making new thumbnail %s for %s (max height %d)" % \
   256                           (thumbpath, picname, opts.THUMB_HEIGHT))
   257         thumbimg = PILI.open(picpath, "r")
   258         thumbimg.thumbnail((100000000, opts.THUMB_HEIGHT), resample=PILI.ANTIALIAS)
   259         thumbimg.save(thumbpath)
   260         info.thumbsize = thumbimg.size
   261     except Exception, e:
   262         logging.warning("Problem when making thumbnail from %s... exiting" % picpath)
   263         #traceback.print_exc()
   264         exit(1)
   266     ## Store info
   267     imgsinfo[picname] = info
   270 #####################
   273 ## Calculate how many pages will be needed
   274 ## TODO: allow all on one page
   275 NUM_PER_PAGE = opts.NUM_ROWS * opts.NUM_COLS
   276 NUM_PAGES = int(math.ceil( len(imgs)/float(NUM_PER_PAGE) ))
   279 ## TODO: Move HTML extension-setting to option parser
   280 ## (or take from template name, e.g. page.html.template -> html)
   281 opts.EXTN = "html"
   284 def getPageFilename(pagenum):
   285     if pagenum == 1:
   286         pagefile = "index.%s" % opts.EXTN
   287     else:
   288         pagefile = "index%02d.%s" % (pagenum, opts.EXTN)
   289     return pagefile
   292 def mkPageLinkStr(pagenum):
   293     "Write the linked page list"
   294     global NUM_PAGES
   295     out = ''
   296     if NUM_PAGES > 1:
   297         out += ""
   298         ## Previous
   299         prev = pagenum - 1
   300         if prev > 0:
   301             out += '<a href="%s">prev</a>' % getPageFilename(prev)
   302         else:
   303             out += 'prev'
   304         out += '&nbsp;'
   305         ## Numbers
   306         for n in range(1, NUM_PAGES+1):
   307             if n != pagenum:
   308                 out += '<a href="%s">%d</a>' % (getPageFilename(n), n)
   309             else:
   310                 out += "%d" % n
   311             out += '&nbsp;'
   312         ## Next
   313         next = pagenum + 1
   314         if next <= NUM_PAGES:
   315             out += '<a href="%s">next</a>' % getPageFilename(next)
   316         else:
   317             out += 'next'
   318     return out
   321 ## Make a zip archive
   322 ZIPFILE = "photo-album.zip"
   323 if True: #opts.WRITE_ZIPFILE:
   324     logging.debug("Making zipped picture archive")
   325     if opts.ZIPFILE is not None:
   326         ZIPFILE = opts.ZIPFILE
   327     elif opts.TITLE is not None or len(opts.TITLE) > 0:
   328         ZIPFILE = safeencode(opts.TITLE)
   329         #ZIPFILE = safename(opts.OUTDIR)
   330     if not "." in ZIPFILE or os.path.splitextn(ZIPFILE)[1] != ".zip":
   331         ZIPFILE += ".zip"
   332     ## Do the zipping
   333     if ZIPFILE:
   334         from zipfile import ZipFile
   335         zf = ZipFile(os.path.join(opts.OUTDIR, ZIPFILE), "w")
   336         for img in imgs:
   337             zf.write(img, os.path.basename(img))
   338         zf.close()
   339     else:
   340         logging.warning("No zip file made because zip filename is empty")
   343 ## Copy Lightbox stuff into place
   344 #if opts.USE_JS:
   345 #    from zipfile import ZipFile
   346 #    zf = ZipFile(os.path.join(opts.OUTDIR, "lightbox.zip"), "r")
   347 #    for img in imgs:
   348 #        zf.write(img, os.path.basename(img))
   349 #    zf.close()
   352 ## Default template
   353 tmplstr = \
   354 """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   355         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
   356 <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
   357   <head>
   358     #set title = $PAGETITLE
   359     #if $NUM_PAGES > 1
   360     #set title = $title + " (page %s)" % $PAGENUM
   361     #end if
   362     <title>$title</title>
   363     <style>
   364       img { border:0; padding:10 10 0 0; }
   365       body { padding:1em; background:white; font-family:sans-serif; }
   366       h1 { font-family:sans-serif; }
   367       a.format { text-decoration:none; font-variant:small-caps; color:grey; font-size:small; }
   368       a.format:hover { color:deeppink; }
   369       a.format:active { color:deeppink; }
   370       .pagelinks { text-decoration:none; font-variant:small-caps; color:grey; margin-top:1em; margin-bottom:1em; }
   371       .pagelinks a:link { color:#22c; text-decoration:none; }
   372       .pagelinks a:hover { color:#55c; text-decoration:none; }
   373       .pagelinks a:active { color:#55c; text-decoration:none; }
   374     </style>
   375     #if $OPTS.USE_JS:
   376     <link rel="stylesheet" href="lightbox/css/lightbox.css" type="text/css" media="screen" />
   377     <script src="lightbox/js/prototype.js" type="text/javascript"></script>
   378     <script src="lightbox/js/scriptaculous.js?load=effects,builder" type="text/javascript"></script>
   379     <script src="lightbox/js/lightbox.js" type="text/javascript"></script>
   380     #end if
   381   </head>
   382   <body>
   383     <h1>$PAGETITLE</h1>
   384     #if $NUM_PAGES > 1
   385     <div class="pagelinks">Pages: $LINKSTR</div>
   386     #end if
   388     <table>
   389     <tr>
   390     #set jsrel = ''
   391     #if $OPTS.USE_JS:
   392     #set jsrel = 'rel="lightbox[emin]"'
   393     #end if
   394     #for n, thumb in enumerate($PAGEPICS)
   395       #if $n % $NUM_COLS == 0 and $n not in (0, len($PAGEPICS)-1)
   396       <tr/><tr>
   397       #end if
   398       #set info = $PICINFO[$thumb]
   399       <td>
   400         <a href="$info.relpath" $jsrel>
   401           <img alt="$thumb" src="$info.relthumbpath" style="border:0;" width="$info.thumbx" height="$info.thumby" />
   402         </a><br/>
   403         #for fmt, name in $info.versions.iteritems()
   404         <a class="format" href="$name">$fmt.lower()</a>
   405         #end for
   406       </td>
   407     #end for
   408     </tr>
   409     </table>
   411     #if $NUM_PAGES > 1
   412     <div class="pagelinks">Pages: $LINKSTR</div>
   413     #end if
   415     #if $OPTS.WRITE_ZIPFILE and $ZIPFILE:
   416     <p>All zipped up: <a href="$ZIPFILE">$ZIPFILE</a></p>
   417     #end if
   418   </body>
   419 </html>
   420 """
   423 ## Override default template with a template file
   424 if opts.TEMPLATE is not None:
   425     logging.info("Using index template file %s" % opts.TEMPLATE)
   426     tf = open(opts.TEMPLATE, "r")
   427     tmplstr = tf.read()
   428     tf.close()
   431 ## Make each index page
   432 for n in range(NUM_PAGES):
   433     PAGENUM = n + 1
   435     ## Choose and open page file
   436     PAGEFILE = getPageFilename(PAGENUM)
   437     PAGEPATH = os.path.join(opts.OUTDIR, PAGEFILE)
   439     ## Write the title
   440     PAGETITLE = opts.TITLE or os.path.basename(opts.SRCDIR)
   442     ## Write the linked page list
   443     LINKSTR = mkPageLinkStr(PAGENUM)
   445     ## Work out the picture offsets for this page
   446     pics_start = n * NUM_PER_PAGE
   447     pics_end = (n+1) * NUM_PER_PAGE - 1
   448     if pics_end >= len(imgs):
   449         pics_end = len(imgs) - 1
   451     PAGEPICS = sorted(imgsinfo.keys())[pics_start: pics_end+1]
   452     PAGEPICNUMS = range(len(PAGEPICS))
   453     relthumbdir = opts.THUMBDIR
   454     #relthumbdir = os.path.relpath(THUMBDIR, OUTDIR)
   455     reloutdir = "."
   456     for k in imgsinfo.keys():
   457         imgsinfo[k].relthumbpath = os.path.normpath(os.path.join(relthumbdir, imgsinfo[k].thumbname))
   458         imgsinfo[k].relpath = os.path.normpath(os.path.join(reloutdir, imgsinfo[k].name))
   459     PICINFO = imgsinfo
   460 #     THUMBNAMES = [t.name for t in thumbsinfo.values()[pics_start : pics_end]]
   461 #     print THUMBNAMES
   462 #     THUMBPATHS = [os.path.join(relthumbdir, name) for name in THUMBNAMES]
   463 #     print THUMBPATHS
   464 #     THUMBDIMS = [t.size for t in thumbsinfo.values()[pics_start : pics_end]]
   466     logging.info("Writing to index file %s" % PAGEPATH)
   467     f = open(PAGEPATH, "w")
   468     logging.debug("Images on page: %s" % PAGEPICS)
   469     tdict = {}
   470     tdict["NUM_PAGES"] = NUM_PAGES
   471     tdict["NUM_PER_PAGE"] = NUM_PER_PAGE
   472     tdict["NUM_ROWS"] = opts.NUM_ROWS
   473     tdict["NUM_COLS"] = opts.NUM_COLS
   474     tdict["PAGEPICS"] = PAGEPICS
   475     tdict["PAGENUM"] = PAGENUM
   476     tdict["PAGETITLE"] = PAGETITLE
   477     tdict["LINKSTR"] = LINKSTR
   478     tdict["PAGEPICNUMS"] = PAGEPICNUMS
   479     tdict["PICINFO"] = imgsinfo
   480 #     tdict["THUMBS"] = thumbsinfo
   481 #     tdict["THUMBNAMES"] = THUMBNAMES
   482 #     tdict["THUMBPATHS"] = THUMBPATHS
   483 #     tdict["THUMBDIMS"] = THUMBDIMS
   484     tdict["ZIPFILE"] = ZIPFILE
   485     tdict["OPTS"] = opts
   486     indexstr = Template(tmplstr, searchList=[tdict])
   487     #print indexstr
   488     f.write(str(indexstr))
   489     f.close()
   491 ## It's over. Nothing to see here.
   492 logging.debug("All done!")

mercurial