pngtile.tile: separate Application dedicated to PNG serving
authorTero Marttila <terom@paivola.fi>
Sun, 14 Sep 2014 17:19:54 +0300
changeset 135 e99dd75afa15
parent 134 08a0056f6175
child 136 caa2509e6404
pngtile.tile: separate Application dedicated to PNG serving
bin/tile-server
bin/tile.wsgi
pngtile/tile.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/tile-server	Sun Sep 14 17:19:54 2014 +0300
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+"""
+    Development server for pngtile.tile serving.
+"""
+
+import argparse
+import pngtile.tile
+import werkzeug.serving
+
+def main ():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('--listen',     metavar='ADDR', default='0.0.0.0',
+            help="Listen on address")
+    parser.add_argument('--port',       metavar='PORT', type=int, default=8080,
+            help="Listen on port")
+
+    parser.add_argument('--reload',     action='store_true',
+            help="Reload")
+    parser.add_argument('--debugger',   action='store_true',
+            help="Debugger")
+
+    parser.add_argument('image_root',   metavar='PATH',
+            help="Path to images")
+
+    args = parser.parse_args()
+
+    application = pngtile.tile.Application(
+        image_root  = args.image_root,
+    )
+
+    werkzeug.serving.run_simple(args.listen, args.port, application,
+            use_reloader    = args.reload,
+            use_debugger    = args.debugger,
+    )
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/tile.wsgi	Sun Sep 14 17:19:54 2014 +0300
@@ -0,0 +1,5 @@
+import pngtile.tile
+
+application = pngtile.tile.Application(
+    image_root  = './var',
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pngtile/tile.py	Sun Sep 14 17:19:54 2014 +0300
@@ -0,0 +1,176 @@
+"""
+    Raw tile handling.
+"""
+
+import os.path
+
+from werkzeug import Request, Response, exceptions
+
+import pypngtile
+
+## Coordinates
+# width of a tile
+TILE_WIDTH = 256
+TILE_HEIGHT = 256
+
+# max. output resolution to allow
+MAX_PIXELS = 1920 * 1200
+
+def scale (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
+
+def scale_center (val, dim, zoom):
+    """
+        Scale value about center by zoom.
+    """
+
+    return scale(val - dim / 2, zoom)
+
+class Application (object):
+    def __init__ (self, image_root):
+        if not os.path.isdir(image_root) :
+            raise Exception("Given image_root does not exist: {image_root}".format(image_root=image_root))
+
+        self.image_root = os.path.abspath(image_root)
+
+        self.image_cache = { }
+
+    def lookup_image (self, url):
+        """
+            Lookup image by request path.
+
+            Returns image_name, image_path.
+        """
+
+        if not os.path.isdir(self.image_root):
+            raise exceptions.InternalServerError("Server image_root has gone missing")
+    
+        # path to image
+        name = url.lstrip('/')
+        
+        # build absolute path
+        path = os.path.abspath(os.path.join(self.image_root, name))
+
+        # ensure the path points inside the data root
+        if not path.startswith(self.image_root):
+            raise exceptions.NotFound(name)
+
+        return name, path
+
+    def get_image (self, url):
+        """
+            Return Image object.
+        """
+
+        name, path = self.lookup_image(url)
+
+        # get Image object
+        image = self.image_cache.get(path)
+
+        if not image:
+            # open
+            image = pypngtile.Image(path)
+
+            # check
+            if image.status() not in (pypngtile.CACHE_FRESH, pypngtile.CACHE_STALE):
+                raise exceptions.InternalServerError("Image cache not available: {name}".format(name=name))
+
+            # load
+            image.open()
+
+            # cache
+            self.image_cache[path] = image
+        
+        return image
+
+    def render_region (self, request, image):
+        """
+            Handle request for an image region
+        """
+
+        width = int(request.args['w'])
+        height = int(request.args['h'])
+        x = int(request.args['x'])
+        y = int(request.args['y'])
+        zoom = int(request.args.get('zoom', "0"))
+
+        # safely limit
+        if width * height > MAX_PIXELS:
+            raise exceptions.BadRequest("Image size: %d * %d > %d" % (width, height, MAX_PIXELS))
+
+        x = scale(x, zoom)
+        y = scale(y, zoom)
+        
+        try:
+            return image.tile_mem(width, height, x, y, zoom)
+
+        except pypngtile.Error as error:
+            raise exceptions.BadRequest(str(error))
+        
+    def render_tile (self, request, image):
+        """
+            Handle request for image tile
+        """
+
+        width = TILE_WIDTH
+        height = TILE_HEIGHT
+        row = int(request.args['row'])
+        col = int(request.args['col'])
+        zoom = int(request.args.get('zoom', "0"))
+
+        x = scale(row * width, zoom)
+        y = scale(col * height, zoom)
+
+        try:
+            return image.tile_mem(width, height, x, y, zoom)
+
+        except pypngtile.Error as error:
+            raise exceptions.BadRequest(str(error))
+
+    def handle (self, request):
+        """
+            Handle request for an image
+        """
+        
+        try:
+            image = self.get_image(request.path)
+        except pypngtile.Error as error:
+            raise exceptions.BadRequest(str(error))
+
+        if 'w' in request.args and 'h' in request.args and 'x' in request.args and 'y' in request.args:
+            png = self.render_region(request, image)
+
+        elif 'row' in request.args and 'col' in request.args:
+            png = self.render_tile(request, image)
+
+        else:
+            raise exceptions.BadRequest("Unknown args")
+
+        return Response(png, content_type='image/png')
+
+    @Request.application
+    def __call__ (self, request):
+        """
+            WSGI entry point.
+        """
+
+        try:
+            return self.handle(request)
+
+        except exceptions.HTTPException as error:
+            return error
+