merge
authorTero Marttila <terom@fixme.fi>
Sun, 14 Sep 2014 15:24:58 +0300
changeset 131 4e6e067b3472
parent 129 305f6d590440 (diff)
parent 130 aaae02944832 (current diff)
child 132 0260aeca943c
merge
--- a/.hgignore	Sun Sep 14 15:19:59 2014 +0300
+++ b/.hgignore	Sun Sep 14 15:24:58 2014 +0300
@@ -4,8 +4,9 @@
 \.pyc$
 
 ^build/
-^bin/
-^lib/
+^bin/pngtile(-static)?$
+^data
+^lib/libpngtile.(so|a)$
 ^doc/html
 ^dist
-^Learning Diary\.pdf$
+^python/pypngtile.c$
--- a/Makefile	Sun Sep 14 15:19:59 2014 +0300
+++ b/Makefile	Sun Sep 14 15:24:58 2014 +0300
@@ -1,12 +1,17 @@
 # :set noexpandtab
 CFLAGS_ALL = -Wall -std=gnu99
+LDFLAGS_ALL =
 CFLAGS_DBG = -g
 CFLAGS_REL = -O2
+CFLAGS_PRF = -g -O2 -pg
+LDFLAGS_PRF = -pg
 
 CFLAGS_SEL = ${CFLAGS_REL}
+LDFLAGS_SEL = ${LDFLAGS_REL}
 
 # warnings, and use C99 with GNU extensions
 CFLAGS = ${CFLAGS_ALL} ${CFLAGS_SEL}
+LDFLAGS = ${LDFLAGS_ALL} ${LDFLAGS_SEL}
 
 # preprocessor flags
 CPPFLAGS = -Iinclude -Isrc/
@@ -15,21 +20,30 @@
 LOADLIBES = -lpng -lpthread
 
 # output name
-DIST_NAME = 78949E-as2
-DIST_RESOURCES = README "Learning Diary.pdf" $(shell "echo python/*.{py,pyx}")
+DIST_NAME = pngtile-${shell hg id -i}
+DIST_DEPS = python/pypngtile.c
+DIST_RESOURCES = README python/ pngtile/ static/ bin/
 
-all: depend lib/libpngtile.so bin/util
+all: depend lib/libpngtile.so bin/pngtile lib/pypngtile.so
 
 lib/libpngtile.so : \
-	build/obj/lib/ctx.o build/obj/lib/image.o build/obj/lib/cache.o build/obj/lib/tile.o build/obj/lib/error.o \
+	build/obj/lib/ctx.o build/obj/lib/image.o build/obj/lib/cache.o build/obj/lib/tile.o build/obj/lib/png.o build/obj/lib/error.o \
+	build/obj/shared/util.o build/obj/shared/log.o
+
+lib/libpngtile.a : \
+	build/obj/lib/ctx.o build/obj/lib/image.o build/obj/lib/cache.o build/obj/lib/tile.o build/obj/lib/png.o build/obj/lib/error.o \
 	build/obj/shared/util.o build/obj/shared/log.o
 
 lib/pypngtile.so : \
 	lib/libpngtile.so
 
