pngtile/wsgi.py
changeset 89 02e5b9b08881
parent 47 201257cbd887
child 90 1c317e0628a7
equal deleted inserted replaced
88:5cc2d044d368 89:02e5b9b08881
     6 from werkzeug import exceptions
     6 from werkzeug import exceptions
     7 import os.path, os
     7 import os.path, os
     8 
     8 
     9 import pypngtile as pt
     9 import pypngtile as pt
    10 
    10 
       
    11 ## Settings
       
    12 # path to images
    11 DATA_ROOT = os.environ.get("PNGTILE_DATA_PATH") or os.path.abspath('data/')
    13 DATA_ROOT = os.environ.get("PNGTILE_DATA_PATH") or os.path.abspath('data/')
    12 
    14 
       
    15 # only open each image once
    13 IMAGE_CACHE = {}
    16 IMAGE_CACHE = {}
    14 
    17 
       
    18 # width of a tile
    15 TILE_WIDTH = 256
    19 TILE_WIDTH = 256
    16 TILE_HEIGHT = 256
    20 TILE_HEIGHT = 256
    17 
    21 
    18 # max. output resolution to allow
    22 # max. output resolution to allow
    19 MAX_PIXELS = 1920 * 1200
    23 MAX_PIXELS = 1920 * 1200
    20 
    24 
    21 def dir_view (req, name, path) :
    25 def dir_url (prefix, name, item) :
       
    26     """
       
    27         Join together an absolute URL prefix, an optional directory name, and a directory item
       
    28     """
       
    29 
       
    30     url = prefix
       
    31 
       
    32     if name :
       
    33         url = os.path.join(url, name)
       
    34 
       
    35     url = os.path.join(url, item)
       
    36 
       
    37     return url
       
    38 
       
    39 def dir_list (dir_path) :
       
    40     """
       
    41         Yield a series of directory items to show for the given dir
       
    42     """
       
    43 
       
    44     # link to parent
       
    45     yield '..'
       
    46 
       
    47     for item in os.listdir(dir_path) :
       
    48         path = os.path.join(dir_path, item)
       
    49 
       
    50         # skip dotfiles
       
    51         if item.startswith('.') :
       
    52             continue
       
    53         
       
    54         # show dirs
       
    55         if os.path.isdir(path) :
       
    56             yield item
       
    57 
       
    58         # examine ext
       
    59         base, ext = os.path.splitext(path)
       
    60 
       
    61         # show .png files with a .cache file
       
    62         if ext == '.png' and os.path.exists(base + '.cache') :
       
    63             yield item
       
    64 
       
    65 ### Render HTML data
       
    66 def render_dir (req, name, path) :
       
    67     """
       
    68         Directory index
       
    69     """
       
    70 
    22     prefix = os.path.dirname(req.script_root).rstrip('/')
    71     prefix = os.path.dirname(req.script_root).rstrip('/')
    23     script_prefix = req.script_root
    72     script_prefix = req.script_root
    24     name = name.rstrip('/')
    73     name = name.rstrip('/')
    25 
       
    26 
    74 
    27     return """\
    75     return """\
    28 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    76 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    29     <head>
    77     <head>
    30         <title>Index of %(dir)s</title>
    78         <title>Index of %(dir)s</title>
    37 %(listing)s
    85 %(listing)s
    38         </ul>
    86         </ul>
    39     </body>
    87     </body>
    40 </html>""" % dict(
    88 </html>""" % dict(
    41         prefix          = prefix,
    89         prefix          = prefix,
    42         dir             = name,
    90         dir             = '/' + name,
    43         
    91         
    44         listing         = "\n".join(
    92         listing         = "\n".join(
       
    93             # <li> link
    45             """<li><a href="%(url)s">%(name)s</a></li>""" % dict(
    94             """<li><a href="%(url)s">%(name)s</a></li>""" % dict(
    46                 url         = '/'.join((script_prefix, name, item)),
    95                 # URL to dir
       
    96                 url         = dir_url(script_prefix, name, item),
       
    97 
       
    98                 # item name
    47                 name        = item,
    99                 name        = item,
    48             ) for item in ['..'] + [i for i in os.listdir(path) if i.endswith('.png') or os.path.isdir(os.path.join(path, i))]
   100             ) for item in dir_list(path)
    49         ),
   101         ),
    50     )
   102     )
    51 
   103 
    52 def image_view (req, image_path, image) :
   104 def render_img_viewport (req, name, image) :
    53     image_name = os.path.basename(image_path)
   105     """
    54 
   106         HTML for image
    55     img_width, img_height = image.info()
   107     """
       
   108     
       
   109     # a little slow, but not so bad - two stats(), heh
       
   110     info = image.info()
       
   111     img_width, img_height = info['img_width'], info['img_height']
    56 
   112 
    57     return """\
   113     return """\
    58 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
   114 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    59     <head>
   115     <head>
    60         <title>%(title)s</title>
   116         <title>%(title)s</title>
    85             var tile_source = new Source("%(tile_url)s", %(tile_width)d, %(tile_height)d, -4, 0, %(img_width)d, %(img_height)d);
   141             var tile_source = new Source("%(tile_url)s", %(tile_width)d, %(tile_height)d, -4, 0, %(img_width)d, %(img_height)d);
    86             var main = new Viewport(tile_source, "viewport");
   142             var main = new Viewport(tile_source, "viewport");
    87         </script>
   143         </script>
    88     </body>
   144     </body>
    89 </html>""" % dict(
   145 </html>""" % dict(
    90         title           = image_name,
   146         title           = name,
    91         prefix          = os.path.dirname(req.script_root).rstrip('/'),
   147         prefix          = os.path.dirname(req.script_root).rstrip('/'),
    92         tile_url        = req.url,
   148         tile_url        = req.url,
    93 
   149 
    94         tile_width      = TILE_WIDTH,
   150         tile_width      = TILE_WIDTH,
    95         tile_height     = TILE_HEIGHT,
   151         tile_height     = TILE_HEIGHT,
    97         img_width       = img_width,
   153         img_width       = img_width,
    98         img_height      = img_height,
   154         img_height      = img_height,
    99     )
   155     )
   100 
   156 
   101 def scale_by_zoom (val, zoom) :
   157 def scale_by_zoom (val, zoom) :
       
   158     """
       
   159         Scale coordinates by zoom factor
       
   160     """
       
   161 
   102     if zoom > 0 :
   162     if zoom > 0 :
   103         return val << zoom
   163         return val << zoom
   104 
   164 
   105     elif zoom > 0 :
   165     elif zoom > 0 :
   106         return val >> -zoom
   166         return val >> -zoom
   107 
   167 
   108     else :
   168     else :
   109         return val
   169         return val
   110 
   170 
   111 def render_tile (image, x, y, zoom, width=TILE_WIDTH, height=TILE_HEIGHT) :
   171 ### Render PNG Data
       
   172 def render_img_tile (image, x, y, zoom, width=TILE_WIDTH, height=TILE_HEIGHT) :
       
   173     """
       
   174         Render given tile, returning PNG data
       
   175     """
       
   176 
   112     return image.tile_mem(
   177     return image.tile_mem(
   113         width, height, 
   178         width, height, 
   114         scale_by_zoom(x, -zoom), scale_by_zoom(y, -zoom), 
   179         scale_by_zoom(x, -zoom), scale_by_zoom(y, -zoom), 
   115         zoom
   180         zoom
   116     )
   181     )
   117 
   182 
   118 def render_image (image, cx, cy, zoom, width, height) :
   183 def render_img_region (image, cx, cy, zoom, width, height) :
       
   184     """
       
   185         Render arbitrary tile, returning PNG data
       
   186     """
       
   187 
   119     x = scale_by_zoom(cx - width / 2, -zoom)
   188     x = scale_by_zoom(cx - width / 2, -zoom)
   120     y = scale_by_zoom(cy - height / 2, -zoom)
   189     y = scale_by_zoom(cy - height / 2, -zoom)
   121 
   190 
   122     # safely limit
   191     # safely limit
   123     if width * height > MAX_PIXELS :
   192     if width * height > MAX_PIXELS :
   127         width, height,
   196         width, height,
   128         x, y,
   197         x, y,
   129         zoom
   198         zoom
   130     )
   199     )
   131 
   200 
   132 def handle_main (req) :
   201 
       
   202 ### Manipulate request data
       
   203 def get_req_path (req) :
       
   204     """
       
   205         Returns the name and path requested
       
   206     """
       
   207 
   133     # path to image
   208     # path to image
   134     image_name = req.path.lstrip('/')
   209     image_name = req.path.lstrip('/')
   135     
   210     
   136     # build absolute path
   211     # build absolute path
   137     image_path = os.path.abspath(os.path.join(DATA_ROOT, image_name))
   212     image_path = os.path.abspath(os.path.join(DATA_ROOT, image_name))
   138 
   213 
   139     
       
   140     # ensure the path points inside the data root
   214     # ensure the path points inside the data root
   141     if not image_path.startswith(DATA_ROOT) :
   215     if not image_path.startswith(DATA_ROOT) :
   142         raise exceptions.NotFound(image_name)
   216         raise exceptions.NotFound(image_name)
   143 
   217 
   144 
   218     return image_name, image_path
   145     if os.path.isdir(image_path) :
   219 
   146         return Response(dir_view(req, image_name, image_path), content_type="text/html")
   220 def get_image (name, path) :
   147     
   221     """
   148     elif not os.path.exists(image_path) :
   222         Gets an Image object from the cache, ensuring that it is cached
   149         raise exceptions.NotFound(image_name)
   223     """
   150 
       
   151     elif not image_name or not image_name.endswith('.png') :
       
   152         raise exceptions.BadRequest("Not a PNG file")
       
   153     
       
   154 
   224 
   155     # get Image object
   225     # get Image object
   156     if image_path in IMAGE_CACHE :
   226     if path in IMAGE_CACHE :
   157         # get from cache
   227         # get from cache
   158         image = IMAGE_CACHE[image_path]
   228         image = IMAGE_CACHE[path]
   159 
   229 
   160     else :
   230     else :
   161         # ensure exists
   231         # open
   162         if not os.path.exists(image_path) :
   232         image = pt.Image(path)
   163             raise exceptions.NotFound(image_name)
   233 
       
   234         # check
       
   235         if image.status() not in (pt.CACHE_FRESH, pt.CACHE_STALE) :
       
   236             raise exceptions.InternalServerError("Image cache not available: %s" % name)
       
   237 
       
   238         # load
       
   239         image.open()
   164 
   240 
   165         # cache
   241         # cache
   166         image = IMAGE_CACHE[image_path] = pt.Image(image_path)
   242         IMAGE_CACHE[path] = image
   167     
   243     
   168     if image.status() == pt.CACHE_NONE :
   244     return image
   169         raise exceptions.InternalServerError("Image not cached: " + image_name)
   245 
       
   246 
       
   247 
       
   248 ### Handle request
       
   249 def handle_dir (req, name, path) :
       
   250     """
       
   251         Handle request for a directory
       
   252     """
       
   253 
       
   254     return Response(render_dir(req, name, path), content_type="text/html")
       
   255 
       
   256 
       
   257 
       
   258 def handle_img_viewport (req, image, name) :
       
   259     """
       
   260         Handle request for image viewport
       
   261     """
       
   262 
       
   263     # viewport
       
   264     return Response(render_img_viewport(req, name, image), content_type="text/html")
       
   265 
       
   266 
       
   267 def handle_img_region (req, image) :
       
   268     """
       
   269         Handle request for an image region
       
   270     """
       
   271 
       
   272     # specific image
       
   273     width = int(req.args['w'])
       
   274     height = int(req.args['h'])
       
   275     cx = int(req.args['cx'])
       
   276     cy = int(req.args['cy'])
       
   277     zoom = int(req.args.get('zl', "0"))
       
   278 
       
   279     # yay full render
       
   280     return Response(render_img_region(image, cx, cy, zoom, width, height), content_type="image/png")
       
   281 
       
   282 
       
   283 def handle_img_tile (req, image) :
       
   284     """
       
   285         Handle request for image tile
       
   286     """
       
   287 
       
   288     # tile
       
   289     x = int(req.args['x'])
       
   290     y = int(req.args['y'])
       
   291     zoom = int(req.args.get('zl', "0"))
       
   292         
       
   293     # yay render
       
   294     return Response(render_img_tile(image, x, y, zoom), content_type="image/png")
       
   295 
       
   296 ## Dispatch req to handle_img_*
       
   297 def handle_img (req, name, path) :
       
   298     """
       
   299         Handle request for an image
       
   300     """
       
   301 
       
   302     # get image object
       
   303     image = get_image(name, path)
   170 
   304 
   171     # what view?
   305     # what view?
   172     if not req.args :
   306     if not req.args :
   173         # viewport
   307         return handle_img_viewport(req, image, name)
   174         return Response(image_view(req, image_path, image), content_type="text/html")
       
   175 
   308 
   176     elif 'w' in req.args and 'h' in req.args and 'cx' in req.args and 'cy' in req.args :
   309     elif 'w' in req.args and 'h' in req.args and 'cx' in req.args and 'cy' in req.args :
   177         # specific image
   310         return handle_img_region(req, image)
   178         width = int(req.args['w'])
       
   179         height = int(req.args['h'])
       
   180         cx = int(req.args['cx'])
       
   181         cy = int(req.args['cy'])
       
   182         zoom = int(req.args.get('zl', "0"))
       
   183 
       
   184         # yay full render
       
   185         return Response(render_image(image, cx, cy, zoom, width, height), content_type="image/png")
       
   186 
   311 
   187     elif 'x' in req.args and 'y' in req.args :
   312     elif 'x' in req.args and 'y' in req.args :
   188         # tile
   313         return handle_img_tile(req, image)
   189         x = int(req.args['x'])
   314 
   190         y = int(req.args['y'])
       
   191         zoom = int(req.args.get('zl', "0"))
       
   192         
       
   193         # yay render
       
   194         return Response(render_tile(image, x, y, zoom), content_type="image/png")
       
   195    
       
   196     else :
   315     else :
   197         raise exceptions.BadRequest("Unknown args")
   316         raise exceptions.BadRequest("Unknown args")
   198    
   317 
       
   318 
       
   319 
       
   320 ## Dispatch request to handle_*
       
   321 def handle_req (req) :
       
   322     """
       
   323         Main request handler
       
   324     """
       
   325     
       
   326     # decode req
       
   327     name, path = get_req_path(req)
       
   328 
       
   329     # determine dir/image
       
   330     if os.path.isdir(path) :
       
   331         # directory
       
   332         return handle_dir(req, name, path)
       
   333     
       
   334     elif not os.path.exists(path) :
       
   335         # no such file
       
   336         raise exceptions.NotFound(name)
       
   337 
       
   338     elif not name or not name.endswith('.png') :
       
   339         # invalid file
       
   340         raise exceptions.BadRequest("Not a PNG file")
       
   341     
       
   342     else :
       
   343         # image
       
   344         return handle_img(req, name, path)
       
   345 
       
   346 
       
   347 
   199 
   348 
   200 @responder
   349 @responder
   201 def application (env, start_response) :
   350 def application (env, start_response) :
       
   351     """
       
   352         Main WSGI entry point
       
   353     """
       
   354 
   202     req = Request(env, start_response)
   355     req = Request(env, start_response)
   203     
   356     
   204     try :
   357     try :
   205         return handle_main(req)
   358         return handle_req(req)
   206 
   359 
   207     except exceptions.HTTPException, e :
   360     except exceptions.HTTPException, e :
   208         return e
   361         return e
   209 
   362