pngtile/wsgi.py
author Tero Marttila <terom@fixme.fi>
Mon, 25 Jan 2010 05:01:34 +0200
changeset 90 1c317e0628a7
parent 89 02e5b9b08881
child 92 e50ec4217fe6
permissions -rw-r--r--
fail if DATA_ROOT is missing
"""
    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 exceptions
import os.path, os

import pypngtile as pt

## Settings
# path to images
DATA_ROOT = os.environ.get("PNGTILE_DATA_PATH") or os.path.abspath('data/')

# only open each image once
IMAGE_CACHE = {}

# 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 = os.path.join(url, name)

    url = os.path.join(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 render_dir (req, name, path) :
    """
        Directory index
    """

    prefix = os.path.dirname(req.script_root).rstrip('/')
    script_prefix = req.script_root
    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(script_prefix, name, item),

                # item name
                name        = item,
            ) for item in dir_list(path)
        ),
    )

def render_img_viewport (req, 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, -4, 0, %(img_width)d, %(img_height)d);
            var main = new Viewport(tile_source, "viewport");
        </script>
    </body>
</html>""" % dict(
        title           = name,
        prefix          = os.path.dirname(req.script_root).rstrip('/'),
        tile_url        = req.url,

        tile_width      = TILE_WIDTH,
        tile_height     = TILE_HEIGHT,

        img_width       = img_width,
        img_height      = img_height,
    )

def scale_by_zoom (val, zoom) :
    """
        Scale coordinates by zoom factor
    """

    if zoom > 0 :
        return val << zoom

    elif zoom > 0 :
        return val >> -zoom

    else :
        return val

### Render PNG Data
def render_img_tile (image, x, y, zoom, width=TILE_WIDTH, height=TILE_HEIGHT) :
    """
        Render given tile, returning PNG data
    """

    return image.tile_mem(
        width, height, 
        scale_by_zoom(x, -zoom), scale_by_zoom(y, -zoom), 
        zoom
    )

def render_img_region (image, cx, cy, zoom, width, height) :
    """
        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 exceptions.Forbidden("Image too large: %d * %d > %d" % (width, height, MAX_PIXELS))

    return image.tile_mem(
        width, height,
        x, y,
        zoom
    )


### Manipulate request data
def get_req_path (req) :
    """
        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 it is cached
    """

    # 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 request
def handle_dir (req, name, path) :
    """
        Handle request for a directory
    """

    return Response(render_dir(req, name, path), content_type="text/html")



def handle_img_viewport (req, image, name) :
    """
        Handle request for image viewport
    """

    # viewport
    return Response(render_img_viewport(req, name, image), content_type="text/html")


def handle_img_region (req, image) :
    """
        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"))

    # yay full render
    return Response(render_img_region(image, cx, cy, zoom, width, height), content_type="image/png")


def handle_img_tile (req, image) :
    """
        Handle request for image tile
    """

    # tile
    x = int(req.args['x'])
    y = int(req.args['y'])
    zoom = int(req.args.get('zl', "0"))
        
    # yay render
    return Response(render_img_tile(image, x, y, zoom), content_type="image/png")

## Dispatch req to handle_img_*
def handle_img (req, name, path) :
    """
        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)

    elif 'x' in req.args and 'y' in req.args :
        return handle_img_tile(req, image)

    else :
        raise exceptions.BadRequest("Unknown args")



## Dispatch request to handle_*
def handle_req (req) :
    """
        Main request handler
    """
    
    # decode req
    name, path = get_req_path(req)

    # 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)




@responder
def application (env, start_response) :
    """
        Main WSGI entry point
    """

    req = Request(env, start_response)
    
    try :
        return handle_req(req)

    except exceptions.HTTPException, e :
        return e