emin

Thu, 04 Apr 2013 16:53:49 +0200

author
Andy Buckley <andy@insectnation.org>
date
Thu, 04 Apr 2013 16:53:49 +0200
changeset 23
4e1d30c61d0e
parent 21
71e18d3b1685
child 24
02f21810d43d
permissions
-rwxr-xr-x

Avoid 'underlined linebreak' in HTML rendering

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

mercurial