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