-bin/util: \
-	lib/libpngtile.so \
-	build/obj/shared/log.o
+bin/pngtile : \
+	build/obj/pngtile/main.o \
+	lib/libpngtile.so build/obj/shared/log.o
+
+bin/pngtile-static : \
+	build/obj/pngtile/main.o \
+	lib/libpngtile.a
 
 SRC_PATHS = $(wildcard src/*/*.c)
 SRC_NAMES = $(patsubst src/%,%,$(SRC_PATHS))
@@ -37,15 +51,18 @@
 
 .PHONY : dirs clean depend dist
 
+dist-clean : clean dirs
+
 dirs: 
 	mkdir -p bin lib dist
 	mkdir -p $(SRC_DIRS:%=build/deps/%)
-	mkdir -p $(SRC_DIRS:%=build/obj/%)
+	mkdir -p $(SRC_DIRS:%=build/obj/%) build/obj/python
 
 clean:
 	rm -f build/obj/*/*.o build/deps/*/*.d
-	rm -f bin/* lib/*.so run/*
-	rm -rf dist/*
+	rm -f bin/pngtile bin/pngtile-static lib/*.so lib/*.a
+	rm -f pngtile/*.pyc 
+	rm -f */.*.swp */*/.*.swp
 
 # .h dependencies
 depend: $(SRC_NAMES:%.c=build/deps/%.d)
@@ -72,27 +89,30 @@
 	$(CC) -c $(CPPFLAGS) $(CFLAGS) $< -o $@
 
 # output binaries
-bin/% : build/obj/%/main.o
+bin/% :
 	$(CC) $(LDFLAGS) $+ $(LOADLIBES) $(LDLIBS) -o $@
 
 # output libraries
 lib/lib%.so :
 	$(CC) -shared $(LDFLAGS) $+ $(LOADLIBES) $(LDLIBS) -o $@
 
-build/pyx/%.c : src/py/%.pyx
+lib/lib%.a :
+	$(AR) rc $@ $+
+
+python/%.c : python/%.pyx
 	cython -o $@ $<
 
-build/obj/py/%.o : build/pyx/%.c
-	$(CC) -c $(CPPFLAGS) $(CFLAGS) $< -o $@
+build/obj/python/%.o : python/%.c
+	$(CC) -c -fPIC -I/usr/include/python2.5 $(CPPFLAGS) $(CFLAGS) $< -o $@
 
-lib/py%.so : build/obj/py/%.o
+lib/py%.so : build/obj/python/py%.o
 	$(CC) -shared $(LDFLAGS) $+ $(LOADLIBES) $(LDLIBS) -o $@
 
-dist:
+dist: $(DIST_DEPS)
+	rm -rf dist/$(DIST_NAME)
 	mkdir -p dist/$(DIST_NAME)
-	cp -rv Makefile $(DIST_RESOURCES) src/ include/ dist/$(DIST_NAME)/
-	rm dist/$(DIST_NAME)/src/*/.*.sw[op]
-	make -C dist/$(DIST_NAME) dirs
+	cp -rv Makefile $(DIST_RESOURCES) src/ include/  dist/$(DIST_NAME)/
+	make -C dist/$(DIST_NAME) dist-clean
 	tar -C dist -czvf dist/$(DIST_NAME).tar.gz $(DIST_NAME)
 	@echo "*** Output at dist/$(DIST_NAME).tar.gz"
 
--- a/README	Sun Sep 14 15:19:59 2014 +0300
+++ b/README	Sun Sep 14 15:24:58 2014 +0300
@@ -9,8 +9,6 @@
     For this purpose, the library linearly decodes the PNG image to an uncompressed memory-mapped file, which can then
     be later used to encode a portion of this raw pixel data back into a PNG image.
 
-    Additionally, the library contains a thread pool for doing asynchronous tile render operations in parallel.
-
 
 NOTES:
     The command-line utility is mainly intended for maintining the image caches and testing, primary usage is expected
@@ -20,7 +18,12 @@
     There is a separate project that provides a web-based tile viewer using Javascript (implemented in Python as a
     WSGI application).
 
-    The .cache files are not portable across different architectures.
+    The .cache files are not portable across different architectures, nor are they compatible across different cache
+    format versions.
+
+    The library supports sparse cache files. A pixel-format byte pattern can be provided with --background using
+    hexadecimal notation (--background 0xFFFFFF - for 24bpp RGB white), and consecutive regions of that color will
+    be omitted in the cache file, which may provide significant gains in space efficiency.
 
 
 COMPILING:
@@ -29,13 +32,13 @@
         * libpng 1.2.15~beta5-3ubuntu0.1
         * NPTL 2.7 (glibc 2.7-10ubuntu5)
 
-    The code was verified to compile and run on cc.hut.fi's Ubuntu computers, e.g. asterix.hut.fi.
-
-    To compile, simply execute
+    To compile dist versions, simply execute
 
         make
 
-    The libpngtile.so will be placed under lib/, and the 'util' binary under bin/.
+    The libpngtile.so and pypngtile.so libraries will be placed under lib/, and the 'pngtile' binary under bin/.
+
+    XXX: If compiling the pypngtile.so library with make fails, then `setup.py build_ext -i` should build it.
 
 
 USAGE:
@@ -45,37 +48,29 @@
     Provide any number of *.png paths as arguments to the ./bin/util command. Each will be opened, and automatically
     updated if the cache doesn't exist yet, or is stale:
 
-        ./bin/util -v data/*.png
+        pngtile -v data/*.png
+    
+    Use -v/--verbose for more detailed output.
 
-    To render a tile from some image, provide appropriate -W/-H and -x/-y options to ./bin/util:
+
+    To render a tile from some image, provide appropriate -W/-H and -x/-y options to pngtile:
         
-        ./bin/util data/*.png -W 1024 -H 1024 -x 8000 -y 4000
+        pngtile data/*.png -W 1024 -H 1024 -x 8000 -y 4000
 
     The output PNG tiles will be written to temporary files, the names of which are shown in the [INFO] output.
 
 
-
     To force-update an image's cache, use the -U/--force-update option:
 
-        ./bin/util --force-update data/*.png
+        pngtile --force-update data/*.png
 
-    To change the number of threads used for tile-render operations, use -j/--threads:
-        
-        time ./bin/util -q data/* -W 4096 -H 4096 -x 8000 -y 4000 -j 1
-        > real    0m3.866s
-        
-        time ./bin/util -q data/* -W 4096 -H 4096 -x 8000 -y 4000 -j 4
-        > real    0m1.463s
+    Alternatively, to not update an image's cache, use the -N/--no-update option.
 
-        (measured on an Intel Core 2 Duo, compiled without optimizations)
-    
 
 TODO/BUGS:
     At this stage, the library is primarily designed to handle a specific set of PNG images, and hence does not support
     all aspects of the PNG format, nor any other image formats.
 
-    Cache updated operations are not executed using the thread pool.
-
     The pt_images opened by main() are not cleaned up before process exit, due to the asynchronous nature of the tile
     render operation's accesses to the underlying pt_cache object.
 
--- a/bin/dev-server	Sun Sep 14 15:19:59 2014 +0300
+++ b/bin/dev-server	Sun Sep 14 15:24:58 2014 +0300
@@ -3,21 +3,30 @@
 import wsgiref.simple_server
 import werkzeug
 from werkzeug.exceptions import NotFound
+import memcache
 
 import pngtile.wsgi
 
-# dispatch on URL
-app = werkzeug.DispatcherMiddleware(pngtile.wsgi.application, {
-    '/static':      werkzeug.SharedDataMiddleware(NotFound(), {
-        '/':            'static',
-    }),
-})
+def main (host='0.0.0.0', port=8000, memcache_host='localhost:11211') :
+    print "Using memcache server at %s" % memcache_host
 
-def main (host='0.0.0.0', port=8000) :
+    # cache
+    cache = memcache.Client([memcache_host])
+
+    # original app
+    app = pngtile.wsgi.WSGIApplication(cache)
+
+    # serve up static content as well
+    app = werkzeug.SharedDataMiddleware(app, {
+        '/static':          'static',
+    })
+    
+    # http server
     httpd = wsgiref.simple_server.make_server(host, port, app)
 
     print "Listening on %s:%d" % (host, port)
-
+    
+    # go
     httpd.serve_forever()
 
 if __name__ == '__main__' :
--- a/bin/pngtile.fcgi	Sun Sep 14 15:19:59 2014 +0300
+++ b/bin/pngtile.fcgi	Sun Sep 14 15:24:58 2014 +0300
@@ -1,10 +1,10 @@
+#!/usr/bin/python
+
 import flup.server.fcgi
 
-def main (app, bind=None) :
-    """
-        Run as a non-threaded single-process non-multiplexed FastCGI server
-    """
+import memcache
 
+def run_fastcgi (app, bind=None) :
     # create WSGIServer
     server = flup.server.fcgi.WSGIServer(app, 
         # try to supress threading
@@ -25,8 +25,22 @@
     # run... threads :(
     server.run()
 
+def main (bind=None) :
+    """
+        Run as a non-threaded single-process non-multiplexed FastCGI server
+    """
+
+    # open cache
+    cache = memcache.Client(['localhost:11211'])
+    
+    # build app
+    app = pngtile.wsgi.WSGIApplication(cache)
+
+    # server
+    run_fastcgi(app, bind)
+
 if __name__ == '__main__' :
     import pngtile.wsgi
 
-    main(pngtile.wsgi.application)
+    main()
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/pypngtile	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+
+"""
+    Python clone of the pngtile binary.
+"""
+
+import optparse, sys
+
+import pypngtile as pt
+
+# CLI options
+options = None
+
+def log (fmt, args) :
+    print >>sys.stderr, fmt % args
+
+def log_debug (fmt, *args) :
+    if options.debug or options.verbose :
+        log(fmt, args)
+
+def log_info (fmt, *args) :
+    if not options.quiet :
+        log(fmt, args)
+
+def log_warn (fmt, *args) :
+    log(fmt, args)
+
+
+def parse_hex (hex) :
+    """
+        Parse a 0xHH.. style string as a raw bytestring
+    """
+
+    if not hex.startswith("0x") :
+        raise ValueError(hex)
+
+    return hex[2:].decode("hex")
+
+def parse_args (args) :
+    """
+        Parse given argv[1:]
+    """
+
+    global options
+
+    parser = optparse.OptionParser(
+            usage   = "Usage: %prog [options] <image file> [...]",
+    )
+
+    # build opts list
+    parser.add_option('-q', "--quiet",          help="Supress informational output",                        action='store_true')
+    parser.add_option('-v', "--verbose",        help="Display more output",                                 action='store_true')
+    parser.add_option('-D', "--debug",          help="Equivalent to -v",                                    action='store_true')
+    parser.add_option('-U', "--force-update",   help="Unconditionally update the image caches",             action='store_true')
+    parser.add_option('-N', "--no-update",      help="Do not update the image caches, even if unusable",    action='store_true')
+    parser.add_option('-B', "--background",     help="Background pattern for sparse cache file",            metavar="0xHH..")
+    parser.add_option('-W', "--width",          help="Output tile width",                                   metavar="PX",       type='int', default=800)
+    parser.add_option('-H', "--height",         help="Output tile height",                                  metavar="PX",       type='int', default=600)
+    parser.add_option('-x', "--x",              help="Tile x offset",                                       metavar="PX",       type='int', default=0)
+    parser.add_option('-y', "--y",              help="Tile y offset",                                       metavar="PX",       type='int', default=0)
+    parser.add_option('-z', "--zoom",           help="Tile zoom level, -n",                                 metavar="ZL",       type='int', default=0)
+    parser.add_option('-o', "--out",            help="Render tile to output file, or -",                    metavar="FILE")
+    
+    # parse
+    options, args = parser.parse_args(args=args)
+
+    # decode
+    if options.background :
+        try :
+            options.background = parse_hex(options.background)
+
+        except ValueError :
+            raise ValueError("Invalid value for --background: %s" % options.background)
+
+    return args
+
+
+def process_tile (image) :
+    """
+        Process output tile for given image
+    """
+    
+    # parse out
+    if options.out == '-' :
+        log_debug("\tUsing stdout as output...")
+        
+        # use stdout
+        out = sys.stdout
+
+    else :
+        log_debug("\tOpening file for output: %s", options.out)
+
+        # open file
+        out = open(options.out, "wb")
+    
+    log_info("\tRender tile %dx%d@(%d:%d)@%d -> %s...", options.width, options.height, options.x, options.y, options.zoom, options.out)
+
+    # render
+    image.tile_file(options.width, options.height, options.x, options.y, options.zoom, out)
+    
+    log_debug("Rendered tile: %s", options.out)
+
+
+def process_image (image) :
+    """
+        Process given image
+    """
+
+    # check cache status
+    status = image.status()
+
+    # update if stale?
+    if status != pt.CACHE_FRESH or options.force_update :
+        # decode status
+        if status == pt.CACHE_NONE :
+            log_info("\tImage cache is missing")
+        
+        elif status == pt.CACHE_STALE :
+            log_info("\tImage cache is stale")
+
+        elif status == pt.CACHE_INCOMPAT :
+            log_info("\tImage cache is incompatible")
+
+        elif status == pt.CACHE_FRESH :
+            log_info("\tImage cache is fresh")
+
+        else :
+            log_warn("\tImage cache status unknown: %d", status)
+
+        
+        # update unless supressed
+        if not options.no_update :
+            log_info("\tUpdating image cache...")
+            
+            # update with optional background color
+            image.update(background_color=options.background)
+
+            log_debug("\tUpdated image cache")
+
+        else :
+            # warn
+            log_warn("\tSupressing cache update even though it is needed")
+
+    else: 
+        log_debug("\tImage cache is fresh")
+
+        # open it
+        image.open()
+
+    # show info
+    info = image.info()
+
+    log_info("\tImage dimensions: %d x %d (%d bpp)", info['img_width'], info['img_height'], info['img_bpp'])
+    log_info("\tImage mtime=%d, bytes=%d", info['image_mtime'], info['image_bytes'])
+    log_info("\tCache mtime=%d, bytes=%d, blocks=%d (%d bytes), version=%d", 
+            info['cache_mtime'], info['cache_bytes'], info['cache_blocks'], info['cache_blocks'] * 512, info['cache_version']
+    )
+    
+
+    # render tile?
+    if options.out :
+        log_debug("\tRendering output tile")
+
+        process_tile(image)
+
+    else :
+        log_debug("\tNot rendering output tile")
+
+def process_images (image_paths) :
+    """
+        Open up each image_path and process using process_image
+    """
+
+    for image_path in image_paths :
+        log_debug("Loading image: %s", image_path)
+
+        # open up
+        image = pt.Image(image_path, pt.OPEN_UPDATE)
+
+        log_info("Opened image: %s", image_path);
+
+        # process
+        process_image(image)
+
+
+def main (args) :
+    """
+        Operate on given argv[1:]
+    """
+
+    # parse opts/args
+    args = parse_args(args)
+    
+    # handle each image
+    process_images(args)
+
+
+if __name__ == '__main__' :
+    from sys import argv
+
+    main(argv[1:])
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/tornado-server	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,9 @@
+#!/usr/bin/python
+"""
+    Script for running the Tornado server + handler
+"""
+
+from pngtile import tornado_handler
+
+tornado_handler.main()
+
--- a/include/pngtile.h	Sun Sep 14 15:19:59 2014 +0300
+++ b/include/pngtile.h	Sun Sep 14 15:24:58 2014 +0300
@@ -8,6 +8,8 @@
  */
 #include <stddef.h>
 #include <stdio.h> // for FILE*
+#include <stdint.h>
+#include <sys/types.h> // for time_t
 
 /**
  * "Global" context shared between images
@@ -21,7 +23,10 @@
 
 /** Bitmask for pt_image_open modes */
 enum pt_open_mode {
-    /** Update cache if needed */
+    /** Open cache for read*/
+    PT_OPEN_READ    = 0x00,
+
+    /** Open cache for update */
     PT_OPEN_UPDATE   = 0x01,
 
     /** Accept stale cache */
@@ -43,12 +48,44 @@
 
     /** Cache exists, but is stale */
     PT_CACHE_STALE      = 2,
+
+    /** Cache exists, but it was generated using an incompatible version of this library */
+    PT_CACHE_INCOMPAT   = 3,
 };
 
-/** Metadata info for image */
+/** Metadata info for image. Values will be set to zero if not available */
 struct pt_image_info {
-    /** Dimensions of image */
-    size_t width, height;
+    /** Dimensions of image. Only available if the cache is open */
+    size_t img_width, img_height;
+
+    /** Bits per pixel */
+    size_t img_bpp;
+
+    /** Last update of image file */
+    time_t image_mtime;
+
+    /** Size of image file in bytes */
+    size_t image_bytes;
+    
+    /** Cache format version or -err */
+    int cache_version;
+
+    /** Last update of cache file */
+    time_t cache_mtime;
+
+    /** Size of cache file in bytes */
+    size_t cache_bytes;
+
+    /** Size of cache file in blocks (for sparse cache files) - 512 bytes / block? */
+    size_t cache_blocks;
+};
+
+/**
+ * Modifyable params for update
+ */
+struct pt_image_params {
+    /** Don't write out any contiguous regions of this color. Left-aligned in whatever format the source image is in */
+    uint8_t background_color[4];
 };
 
 /**
@@ -88,15 +125,19 @@
 /**
  * Open a new pt_image for use.
  *
+ * The pt_ctx is optional, but required for pt_image_tile_async.
+ *
  * @param img_ptr returned pt_image handle
- * @param ctx global state to use
+ * @param ctx global state to use (optional)
  * @param path filesystem path to .png file
  * @param mode combination of PT_OPEN_* flags
  */
 int pt_image_open (struct pt_image **image_ptr, struct pt_ctx *ctx, const char *png_path, int cache_mode);
 
 /**
- * Get the image's metadata
+ * Get the image's metadata.
+ *
+ * XXX: return void, this never fails, just returns partial info
  */
 int pt_image_info (struct pt_image *image, const struct pt_image_info **info_ptr);
 
@@ -109,11 +150,15 @@
 
 /**
  * Update the given image's cache.
+ *
+ * @param params optional parameters to use for the update process
  */
-int pt_image_update (struct pt_image *image);
+int pt_image_update (struct pt_image *image, const struct pt_image_params *params);
 
 /**
  * Load the image's cache in read-only mode without trying to update it.
+ *
+ * Fails if the cache doesn't exist.
  */
 // XXX: rename to pt_image_open?
 int pt_image_load (struct pt_image *image);
@@ -122,6 +167,8 @@
  * Render a PNG tile to a FILE*.
  *
  * The PNG data will be written to the given stream, which will be flushed, but not closed.
+ *
+ * Tile render operations are threadsafe as long as the pt_image is not modified during execution.
  */
 int pt_image_tile_file (struct pt_image *image, const struct pt_tile_info *info, FILE *out);
 
@@ -130,6 +177,8 @@
  *
  * The PNG data will be written to a malloc'd buffer.
  *
+ * Tile render operations are threadsafe as long as the pt_image is not modified during execution.
+ *
  * @param image render from image's cache
  * @param info tile parameters
  * @param buf_ptr returned heap buffer
@@ -144,6 +193,8 @@
  *
  * This function may return before the PNG has been rendered.
  *
+ * Fails with PT_ERR if not pt_ctx was given to pt_image_open.
+ *
  * @param image render from image's cache. The cache must have been opened previously!
  * @param info tile parameters
  * @param out IO stream to write PNG data to, and close once done
@@ -159,14 +210,20 @@
  * Error codes returned
  */
 enum pt_error {
+    /** No error */
     PT_SUCCESS = 0,
+    
+    /** Generic error */
+    PT_ERR = 1,
+
     PT_ERR_MEM,
 
     PT_ERR_PATH,
     PT_ERR_OPEN_MODE,
     
     PT_ERR_IMG_STAT,
-    PT_ERR_IMG_FOPEN,
+    PT_ERR_IMG_OPEN,
+    PT_ERR_IMG_FORMAT,
     
     PT_ERR_PNG_CREATE,
     PT_ERR_PNG,
@@ -180,13 +237,17 @@
     PT_ERR_CACHE_TRUNC,
     PT_ERR_CACHE_MMAP,
     PT_ERR_CACHE_RENAME_TMP,
-
+    PT_ERR_CACHE_VERSION,
+    PT_ERR_CACHE_MUNMAP,
+    PT_ERR_CACHE_CLOSE,
+    
+    PT_ERR_TILE_DIM,
     PT_ERR_TILE_CLIP,
+    PT_ERR_TILE_ZOOM,
 
     PT_ERR_PTHREAD_CREATE,
     PT_ERR_CTX_SHUTDOWN,
 
-    PT_ERR_ZOOM,
 
     PT_ERR_MAX,
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pngtile/handlers.py	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,172 @@
+import os, os.path
+import pypngtile as pt
+
+from werkzeug import Response, exceptions
+
+from pngtile import render
+
+# path to images
+DATA_ROOT = os.environ.get("PNGTILE_DATA_PATH") or os.path.abspath('data/')
+
+# only open each image once
+IMAGE_CACHE = {}
+
+### Parse request data
+def get_path (req_path) :
+    """
+        Returns the name and path requested
+    """
+    
+    # check DATA_ROOT exists..
+    if not os.path.isdir(DATA_ROOT) :
+        raise exceptions.InternalServerError("Missing DATA_ROOT")
+
+    # path to image
+    image_name = req_path.lstrip('/')
+    
+    # build absolute path
+    image_path = os.path.abspath(os.path.join(DATA_ROOT, image_name))
+
+    # ensure the path points inside the data root
+    if not image_path.startswith(DATA_ROOT) :
+        raise exceptions.NotFound(image_name)
+
+    return image_name, image_path
+
+def get_image (name, path) :
+    """
+        Gets an Image object from the cache, ensuring that the cached is available
+    """
+
+    # get Image object
+    if path in IMAGE_CACHE :
+        # get from cache
+        image = IMAGE_CACHE[path]
+
+    else :
+        # open
+        image = pt.Image(path)
+
+        # check
+        if image.status() not in (pt.CACHE_FRESH, pt.CACHE_STALE) :
+            raise exceptions.InternalServerError("Image cache not available: %s" % name)
+
+        # load
+        image.open()
+
+        # cache
+        IMAGE_CACHE[path] = image
+    
+    return image
+
+
+### Handle werkzeug.Request objects -> werkzeug.Response
+def handle_dir (req, name, path) :
+    """
+        Handle request for a directory
+    """
+    
+    prefix = req.script_root
+    
+    return Response(render.dir_html(prefix, name, path), content_type="text/html")
+
+
+
+def handle_img_viewport (req, image, name) :
+    """
+        Handle request for image viewport
+    """
+    
+    prefix = req.script_root
+
+    # viewport
+    return Response(render.img_html(prefix, name, image), content_type="text/html")
+
+
+def handle_img_region (req, image, cache) :
+    """
+        Handle request for an image region
+    """
+
+    # specific image
+    width = int(req.args['w'])
+    height = int(req.args['h'])
+    cx = int(req.args['cx'])
+    cy = int(req.args['cy'])
+    zoom = int(req.args.get('zl', "0"))
+    
+    try :
+        # yay full render
+        return Response(render.img_png_region(image, cx, cy, zoom, width, height, cache), content_type="image/png")
+
+    except ValueError, ex :
+        # too large
+        raise exceptions.Forbidden(str(ex))
+
+
+def handle_img_tile (req, image, cache) :
+    """
+        Handle request for image tile
+    """
+
+    # tile
+    x = int(req.args['x'])
+    y = int(req.args['y'])
+    zoom = int(req.args.get('zl', "0"))
+
+    # cache?
+        
+    # yay render
+    return Response(render.img_png_tile(image, x, y, zoom, cache), content_type="image/png")
+
+## Dispatch req to handle_img_*
+def handle_img (req, name, path, cache) :
+    """
+        Handle request for an image
+    """
+
+    # get image object
+    image = get_image(name, path)
+
+    # what view?
+    if not req.args :
+        return handle_img_viewport(req, image, name)
+
+    elif 'w' in req.args and 'h' in req.args and 'cx' in req.args and 'cy' in req.args :
+        return handle_img_region(req, image, cache)
+
+    elif 'x' in req.args and 'y' in req.args :
+        return handle_img_tile(req, image, cache)
+
+    else :
+        raise exceptions.BadRequest("Unknown args")
+
+
+
+## Dispatch request to handle_*
+def handle_req (req, cache) :
+    """
+        Main request handler
+    """
+    
+    # decode req
+    name, path = get_path(req.path)
+
+    # determine dir/image
+    if os.path.isdir(path) :
+        # directory
+        return handle_dir(req, name, path)
+    
+    elif not os.path.exists(path) :
+        # no such file
+        raise exceptions.NotFound(name)
+
+    elif not name or not name.endswith('.png') :
+        # invalid file
+        raise exceptions.BadRequest("Not a PNG file")
+    
+    else :
+        # image
+        return handle_img(req, name, path, cache)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pngtile/render.py	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,245 @@
+"""
+    Rendering output
+"""
+
+import os, os.path
+
+## Settings
+# width of a tile
+TILE_WIDTH = 256
+TILE_HEIGHT = 256
+
+# max. output resolution to allow
+MAX_PIXELS = 1920 * 1200
+
+def dir_url (prefix, name, item) :
+    """
+        Join together an absolute URL prefix, an optional directory name, and a directory item
+    """
+
+    url = prefix
+
+    if name :
+        url += '/' + name
+    
+    url += '/' + item
+
+    return url
+
+def dir_list (dir_path) :
+    """
+        Yield a series of directory items to show for the given dir
+    """
+
+    # link to parent
+    yield '..'
+
+    for item in os.listdir(dir_path) :
+        path = os.path.join(dir_path, item)
+
+        # skip dotfiles
+        if item.startswith('.') :
+            continue
+        
+        # show dirs
+        if os.path.isdir(path) :
+            yield item
+
+        # examine ext
+        base, ext = os.path.splitext(path)
+
+        # show .png files with a .cache file
+        if ext == '.png' and os.path.exists(base + '.cache') :
+            yield item
+
+
+### Render HTML data
+def dir_html (prefix, name, path) :
+    """
+        Directory index
+    """
+
+    name = name.rstrip('/')
+
+    return """\
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>Index of %(dir)s</title>
+        <link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css">
+    </head>
+    <body>
+        <h1>Index of %(dir)s</h1>
+
+        <ul>
+%(listing)s
+        </ul>
+    </body>
+</html>""" % dict(
+        prefix          = prefix,
+        dir             = '/' + name,
+        
+        listing         = "\n".join(
+            # <li> link
+            """<li><a href="%(url)s">%(name)s</a></li>""" % dict(
+                # URL to dir
+                url         = dir_url(prefix, name, item),
+
+                # item name
+                name        = item,
+            ) for item in dir_list(path)
+        ),
+    )
+
+def img_html (prefix, name, image) :
+    """
+        HTML for image
+    """
+    
+    # a little slow, but not so bad - two stats(), heh
+    info = image.info()
+    img_width, img_height = info['img_width'], info['img_height']
+
+    return """\
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>%(title)s</title>
+        <script src="%(prefix)s/static/prototype.js" type="text/javascript"></script>
+        <script src="%(prefix)s/static/dragdrop.js" type="text/javascript"></script>
+        <script src="%(prefix)s/static/builder.js" type="text/javascript"></script>
+        <script src="%(prefix)s/static/tiles2.js" type="text/javascript"></script>
+        <link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css">
+    </head>
+    <body>
+        <div id="wrapper">
+            <div id="viewport" style="width: 100%%; height: 100%%">
+                <div class="overlay">
+                    <input type="button" id="btn-zoom-in" value="Zoom In" />
+                    <input type="button" id="btn-zoom-out" value="Zoom Out" />
+                    <a class="link" id="lnk-image" href="#"></a>
+                </div>
+
+                <div class="substrate"></div>
+
+                <div class="background">
+                    Loading...
+                </div>
+            </div>
+        </div>
+
+        <script type="text/javascript">
+            var tile_source = new Source("%(tile_url)s", %(tile_width)d, %(tile_height)d, 0, 4, %(img_width)d, %(img_height)d);
+            var main = new Viewport(tile_source, "viewport");
+        </script>
+    </body>
+</html>""" % dict(
+        title           = name,
+        prefix          = prefix,
+        tile_url        = prefix + '/' + name,
+
+        tile_width      = TILE_WIDTH,
+        tile_height     = TILE_HEIGHT,
+
+        img_width       = img_width,
+        img_height      = img_height,
+    )
+
+
+# threshold to cache images on - only images with a source data region *larger* than this are cached
+CACHE_THRESHOLD = 512 * 512
+
+def scale_by_zoom (val, zoom) :
+    """
+        Scale dimension by zoom factor
+
+        zl > 0 -> bigger
+        zl < 0 -> smaller
+    """
+
+    if zoom > 0 :
+        return val << zoom
+
+    elif zoom > 0 :
+        return val >> -zoom
+
+    else :
+        return val
+
+### Image caching
+def check_cache_threshold (width, height, zl) :
+    """
+        Checks if a tile with the given dimensions should be cached
+    """
+
+    return (scale_by_zoom(width, zl) * scale_by_zoom(height, zl)) > CACHE_THRESHOLD
+
+def render_raw (image, width, height, x, y, zl) :
+    """
+        Render and return tile
+    """
+
+    return image.tile_mem(
+            width, height,
+            x, y, zl
+    )
+
+def render_cache (cache, image, width, height, x, y, zl) :
+    """
+        Perform a cached render of the given tile
+    """
+    
+    if cache :
+        # cache key
+        key = "tl_%d:%d_%d:%d:%d_%s" % (x, y, width, height, zl, image.path)
+        
+        # lookup
+        data = cache.get(key)
+
+    else :
+        # oops, no cache
+        data = None
+
+    if not data :
+        # cache miss, render
+        data = render_raw(image, width, height, x, y, zl)
+        
+        if cache :
+            # store
+            cache.add(key, data)
+    
+    # ok
+    return data
+
+### Render PNG Data
+def img_png_tile (image, x, y, zoom, cache) :
+    """
+        Render given tile, returning PNG data
+    """
+
+    # remap coordinates by zoom
+    x = scale_by_zoom(x, zoom)
+    y = scale_by_zoom(y, zoom)
+
+    # do we want to cache this?
+    if check_cache_threshold(TILE_WIDTH, TILE_HEIGHT, zoom) :
+        # go via the cache
+        return render_cache(cache, image, TILE_WIDTH, TILE_HEIGHT, x, y, zoom)
+
+    else :
+        # just go raw
+        return render_raw(image, TILE_WIDTH, TILE_HEIGHT, x, y, zoom)
+
+def img_png_region (image, cx, cy, zoom, width, height, cache) :
+    """
+        Render arbitrary tile, returning PNG data
+    """
+
+    x = scale_by_zoom(cx - width / 2, zoom)
+    y = scale_by_zoom(cy - height / 2, zoom)
+
+    # safely limit
+    if width * height > MAX_PIXELS :
+        raise ValueError("Image size: %d * %d > %d" % (width, height, MAX_PIXELS))
+    
+    # these images are always cached
+    return render_cache(cache, image, width, height, x, y, zoom)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pngtile/tornado_handler.py	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,108 @@
+"""
+    A tornado-based HTTP app
+"""
+
+import tornado.web
+import tornado.httpserver
+import tornado.wsgi
+import werkzeug
+
+from pngtile import handlers
+
+class MainHandler (tornado.web.RequestHandler) :
+    """
+        Main handler for the / URL, pass off requests to werkzeug-based handlers...
+    """
+
+    def build_environ (self, path) :
+        """
+            Yield a series of (key, value) pairs suitable for use with WSGI
+        """
+
+        request = self.request
+
+        hostport = request.host.split(":")
+
+        if len(hostport) == 2:
+            host = hostport[0]
+            port = int(hostport[1])
+        else:
+            host = request.host
+            port = 443 if request.protocol == "https" else 80
+
+        yield "REQUEST_METHOD", request.method
+        yield "SCRIPT_NAME", ""
+        yield "PATH_INFO", path
+        yield "QUERY_STRING", request.query
+        yield "SERVER_NAME", host
+        yield "SERVER_PORT", port
+
+        yield "wsgi.version", (1, 0)
+        yield "wsgi.url_scheme", request.protocol
+        
+        yield "CONTENT_TYPE", request.headers.get("Content-Type")
+        yield "CONTENT_LENGTH", request.headers.get("Content-Length")
+
+        for key, value in request.headers.iteritems():
+            yield "HTTP_" + key.replace("-", "_").upper(), value
+    
+    def get (self, path) :
+        environ = dict(self.build_environ(path))
+
+        # build Request
+        request = werkzeug.Request(environ)
+
+        # handle
+        try :
+            response = handlers.handle_req(request)
+
+        except werkzeug.exceptions.HTTPException, ex :
+            response = ex
+
+        # return
+        def start_response (_status, _headers) :
+            status = int(_status.split()[0])
+            headers = _headers
+
+            self.set_status(status)
+
+            for name, value in headers :
+                self.set_header(name, value)
+        
+        # invoke Response
+        data = response(environ, start_response)
+
+        # output data
+        for chunk in data :
+            self.write(chunk)
+        
+def build_app () :
+    return tornado.web.Application([
+        # static, from $CWD/static/
+        (r"/static/(.*)",           tornado.web.StaticFileHandler,  dict(path = "static/")),
+        
+        # dir listings, image html, PNG tiles
+        (r"(/.*)",     MainHandler),
+    ])  
+
+def build_httpserver (app, port) :
+    server = tornado.httpserver.HTTPServer(app)
+    server.listen(port)
+
+    return server
+
+def main (port=8000) :
+    """
+        Build the app, http server and run the main loop
+    """
+
+    import logging
+
+    logging.basicConfig(level=logging.DEBUG)
+
+    app = build_app()
+    server = build_httpserver(app, port)
+
+    tornado.ioloop.IOLoop.instance().start()
+
+
--- a/pngtile/wsgi.py	Sun Sep 14 15:19:59 2014 +0300
+++ b/pngtile/wsgi.py	Sun Sep 14 15:24:58 2014 +0300
@@ -2,190 +2,37 @@
     Our WSGI web interface, which can serve the JS UI and any .png tiles via HTTP.
 """
 
-from werkzeug import Request, Response, responder
+from werkzeug import Request, responder
 from werkzeug import exceptions
-import os.path
-
-import pypngtile as pt
-
-DATA_ROOT = os.path.abspath('data')
 
-IMAGE_CACHE = {}
-
-TILE_WIDTH = 256
-TILE_HEIGHT = 256
-
-def dir_view (req, name, path) :
-    prefix = os.path.dirname(req.script_root).rstrip('/')
-    name = name.rstrip('/')
+from pngtile import handlers
 
 
-    return """\
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-    <head>
-        <title>Index of %(dir)s</title>
-        <link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css">
-    </head>
-    <body>
-        <h1>Index of %(dir)s</h1>
-
-        <ul>
-%(listing)s
-        </ul>
-    </body>
-</html>""" % dict(
-        prefix          = prefix,
-        dir             = name,
-        
-        listing         = "\n".join(
-            """<li><a href="%(url)s">%(name)s</a></li>""" % dict(
-                url         = '/'.join((prefix, name, item)),
-                name        = item,
-            ) for item in ['..'] + os.listdir(path)
-        ),
-    )
-
-def image_view (req, image_path, image) :
-    image_name = os.path.basename(image_path)
-
-    return """\
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-    <head>
-        <title>%(title)s</title>
-        <script src="%(prefix)s/static/prototype.js" type="text/javascript"></script>
-        <script src="%(prefix)s/static/dragdrop.js" type="text/javascript"></script>
-        <script src="%(prefix)s/static/builder.js" type="text/javascript"></script>
-        <script src="%(prefix)s/static/tiles2.js" type="text/javascript"></script>
-        <link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css">
-    </head>
-    <body>
-        <div id="wrapper">
-            <div id="viewport" style="width: 100%%; height: 100%%">
-                <div class="overlay">
-                    <input type="button" id="btn-zoom-in" value="Zoom In" />
-                    <input type="button" id="btn-zoom-out" value="Zoom Out" />
-                    <a class="link" id="lnk-image" href="#"></a>
-                </div>
-
-                <div class="substrate"></div>
-            </div>
-        </div>
-
-        <script type="text/javascript">
-            var tile_source = new Source("%(tile_url)s", %(tile_width)d, %(tile_height)d, -4, 0);
-            var main = new Viewport(tile_source, "viewport");
-        </script>
-    </body>
-</html>""" % dict(
-        title           = image_name,
-        prefix          = os.path.dirname(req.script_root).rstrip('/'),
-        tile_url        = req.url,
-
-        tile_width      = TILE_WIDTH,
-        tile_height     = TILE_HEIGHT,
-    )
-
-def scale_by_zoom (val, zoom) :
-    if zoom > 0 :
-        return val << zoom
-
-    elif zoom > 0 :
-        return val >> -zoom
-
-    else :
-        return val
-
-def render_tile (image, x, y, zoom, width=TILE_WIDTH, height=TILE_HEIGHT) :
-    return image.tile_mem(
-        width, height, 
-        scale_by_zoom(x, -zoom), scale_by_zoom(y, -zoom), 
-        zoom
-    )
+class WSGIApplication (object) :
+    """
+        Simple WSGI application invoking the werkzeug handlers
+    """
 
-def render_image (image, cx, cy, zoom, width, height) :
-    x = scale_by_zoom(cx - width / 2, -zoom)
-    y = scale_by_zoom(cy - height / 2, -zoom)
-
-    return image.tile_mem(
-        width, height,
-        x, y,
-        zoom
-   )
-
-def handle_main (req) :
-    # path to image
-    image_name = req.path.lstrip('/')
-    
-    # build absolute path
-    image_path = os.path.abspath(os.path.join(DATA_ROOT, image_name))
-    
-    # ensure the path points inside the data root
-    if not image_path.startswith(DATA_ROOT) :
-        raise exceptions.NotFound(image_name)
-
-
-    if os.path.isdir(image_path) :
-        return Response(dir_view(req, image_name, image_path), content_type="text/html")
-    
-    elif not os.path.exists(image_path) :
-        raise exceptions.NotFound(image_name)
-
-    elif not image_name or not image_name.endswith('.png') :
-        raise exceptions.BadRequest("Not a PNG file")
-    
-
-    # get Image object
-    if image_path in IMAGE_CACHE :
-        # get from cache
-        image = IMAGE_CACHE[image_path]
-
-    else :
-        # ensure exists
-        if not os.path.exists(image_path) :
-            raise exceptions.NotFound(image_name)
+    def __init__ (self, cache=None) :
+        """
+            Use given cache if any
+        """
 
-        # cache
-        image = IMAGE_CACHE[image_path] = pt.Image(image_path)
-    
-    if image.status() == pt.CACHE_NONE :
-        raise exceptions.InternalServerError("Image not cached: " + image_name)
-
-    # what view?
-    if not req.args :
-        # viewport
-        return Response(image_view(req, image_path, image), content_type="text/html")
-
-    elif 'w' in req.args and 'h' in req.args and 'cx' in req.args and 'cy' in req.args :
-        # specific image
-        width = int(req.args['w'])
-        height = int(req.args['h'])
-        cx = int(req.args['cx'])
-        cy = int(req.args['cy'])
-        zoom = int(req.args.get('zl', "0"))
-
-        # yay full render
-        return Response(render_image(image, cx, cy, zoom, width, height), content_type="image/png")
+        self.cache = cache
 
-    elif 'x' in req.args and 'y' in req.args :
-        # tile
-        x = int(req.args['x'])
-        y = int(req.args['y'])
-        zoom = int(req.args.get('zl', "0"))
+    @responder
+    def __call__ (self, env, start_response) :
+        """
+            Main WSGI entry point.
+
+            This is wrapped with werkzeug, so we can return a Response object
+        """
+
+        req = Request(env, start_response)
         
-        # yay render
-        return Response(render_tile(image, x, y, zoom), content_type="image/png")
-   
-    else :
-        raise exceptions.BadRequest("Unknown args")
-   
+        try :
+            return handlers.handle_req(req, self.cache)
 
-@responder
-def application (env, start_response) :
-    req = Request(env, start_response)
-    
-    try :
-        return handle_main(req)
+        except exceptions.HTTPException, e :
+            return e
 
-    except exceptions.HTTPException, e :
-        return e
-
--- a/python/pypngtile.pyx	Sun Sep 14 15:19:59 2014 +0300
+++ b/python/pypngtile.pyx	Sun Sep 14 15:24:58 2014 +0300
@@ -4,6 +4,9 @@
 cdef extern from "string.h" :
     char* strerror (int err)
 
+    void* memset (void *, int, size_t)
+    void* memcpy (void *, void *, size_t)
+
 cimport stdio
 cimport stdlib
 cimport python_string
@@ -22,79 +25,217 @@
         pass
 
     enum pt_open_mode :
+        PT_OPEN_READ    # 0
         PT_OPEN_UPDATE
 
     enum pt_cache_status :
-        PT_CACHE_ERROR
+        PT_CACHE_ERROR  # -1
         PT_CACHE_FRESH
         PT_CACHE_NONE
         PT_CACHE_STALE
+        PT_CACHE_INCOMPAT
 
     struct pt_image_info :
-        size_t width, height
+        size_t img_width, img_height, img_bpp
+        int image_mtime, cache_mtime, cache_version
+        size_t image_bytes, cache_bytes
+        size_t cache_blocks
 
+    struct pt_image_params :
+        int background_color[4]
+    
     struct pt_tile_info :
         size_t width, height
         size_t x, y
         int zoom
-        
-    int pt_image_open (pt_image **image_ptr, pt_ctx *ctx, char *png_path, int cache_mode)
-    int pt_image_info_func "pt_image_info" (pt_image *image, pt_image_info **info_ptr)
-    int pt_image_status (pt_image *image)
-    int pt_image_update (pt_image *image)
-    int pt_image_tile_file (pt_image *image, pt_tile_info *info, stdio.FILE *out)
-    int pt_image_tile_mem (pt_image *image, pt_tile_info *info, char **buf_ptr, size_t *len_ptr)
-    void pt_image_destroy (pt_image *image)
 
+    ctypedef pt_image_info* const_image_info_ptr "const struct pt_image_info *"
+    
+    ## functions
+    int pt_image_open (pt_image **image_ptr, pt_ctx *ctx, char *png_path, int cache_mode) nogil
+    int pt_image_info_ "pt_image_info" (pt_image *image, pt_image_info **info_ptr) nogil
+    int pt_image_status (pt_image *image) nogil
+    int pt_image_load (pt_image *image) nogil
+    int pt_image_update (pt_image *image, pt_image_params *params) nogil
+    int pt_image_tile_file (pt_image *image, pt_tile_info *info, stdio.FILE *out) nogil
+    int pt_image_tile_mem (pt_image *image, pt_tile_info *info, char **buf_ptr, size_t *len_ptr) nogil
+    void pt_image_destroy (pt_image *image) nogil
+    
+    # error code -> name
     char* pt_strerror (int err)
 
-OPEN_UPDATE = PT_OPEN_UPDATE
-CACHE_ERROR = PT_CACHE_ERROR
-CACHE_FRESH = PT_CACHE_FRESH
-CACHE_NONE = PT_CACHE_NONE
-CACHE_STALE = PT_CACHE_STALE
+## constants
+# Image()
+OPEN_READ       = PT_OPEN_READ
+OPEN_UPDATE     = PT_OPEN_UPDATE
+
+# Image.status -> ...
+CACHE_FRESH     = PT_CACHE_FRESH
+CACHE_NONE      = PT_CACHE_NONE
+CACHE_STALE     = PT_CACHE_STALE
+CACHE_INCOMPAT  = PT_CACHE_INCOMPAT
 
 class Error (BaseException) :
-    pass
+    """
+        Base class for errors raised by pypngtile.
+    """
 
-cdef int trap_err (char *op, int ret) except -1 :
-    if ret < 0 :
-        raise Error("%s: %s: %s" % (op, pt_strerror(ret), strerror(errno)))
-
-    else :
-        return ret
+    def __init__ (self, func, err) :
+        super(Error, self).__init__("%s: %s: %s" % (func, pt_strerror(err), strerror(errno)))
 
 cdef class Image :
+    """
+        An image file on disk (.png) and an associated .cache file.
+        
+        Open the .png file at the given path using the given mode.
+
+        path        - filesystem path to .png file
+        mode        - mode to operate cache in
+            OPEN_READ       - read-only access to cache
+            OPEN_UPDATE     - allow .update()
+    """
+
     cdef pt_image *image
 
-    def __cinit__ (self, char *png_path, int cache_mode = 0) :
-        trap_err("pt_image_open", 
-            pt_image_open(&self.image, NULL, png_path, cache_mode)
-        )
+    # XXX: should really be a pt_image property...
+    cdef readonly object path
+
     
-    def info (self) :
-        cdef pt_image_info *image_info
+    # open the pt_image
+    def __cinit__ (self, char *path, int mode = 0) :
+        cdef int err
+
+        # store
+        self.path = path
         
-        trap_err("pt_image_info",
-            pt_image_info_func(self.image, &image_info)
-        )
+        # open
+        with nogil :
+            # XXX: I hope use of path doesn't break...
+            err = pt_image_open(&self.image, NULL, path, mode)
 
-        return (image_info.width, image_info.height)
-    
+        if err :
+            raise Error("pt_image_open", err)
+
+
+    def info (self) :
+        """
+            Return a dictionary containing various information about the image.
+
+            img_width           - pixel dimensions of the source image
+            img_height            only available if the cache was opened
+            img_bpp             - bits per pixel for the source image
+
+            image_mtime         - last modification timestamp for source image
+            image_bytes         - size of source image file in bytes
+
+            cache_version       - version of cache file available
+            cache_mtime         - last modification timestamp for cache file
+            cache_bytes         - size of cache file in bytes
+            cache_blocks        - size of cache file in disk blocks - 512 bytes / block
+        """
+
+        cdef const_image_info_ptr infop
+        cdef int err
+
+        with nogil :
+            err = pt_image_info_(self.image, &infop)
+
+        if err :
+            raise Error("pt_image_info", err)
+        
+        # return as a struct
+        return infop[0]
+
+
     def status (self) :
-        return trap_err("pt_image_status", 
-            pt_image_status(self.image)
-        )
+        """
+            Return a code describing the status of the underlying cache file.
+
+            CACHE_FRESH         - the cache file exists and is up-to-date
+            CACHE_NONE          - the cache file does not exist
+            CACHE_STALE         - the cache file exists, but is older than the source image
+            CACHE_INCOMPAT      - the cache file exists, but is incompatible with this version of the library
+        """
+
+        cdef int ret
+
+        with nogil :
+            ret = pt_image_status(self.image)
+
+        if ret :
+            raise Error("pt_image_status", ret)
+        
+        return ret
+
+    def open (self) :
+        """
+            Open the underlying cache file for reading, if available.
+        """
+
+        cdef int err
+
+        with nogil :
+            err = pt_image_load(self.image)
+
+        if err :
+            raise Error("pt_image_load", err)
+
+
+    def update (self, background_color = None) :
+        """
+            Update the underlying cache file from the source image.
+
+            background_color    - skip consecutive pixels that match this byte pattern in output
+
+            Requires that the Image was opened using OPEN_UPDATE.
+        """
+
+        cdef pt_image_params params
+        cdef char *bgcolor
+        cdef int err
+
+        memset(&params, 0, sizeof(params))
+
+        # params
+        if background_color :
+            # cast
+            bgcolor = <char *>background_color
+
+            if 0 >= len(bgcolor) > 4 :
+                raise ValueError("background_color must be a str of between 1 and 4 bytes")
+
+            # decode 
+            memcpy(params.background_color, bgcolor, len(bgcolor))
     
-    def update (self) :
-        trap_err("pt_image_update", 
-            pt_image_update(self.image)
-        )
+        # run update
+        with nogil :
+            err = pt_image_update(self.image, &params)
+
+        if err :
+            raise Error("pt_image_update", err)
+
 
     def tile_file (self, size_t width, size_t height, size_t x, size_t y, int zoom, object out) :
+        """
+            Render a region of the source image as a PNG tile to the given output file.
+
+            width       - dimensions of the output tile in px
+            height      
+            x           - coordinates in the source file
+            y
+            zoom        - zoom level: out = 2**(-zoom) * in
+            out         - output file
+
+            Note that the given file object MUST be a *real* stdio FILE*, not a fake Python object.
+        """
+
         cdef stdio.FILE *outf
         cdef pt_tile_info ti
+        cdef int err
 
+        memset(&ti, 0, sizeof(ti))
+        
+        # convert to FILE
         if not PyFile_Check(out) :
             raise TypeError("out: must be a file object")
 
@@ -102,32 +243,53 @@
 
         if not outf :
             raise TypeError("out: must have a FILE*")
-    
+        
+        # pack params
         ti.width = width
         ti.height = height
         ti.x = x
         ti.y = y
         ti.zoom = zoom
         
-        trap_err("pt_image_tile_file", 
-            pt_image_tile_file(self.image, &ti, outf)
-        )
+        # render
+        with nogil :
+            err = pt_image_tile_file(self.image, &ti, outf)
+
+        if err :
+            raise Error("pt_image_tile_file", err)
+
 
     def tile_mem (self, size_t width, size_t height, size_t x, size_t y, int zoom) :
+        """
+            Render a region of the source image as a PNG tile, and return the PNG data a a string.
+
+            width       - dimensions of the output tile in px
+            height      
+            x           - coordinates in the source file
+            y
+            zoom        - zoom level: out = 2**(-zoom) * in
+        """
+
         cdef pt_tile_info ti
         cdef char *buf
         cdef size_t len
-
+        cdef int err
+        
+        memset(&ti, 0, sizeof(ti))
+        
+        # pack params
         ti.width = width
         ti.height = height
         ti.x = x
         ti.y = y
         ti.zoom = zoom
         
-        # render and return ptr to buffer
-        trap_err("pt_image_tile_mem", 
-            pt_image_tile_mem(self.image, &ti, &buf, &len)
-        )
+        # render and return via buf/len
+        with nogil :
+            err = pt_image_tile_mem(self.image, &ti, &buf, &len)
+
+        if err :
+            raise Error("pt_image_tile_mem", err)
         
         # copy buffer as str...
         data = python_string.PyString_FromStringAndSize(buf, len)
@@ -137,7 +299,10 @@
 
         return data
 
+    # release the pt_image
     def __dealloc__ (self) :
         if self.image :
             pt_image_destroy(self.image)
 
+            self.image = NULL
+
--- a/setup.py	Sun Sep 14 15:19:59 2014 +0300
+++ b/setup.py	Sun Sep 14 15:24:58 2014 +0300
@@ -1,12 +1,37 @@
 from distutils.core import setup
 from distutils.extension import Extension
-from Cython.Distutils import build_ext
+
+import os.path
+
+build_root = os.path.abspath(os.path.dirname(__file__))
+
+pypngtile_c = "python/pypngtile.c"
+pypngtile_name = "python/pypngtile.pyx"
+
+cmdclass = dict()
+
+try :
+    from Cython.Distutils import build_ext
+    
+    cmdclass['build_ext'] = build_ext
+
+except ImportError :
+    path = os.path.join(build_root, pypngtile_c)
+
+    if os.path.exists(path) :
+        print "Warning: falling back from .pyx -> .c due to missing Cython"
+        # just use the .c
+        pypngtile_name = pypngtile_c
+    
+    else :
+        # fail
+        raise
 
 setup(
     name = 'pngtiles',
-    cmdclass = {'build_ext': build_ext},
+    cmdclass = cmdclass,
     ext_modules = [
-        Extension("pypngtile", ["python/pypngtile.pyx"],
+        Extension("pypngtile", [pypngtile_name],
             include_dirs = ['include'],
             library_dirs = ['lib'],
             libraries = ['pngtile'],
--- a/src/lib/cache.c	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/cache.c	Sun Sep 14 15:24:58 2014 +0300
@@ -42,9 +42,107 @@
     return err;
 }
 
+/**
+ * Force-clean pt_cache, warn on errors
+ */
+static void pt_cache_abort (struct pt_cache *cache)
+{
+    if (cache->file != NULL) {
+        if (munmap(cache->file, sizeof(struct pt_cache_file) + cache->file->header.data_size))
+            log_warn_errno("munmap: %p, %zu", cache->file, sizeof(struct pt_cache_file) + cache->file->header.data_size);
+
+        cache->file = NULL;
+    }
+
+    if (cache->fd >= 0) {
+        if (close(cache->fd))
+            log_warn_errno("close: %d", cache->fd);
+
+        cache->fd = -1;
+    }
+}
+
+/**
+ * Open the cache file as an fd for reading
+ */
+static int pt_cache_open_read_fd (struct pt_cache *cache, int *fd_ptr)
+{
+    int fd;
+    
+    // actual open()
+    if ((fd = open(cache->path, O_RDONLY)) < 0)
+        RETURN_ERROR_ERRNO(PT_ERR_OPEN_MODE, EACCES);
+
+    // ok
+    *fd_ptr = fd;
+
+    return 0;
+}
+
+/**
+ * Read in the cache header from the open file
+ */
+static int pt_cache_read_header (int fd, struct pt_cache_header *header)
+{
+    size_t len = sizeof(*header);
+    char *buf = (char *) header;
+
+    // seek to start
+    if (lseek(fd, 0, SEEK_SET) != 0)
+        RETURN_ERROR(PT_ERR_CACHE_SEEK);
+
+    // write out full header
+    while (len) {
+        ssize_t ret;
+        
+        // try and write out the header
+        if ((ret = read(fd, buf, len)) <= 0)
+            RETURN_ERROR(PT_ERR_CACHE_READ);
+
+        // update offset
+        buf += ret;
+        len -= ret;
+    }
+
+    // done
+    return 0;
+}
+
+/**
+ * Read and return the version number from the cache file, temporarily opening it if needed
+ */
+static int pt_cache_version (struct pt_cache *cache)
+{
+    int fd;
+    struct pt_cache_header header;
+    int ret;
+
+    // already open?
+    if (cache->file)
+        return cache->file->header.version;
+
+    // temp. open
+    if ((ret = pt_cache_open_read_fd(cache, &fd)))
+        return ret;
+
+    // read header
+    if ((ret = pt_cache_read_header(fd, &header)))
+        JUMP_ERROR(ret);
+
+    // ok
+    ret = header.version;
+    
+error:
+    // close
+    close(fd);
+    
+    return ret;
+}
+
 int pt_cache_status (struct pt_cache *cache, const char *img_path)
 {
     struct stat st_img, st_cache;
+    int ver;
     
     // test original file
     if (stat(img_path, &st_img) < 0)
@@ -62,61 +160,61 @@
     // compare mtime
     if (st_img.st_mtime > st_cache.st_mtime)
         return PT_CACHE_STALE;
+    
+    // read version
+    if ((ver = pt_cache_version(cache)) < 0)
+        // fail
+        return ver;
 
-    else
-        return PT_CACHE_FRESH;
+    // compare version
+    if (ver != PT_CACHE_VERSION)
+        return PT_CACHE_INCOMPAT;
+    
+    // ok, should be in order
+    return PT_CACHE_FRESH;
 }
 
-int pt_cache_info (struct pt_cache *cache, struct pt_image_info *info)
+void pt_cache_info (struct pt_cache *cache, struct pt_image_info *info)
 {
-    int err;
+    struct stat st;
 
-    // ensure open
-    if ((err = pt_cache_open(cache)))
-        return err;
+    if (cache->file)
+        // img info
+        pt_png_info(&cache->file->header.png, info);
 
-    info->width = cache->header->width;
-    info->height = cache->header->height;
+    // stat
+    if (stat(cache->path, &st) < 0) {
+        // unknown
+        info->cache_mtime = 0;
+        info->cache_bytes = 0;
+        info->cache_blocks = 0;
+
+    } else {
+        // store
+        info->cache_version = pt_cache_version(cache);
+        info->cache_mtime = st.st_mtime;
+        info->cache_bytes = st.st_size;
+        info->cache_blocks = st.st_blocks;
+    }
+}
+
+static int pt_cache_tmp_name (struct pt_cache *cache, char tmp_path[], size_t tmp_len)
+{
+    // get .tmp path
+    if (path_with_fext(cache->path, tmp_path, tmp_len, ".tmp"))
+        RETURN_ERROR(PT_ERR_PATH);
 
     return 0;
 }
 
 /**
- * Abort any incomplete open operation, cleaning up
+ * Compute and return the full size of the .cache file
  */
-static void pt_cache_abort (struct pt_cache *cache)
+static size_t pt_cache_size (size_t data_size)
 {
-    if (cache->header != NULL) {
-        munmap(cache->header, PT_CACHE_HEADER_SIZE + cache->size);
-
-        cache->header = NULL;
-        cache->data = NULL;
-    }
-
-    if (cache->fd >= 0) {
-        close(cache->fd);
+    assert(sizeof(struct pt_cache_file) == PT_CACHE_HEADER_SIZE);
 
-        cache->fd = -1;
-    }
-}
-
-/**
- * Open the cache file as an fd for reading
- *
- * XXX: needs locking
- */
-static int pt_cache_open_read_fd (struct pt_cache *cache, int *fd_ptr)
-{
-    int fd;
-    
-    // actual open()
-    if ((fd = open(cache->path, O_RDONLY)) < 0)
-        RETURN_ERROR_ERRNO(PT_ERR_OPEN_MODE, EACCES);
-
-    // ok
-    *fd_ptr = fd;
-
-    return 0;
+    return sizeof(struct pt_cache_file) + data_size;
 }
 
 /**
@@ -126,14 +224,14 @@
 {
     int fd;
     char tmp_path[1024];
-    
+    int err;
+
     // get .tmp path
-    if (path_with_fext(cache->path, tmp_path, sizeof(tmp_path), ".tmp"))
-        RETURN_ERROR(PT_ERR_PATH);
+    if ((err = pt_cache_tmp_name(cache, tmp_path, sizeof(tmp_path))))
+        return err;
 
-    // open for write, create
-    // XXX: locking?
-    if ((fd = open(tmp_path, O_RDWR | O_CREAT, 0644)) < 0)
+    // open for write, create, fail if someone else already opened it for update
+    if ((fd = open(tmp_path, O_RDWR | O_CREAT | O_EXCL, 0644)) < 0)
         RETURN_ERROR(PT_ERR_CACHE_OPEN_TMP);
 
     // ok
@@ -144,9 +242,9 @@
 
 
 /**
- * Mmap the opened cache file using PT_CACHE_HEADER_SIZE plus the calculated size stored in cache->size
+ * Mmap the pt_cache_file using pt_cache_size(data_size)
  */
-static int pt_cache_open_mmap (struct pt_cache *cache, void **addr_ptr, bool readonly)
+static int pt_cache_open_mmap (struct pt_cache *cache, struct pt_cache_file **file_ptr, size_t data_size, bool readonly)
 {
     int prot = 0;
     void *addr;
@@ -160,43 +258,49 @@
         prot |= PROT_WRITE;
     }
 
-    // perform mmap() from second page on
-    if ((addr = mmap(NULL, PT_CACHE_HEADER_SIZE + cache->size, prot, MAP_SHARED, cache->fd, 0)) == MAP_FAILED)
+    // mmap() the full file including header
+    if ((addr = mmap(NULL, pt_cache_size(data_size), prot, MAP_SHARED, cache->fd, 0)) == MAP_FAILED)
         RETURN_ERROR(PT_ERR_CACHE_MMAP);
 
     // ok
-    *addr_ptr = addr;
+    *file_ptr = addr;
 
     return 0;
 }
 
-/**
- * Read in the cache header from the open file
- */
-static int pt_cache_read_header (struct pt_cache *cache, struct pt_cache_header *header)
+int pt_cache_open (struct pt_cache *cache)
 {
-    size_t len = sizeof(*header);
-    char *buf = (char *) header;
-
-    // seek to start
-    if (lseek(cache->fd, 0, SEEK_SET) != 0)
-        RETURN_ERROR(PT_ERR_CACHE_SEEK);
+    struct pt_cache_header header;
+    int err;
 
-    // write out full header
-    while (len) {
-        ssize_t ret;
-        
-        // try and write out the header
-        if ((ret = read(cache->fd, buf, len)) <= 0)
-            RETURN_ERROR(PT_ERR_CACHE_READ);
+    // ignore if already open
+    if (cache->file)
+        return 0;
 
-        // update offset
-        buf += ret;
-        len -= ret;
-    }
+    // open the .cache in readonly mode
+    if ((err = pt_cache_open_read_fd(cache, &cache->fd)))
+        return err;
+
+    // read in header
+    if ((err = pt_cache_read_header(cache->fd, &header)))
+        JUMP_ERROR(err);
+
+    // check version
+    if (header.version != PT_CACHE_VERSION)
+        JUMP_SET_ERROR(err, PT_ERR_CACHE_VERSION);
+
+    // mmap the header + data
+    if ((err = pt_cache_open_mmap(cache, &cache->file, header.data_size, true)))
+        JUMP_ERROR(err);
 
     // done
     return 0;
+
+error:
+    // cleanup
+    pt_cache_abort(cache);
+
+    return err;
 }
 
 /**
@@ -233,34 +337,24 @@
  */
 static int pt_cache_create (struct pt_cache *cache, struct pt_cache_header *header)
 {
-    void *base;
     int err;
 
-    // no access
-    if (!(cache->mode & PT_OPEN_UPDATE))
-        RETURN_ERROR(PT_ERR_OPEN_MODE);
+    assert(cache->mode & PT_OPEN_UPDATE);
 
     // open as .tmp
     if ((err = pt_cache_open_tmp_fd(cache, &cache->fd)))
         return err;
 
-    // calculate data size
-    cache->size = sizeof(*header) + header->height * header->row_bytes;
+    // write header
+    if ((err = pt_cache_write_header(cache, header)))
+        JUMP_ERROR(err);
 
     // grow file
-    if (ftruncate(cache->fd, PT_CACHE_HEADER_SIZE + cache->size) < 0)
+    if (ftruncate(cache->fd, pt_cache_size(header->data_size)) < 0)
         JUMP_SET_ERROR(err, PT_ERR_CACHE_TRUNC);
 
     // mmap header and data
-    if ((err = pt_cache_open_mmap(cache, &base, false)))
-        JUMP_ERROR(err);
-
-    cache->header = base;
-    cache->data = base + PT_CACHE_HEADER_SIZE;
-
-    // write header
-    // XXX: should just mmap...
-    if ((err = pt_cache_write_header(cache, header)))
+    if ((err = pt_cache_open_mmap(cache, &cache->file, header->data_size, false)))
         JUMP_ERROR(err);
 
     // done
@@ -279,10 +373,11 @@
 static int pt_cache_create_done (struct pt_cache *cache)
 {
     char tmp_path[1024];
+    int err;
     
     // get .tmp path
-    if (path_with_fext(cache->path, tmp_path, sizeof(tmp_path), ".tmp"))
-        RETURN_ERROR(PT_ERR_PATH);
+    if ((err = pt_cache_tmp_name(cache, tmp_path, sizeof(tmp_path))))
+        return err;
 
     // rename
     if (rename(tmp_path, cache->path) < 0)
@@ -292,355 +387,76 @@
     return 0;
 }
 
-int pt_cache_open (struct pt_cache *cache)
+/**
+ * Abort a failed cache update after cache_create
+ */
+static void pt_cache_create_abort (struct pt_cache *cache)
+{
+    char tmp_path[1024];
+    int err;
+    
+    // close open stuff
+    pt_cache_abort(cache);
+
+    // get .tmp path
+    if ((err = pt_cache_tmp_name(cache, tmp_path, sizeof(tmp_path)))) {
+        log_warn_errno("pt_cache_tmp_name: %s: %s", cache->path, pt_strerror(err));
+        
+        return;
+    }
+
+    // remove .tmp
+    if (unlink(tmp_path))
+        log_warn_errno("unlink: %s", tmp_path);
+}
+
+int pt_cache_update (struct pt_cache *cache, struct pt_png_img *img, const struct pt_image_params *params)
 {
     struct pt_cache_header header;
-    void *base;
     int err;
 
-    // ignore if already open
-    if (cache->header && cache->data)
-        return 0;
+    // check mode
+    if (!(cache->mode & PT_OPEN_UPDATE))
+        RETURN_ERROR(PT_ERR_OPEN_MODE);
 
-    // open the .cache
-    if ((err = pt_cache_open_read_fd(cache, &cache->fd)))
+    // close if open
+    if ((err = pt_cache_close(cache)))
+        return err;
+    
+    // prep header
+    header.version = PT_CACHE_VERSION;
+    header.format = PT_IMG_PNG;
+
+    // read img header
+    if ((err = pt_png_read_header(img, &header.png, &header.data_size)))
         return err;
 
-    // read in header
-    if ((err = pt_cache_read_header(cache, &header)))
-        JUMP_ERROR(err);
-
-    // calculate data size
-    cache->size = sizeof(header) + header.height * header.row_bytes;
+    // save any params
+    if (params)
+        header.params = *params;
 
-    // mmap header and data
-    if ((err = pt_cache_open_mmap(cache, &base, true)))
-        JUMP_ERROR(err);
+    // create/open .tmp and write out header
+    if ((err = pt_cache_create(cache, &header)))
+        return err;
 
-    cache->header = base;
-    cache->data = base + PT_CACHE_HEADER_SIZE;
+    // decode to disk
+    if ((err = pt_png_decode(img, &cache->file->header.png, &cache->file->header.params, cache->file->data)))
+        goto error;
 
-    // done
+    // done, commit .tmp
+    if ((err = pt_cache_create_done(cache)))
+        goto error;
+    
     return 0;
 
 error:
-    // cleanup
-    pt_cache_abort(cache);
+    // cleanup .tmp
+    pt_cache_create_abort(cache);
 
     return err;
 }
 
-int pt_cache_update_png (struct pt_cache *cache, png_structp png, png_infop info)
-{
-    struct pt_cache_header header;
-    int err;
-    
-    // XXX: check cache_mode
-    // XXX: check image doesn't use any options we don't handle
-    // XXX: close any already-opened cache file
-
-    memset(&header, 0, sizeof(header));
-
-    // fill in basic info
-    header.width = png_get_image_width(png, info);
-    header.height = png_get_image_height(png, info);
-    header.bit_depth = png_get_bit_depth(png, info);
-    header.color_type = png_get_color_type(png, info);
-
-    log_debug("width=%u, height=%u, bit_depth=%u, color_type=%u", 
-            header.width, header.height, header.bit_depth, header.color_type
-    );
-
-    // only pack 1 pixel per byte, changes rowbytes
-    if (header.bit_depth < 8)
-        png_set_packing(png);
-
-    // fill in other info
-    header.row_bytes = png_get_rowbytes(png, info);
-
-    // calculate bpp as num_channels * bpc
-    // XXX: this assumes the packed bit depth will be either 8 or 16
-    header.col_bytes = png_get_channels(png, info) * (header.bit_depth == 16 ? 2 : 1);
-
-    log_debug("row_bytes=%u, col_bytes=%u", header.row_bytes, header.col_bytes);
-    
-    // palette etc.
-    if (header.color_type == PNG_COLOR_TYPE_PALETTE) {
-        int num_palette;
-        png_colorp palette;
-
-        if (png_get_PLTE(png, info, &palette, &num_palette) == 0)
-            // XXX: PLTE chunk not read?
-            RETURN_ERROR(PT_ERR_PNG);
-        
-        // should only be 256 of them at most
-        assert(num_palette <= PNG_MAX_PALETTE_LENGTH);
-    
-        // copy
-        header.num_palette = num_palette;
-        memcpy(&header.palette, palette, num_palette * sizeof(*palette));
-        
-        log_debug("num_palette=%u", num_palette);
-    }
-
-    // create .tmp and write out header
-    if ((err = pt_cache_create(cache, &header)))
-        return err;
-
-
-    // write out raw image data a row at a time
-    for (size_t row = 0; row < header.height; row++) {
-        // read row data, non-interlaced
-        png_read_row(png, cache->data + row * header.row_bytes, NULL);
-    }
-
-
-    // move from .tmp to .cache
-    if ((err = pt_cache_create_done(cache)))
-        // XXX: pt_cache_abort?
-        return err;
-
-    // done!
-    return 0;
-}
-
-/**
- * Return a pointer to the pixel data on \a row, starting at \a col.
- */
-static inline void* tile_row_col (struct pt_cache *cache, size_t row, size_t col)
-{
-    return cache->data + (row * cache->header->row_bytes) + (col * cache->header->col_bytes);
-}
-
-/**
- * Fill in a clipped region of \a width_px pixels at the given row segment
- */
-static inline void tile_row_fill_clip (struct pt_cache *cache, png_byte *row, size_t width_px)
-{
-    // XXX: use a configureable background color, or full transparency?
-    memset(row, /* 0xd7 */ 0x00, width_px * cache->header->col_bytes);
-}
-
-/**
- * Write raw tile image data, directly from the cache
- */
-static int write_png_data_direct (struct pt_cache *cache, png_structp png, png_infop info, const struct pt_tile_info *ti)
-{
-    for (size_t row = ti->y; row < ti->y + ti->height; row++)
-        // write data directly
-        png_write_row(png, tile_row_col(cache, row, ti->x));
-
-    return 0;
-}
-
-/**
- * Write clipped tile image data (a tile that goes over the edge of the actual image) by aligning the data from the cache as needed
- */
-static int write_png_data_clipped (struct pt_cache *cache, png_structp png, png_infop info, const struct pt_tile_info *ti)
-{
-    png_byte *rowbuf;
-    size_t row;
-    
-    // image data goes from (ti->x ... clip_x, ti->y ... clip_y), remaining region is filled
-    size_t clip_x, clip_y;
-
-
-    // figure out if the tile clips over the right edge
-    // XXX: use min()
-    if (ti->x + ti->width > cache->header->width)
-        clip_x = cache->header->width;
-    else
-        clip_x = ti->x + ti->width;
-    
-    // figure out if the tile clips over the bottom edge
-    // XXX: use min()
-    if (ti->y + ti->height > cache->header->height)
-        clip_y = cache->header->height;
-    else
-        clip_y = ti->y + ti->height;
-
-
-    // allocate buffer for a single row of image data
-    if ((rowbuf = malloc(ti->width * cache->header->col_bytes)) == NULL)
-        RETURN_ERROR(PT_ERR_MEM);
-
-    // how much data we actually have for each row, in px and bytes
-    // from [(tile x)---](clip x)
-    size_t row_px = clip_x - ti->x;
-    size_t row_bytes = row_px * cache->header->col_bytes;
-
-    // write the rows that we have
-    // from [(tile y]---](clip y)
-    for (row = ti->y; row < clip_y; row++) {
-        // copy in the actual tile data...
-        memcpy(rowbuf, tile_row_col(cache, row, ti->x), row_bytes);
-
-        // generate the data for the remaining, clipped, columns
-        tile_row_fill_clip(cache, rowbuf + row_bytes, (ti->width - row_px));
-
-        // write
-        png_write_row(png, rowbuf);
-    }
-
-    // generate the data for the remaining, clipped, rows
-    tile_row_fill_clip(cache, rowbuf, ti->width);
-    
-    // write out the remaining rows as clipped data
-    for (; row < ti->y + ti->height; row++)
-        png_write_row(png, rowbuf);
-
-    // ok
-    return 0;
-}
-
-static size_t scale_by_zoom_factor (size_t value, int z)
-{
-    if (z > 0)
-        return value << z;
-
-    else if (z < 0)
-        return value >> -z;
-
-    else
-        return value;
-}
-
-#define ADD_AVG(l, r) (l) = ((l) + (r)) / 2
-
-static int png_pixel_data (png_color *out, struct pt_cache *cache, size_t row, size_t col)
-{
-    if (cache->header->color_type == PNG_COLOR_TYPE_PALETTE) {
-        // palette entry number
-        int p;
-
-        if (cache->header->bit_depth == 8)
-            p = *((uint8_t *) tile_row_col(cache, row, col));
-        else
-            return -1;
-
-        if (p >= cache->header->num_palette)
-            return -1;
-        
-        // reference data from palette
-        *out = cache->header->palette[p];
-
-        return 0;
-
-    } else {
-        return -1;
-    }
-}
-
-/**
- * Write unscaled tile data
- */
-static int write_png_data_unzoomed (struct pt_cache *cache, png_structp png, png_infop info, const struct pt_tile_info *ti)
-{
-    int err;
-
-    // set basic info
-    png_set_IHDR(png, info, ti->width, ti->height, cache->header->bit_depth, cache->header->color_type,
-            PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT
-    );
-
-    // set palette?
-    if (cache->header->color_type == PNG_COLOR_TYPE_PALETTE)
-        png_set_PLTE(png, info, cache->header->palette, cache->header->num_palette);
-
-    // write meta-info
-    png_write_info(png, info);
-
-    // our pixel data is packed into 1 pixel per byte (8bpp or 16bpp)
-    png_set_packing(png);
-    
-    // figure out if the tile clips
-    if (ti->x + ti->width <= cache->header->width && ti->y + ti->height <= cache->header->height)
-        // doesn't clip, just use the raw data
-        err = write_png_data_direct(cache, png, info, ti);
-
-    else
-        // fill in clipped regions
-        err = write_png_data_clipped(cache, png, info, ti);
-    
-    return err;
-}
-
-/**
- * Write scaled tile data
- */
-static int write_png_data_zoomed (struct pt_cache *cache, png_structp png, png_infop info, const struct pt_tile_info *ti)
-{
-    // size of the image data in px
-    size_t data_width = scale_by_zoom_factor(ti->width, -ti->zoom);
-    size_t data_height = scale_by_zoom_factor(ti->height, -ti->zoom);
-
-    // input pixels per output pixel
-    size_t pixel_size = scale_by_zoom_factor(1, -ti->zoom);
-
-    // bytes per output pixel
-    size_t pixel_bytes = 3;
-
-    // size of the output tile in px
-    size_t row_width = ti->width;
-
-    // size of an output row in bytes (RGB)
-    size_t row_bytes = row_width * 3;
-
-    // buffer to hold output rows
-    uint8_t *row_buf;
-    
-    // XXX: only supports zooming out...
-    if (ti->zoom >= 0)
-        RETURN_ERROR(PT_ERR_ZOOM);
-
-    if ((row_buf = malloc(row_bytes)) == NULL)
-        RETURN_ERROR(PT_ERR_MEM);
-
-
-    // define pixel format: 8bpp RGB
-    png_set_IHDR(png, info, ti->width, ti->height, 8, PNG_COLOR_TYPE_RGB,
-            PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT
-    );
-    
-    // write meta-info
-    png_write_info(png, info);
-
-    // ...each output row
-    for (size_t out_row = 0; out_row < ti->height; out_row++) {
-        memset(row_buf, 0, row_bytes);
-
-        // ...includes pixels starting from this row.
-        size_t in_row_offset = ti->y + scale_by_zoom_factor(out_row, -ti->zoom);
-        
-        // ...each out row includes pixel_size in rows
-        for (size_t in_row = in_row_offset; in_row < in_row_offset + pixel_size && in_row < cache->header->height; in_row++) {
-            // and includes each input pixel
-            for (size_t in_col = ti->x; in_col < ti->x + data_width && in_col < cache->header->width; in_col++) {
-                png_color c;
-
-                // ...for this output pixel
-                size_t out_col = scale_by_zoom_factor(in_col - ti->x, ti->zoom);
-                
-                // get pixel RGB data
-                if (png_pixel_data(&c, cache, in_row, in_col))
-                    return -1;
-                
-                // average the RGB data        
-                ADD_AVG(row_buf[out_col * pixel_bytes + 0], c.red);
-                ADD_AVG(row_buf[out_col * pixel_bytes + 1], c.green);
-                ADD_AVG(row_buf[out_col * pixel_bytes + 2], c.blue);
-            }
-        }
-
-        // output
-        png_write_row(png, row_buf);
-    }
-
-    // done
-    return 0;
-}
-
-int pt_cache_tile_png (struct pt_cache *cache, png_structp png, png_infop info, const struct pt_tile_info *ti)
+int pt_cache_tile (struct pt_cache *cache, struct pt_tile *tile)
 {
     int err;
 
@@ -648,34 +464,38 @@
     if ((err = pt_cache_open(cache)))
         return err;
 
-    // check within bounds
-    if (ti->x >= cache->header->width || ti->y >= cache->header->height)
-        // completely outside
-        RETURN_ERROR(PT_ERR_TILE_CLIP);
-   
-    // unscaled or scaled?
-    if (ti->zoom)
-        err = write_png_data_zoomed(cache, png, info, ti);
+    // render
+    if ((err = pt_png_tile(&cache->file->header.png, cache->file->data, tile)))
+        return err;
 
-    else
-        err = write_png_data_unzoomed(cache, png, info, ti);
+    return 0;
+}
 
-    if (err)
-        return err;
-    
-    // done, flush remaining output
-    png_write_flush(png);
+int pt_cache_close (struct pt_cache *cache)
+{
+    if (cache->file != NULL) {
+        if (munmap(cache->file, sizeof(struct pt_cache_file) + cache->file->header.data_size))
+            RETURN_ERROR(PT_ERR_CACHE_MUNMAP);
 
-    // ok
+        cache->file = NULL;
+    }
+
+    if (cache->fd >= 0) {
+        if (close(cache->fd))
+            RETURN_ERROR(PT_ERR_CACHE_CLOSE);
+
+        cache->fd = -1;
+    }
+
     return 0;
 }
 
 void pt_cache_destroy (struct pt_cache *cache)
 {
-    free(cache->path);
-
+    // cleanup
     pt_cache_abort(cache);
 
+    free(cache->path);
     free(cache);
 }
 
--- a/src/lib/cache.h	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/cache.h	Sun Sep 14 15:24:58 2014 +0300
@@ -7,14 +7,61 @@
  * Internal image cache implementation
  */
 #include "image.h"
+#include "png.h"
 
 #include <stdint.h>
 #include <stdbool.h>
 
-#include <png.h>
+/**
+ * Cache format version
+ */
+#define PT_CACHE_VERSION 2
 
 /**
- * State for cache access
+ * Size used to store the cache header
+ */
+#define PT_CACHE_HEADER_SIZE 4096
+
+/**
+ * On-disk header
+ */
+struct pt_cache_header {
+    /** Set to PT_CACHE_VERSION */
+    uint16_t version;
+
+    /** Image format */
+    enum pt_img_format {
+        PT_IMG_PNG,     ///< @see pt_png
+    } format;
+    
+    /** Data header by format  */
+    union {
+        struct pt_png_header png;
+    };
+    
+    /** Parameters used */
+    struct pt_image_params params;
+    
+    /** Size of the data segment */
+    size_t data_size;
+};
+
+/**
+ * On-disk data format. This struct is always exactly PT_CACHE_HEADER_SIZE long
+ */
+struct pt_cache_file {
+    /** Header */
+    struct pt_cache_header header;
+
+    /** Padding for data */
+    uint8_t padding[PT_CACHE_HEADER_SIZE - sizeof(struct pt_cache_header)];
+
+    /** Data follows, header.data_size bytes */
+    uint8_t data[];
+};
+
+/**
+ * Cache state
  */
 struct pt_cache {
     /** Filesystem path to cache file */
@@ -26,42 +73,11 @@
     /** Opened file */
     int fd;
 
-    /** The mmap'd header */
-    struct pt_cache_header *header;
-
-    /** Memory-mapped file data, starting at PT_CACHE_HEADER_SIZE */
-    uint8_t *data;
-
     /** Size of the data segment in bytes, starting at PT_CACHE_HEADER_SIZE */
-    size_t size;
-};
-
-/**
- * Size used to store the cache header
- */
-#define PT_CACHE_HEADER_SIZE 4096
+    size_t data_size;
 
-/**
- * On-disk header
- */
-struct pt_cache_header {
-    /** Pixel dimensions of image */
-    uint32_t width, height;
-    
-    /** Pixel format */
-    uint8_t bit_depth, color_type;
-
-    /** Number of png_color entries that follow */
-    uint16_t num_palette;
-
-    /** Number of bytes per row */
-    uint32_t row_bytes;
-    
-    /** Number of bytes per pixel */
-    uint8_t col_bytes;
-
-    /** Palette entries, up to 256 entries used */
-    png_color palette[PNG_MAX_PALETTE_LENGTH];
+    /** The mmap'd file */
+    struct pt_cache_file *file;
 };
 
 /**
@@ -77,14 +93,16 @@
 int pt_cache_status (struct pt_cache *cache, const char *img_path);
 
 /**
- * Get info for the cached image, open it if not already open.
+ * Get info for the cached image.
+ *
+ * Does not open it if not yet opened.
  */
-int pt_cache_info (struct pt_cache *cache, struct pt_image_info *info);
+void pt_cache_info (struct pt_cache *cache, struct pt_image_info *info);
 
 /**
- * Update the cache data from the given PNG image.
+ * Update the cache data from the given image data
  */
-int pt_cache_update_png (struct pt_cache *cache, png_structp png, png_infop info);
+int pt_cache_update (struct pt_cache *cache, struct pt_png_img *img, const struct pt_image_params *params);
 
 /**
  * Open the existing .cache for use. If already opened, does nothing.
@@ -92,11 +110,16 @@
 int pt_cache_open (struct pt_cache *cache);
 
 /**
- * Render out a PNG tile as given, into the established png object, up to (but not including) the png_write_end.
+ * Render out the given tile
  *
  * If the cache is not yet open, this will open it
  */
-int pt_cache_tile_png (struct pt_cache *cache, png_structp png, png_infop info, const struct pt_tile_info *ti);
+int pt_cache_tile (struct pt_cache *cache, struct pt_tile *tile);
+
+/**
+ * Close the cache, if opened
+ */
+int pt_cache_close (struct pt_cache *cache);
 
 /**
  * Release all resources associated with the given cache object without any cleanup.
--- a/src/lib/error.c	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/error.c	Sun Sep 14 15:24:58 2014 +0300
@@ -5,13 +5,15 @@
  */
 const char *error_names[PT_ERR_MAX] = {
     [PT_SUCCESS]                = "Success",
+    [PT_ERR]                    = "Unspecified error",
     [PT_ERR_MEM]                = "malloc()",
 
     [PT_ERR_PATH]               = "path",
     [PT_ERR_OPEN_MODE]          = "open_mode",
     
     [PT_ERR_IMG_STAT]           = "stat(.png)",
-    [PT_ERR_IMG_FOPEN]          = "fopen(.png)",
+    [PT_ERR_IMG_OPEN]           = "open(.png)",
+    [PT_ERR_IMG_FORMAT]         = "Unknown image format",
     
     [PT_ERR_PNG_CREATE]         = "png_create()",
     [PT_ERR_PNG]                = "png_*()",
@@ -25,12 +27,16 @@
     [PT_ERR_CACHE_TRUNC]        = "truncate(.cache)",
     [PT_ERR_CACHE_MMAP]         = "mmap(.cache)",
     [PT_ERR_CACHE_RENAME_TMP]   = "rename(.tmp, .cache)",
+    [PT_ERR_CACHE_VERSION]      = "Incompatible .cache version",
+    [PT_ERR_CACHE_MUNMAP]       = "munmap(cache->file)",
+    [PT_ERR_CACHE_CLOSE]        = "close(cache->fd)",
 
     [PT_ERR_PTHREAD_CREATE]     = "pthread_create",
     [PT_ERR_CTX_SHUTDOWN]       = "pt_ctx is shutting down",
 
+    [PT_ERR_TILE_DIM]           = "Invalid tile dimensions",
     [PT_ERR_TILE_CLIP]          = "Tile outside of image",
-    [PT_ERR_ZOOM]               = "Invalid zoom level",
+    [PT_ERR_TILE_ZOOM]          = "Invalid zoom level",
 };
 
 const char *pt_strerror (int err)
@@ -39,7 +45,11 @@
         err = -err;
 
     if (err < PT_SUCCESS || err >= PT_ERR_MAX)
-        return "Unknown error";
+        return "Invalid error code";
+    
+    else if (!error_names[err])
+        return "Missing string for error code";
+
     else
         return error_names[err];
 }
--- a/src/lib/image.c	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/image.c	Sun Sep 14 15:24:58 2014 +0300
@@ -1,5 +1,6 @@
 #include "image.h"
 #include "ctx.h"
+#include "png.h"
 #include "cache.h"
 #include "tile.h"
 #include "error.h"
@@ -7,10 +8,10 @@
 #include "shared/log.h"
 
 #include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
 #include <errno.h>
 
-#include <png.h>
-
 static int pt_image_new (struct pt_image **image_ptr, struct pt_ctx *ctx, const char *path)
 {
     struct pt_image *image;
@@ -38,128 +39,6 @@
 }
 
 /**
- * Open the image's FILE
- */
-static int pt_image_open_file (struct pt_image *image, FILE **file_ptr)
-{
-    FILE *fp;
-    
-    // open
-    if ((fp = fopen(image->path, "rb")) == NULL)
-        RETURN_ERROR(PT_ERR_IMG_FOPEN);
-
-    // ok
-    *file_ptr = fp;
-
-    return 0;
-}
-
-/**
- * Open the PNG image, setting up the I/O and returning the png_structp and png_infop
- */
-static int pt_image_open_png (struct pt_image *image, png_structp *png_ptr, png_infop *info_ptr)
-{
-    FILE *fp = NULL;
-    png_structp png = NULL;
-    png_infop info = NULL;
-    int err;
-    
-    // open I/O
-    if ((err = pt_image_open_file(image, &fp)))
-        JUMP_ERROR(err);
-
-    // create the struct
-    if ((png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL)
-        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
-
-    // create the info
-    if ((info = png_create_info_struct(png)) == NULL)
-        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
-
-    // setup error trap for the I/O
-    if (setjmp(png_jmpbuf(png)))
-        JUMP_SET_ERROR(err, PT_ERR_PNG);
-    
-    // setup I/O to FILE
-    png_init_io(png, fp);
-
-    // ok
-    // XXX: what to do with fp? Should fclose() when done?
-    *png_ptr = png;
-    *info_ptr = info;
-
-    return 0;
-
-error:
-    // cleanup file
-    if (fp) fclose(fp);
-
-    // cleanup PNG state
-    png_destroy_read_struct(&png, &info, NULL);
-    
-    return err;
-}
-
-/**
- * Update the image_info field from the given png object.
- *
- * Must be called under libpng-error-trap!
- *
- * XXX: currently this info is not used, pulled from the cache instead
- */
-static int pt_image_update_info (struct pt_image *image, png_structp png, png_infop info)
-{
-    // query png_get_*
-    image->info.width = png_get_image_width(png, info); 
-    image->info.height = png_get_image_height(png, info); 
-
-    return 0;
-}
-
-/**
- * Open the PNG image, and write out to the cache
- */
-static int pt_image_update_cache (struct pt_image *image)
-{
-    png_structp png;
-    png_infop info;
-    int err = 0;
-
-    // pre-check enabled
-    if (!(image->cache->mode & PT_OPEN_UPDATE))
-        RETURN_ERROR_ERRNO(PT_ERR_OPEN_MODE, EACCES);
-
-    // open .png
-    if ((err = pt_image_open_png(image, &png, &info)))
-        return err;
-    
-    // setup error trap
-    if (setjmp(png_jmpbuf(png)))
-        JUMP_SET_ERROR(err, PT_ERR_PNG);
-
-    // read meta-info
-    png_read_info(png, info);
-
-    // update our meta-info
-    if ((err = pt_image_update_info(image, png, info)))
-        JUMP_ERROR(err);
-
-    // pass to cache object
-    if ((err = pt_cache_update_png(image->cache, png, info)))
-        JUMP_ERROR(err);
-
-    // finish off, ignore trailing data
-    png_read_end(png, NULL);
-
-error:
-    // clean up
-    // XXX: we need to close the fopen'd .png
-    png_destroy_read_struct(&png, &info, NULL);
-
-    return err;
-}
-
-/**
  * Build a filesystem path representing the appropriate path for this image's cache entry, and store it in the given
  * buffer.
  */
@@ -177,7 +56,13 @@
     char cache_path[1024];
     int err;
 
-    // XXX: verify that the path exists and looks like a PNG file
+    // verify that the path exists and looks like a PNG file
+    if ((err = pt_png_check(path)) < 0)
+        return err;
+    
+    if (err)
+        // fail, not a PNG
+        RETURN_ERROR(PT_ERR_IMG_FORMAT);
 
     // alloc
     if ((err = pt_image_new(&image, ctx, path)))
@@ -202,13 +87,66 @@
     return err;
 }
 
+int pt_image_open_file (struct pt_image *image, FILE **file_ptr)
+{
+    FILE *fp;
+    
+    // open
+    if ((fp = fopen(image->path, "rb")) == NULL)
+        RETURN_ERROR(PT_ERR_IMG_OPEN);
+
+    // ok
+    *file_ptr = fp;
+
+    return 0;
+}
+
+/**
+ * Open the PNG image, and write out to the cache
+ */
+int pt_image_update (struct pt_image *image, const struct pt_image_params *params)
+{
+    struct pt_png_img img;
+    int err = 0;
+
+    // pre-check enabled
+    if (!(image->cache->mode & PT_OPEN_UPDATE))
+        RETURN_ERROR_ERRNO(PT_ERR_OPEN_MODE, EACCES);
+
+    // open .png
+    if ((err = pt_png_open(image, &img)))
+        return err;
+    
+    // pass to cache object
+    if ((err = pt_cache_update(image->cache, &img, params)))
+        JUMP_ERROR(err);
+
+error:
+    // clean up
+    pt_png_release_read(&img);
+
+    return err;
+}
+
+
 int pt_image_info (struct pt_image *image, const struct pt_image_info **info_ptr)
 {
-    int err;
+    struct stat st;
 
-    // update info
-    if ((err = pt_cache_info(image->cache, &image->info)))
-        return err;
+    // update info from cache
+    pt_cache_info(image->cache, &image->info);
+
+    // stat our info
+    if (stat(image->path, &st) < 0) {
+        // unknown
+        image->info.image_mtime = 0;
+        image->info.image_bytes = 0;
+
+    } else {
+        // store
+        image->info.image_mtime = st.st_mtime;
+        image->info.image_bytes = st.st_size;
+    }
     
     // return pointer
     *info_ptr = &image->info;
@@ -221,10 +159,6 @@
     return pt_cache_status(image->cache, image->path);
 }
 
-int pt_image_update (struct pt_image *image)
-{
-    return pt_image_update_cache(image);
-}
 
 int pt_image_load (struct pt_image *image)
 {
@@ -278,6 +212,9 @@
     return err;
 }
 
+/**
+ * Async work func for pt_image_tile_async
+ */
 static void _pt_image_tile_async (void *arg)
 {
     struct pt_tile *tile = arg;
@@ -300,6 +237,10 @@
     struct pt_tile *tile;
     int err;
 
+    // need a ctx for this
+    if (!image->ctx)
+        return -1;
+
     // alloc
     if ((err = pt_tile_new(&tile)))
         return err;
--- a/src/lib/image.h	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/image.h	Sun Sep 14 15:24:58 2014 +0300
@@ -22,5 +22,9 @@
     struct pt_image_info info;
 };
 
+/**
+ * Open the image's FILE
+ */
+int pt_image_open_file (struct pt_image *image, FILE **file_ptr);
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/png.c	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,610 @@
+#include "png.h" // pt_png header
+#include "error.h"
+#include "shared/log.h" // debug only
+
+#include <png.h> // sysmtem libpng header
+#include <assert.h>
+
+
+#define min(a, b) (((a) < (b)) ? (a) : (b))
+
+int pt_png_check (const char *path)
+{
+    FILE *fp;
+    uint8_t header[8];
+    int ret;
+
+    // fopen
+    if ((fp = fopen(path, "rb")) == NULL)
+        RETURN_ERROR(PT_ERR_IMG_OPEN);
+
+    // read
+    if (fread(header, 1, sizeof(header), fp) != sizeof(header))
+        JUMP_SET_ERROR(ret, PT_ERR_IMG_FORMAT);
+      
+    // compare signature  
+    if (png_sig_cmp(header, 0, sizeof(header)))
+        // not a PNG file
+        ret = 1;
+
+    else
+        // valid PNG file
+        ret = 0;
+
+error:
+    // cleanup
+    fclose(fp);
+
+    return ret;
+}
+
+int pt_png_open (struct pt_image *image, struct pt_png_img *img)
+{
+    int err;
+
+    // init
+    memset(img, 0, sizeof(*img));
+    
+    // open I/O
+    if ((err = pt_image_open_file(image, &img->fh)))
+        JUMP_ERROR(err);
+
+    // create the struct
+    if ((img->png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL)
+        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
+
+    // create the info
+    if ((img->info = png_create_info_struct(img->png)) == NULL)
+        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
+
+    // setup error trap for the I/O
+    if (setjmp(png_jmpbuf(img->png)))
+        JUMP_SET_ERROR(err, PT_ERR_PNG);
+    
+    // setup error trap
+    if (setjmp(png_jmpbuf(img->png)))
+        JUMP_SET_ERROR(err, PT_ERR_PNG);
+    
+
+    // setup I/O to FILE
+    png_init_io(img->png, img->fh);
+
+    // read meta-info
+    png_read_info(img->png, img->info);
+
+
+    // img->fh will be closed by pt_png_release_read
+    return 0;
+
+error:
+    // cleanup
+    pt_png_release_read(img);
+    
+    return err;
+}
+
+int pt_png_read_header (struct pt_png_img *img, struct pt_png_header *header, size_t *data_size)
+{
+    // check image doesn't use any options we don't handle
+    if (png_get_interlace_type(img->png, img->info) != PNG_INTERLACE_NONE) {
+        log_warn("Can't handle interlaced PNG");
+
+        RETURN_ERROR(PT_ERR_IMG_FORMAT);
+    }
+
+
+    // initialize
+    memset(header, 0, sizeof(*header));
+
+    // fill in basic info
+    header->width = png_get_image_width(img->png, img->info);
+    header->height = png_get_image_height(img->png, img->info);
+    header->bit_depth = png_get_bit_depth(img->png, img->info);
+    header->color_type = png_get_color_type(img->png, img->info);
+
+    log_debug("width=%u, height=%u, bit_depth=%u, color_type=%u", 
+            header->width, header->height, header->bit_depth, header->color_type
+    );
+
+    // only pack 1 pixel per byte, changes rowbytes
+    if (header->bit_depth < 8)
+        png_set_packing(img->png);
+
+    // fill in other info
+    header->row_bytes = png_get_rowbytes(img->png, img->info);
+
+    // calculate bpp as num_channels * bpc
+    // this assumes the packed bit depth will be either 8 or 16
+    header->col_bytes = png_get_channels(img->png, img->info) * (header->bit_depth == 16 ? 2 : 1);
+
+    log_debug("row_bytes=%u, col_bytes=%u", header->row_bytes, header->col_bytes);
+    
+    // palette etc.
+    if (header->color_type == PNG_COLOR_TYPE_PALETTE) {
+        int num_palette;
+        png_colorp palette;
+
+        if (png_get_PLTE(img->png, img->info, &palette, &num_palette) == 0)
+            // PLTE chunk not read?
+            RETURN_ERROR(PT_ERR_PNG);
+        
+        // should only be 256 of them at most
+        assert(num_palette <= PNG_MAX_PALETTE_LENGTH);
+    
+        // copy
+        header->num_palette = num_palette;
+        memcpy(&header->palette, palette, num_palette * sizeof(*palette));
+        
+        log_debug("num_palette=%u", num_palette);
+    }
+
+    // calculate data size
+    *data_size = header->height * header->row_bytes;
+    
+    return 0;
+}
+
+/**
+ * Decode the PNG data directly to memory - not good for sparse backgrounds
+ */
+static int pt_png_decode_direct (struct pt_png_img *img, const struct pt_png_header *header, const struct pt_image_params *params, uint8_t *out)
+{
+    // write out raw image data a row at a time
+    for (size_t row = 0; row < header->height; row++) {
+        // read row data, non-interlaced
+        png_read_row(img->png, out + row * header->row_bytes, NULL);
+    }
+
+    return 0;
+}
+
+/**
+ * Decode the PNG data, filtering it for sparse regions
+ */
+static int pt_png_decode_sparse (struct pt_png_img *img, const struct pt_png_header *header, const struct pt_image_params *params, uint8_t *out)
+{
+    // one row of pixel data
+    uint8_t *row_buf;
+
+    // alloc
+    if ((row_buf = malloc(header->row_bytes)) == NULL)
+        RETURN_ERROR(PT_ERR_MEM);
+
+    // decode each row at a time
+    for (size_t row = 0; row < header->height; row++) {
+        // read row data, non-interlaced
+        png_read_row(img->png, row_buf, NULL);
+        
+        // skip background-colored regions to keep the cache file sparse
+        // ...in blocks of PT_CACHE_BLOCK_SIZE bytes
+        for (size_t col_base = 0; col_base < header->width; col_base += PT_IMG_BLOCK_SIZE) {
+            // size of this block in bytes
+            size_t block_size = min(PT_IMG_BLOCK_SIZE * header->col_bytes, header->row_bytes - col_base);
+
+            // ...each pixel
+            for (
+                    size_t col = col_base;
+
+                    // BLOCK_SIZE * col_bytes wide, don't go over the edge
+                    col < col_base + block_size; 
+
+                    col += header->col_bytes
+            ) {
+                // test this pixel
+                if (bcmp(row_buf + col, params->background_color, header->col_bytes)) {
+                    // differs
+                    memcpy(
+                            out + row * header->row_bytes + col_base, 
+                            row_buf + col_base,
+                            block_size
+                    );
+                    
+                    // skip to next block
+                    break;
+                }
+            }
+
+            // skip this block
+            continue;
+        }
+    }
+
+    return 0;
+}
+
+int pt_png_decode (struct pt_png_img *img, const struct pt_png_header *header, const struct pt_image_params *params, uint8_t *out)
+{
+    int err;
+
+    // decode
+    // XXX: it's an array, you silly
+    if (params->background_color)
+        err = pt_png_decode_sparse(img, header, params, out);
+
+    else
+        err = pt_png_decode_direct(img, header, params, out);
+    
+    if (err)
+        return err;
+    
+    // finish off, ignore trailing data
+    png_read_end(img->png, NULL);
+
+    return 0;
+}
+
+int pt_png_info (struct pt_png_header *header, struct pt_image_info *info)
+{
+    // fill in info from header
+    info->img_width = header->width;
+    info->img_height = header->height;
+    info->img_bpp = header->bit_depth;
+
+    return 0;
+}
+
+/** 
+ * libpng I/O callback: write out data
+ */
+static void pt_png_mem_write (png_structp png, png_bytep data, png_size_t length)
+{
+    struct pt_tile_mem *buf = png_get_io_ptr(png);
+    int err;
+    
+    // write to buffer
+    if ((err = pt_tile_mem_write(buf, data, length)))
+        // drop err, because png_error doesn't do formatted output
+        png_error(png, "pt_tile_mem_write: ...");
+}
+
+/** 
+ * libpng I/O callback: flush buffered data
+ */
+static void pt_png_mem_flush (png_structp png_ptr)
+{
+    // no-op
+}
+
+
+/**
+ * Return a pointer to the pixel data on \a row, starting at \a col.
+ */
+static inline const void* tile_row_col (const struct pt_png_header *header, const uint8_t *data, size_t row, size_t col)
+{
+    return data + (row * header->row_bytes) + (col * header->col_bytes);
+}
+
+/**
+ * Write raw tile image data, directly from the cache
+ */
+static int pt_png_encode_direct (struct pt_png_img *img, const struct pt_png_header *header, const uint8_t *data, const struct pt_tile_info *ti)
+{
+    for (size_t row = ti->y; row < ti->y + ti->height; row++)
+        // write data directly
+        // missing const...
+        png_write_row(img->png, (const png_bytep) tile_row_col(header, data, row, ti->x));
+
+    return 0;
+}
+
+/**
+ * Fill in a clipped region of \a width_px pixels at the given row segment
+ */
+static inline void tile_row_fill_clip (const struct pt_png_header *header, png_byte *row, size_t width_px)
+{
+    // XXX: use a configureable background color, or full transparency?
+    memset(row, /* 0xd7 */ 0x00, width_px * header->col_bytes);
+}
+
+/**
+ * Write clipped tile image data (a tile that goes over the edge of the actual image) by aligning the data from the cache as needed
+ */
+static int pt_png_encode_clipped (struct pt_png_img *img, const struct pt_png_header *header, const uint8_t *data, const struct pt_tile_info *ti)
+{
+    png_byte *rowbuf;
+    size_t row;
+    
+    // image data goes from (ti->x ... clip_x, ti->y ... clip_y), remaining region is filled
+    size_t clip_x, clip_y;
+
+
+    // fit the left/bottom edge against the image dimensions
+    clip_x = min(ti->x + ti->width, header->width);
+    clip_y = min(ti->y + ti->height, header->height);
+
+
+    // allocate buffer for a single row of image data
+    if ((rowbuf = malloc(ti->width * header->col_bytes)) == NULL)
+        RETURN_ERROR(PT_ERR_MEM);
+
+    // how much data we actually have for each row, in px and bytes
+    // from [(tile x)---](clip x)
+    size_t row_px = clip_x - ti->x;
+    size_t row_bytes = row_px * header->col_bytes;
+
+    // write the rows that we have
+    // from [(tile y]---](clip y)
+    for (row = ti->y; row < clip_y; row++) {
+        // copy in the actual tile data...
+        memcpy(rowbuf, tile_row_col(header, data, row, ti->x), row_bytes);
+
+        // generate the data for the remaining, clipped, columns
+        tile_row_fill_clip(header, rowbuf + row_bytes, (ti->width - row_px));
+
+        // write
+        png_write_row(img->png, rowbuf);
+    }
+
+    // generate the data for the remaining, clipped, rows
+    tile_row_fill_clip(header, rowbuf, ti->width);
+    
+    // write out the remaining rows as clipped data
+    for (; row < ti->y + ti->height; row++)
+        png_write_row(img->png, rowbuf);
+
+    // ok
+    return 0;
+}
+
+/**
+ * Write unscaled tile data
+ */
+static int pt_png_encode_unzoomed (struct pt_png_img *img, const struct pt_png_header *header, const uint8_t *data, const struct pt_tile_info *ti)
+{
+    int err;
+
+    // set basic info
+    png_set_IHDR(img->png, img->info, ti->width, ti->height, header->bit_depth, header->color_type,
+            PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT
+    );
+
+    // set palette?
+    if (header->color_type == PNG_COLOR_TYPE_PALETTE)
+        // oops... missing const
+        png_set_PLTE(img->png, img->info, (png_colorp) header->palette, header->num_palette);
+
+    // write meta-info
+    png_write_info(img->png, img->info);
+
+    // our pixel data is packed into 1 pixel per byte (8bpp or 16bpp)
+    png_set_packing(img->png);
+    
+    // figure out if the tile clips
+    if (ti->x + ti->width <= header->width && ti->y + ti->height <= header->height)
+        // doesn't clip, just use the raw data
+        err = pt_png_encode_direct(img, header, data, ti);
+
+    else
+        // fill in clipped regions
+        err = pt_png_encode_clipped(img, header, data, ti);
+    
+    return err;
+}
+
+/**
+ * Manipulate powers of two
+ */
+static inline size_t scale_by_zoom_factor (size_t value, int z)
+{
+    if (z > 0)
+        return value << z;
+
+    else if (z < 0)
+        return value >> -z;
+
+    else
+        return value;
+}
+
+#define ADD_AVG(l, r) (l) = ((l) + (r)) / 2
+
+/**
+ * Converts a pixel's data into a png_color
+ */
+static inline void png_pixel_data (const png_color **outp, const struct pt_png_header *header, const uint8_t *data, size_t row, size_t col)
+{
+    // palette entry number
+    int p;
+
+    switch (header->color_type) {
+        case PNG_COLOR_TYPE_PALETTE:
+            switch (header->bit_depth) {
+                case 8:
+                    // 8bpp palette
+                    p = *((uint8_t *) tile_row_col(header, data, row, col));
+
+                    break;
+                
+                default :
+                    // unknown
+                    return;
+            }
+
+            // hrhr - assume our working data is valid (or we have 255 palette entries, so it doesn't matter...)
+            assert(p < header->num_palette);
+            
+            // reference data from palette
+            *outp = &header->palette[p];
+            
+            return;
+
+        default :
+            // unknown pixel format
+            return;
+    }
+}
+
+/**
+ * Write scaled tile data
+ */
+static int pt_png_encode_zoomed (struct pt_png_img *img, const struct pt_png_header *header, const uint8_t *data, const struct pt_tile_info *ti)
+{
+    // size of the image data in px
+    size_t data_width = scale_by_zoom_factor(ti->width, ti->zoom);
+    size_t data_height = scale_by_zoom_factor(ti->height, ti->zoom);
+
+    // input pixels per output pixel
+    size_t pixel_size = scale_by_zoom_factor(1, ti->zoom);
+
+    // bytes per output pixel
+    size_t pixel_bytes = 3;
+
+    // size of the output tile in px
+    size_t row_width = ti->width;
+
+    // size of an output row in bytes (RGB)
+    size_t row_bytes = row_width * 3;
+
+    // buffer to hold output rows
+    uint8_t *row_buf;
+                
+    // color entry for pixel
+    const png_color *c = &header->palette[0];
+    
+    // only supports zooming out...
+    if (ti->zoom < 0)
+        RETURN_ERROR(PT_ERR_TILE_ZOOM);
+
+    if ((row_buf = malloc(row_bytes)) == NULL)
+        RETURN_ERROR(PT_ERR_MEM);
+
+    // suppress warning...
+    (void) data_height;
+
+    // define pixel format: 8bpp RGB
+    png_set_IHDR(img->png, img->info, ti->width, ti->height, 8, PNG_COLOR_TYPE_RGB,
+            PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT
+    );
+    
+    // write meta-info
+    png_write_info(img->png, img->info);
+
+    // ...each output row
+    for (size_t out_row = 0; out_row < ti->height; out_row++) {
+        memset(row_buf, 0, row_bytes);
+
+        // ...includes pixels starting from this row.
+        size_t in_row_offset = ti->y + scale_by_zoom_factor(out_row, ti->zoom);
+        
+        // ...each out row includes pixel_size in rows
+        for (size_t in_row = in_row_offset; in_row < in_row_offset + pixel_size && in_row < header->height; in_row++) {
+            // and includes each input pixel
+            for (size_t in_col = ti->x; in_col < ti->x + data_width && in_col < header->width; in_col++) {
+
+                // ...for this output pixel
+                size_t out_col = scale_by_zoom_factor(in_col - ti->x, -ti->zoom);
+                
+                // get pixel RGB data
+                png_pixel_data(&c, header, data, in_row, in_col);
+                
+                // average the RGB data        
+                ADD_AVG(row_buf[out_col * pixel_bytes + 0], c->red);
+                ADD_AVG(row_buf[out_col * pixel_bytes + 1], c->green);
+                ADD_AVG(row_buf[out_col * pixel_bytes + 2], c->blue);
+            }
+        }
+
+        // output
+        png_write_row(img->png, row_buf);
+    }
+
+    // done
+    return 0;
+}
+
+int pt_png_tile (const struct pt_png_header *header, const uint8_t *data, struct pt_tile *tile)
+{
+    struct pt_png_img _img, *img = &_img;
+    struct pt_tile_info *ti = &tile->info;
+    int err;
+
+    // init img
+    memset(img, 0, sizeof(*img));
+
+    // check within bounds
+    if (ti->x >= header->width || ti->y >= header->height)
+        // completely outside
+        RETURN_ERROR(PT_ERR_TILE_CLIP);
+    
+    // open PNG writer
+    if ((img->png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL)
+        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
+    
+    if ((img->info = png_create_info_struct(img->png)) == NULL)
+        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
+
+    // libpng error trap
+    if (setjmp(png_jmpbuf(img->png)))
+        JUMP_SET_ERROR(err, PT_ERR_PNG);
+ 
+
+
+    // setup output I/O
+    switch (tile->out_type) {
+        case PT_TILE_OUT_FILE:
+            // use default FILE* operation
+            // do NOT store in img->fh
+            png_init_io(img->png, tile->out.file);
+
+            break;
+
+        case PT_TILE_OUT_MEM:
+            // use pt_tile_mem struct via pt_png_mem_* callbacks
+            png_set_write_fn(img->png, &tile->out.mem, pt_png_mem_write, pt_png_mem_flush);
+
+            break;
+
+        default:
+            FATAL("tile->out_type: %d", tile->out_type);
+    }
+
+
+  
+    // unscaled or scaled?
+    if (ti->zoom)
+        err = pt_png_encode_zoomed(img, header, data, ti);
+
+    else
+        err = pt_png_encode_unzoomed(img, header, data, ti);
+
+    if (err)
+        goto error;
+    
+
+    // flush remaining output
+    png_write_flush(img->png);
+
+    // done
+    png_write_end(img->png, img->info);
+
+error:
+    // cleanup
+    pt_png_release_write(img);
+
+    return err;
+}
+
+
+void pt_png_release_read (struct pt_png_img *img)
+{
+    png_destroy_read_struct(&img->png, &img->info, NULL);
+    
+    // close possible filehandle
+    if (img->fh) {
+        if (fclose(img->fh))
+            log_warn_errno("fclose");
+    }
+}
+
+void pt_png_release_write (struct pt_png_img *img)
+{
+    png_destroy_write_struct(&img->png, &img->info);
+
+    // close possible filehandle
+    if (img->fh) {
+        if (fclose(img->fh))
+            log_warn_errno("fclose");
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/png.h	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,97 @@
+#ifndef PNGTILE_PNG_H
+#define PNGTILE_PNG_H
+
+/**
+ * @file
+ * PNG-specific handling
+ */
+#include <png.h>
+#include <stdint.h>
+
+/**
+ * Handle sparse data at this granularity (pixels)
+ */
+#define PT_IMG_BLOCK_SIZE 64
+
+/**
+ * PNG img state
+ */
+struct pt_png_img {
+    /** libpng state */
+    png_struct *png;
+    png_info *info;
+
+    /** Possible opened I/O file */
+    FILE *fh;
+};
+
+/**
+ * Cache header layout for PNG-format images
+ */
+struct pt_png_header {
+    /** Pixel dimensions of image */
+    uint32_t width, height;
+    
+    /** Pixel format */
+    uint8_t bit_depth, color_type;
+
+    /** Number of png_color entries that follow */
+    uint16_t num_palette;
+
+    /** Number of bytes per row */
+    uint32_t row_bytes;
+    
+    /** Number of bytes per pixel */
+    uint8_t col_bytes;
+
+    /** Palette entries, up to 256 entries used */
+    png_color palette[PNG_MAX_PALETTE_LENGTH];
+};
+
+
+#include "image.h"
+#include "tile.h"
+
+/**
+ * Check if the given path looks like a .png file.
+ *
+ * Returns 0 if ok, 1 if non-png file, -1 on error
+ */
+int pt_png_check (const char *path);
+
+/**
+ * Open the given .png image, and read info
+ */
+int pt_png_open (struct pt_image *image, struct pt_png_img *img);
+
+/**
+ * Fill in the PNG header and return the size of the pixel data
+ */
+int pt_png_read_header (struct pt_png_img *img, struct pt_png_header *header, size_t *data_size);
+
+/**
+ * Decode the PNG data into the given data segment, using the header as decoded by pt_png_read_header
+ */
+int pt_png_decode (struct pt_png_img *img, const struct pt_png_header *header, const struct pt_image_params *params, uint8_t *out);
+
+/**
+ * Fill in img_* fields of pt_image_info from header
+ */
+int pt_png_info (struct pt_png_header *header, struct pt_image_info *info);
+
+/**
+ * Render out a tile
+ */
+int pt_png_tile (const struct pt_png_header *header, const uint8_t *data, struct pt_tile *tile);
+
+/**
+ * Release pt_png_ctx resources as allocated by pt_png_open
+ */
+void pt_png_release_read (struct pt_png_img *img);
+
+/**
+ * Release pt_png_ctx resources as allocated by pt_png_...
+ */
+void pt_png_release_write (struct pt_png_img *img);
+
+#endif
--- a/src/lib/tile.c	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/tile.c	Sun Sep 14 15:24:58 2014 +0300
@@ -3,6 +3,34 @@
 #include "shared/log.h" // only FATAL
 
 #include <stdlib.h>
+#include <assert.h>
+
+int pt_tile_mem_write (struct pt_tile_mem *buf, void *data, size_t len)
+{
+    size_t buf_len = buf->len;
+
+    // grow?
+    while (buf->off + len > buf_len)
+        buf_len *= 2;
+
+    if (buf_len != buf->len) {
+        char *tmp;
+
+        if ((tmp = realloc(buf->base, buf_len)) == NULL)
+            RETURN_ERROR(PT_ERR_MEM);
+
+        buf->base = tmp;
+        buf->len = buf_len;
+    }
+
+    // copy
+    memcpy(buf->base + buf->off, data, len);
+
+    buf->off += len;
+    
+    return 0;
+}
+
 
 int pt_tile_new (struct pt_tile **tile_ptr)
 {
@@ -47,84 +75,14 @@
     return 0;
 }
 
-static void pt_tile_mem_write (png_structp png, png_bytep data, png_size_t length)
-{
-    struct pt_tile_mem *buf = png_get_io_ptr(png);
-    size_t buf_len = buf->len;
-
-    // grow?
-    while (buf->off + length > buf_len)
-        buf_len *= 2;
-
-    if (buf_len != buf->len) {
-        char *tmp;
-
-        if ((tmp = realloc(buf->base, buf_len)) == NULL)
-            png_error(png, "pt_tile_buf_write - realloc failed");
-
-        buf->base = tmp;
-        buf->len = buf_len;
-    }
-
-    // copy
-    memcpy(buf->base + buf->off, data, length);
-
-    buf->off += length;
-}
-
-static void pt_tile_mem_flush (png_structp png_ptr)
-{
-    // no-op
-}
-
 
 int pt_tile_render (struct pt_tile *tile)
 {
-    png_structp png = NULL;
-    png_infop info = NULL;
-    int err = 0;
-
-    // open PNG writer
-    if ((png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL)
-        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
-    
-    if ((info = png_create_info_struct(png)) == NULL)
-        JUMP_SET_ERROR(err, PT_ERR_PNG_CREATE);
-
-    // libpng error trap
-    if (setjmp(png_jmpbuf(png)))
-        JUMP_SET_ERROR(err, PT_ERR_PNG);
- 
-    // setup output I/O
-    switch (tile->out_type) {
-        case PT_TILE_OUT_FILE:
-            // use default FILE* operation
-            png_init_io(png, tile->out.file);
+    // validate dimensions
+    if (!tile->info.width || !tile->info.height)
+        RETURN_ERROR(PT_ERR_TILE_DIM);
 
-            break;
-
-        case PT_TILE_OUT_MEM:
-            // use pt_tile_mem struct via pt_tile_mem_* callbacks
-            png_set_write_fn(png, &tile->out.mem, pt_tile_mem_write, pt_tile_mem_flush);
-
-            break;
-
-        default:
-            FATAL("tile->out_type: %d", tile->out_type);
-    }
-
-    // render tile
-    if ((err = pt_cache_tile_png(tile->cache, png, info, &tile->info)))
-        JUMP_ERROR(err);
-
-    // done
-    png_write_end(png, info);
-
-error:
-    // cleanup
-    png_destroy_write_struct(&png, &info);
-
-    return err;
+    return pt_cache_tile(tile->cache, tile);
 }
 
 void pt_tile_abort (struct pt_tile *tile)
--- a/src/lib/tile.h	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/lib/tile.h	Sun Sep 14 15:24:58 2014 +0300
@@ -2,8 +2,12 @@
 #define PNGTILE_TILE_H
 
 /**
+ * @file
  * Generating PNG tiles from a cache
  */
+
+struct pt_tile;
+
 #include "pngtile.h"
 #include "cache.h"
 
@@ -42,6 +46,12 @@
 };
 
 /**
+ * Write to the tile's output buffer
+ */
+int pt_tile_mem_write (struct pt_tile_mem *buf, void *data, size_t len);
+
+
+/**
  * Alloc a new pt_tile, which must be initialized using pt_tile_init_*
  */
 int pt_tile_new (struct pt_tile **tile_ptr);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pngtile/main.c	Sun Sep 14 15:24:58 2014 +0300
@@ -0,0 +1,435 @@
+#include "shared/log.h"
+
+#include "pngtile.h"
+
+#include <getopt.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdbool.h>
+
+enum option_names {
+
+    _OPT_LONGONLY       = 255,
+
+    OPT_BENCHMARK,
+    OPT_RANDOMIZE,
+};
+
+/**
+ * Command-line options
+ */
+static const struct option options[] = {
+    { "help",           false,  NULL,   'h' },
+    { "quiet",          false,  NULL,   'q' },
+    { "verbose",        false,  NULL,   'v' },
+    { "debug",          false,  NULL,   'D' },
+    { "force-update",   false,  NULL,   'U' },
+    { "no-update",      false,  NULL,   'N' },
+    { "background",     true,   NULL,   'B' },
+    { "width",          true,   NULL,   'W' },
+    { "height",         true,   NULL,   'H' },
+    { "x",              true,   NULL,   'x' },
+    { "y",              true,   NULL,   'y' },
+    { "zoom",           true,   NULL,   'z' },
+    { "out",            true,   NULL,   'o' },
+    { "threads",        true,   NULL,   'j' },
+    
+    // --long-only options
+    { "benchmark",      true,   NULL,   OPT_BENCHMARK   },
+    { "randomize",      false,  NULL,   OPT_RANDOMIZE   },
+    { 0,                0,      0,      0               }
+};
+
+/**
+ * Print usage/help info on stderr
+ */
+void help (const char *argv0)
+{
+    fprintf(stderr, "Usage: %s [options] <image> [...]\n", argv0);
+    fprintf(stderr,
+        "Open each of the given image files, check cache status, optionally update their cache, display image info, and\n"
+        "optionally render a tile of each.\n"
+        "\n"
+        "\t-h, --help               show this help and exit\n"
+        "\t-q, --quiet              supress informational output\n"
+        "\t-v, --verbose            display more informational output\n"
+        "\t-D, --debug              equivalent to -v\n"
+        "\t-U, --force-update       unconditionally update image caches\n"
+        "\t-N, --no-update          do not update the image cache\n"
+        "\t-B, --background         set background pattern for sparse cache file: 0xHH..\n"
+        "\t-W, --width      PX      set tile width\n"
+        "\t-H, --height     PX      set tile height\n"
+        "\t-x, --x          PX      set tile x offset\n"
+        "\t-y, --y          PX      set tile y offset\n"
+        "\t-z, --zoom       ZL      set zoom factor (<0)\n"
+        "\t-o, --out        FILE    set tile output file\n"
+        "\t-j, --threads    N       number of threads\n"
+        "\t--benchmark      N       do N tile renders\n"
+        "\t--randomize              randomize tile x/y coords\n"
+    );
+}
+
+unsigned long parse_uint (const char *val, const char *name)
+{
+    char *endptr;
+    long int out;
+
+    // decode
+    out = strtol(val, &endptr, 0);
+
+    // validate
+    if (*endptr || out < 0)
+        EXIT_ERROR(EXIT_FAILURE, "Invalid value for %s: %s", name, val);
+
+    // ok
+    return out;
+}
+
+signed long parse_sint (const char *val, const char *name)
+{
+    char *endptr;
+    long int out;
+
+    // decode
+    out = strtol(val, &endptr, 0);
+
+    // validate
+    if (*endptr)
+        EXIT_ERROR(EXIT_FAILURE, "Invalid value for %s: %s", name, val);
+
+    // ok
+    return out;
+}
+
+long randrange (long start, long end)
+{
+    return start + (rand() * (end - start) / RAND_MAX);
+}
+
+/**
+ * Randomize tile x/y with given image info
+ */
+void randomize_tile (struct pt_tile_info *ti, const struct pt_image_info *info)
+{
+    ti->x = randrange(0, info->img_width - ti->width);
+    ti->y = randrange(0, info->img_height - ti->height);
+}
+
+/**
+ * Render a tile
+ */
+int do_tile (struct pt_ctx *ctx, struct pt_image *image, const struct pt_tile_info *ti, const char *out_path)
+{
+    FILE *out_file = NULL;
+    char tmp_name[] = "pt-tile-XXXXXX";
+    int err = 0;
+    
+    if (!out_path) {
+        int fd;
+        
+        // temporary file for output
+        if ((fd = mkstemp(tmp_name)) < 0) {
+            log_errno("mkstemp");
+            goto error;
+        }
+
+        out_path = tmp_name;
+
+        // open out
+        if ((out_file = fdopen(fd, "wb")) == NULL) {
+            log_errno("fdopen");
+            goto error;
+        }
+
+    } else if (strcmp(out_path, "-") == 0) {
+        // use stdout
+        if ((out_file = fdopen(STDOUT_FILENO, "wb")) == NULL) {
+            log_errno("fdopen: STDOUT_FILENO");
+            goto error;
+        }
+
+    } else {
+        // use file
+        if ((out_file = fopen(out_path, "wb")) == NULL) {
+            log_errno("fopen: %s", out_path);
+            goto error;
+        }
+
+    }
+
+    
+    if (ctx) {
+        // render
+        log_info("\tAsync render tile %zux%zu@(%zu,%zu) -> %s", ti->width, ti->height, ti->x, ti->y, out_path);
+
+        if ((err = pt_image_tile_async(image, ti, out_file))) {
+            log_errno("pt_image_tile_async: %s", pt_strerror(err));
+            goto error;
+        }
+
+        // will close it itself
+        out_file = NULL;
+
+    } else {
+        // render
+        log_info("\tRender tile %zux%zu@(%zu,%zu) -> %s", ti->width, ti->height, ti->x, ti->y, out_path);
+
+        if ((err = pt_image_tile_file(image, ti, out_file))) {
+            log_errno("pt_image_tile_file: %s", pt_strerror(err));
+            goto error;
+        }
+    }
+
+error:
+    // cleanup
+    if (out_file && fclose(out_file))
+        log_warn_errno("fclose: out_file");
+
+    return err;
+}
+
+int main (int argc, char **argv)
+{
+    int opt;
+    bool force_update = false, no_update = false, randomize = false;
+    struct pt_tile_info ti = {
+        .width  = 800,
+        .height = 600,
+        .x      = 0,
+        .y      = 0,
+        .zoom   = 0
+    };
+    struct pt_image_params update_params = { };
+    const char *out_path = NULL;
+    int threads = 0, benchmark = 0;
+    int err;
+    
+    // parse arguments
+    while ((opt = getopt_long(argc, argv, "hqvDUNB:W:H:x:y:z:o:j:", options, NULL)) != -1) {
+        switch (opt) {
+            case 'h':
+                // display help
+                help(argv[0]);
+                
+                return EXIT_SUCCESS;
+
+            case 'q':
+                // supress excess log output
+                set_log_level(LOG_WARN); break;
+
+            case 'v':
+            case 'D':
+                // display additional output
+                set_log_level(LOG_DEBUG); break;
+            
+            case 'U':
+                // force update of image caches
+                force_update = true; break;
+            
+            case 'N':
+                // supress update of image caches
+                no_update = true; break;
+
+            case 'B':
+                // background pattern
+                {
+                    unsigned int b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+                    
+                    // parse 0xXXXXXXXX
+                    if (sscanf(optarg, "0x%02x%02x%02x%02x", &b1, &b2, &b3, &b4) < 1)
+                        FATAL("Invalid hex value for -B/--background: %s", optarg);
+                    
+                    // store
+                    update_params.background_color[0] = b1;
+                    update_params.background_color[1] = b2;
+                    update_params.background_color[2] = b3;
+                    update_params.background_color[3] = b4;
+
+                } break;
+
+            case 'W':
+                ti.width = parse_uint(optarg, "--width"); break;
+
+            case 'H':
+                ti.height = parse_uint(optarg, "--height"); break;
+
+            case 'x':
+                ti.x = parse_uint(optarg, "--x"); break;
+
+            case 'y':
+                ti.y = parse_uint(optarg, "--y"); break;
+
+            case 'z':
+                ti.zoom = parse_sint(optarg, "--zoom"); break;
+
+            case 'o':
+                // output file
+                out_path = optarg; break;
+
+            case 'j':
+                threads = parse_uint(optarg, "--threads"); break;
+
+            case OPT_BENCHMARK:
+                benchmark = parse_uint(optarg, "--benchmark"); break;
+            
+            case OPT_RANDOMIZE:
+                randomize = true; break;
+
+            case '?':
+                // useage error
+                help(argv[0]);
+
+                return EXIT_FAILURE;
+
+            default:
+                // getopt???
+                FATAL("getopt_long returned unknown code %d", opt);
+        }
+    }
+    
+    // end-of-arguments?
+    if (!argv[optind])
+        EXIT_WARN(EXIT_FAILURE, "No images given");
+    
+
+
+    struct pt_ctx *ctx = NULL;
+    struct pt_image *image = NULL;
+    enum pt_cache_status status;
+    
+    if (threads) {
+        // build ctx
+        log_debug("Construct pt_ctx with %d threads", threads);
+
+        if ((err = pt_ctx_new(&ctx, threads)))
+            EXIT_ERROR(EXIT_FAILURE, "pt_ctx_new: threads=%d", threads);
+    }
+    
+    // process each image in turn
+    log_debug("Processing %d images...", argc - optind);
+
+    for (int i = optind; i < argc; i++) {
+        const char *img_path = argv[i];
+
+        log_debug("Loading image from: %s...", img_path);
+
+        // open
+        if ((err = pt_image_open(&image, ctx, img_path, PT_OPEN_UPDATE))) {
+            log_errno("pt_image_open: %s: %s", img_path, pt_strerror(err));
+            continue;
+        }
+
+        log_info("Opened image at: %s", img_path);
+        
+        // check if stale
+        if ((status = pt_image_status(image)) < 0) {
+            log_errno("pt_image_status: %s: %s", img_path, pt_strerror(status));
+            goto error;
+        }
+        
+        // update if stale
+        if (status != PT_CACHE_FRESH || force_update) {
+            if (status == PT_CACHE_NONE)
+                log_info("\tImage cache is missing");
+
+            else if (status == PT_CACHE_STALE)
+                log_info("\tImage cache is stale");
+            
+            else if (status == PT_CACHE_INCOMPAT)
+                log_info("\tImage cache is incompatible");
+
+            else if (status == PT_CACHE_FRESH)
+                log_info("\tImage cache is fresh");
+
+            else
+                log_warn("\tImage cache status is unknown");
+            
+            if (!no_update) {
+                log_info("\tUpdating image cache...");
+
+                if ((err = pt_image_update(image, &update_params))) {
+                    log_error("pt_image_update: %s: %s", img_path, pt_strerror(err));
+                    goto error;
+                }
+
+                log_debug("\tUpdated image cache");
+
+            } else {
+                log_warn("\tSupressing cache update");
+            }
+
+        } else {    
+            log_debug("\tImage cache is fresh");
+
+            // ensure it's loaded
+            log_info("\tLoad image cache...");
+
+            if ((err = pt_image_load(image))) {
+                log_errno("pt_image_load: %s", pt_strerror(err));
+                goto error;
+            }
+        }
+
+        // show info
+        const struct pt_image_info *info;
+        
+        if ((err = pt_image_info(image, &info))) {
+            log_warn_errno("pt_image_info: %s: %s", img_path, pt_strerror(err));
+
+        } else {
+            log_info("\tImage dimensions: %zux%zu (%zu bpp)", info->img_width, info->img_height, info->img_bpp);
+            log_info("\tImage mtime=%ld, bytes=%zu", (long) info->image_mtime, info->image_bytes);
+            log_info("\tCache mtime=%ld, bytes=%zu, blocks=%zu (%zu bytes), version=%d", 
+                    (long) info->cache_mtime, info->cache_bytes, info->cache_blocks, info->cache_blocks * 512, info->cache_version
+            );
+        }
+
+        // render tile?
+        if (benchmark) {
+            log_info("\tRunning %d %stile renders...", benchmark, randomize ? "randomized " : "");
+
+            // n times
+            for (int i = 0; i < benchmark; i++) {
+                // randomize x, y
+                if (randomize)
+                    randomize_tile(&ti, info);
+
+                if (do_tile(ctx, image, &ti, out_path))
+                    goto error;
+            }
+
+        } else if (out_path) {
+            // randomize x, y
+            if (randomize)
+                randomize_tile(&ti, info);
+
+            // just once
+            if (do_tile(ctx, image, &ti, out_path))
+                goto error;
+
+        }
+        // cleanup
+        // XXX: leak because of async
+        if (!ctx)
+            pt_image_destroy(image);
+        
+        continue;
+
+error:
+        // quit
+        EXIT_ERROR(EXIT_FAILURE, "Processing image failed: %s", img_path);
+    }
+    
+    if (ctx) {
+        log_info("Waiting for images to finish...");
+
+        // wait for tile operations to finish...
+        pt_ctx_shutdown(ctx);
+    }
+
+    log_info("Done");
+
+    return 0;
+}
+
--- a/src/shared/log.c	Sun Sep 14 15:19:59 2014 +0300
+++ b/src/shared/log.c	Sun Sep 14 15:24:58 2014 +0300
@@ -45,7 +45,7 @@
 
 size_t str_append_fmt_va (char *buf_ptr, size_t *buf_size, const char *fmt, va_list args)
 {
-    int ret;
+    int ret = 0;
 
     if (*buf_size && (ret = vsnprintf(buf_ptr, *buf_size, fmt, args)) < 0)
         return 0;
--- a/src/util/main.c	Sun Sep 14 15:19:59 2014 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,239 +0,0 @@
-#include "shared/log.h"
-
-#include "pngtile.h"
-
-#include <getopt.h>
-#include <stdio.h>
-#include <stdbool.h>
-
-/**
- * Command-line options
- */
-static const struct option options[] = {
-    { "help",           false,  NULL,   'h' },
-    { "quiet",          false,  NULL,   'q' },
-    { "verbose",        false,  NULL,   'v' },
-    { "debug",          false,  NULL,   'D' },
-    { "force-update",   false,  NULL,   'U' },
-    { "width",          true,   NULL,   'W' },
-    { "height",         true,   NULL,   'H' },
-    { "x",              true,   NULL,   'x' },
-    { "y",              true,   NULL,   'y' },
-    { "zoom",           true,   NULL,   'z' },
-    { "threads",        true,   NULL,   'j' },
-    { 0,                0,      0,      0   }
-};
-
-/**
- * Print usage/help info on stderr
- */
-void help (const char *argv0)
-{
-    fprintf(stderr, "Usage: %s [options] <image> [...]\n", argv0);
-    fprintf(stderr,
-        "XXX: Process some image files.\n"
-        "\n"
-        "\t-h, --help           show this help and exit\n"
-        "\t-q, --quiet          supress informational output\n"
-        "\t-v, --verbose        display more informational output\n"
-        "\t-D, --debug          equivalent to -v\n"
-        "\t-U, --force-update   unconditionally update image caches\n"
-        "\t-W, --width          set tile width\n"
-        "\t-H, --height         set tile height\n"
-        "\t-x, --x              set tile x offset\n"
-        "\t-y, --y              set tile z offset\n"
-        "\t-z, --zoom           set zoom factor (<0)\n"
-        "\t-j, --threads        number of threads\n"
-    );
-}
-
-int main (int argc, char **argv)
-{
-    int opt;
-    bool force_update = false;
-    struct pt_tile_info ti = {0, 0, 0, 0, 0};
-    int threads = 2;
-    int tmp, err;
-    
-    // parse arguments
-    while ((opt = getopt_long(argc, argv, "hqvDUW:H:x:y:z:j:", options, NULL)) != -1) {
-        switch (opt) {
-            case 'h':
-                // display help
-                help(argv[0]);
-                
-                return EXIT_SUCCESS;
-
-            case 'q':
-                // supress excess log output
-                set_log_level(LOG_WARN);
-
-                break;
-
-            case 'v':
-            case 'D':
-                // display additional output
-                set_log_level(LOG_DEBUG);
-
-                break;
-            
-            case 'U':
-                // force update of image caches
-                force_update = true;
-                
-                break;
-
-            case 'W':
-                ti.width = strtol(optarg, NULL, 0); break;
-
-            case 'H':
-                ti.height = strtol(optarg, NULL, 0); break;
-
-            case 'x':
-                ti.x = strtol(optarg, NULL, 0); break;
-
-            case 'y':
-                ti.y = strtol(optarg, NULL, 0); break;
-
-            case 'z':
-                ti.zoom = strtol(optarg, NULL, 0); break;
-
-            case 'j':
-                if ((tmp = strtol(optarg, NULL, 0)) < 1)
-                    FATAL("Invalid value for -j/--threads");
-
-                threads = tmp; break;
-
-            case '?':
-                // useage error
-                help(argv[0]);
-
-                return EXIT_FAILURE;
-
-            default:
-                // getopt???
-                FATAL("getopt_long returned unknown code %d", opt);
-        }
-    }
-    
-    // end-of-arguments?
-    if (!argv[optind])
-        EXIT_WARN(EXIT_FAILURE, "No images given");
-    
-
-
-    struct pt_ctx *ctx = NULL;
-    struct pt_image *image = NULL;
-    enum pt_cache_status status;
-
-    // build ctx
-    log_debug("Construct pt_ctx with %d threads", threads);
-
-    if ((err = pt_ctx_new(&ctx, threads)))
-        EXIT_ERROR(EXIT_FAILURE, "pt_ctx_new: threads=%d", threads);
-
-    
-    // process each image in turn
-    log_debug("Processing %d images...", argc - optind);
-
-    for (int i = optind; i < argc; i++) {
-        const char *img_path = argv[i];
-
-        log_debug("Loading image from: %s...", img_path);
-
-        // open
-        if ((err = pt_image_open(&image, ctx, img_path, PT_OPEN_UPDATE))) {
-            log_errno("pt_image_open: %s: %s", img_path, pt_strerror(err));
-            continue;
-        }
-
-        log_info("Opened image at: %s", img_path);
-        
-        // check if stale
-        if ((status = pt_image_status(image)) < 0) {
-            log_errno("pt_image_status: %s: %s", img_path, pt_strerror(status));
-            goto error;
-        }
-        
-        // update if stale
-        if (status != PT_CACHE_FRESH || force_update) {
-            if (status == PT_CACHE_NONE)
-                log_debug("\tImage cache is missing");
-
-            else if (status == PT_CACHE_STALE)
-                log_debug("\tImage cache is stale");
-
-            else if (status == PT_CACHE_FRESH)
-                log_debug("\tImage cache is fresh");
-
-            log_debug("\tUpdating image cache...");
-
-            if ((err = pt_image_update(image))) {
-                log_warn_errno("pt_image_update: %s: %s", img_path, pt_strerror(err));
-            }
-
-            log_info("\tUpdated image cache");
-
-        } else {
-            log_debug("\tImage cache is fresh");
-        }
-
-        // show info
-        const struct pt_image_info *img_info;
-        
-        if ((err = pt_image_info(image, &img_info)))
-            log_warn_errno("pt_image_info: %s: %s", img_path, pt_strerror(err));
-
-        else
-            log_info("\tImage dimensions: %zux%zu", img_info->width, img_info->height);
-
-        // render tile?
-        if (ti.width && ti.height) {
-            char tmp_name[] = "pt-tile-XXXXXX";
-            int fd;
-            FILE *out;
-            
-            // temporary file for output
-            if ((fd = mkstemp(tmp_name)) < 0) {
-                log_errno("mkstemp");
-                
-                continue;
-            }
-
-            // open out
-            if ((out = fdopen(fd, "w")) == NULL) {
-                log_errno("fdopen");
-
-                continue;
-            }
-            
-            // ensure it's loaded
-            log_debug("\tLoad image cache...");
-
-            if ((err = pt_image_load(image)))
-                log_errno("pt_image_load: %s", pt_strerror(err));
-
-            // render
-            log_info("\tAsync render tile %zux%zu@(%zu,%zu) -> %s", ti.width, ti.height, ti.x, ti.y, tmp_name);
-
-
-            if ((err = pt_image_tile_async(image, &ti, out)))
-                log_errno("pt_image_tile: %s: %s", img_path, pt_strerror(err));
-        }
-
-error:
-        // cleanup
-        // XXX: leak because of async: pt_image_destroy(image);
-        ;
-    }
-
-    log_info("Waiting for images to finish...");
-
-    // wait for tile operations to finish...
-    pt_ctx_shutdown(ctx);
-
-    log_info("Done");
-
-    return 0;
-}
-
--- a/static/dragdrop.js	Sun Sep 14 15:19:59 2014 +0300
+++ b/static/dragdrop.js	Sun Sep 14 15:24:58 2014 +0300
@@ -240,8 +240,7 @@
       style.top  = p[1] + "px";
     
     if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
-  },
-   
+  }
 });
 
 Draggable._dragging = { };
--- a/static/index.html	Sun Sep 14 15:19:59 2014 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-    <head>
-        <title>Mandelbrot</title>
-        <script src="/static/prototype.js" type="text/javascript"></script>
-        <script src="/static/dragdrop.js" type="text/javascript"></script>
-        <script src="/static/builder.js" type="text/javascript"></script>
-        <script src="/static/tiles2.js" type="text/javascript"></script>
-        <link rel="Stylesheet" type="text/css" href="static/style.css">
-    </head>
-    <body>
-        <div id="wrapper">
-            <div id="viewport" style="width: 100%; height: 100%">
-                <div class="substrate"></div>
-            </div>
-        </div>
-
-        <script type="text/javascript">
-            var tile_source = new Source("/tile", 256, 256, 0, 13);
-            var main = new Viewport(tile_source, "viewport");
-        </script>
-    </body>
-</html>
--- a/static/style.css	Sun Sep 14 15:19:59 2014 +0300
+++ b/static/style.css	Sun Sep 14 15:24:58 2014 +0300
@@ -45,3 +45,13 @@
 
     background-color: #FFFFFF;
 }
+
+.background {
+    z-index: 1;
+
+    font-size: xx-large;
+
+    text-align: center;
+
+    margin: 30%;
+}
--- a/static/tiles2.js	Sun Sep 14 15:19:59 2014 +0300
+++ b/static/tiles2.js	Sun Sep 14 15:24:58 2014 +0300
@@ -1,36 +1,60 @@
 // A source of tile images of a specific width/height, zoom level range, and some other attributes
+
+/**
+ * A source of image tiles.
+ *
+ * The image source is expected to serve fixed-size tiles (tile_width × tile_height) of image data based on the 
+ * x, y, zl URL query parameters. 
+ *
+ *  x, y        - the pixel coordinates of the top-left corner
+ *                XXX: these are scaled from the image coordinates by the zoom level
+ *
+ *  zl          - the zoom level used, in < zl < out.
+ *                The image pixels are scaled by powers-of-two, so a 256x256 tile at zl=1 shows a 512x512 area of the
+ *                1:1 image.
+ */
 var Source = Class.create({
-    initialize: function (path, tile_width, tile_height, zoom_min, zoom_max) {
+    initialize: function (path, tile_width, tile_height, zoom_min, zoom_max, img_width, img_height) {
         this.path = path;
         this.tile_width = tile_width;
         this.tile_height = tile_height;
         this.zoom_min = zoom_min;
         this.zoom_max = zoom_max;
+        this.img_width = img_width;
+        this.img_height = img_height;
 
         this.refresh = false;
         this.opt_key = this.opt_value = null;
     },
     
-    // build a URL for the given tile image
-    build_url: function (col, row, zl, sw, sh) {
-        // two-bit hash (0-4) based on the (col, row)
-        var hash = ( (col % 2) << 1 | (row % 2) ) + 1;
-        
-        // the subdomain to use
-        var subdomain = "";
-        
-        if (0)
+    /**
+     * Return an URL representing the tile at the given viewport (row, col) at the given zl.
+     *
+     * XXX: sw/sh = screen-width/height, used to choose an appropriate output size for dynamic image sources...
+     */
+    build_url: function (col, row, zl /*, sw, sh */) {
+        // XXX: distribute tile requests across tile*.foo.bar
+        if (0) {
+            // two-bit hash (0-4) based on the (col, row)
+            var hash = ( (col % 2) << 1 | (row % 2) ) + 1;
+            
+            // the subdomain to use
+            var subdomain = "";
+            
             subdomain = "tile" + hash + ".";
+        }
 
         // the (x, y) co-ordinates
         var x = col * this.tile_width;
         var y = row * this.tile_height;
 
         var url = this.path + "?x=" + x + "&y=" + y + "&zl=" + zl; // + "&sw=" + sw + "&sh=" + sh;
-
+        
+        // refresh the tile each time it is loaded?
         if (this.refresh)
             url += "&ts=" + new Date().getTime();
-
+        
+        // XXX: additional parameters, not used
         if (this.opt_key && this.opt_value)
             url += "&" + this.opt_key + "=" + this.opt_value;
 
@@ -43,40 +67,38 @@
     }
 });
 
-// a viewport that contains a substrate which contains several zoom layers which contain many tiles
+/**
+ * Viewport implements the tiles-UI. It consists of a draggable substrate, which in turn consists of several
+ * ZoomLayers, which then contain the actual tile images.
+ *
+ * Vars:
+ *      scroll_x/y      - the visible pixel offset of the top-left corner
+ */
 var Viewport = Class.create({
     initialize: function (source, viewport_id) {
         this.source = source;
-
+        
+        // get a handle on the UI elements
         this.id = viewport_id;
         this.div = $(viewport_id);
         this.substrate = this.div.down("div.substrate");
-    
-        // the stack of zoom levels
-        this.zoom_layers = [];
-        
-        // pre-populate the stack
-        for (var zoom_level = source.zoom_min; zoom_level <= source.zoom_max; zoom_level++) {
-            var zoom_layer = new ZoomLayer(source, zoom_level);
-
-            this.substrate.appendChild(zoom_layer.div);
-            this.zoom_layers[zoom_level] = zoom_layer;
-        }
 
         // make the substrate draggable
         this.draggable = new Draggable(this.substrate, {
             onStart: this.on_scroll_start.bind(this),
             onDrag: this.on_scroll_move.bind(this),
             onEnd: this.on_scroll_end.bind(this),
+
+            zindex: false
         });
 
-        // event handlers
+        // register event handlers for other UI functions
         Event.observe(this.substrate, "dblclick", this.on_dblclick.bindAsEventListener(this));
         Event.observe(this.substrate, "mousewheel", this.on_mousewheel.bindAsEventListener(this));
         Event.observe(this.substrate, "DOMMouseScroll", this.on_mousewheel.bindAsEventListener(this));     // mozilla
         Event.observe(document, "resize", this.on_resize.bindAsEventListener(this));
         
-        // zoom buttons
+        // init zoom UI buttons
         this.btn_zoom_in = $("btn-zoom-in");
         this.btn_zoom_out = $("btn-zoom-out");
 
@@ -85,54 +107,98 @@
         
         if (this.btn_zoom_out)
             Event.observe(this.btn_zoom_out, "click", this.zoom_out.bindAsEventListener(this));
-    
-        // set viewport size
-        this.update_size();
+           
+        // initial view location (centered)
+        var cx = this.source.img_width / 2;
+        var cy = this.source.img_height / 2;
+        var zl = 0; // XXX: would need to scale x/y for this: (this.source.zoom_min + this.source.zoom_max) / 2;
         
-        // this comes after update_size, since it must be updated once we have the size and zoom layer...
-        this.image_link = $("lnk-image");
-        
-        // initial location?    
+        // from link?
         if (document.location.hash) {
-            // x:y:z tuple
+            // parse x:y:z tuple
             var pt = document.location.hash.substr(1).split(":");
             
-            // unpack
-            var cx = 0, cy = 0, z = 0;
-            
-            if (pt.length) cx = parseInt(pt.shift());
-            if (pt.length) cy = parseInt(pt.shift());
-            if (pt.length) z = parseInt(pt.shift());
+            // unpack    
+            if (pt.length) cx = parseInt(pt.shift()) || cx;
+            if (pt.length) cy = parseInt(pt.shift()) || cy;
+            if (pt.length) zl = parseInt(pt.shift()) || zl;
+        }
 
-            // initial view
-            this.zoom_scaled(
-                cx - this.center_offset_x, 
-                cy - this.center_offset_y, 
-                z
-            );
+        // initialize zoom state to given zl
+        this._init_zoom(zl);
+        
+        // initialize viewport size
+        this.update_size();
+        
+        // initialize scroll offset
+        this._init_scroll(cx, cy);
+        
+        // this comes after update_size, so that the initial update_size doesn't try and update the image_link,
+        // since this only works once we have the zoom layers set up...
+        this.image_link = $("lnk-image");
+        this.update_image_link();
 
-        } else {
-            // this sets the scroll offsets, zoom level, and loads the tiles
-            this.zoom_to(0, 0, 0);
+        // display tiles!
+        this.update_tiles();
+    },
+
+/*
+ * Initializers - only run once
+ */
+                    
+    /** Initialize the zoom state to show the given zoom level */
+    _init_zoom: function (zl) {
+        // the stack of zoom levels
+        this.zoom_layers = [];
+        
+        // populate the zoom-layers stack based on the number of zoom levels defined for the source
+        for (var zoom_level = this.source.zoom_min; zoom_level <= this.source.zoom_max; zoom_level++) {
+            var zoom_layer = new ZoomLayer(this.source, zoom_level);
+
+            this.substrate.appendChild(zoom_layer.div);
+            this.zoom_layers[zoom_level] = zoom_layer;
         }
+
+        // is the new zoom level valid?
+        if (!this.zoom_layers[zl])
+            // XXX: nasty, revert to something else?
+            return false;
+        
+        // set the zoom layyer
+        this.zoom_layer = this.zoom_layers[zl];
+        
+        // enable it with initial z-index
+        this.zoom_layer.enable(11);
+        
+        // init the UI accordingly
+        this.update_zoom_ui();
     },
+
+    /** Initialize the scroll state to show the given (scaled) centered coordinates */
+    _init_scroll: function (cx, cy) {
+        // scroll center
+        this.scroll_center_to(cx, cy);
+    },
+
     
-    /* event handlers */
+/*
+ * Handle input events
+ */    
 
-    // window resized
+    /** Viewport resized */
     on_resize: function (ev) {
         this.update_size();
         this.update_tiles();
     },
     
-    // double-click handler
+    /** Double-click to zoom and center */
     on_dblclick: function (ev) {
         var offset = this.event_offset(ev);
         
-        this.zoom_center_to(
+        // center view and zoom in
+        this.center_and_zoom_in(
             this.scroll_x + offset.x,
-            this.scroll_y + offset.y,
-            1   // zoom in
+            this.scroll_y + offset.y
         );
     },
 
@@ -168,7 +234,7 @@
         
         // delta > 0 : scroll up, zoom in
         // delta < 0 : scroll down, zoom out
-        delta = delta < 0 ? -1 : 1;
+        delta = delta < 0 ? 1 : -1;
 
         // Firefox's DOMMouseEvent's pageX/Y attributes are broken. layerN is for mozilla, offsetN for IE, seems to work
 
@@ -180,25 +246,25 @@
         this.zoom_center_to(x, y, delta);
     },
     
-    // substrate scroll was started
+    /** Substrate scroll was started */
     on_scroll_start: function (ev) {
 
     },
     
-    // substrate was scrolled, update scroll_{x,y}, and then update tiles after 100ms
+    /** Substrate was scrolled, update scroll_{x,y}, and then update tiles after 100ms */
     on_scroll_move: function (ev) {
         this.update_scroll();
-        this.update_after_timeout();
+
+        // fast-update at 100ms intervals
+        this.update_after_timeout(true);
     },
     
-    // substrate scroll was ended, update tiles now
+    /** Substrate scroll was ended, update tiles now */
     on_scroll_end: function (ev) {
         this.update_now();
     },
 
-    /* get state */
-
-    // return the absolute (x, y) coords of the given event inside the viewport
+    /** Calculate the absolute (x, y) coords of the given event inside the viewport */
     event_offset: function (ev) {
         var offset = this.div.cumulativeOffset();
 
@@ -208,9 +274,11 @@
         };
     },
 
-    /* modify state */
+/*
+ * Change view - scroll/zoom
+ */                  
 
-    // scroll the view to place the given absolute (x, y) co-ordinate at the top left
+    /** Scroll the view to place the given absolute (x, y) co-ordinate at the top left */
     scroll_to: function (x, y) {
         // update it via the style
         this.substrate.style.top = "-" + y + "px";
@@ -221,7 +289,7 @@
         this.scroll_y = y;
     },
 
-    // scroll the view to place the given absolute (x, y) co-ordinate at the center
+    /** Scroll the view to place the given absolute (x, y) co-ordinate at the center */
     scroll_center_to: function (x, y) {
         return this.scroll_to(
             x - this.center_offset_x,
@@ -229,40 +297,40 @@
         );
     },
  
-    // zoom à la delta such that the given (zoomed) absolute (x, y) co-ordinates will be at the top left
+    /** Zoom à la delta such that the given (zoomed) absolute (x, y) co-ordinates will be at the top left */
     zoom_scaled: function (x, y, delta) {
         if (!this.update_zoom(delta))
+            // couldn't zoom, scaled coords are wrong
             return false;
 
         // scroll to the new position
         this.scroll_to(x, y);
         
-        // update view
-        // XXX: ...
+        // update view after 100ms - in case we zoom again?
         this.update_after_timeout();
         
         return true;
     },
    
-    // zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the top left
+    /** Zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the top left */
     zoom_to: function (x, y, delta) {
         return this.zoom_scaled(
-            scaleByZoomDelta(x, delta),
-            scaleByZoomDelta(y, delta),
+            scaleByZoomDelta(x, -delta),
+            scaleByZoomDelta(y, -delta),
             delta
         );
     },
     
-    // zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the center
+    /** Zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the center */
     zoom_center_to: function (x, y, delta) {
         return this.zoom_scaled(
-            scaleByZoomDelta(x, delta) - this.center_offset_x,
-            scaleByZoomDelta(y, delta) - this.center_offset_y,
+            scaleByZoomDelta(x, -delta) - this.center_offset_x,
+            scaleByZoomDelta(y, -delta) - this.center_offset_y,
             delta
         );
     },
 
-    // zoom à la delta, keeping the view centered
+    /** Zoom à la delta, keeping the view centered */
     zoom_centered: function (delta) {
         return this.zoom_center_to(
             this.scroll_x + this.center_offset_x,
@@ -271,105 +339,144 @@
         );
     },
 
-    // zoom in one level, keeping the view centered
+    /** Zoom in one level, keeping the view centered */
     zoom_in: function () {
-        return this.zoom_centered(+1);
+        return this.zoom_centered(-1);
     },
     
-    // zoom out one leve, keeping the view centered
+    /** Zoom out one level, keeping the view centered */
     zoom_out: function () {
-        return this.zoom_centered(-1);
+        return this.zoom_centered(+1);
     },
 
-    /* update view/state to reflect reality */
+    /** Center the view on the given coords, and zoom in, if possible */
+    center_and_zoom_in: function (cx, cy) {
+        // try and zoom in
+        if (this.update_zoom(-1)) {
+            // scaled coords
+            cx = scaleByZoomDelta(cx, 1);
+            cy = scaleByZoomDelta(cy, 1);
+        }
 
-    // figure out the viewport dimensions
+        // re-center
+        this.scroll_center_to(cx, cy);
+        
+        // update view after 100ms - in case we zoom again?
+        this.update_after_timeout();
+    },
+
+/*
+ * Update view state
+ */              
+ 
+    /** Update the view_* / center_offset_* vars, and any dependent items */
     update_size: function () {
         this.view_width = this.div.getWidth();
         this.view_height = this.div.getHeight();
 
         this.center_offset_x = Math.floor(this.view_width / 2);
         this.center_offset_y = Math.floor(this.view_height / 2);
-
+        
+        // the link-to-image uses the current view size
         this.update_image_link();
     },
-    
-    // figure out the scroll offset as absolute pixel co-ordinates at the top left
+   
+    /**
+     * Update the scroll_x/y state
+     */    
     update_scroll: function() {
         this.scroll_x = -parseInt(this.substrate.style.left);
         this.scroll_y = -parseInt(this.substrate.style.top);
     },
-
-    // wiggle the ZoomLevels around to match the current zoom level
-    update_zoom: function(delta) {
-        if (!this.zoom_layer) {
-            // first zoom operation
-
-            // is the new zoom level valid?
-            if (!this.zoom_layers[delta])
-                return false;
-            
-            // set the zoom layyer
-            this.zoom_layer = this.zoom_layers[delta];
-            
-            // enable it
-            this.zoom_layer.enable(11);
-            
-            // no need to .update_tiles or anything like that
-            
-        } else {
-            // is the new zoom level valid?
-            if (!this.zoom_layers[this.zoom_layer.level + delta])
-                return false;
+    
+    /**
+     * Switch zoom layers
+     */
+    update_zoom: function (delta) {
+        // is the new zoom level valid?
+        if (!this.zoom_layers[this.zoom_layer.level + delta])
+            return false;
 
-            var zoom_old = this.zoom_layer;
-            var zoom_new = this.zoom_layers[this.zoom_layer.level + delta];
-            
-            // XXX: ugly hack
-            if (this.zoom_timer) {
-                clearTimeout(this.zoom_timer);
-                this.zoom_timer = null;
-            }
-            
-            // get other zoom layers out of the way
-            this.zoom_layers.each(function (zl) {
-                zl.disable();
-            });
-            
-            // update the zoom layer
-            this.zoom_layer = zoom_new;
-            
-            // apply new z-indexes, preferr the current one over the new one
-            zoom_new.enable(11);
-            zoom_old.enable(10);
-            
-            // resize the tiles in the two zoom layers
-            zoom_new.update_tiles(zoom_new.level);
-            zoom_old.update_tiles(zoom_old.level);
-            
-            // XXX: ugly hack
-            this.zoom_timer = setTimeout(function () { zoom_old.disable()}, 1000);
+        var zoom_old = this.zoom_layer;
+        var zoom_new = this.zoom_layers[this.zoom_layer.level + delta];
+        
+        // XXX: clear hide-zoom-after-timeout
+        if (this.zoom_timer) {
+            clearTimeout(this.zoom_timer);
+            this.zoom_timer = null;
         }
+        
+        // get other zoom layers out of the way
+        // XXX: u
+        this.zoom_layers.each(function (zl) {
+            zl.disable();
+        });
+        
+        // update the current zoom layer
+        this.zoom_layer = zoom_new;
+        
+        // layer them such that the old on remains visible underneath the new one
+        zoom_new.enable(11);
+        zoom_old.enable(10);
+        
+        // resize the tiles in the two zoom layers
+        zoom_new.update_tiles(zoom_new.level);
+        zoom_old.update_tiles(zoom_new.level);
+        
+        // disable the old zoom layer after 1000ms - after the new zoom layer has loaded - not an optimal solution
+        this.zoom_timer = setTimeout(function () { zoom_old.disable()}, 1000);
 
-        // update UI
+        // update UI state
         this.update_zoom_ui();
 
         return true;
     },
 
-    // keep the zoom buttons, if any, updated
-    update_zoom_ui: function () {
-        if (this.btn_zoom_in)
-            (this.zoom_layer.level >= this.source.zoom_max) ? this.btn_zoom_in.disable() : this.btn_zoom_in.enable();
+    /** Run update_tiles() at 500ms intervals */
+    update_after_timeout: function (fast) {
+        // have not called update_tiles() yet
+        this._update_idle = false;
         
-        if (this.btn_zoom_out)
-            (this.zoom_layer.level <= this.source.zoom_min) ? this.btn_zoom_out.disable() : this.btn_zoom_out.enable();
-        
-        this.update_image_link();
+        // cancel old timeout
+        if (!fast && this._update_timeout) {
+            clearTimeout(this._update_timeout);
+            this._update_timeout = null;
+        }
+
+        if (!this._update_timeout)
+            // trigger after delay
+            this._update_timeout = setTimeout(this._update_timeout_trigger.bind(this), fast ? 500 : 100);
     },
     
-    // ensure that all tiles that are currently visible are loaded
-    update_tiles: function() {
+    _update_timeout_trigger: function () {
+        this._update_timeout = null;
+
+        // have called update_tiles()
+        this._update_idle = true;
+        
+        this.update_tiles();
+    },
+
+    /** 
+     * Unschedule the call to update_tiles() and call it now, unless it's already been triggered by the previous call to
+     * update_after_timeout
+     */
+    update_now: function () {
+        // abort trigger if active
+        if (this._update_timeout) {
+            clearTimeout(this._update_timeout);
+            this._update_timeout = null;
+        }
+
+        // update now unless already done
+        if (!this._update_idle)
+            this.update_tiles();
+    },
+
+    /**
+     * Determine the set of visible tiles, and ensure they are loaded
+     */
+    update_tiles: function () {
         // short names for some vars...
         var x = this.scroll_x;
         var y = this.scroll_y;
@@ -379,40 +486,41 @@
         var th = this.source.tile_height;
         var zl = this.zoom_layer.level;
         
-        // figure out what set of columns are visible
+        // figure out which set of cols/rows are visible 
         var start_col = Math.max(0, Math.floor(x / tw));
         var start_row = Math.max(0, Math.floor(y / th));
         var end_col = Math.floor((x + sw) / tw);
         var end_row = Math.floor((y + sh) / th);
 
-        // loop through all those tiles
+        // loop through all visible tiles
         for (var col = start_col; col <= end_col; col++) {
             for (var row = start_row; row <= end_row; row++) {
                 // the tile's id
-                var id = "tile_" + this.zoom_layer.level + "_" + col + "_" + row;
+                var id = "tile_" + zl + "_" + col + "_" + row;
                 
                 // does the element exist already?
+                // XXX: use basic document.getElementById for perf?
                 var t = $(id);
 
                 if (!t) {
                     // build a new tile
                     t = Builder.node("img", {
-                            src:    this.source.build_url(col, row, zl, sw, sh),
-                            id:     id,
-                            title:  "(" + col + ", " + row + ")",
+                            src:    this.source.build_url(col, row, zl /* , sw, sh */),
+                            id:     id //,
+                            // title:  "(" + col + ", " + row + ")",
                             // style set later
                         }
                     );
                     
-                    // set the CSS style stuff
+                    // position
                     t.style.top = th * row;
                     t.style.left = tw * col;
                     t.style.display = "none";
                     
-                    // wait for it to load
+                    // display once loaded
                     Event.observe(t, "load", _tile_loaded.bindAsEventListener(t));
 
-                    // store the col/row
+                    // remember the col/row
                     t.__col = col;
                     t.__row = row;
                     
@@ -429,17 +537,41 @@
         
         this.update_scroll_ui();
     }, 
-    
-    // update scroll-dependant UI elements
+
+/*
+ * UI state
+ */                  
+
+    /**
+     * Update any zl-dependant UI elements
+     */
+    update_zoom_ui: function () {
+        // deactivate zoom-in button if zoomed in
+        if (this.btn_zoom_in)
+            (this.zoom_layer.level <= this.source.zoom_min) ? this.btn_zoom_in.disable() : this.btn_zoom_in.enable();
+        
+        // deactivate zoom-out button if zoomed out
+        if (this.btn_zoom_out)
+            (this.zoom_layer.level >= this.source.zoom_max) ? this.btn_zoom_out.disable() : this.btn_zoom_out.enable();
+        
+        // link-to-image
+        this.update_image_link();
+    },
+ 
+    /**
+     * Update any position-dependant UI elements
+     */ 
     update_scroll_ui: function () {
+        // update the link-to-this-page thing
+        document.location.hash = (this.scroll_x + this.center_offset_x) + ":" + (this.scroll_y + this.center_offset_y) + ":" + this.zoom_layer.level;
+        
         // update link-to-image
         this.update_image_link();
-
-        // update the link-to-this-page thing
-        document.location.hash = "#" + (this.scroll_x + this.center_offset_x) + ":" + (this.scroll_y + this.center_offset_y) + ":" + this.zoom_layer.level;
     },
     
-    // update image link with size, zoom, pos
+    /**
+     * Update the link-to-image-of-this-view link with dimensions, zoom, position
+     */
     update_image_link: function () {
         if (!this.image_link)
             return;
@@ -452,41 +584,17 @@
         );
 
         this.image_link.innerHTML = this.view_width + "x" + this.view_height + "@" + this.zoom_layer.level;
-    },
-
-    // do update_tiles after 100ms, unless we are called again
-    update_after_timeout: function () {
-        this._update_idle = false;
-
-        if (this._update_timeout)
-            clearTimeout(this._update_timeout);
-
-        this._update_timeout = setTimeout(this._update_timeout_trigger.bind(this), 100);  
-    },
-    
-    _update_timeout_trigger: function () {
-        this._update_idle = true;
-
-        this.update_tiles();
-    },
-
-    // call update_tiles if it hasn't been called due to update_after_timeout
-    update_now: function () {
-        if (this._update_timeout)
-            clearTimeout(this._update_timeout);
-        
-        if (!this._update_idle)
-            this.update_tiles();
-    },
-
+    }
 });
 
-// used by Viewport.update_tiles to make a tile visible after it has loaded
+/** Used by Viewport.update_tiles to make a tile visible after it has loaded */
 function _tile_loaded (ev) {
     this.style.display = "block";
 }
 
-// a zoom layer containing the tiles for one zoom level
+/**
+ * A zoom layer contains a (col, row) grid of tiles at a specific zoom level. 
+ */
 var ZoomLayer = Class.create({
     initialize: function (source, zoom_level) {
         this.source = source;
@@ -497,28 +605,36 @@
         this.tiles = [];
     },
     
-    // add a tile to this zoom layer
+    /** Add a tile to this zoom layer's grid */
     add_tile: function (tile) {
         this.div.appendChild(tile);
         this.tiles.push(tile);
     },
     
-    // make this zoom layer visible with the given z-index
+    /** Make this zoom layer visible with the given z-index */
     enable: function (z_index) {
         this.div.style.zIndex = z_index;
-        this.div.show();
+
+        // XXX: IE8 needs this for some reason
+        $(this.div).show();
     },
    
-    // hide this zoom layer
-    disable: function (z_index) {
-        this.div.hide();
+    /** Hide this zoom layer */
+    disable: function () {
+        // XXX: IE8
+        $(this.div).hide();
     },
     
-    // update the tiles in this zoom layer so that they are in the correct position and of the correct size when
-    // viewed with the given zoom level
+    /** 
+     * Update the tile grid in this zoom layer such the tiles are in the correct position and of the correct size
+     * when viewed with the given zoom level.
+     *
+     * For zoom levels different than this layer's level, this will resize the tiles!
+     */
     update_tiles: function (zoom_level) {
-        var zd = zoom_level - this.level;
-
+        var zd = this.level - zoom_level;
+        
+        // desired tile size
         var tw = scaleByZoomDelta(this.source.tile_width, zd);
         var th = scaleByZoomDelta(this.source.tile_height, zd);
 
@@ -526,23 +642,22 @@
         var tiles_len = tiles.length;
         var t, ts;
         
+        // XXX: *all* tiles? :/
         for (var i = 0; i < tiles_len; i++) {
             t = tiles[i];
             ts = t.style;
-
+            
+            // reposition
             ts.width = tw;
             ts.height = th;
-            ts.top = th*t.__row;
-            ts.left = tw*t.__col;
+            ts.top = th * t.__row;
+            ts.left = tw * t.__col;
         }
-    },
-
-
-
+    }
 });
 
-// scale the given co-ordinate by a zoom delta. If we zoom in (dz > 0), n will become larger, and if we zoom 
-// out (dz < 0), n will become smaller.
+// scale the given co-ordinate by a zoom delta. If we zoom out (dz > 0), n will become larger, and if we zoom 
+// in (dz < 0), n will become smaller.
 function scaleByZoomDelta (n, dz) {
     if (dz > 0)
         return n << dz;