App Engine Python SDK version 1.8.9
[gae.git] / python / google / appengine / tools / dev_appserver_blobimage.py
blobc83a467baa7aec54eeb9991cef6f96d856f8fc19
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 """Dispatcher for dynamic image serving requests.
23 Classes:
25 CreateBlobImageDispatcher:
26 Creates a dispatcher that will handle an image serving request. It will
27 fetch an image from blobstore and dynamically resize it.
28 """
31 import logging
32 import re
33 import urlparse
35 from google.appengine.api import datastore
36 from google.appengine.api import datastore_errors
37 from google.appengine.api.images import images_service_pb
39 BLOBIMAGE_URL_PATTERN = '/_ah/img(?:/.*)?'
41 BLOBIMAGE_RESPONSE_TEMPLATE = (
42 'Status: %(status)s\r\nContent-Type: %(content_type)s\r\n'
43 'Cache-Control: public, max-age=600, no-transform'
44 '\r\n\r\n%(data)s')
47 BLOB_SERVING_URL_KIND = '__BlobServingUrl__'
50 DEFAULT_SERVING_SIZE = 512
52 def CreateBlobImageDispatcher(images_stub):
53 """Function to create a dynamic image serving stub.
55 Args:
56 images_stub: an images_stub to perform the image resizing on blobs.
59 Returns:
60 New dispatcher capable of dynamic image serving requests.
61 """
65 from google.appengine.tools import dev_appserver
67 class BlobImageDispatcher(dev_appserver.URLDispatcher):
68 """Dispatcher that handles image serving requests."""
70 _size_limit = 1600
71 _mime_type_map = {images_service_pb.OutputSettings.JPEG: 'image/jpeg',
72 images_service_pb.OutputSettings.PNG: 'image/png',
73 images_service_pb.OutputSettings.WEBP: 'image/webp'}
75 def __init__(self, images_stub):
76 """Constructor.
78 Args:
79 images_stub: an images_stub to perform the image resizing on blobs.
80 """
81 self._images_stub = images_stub
83 def _TransformImage(self, blob_key, options):
84 """Construct and execute transform request to the images stub.
86 Args:
87 blob_key: blob_key to the image to transform.
88 options: resize and crop option string to apply to the image.
90 Returns:
91 The tranformed (if necessary) image bytes.
92 """
93 resize, crop = self._ParseOptions(options)
95 image_data = images_service_pb.ImageData()
96 image_data.set_blob_key(blob_key)
97 image = self._images_stub._OpenImageData(image_data)
98 original_mime_type = image.format
99 width, height = image.size
102 if crop:
103 crop_xform = None
104 if width > height:
106 crop_xform = images_service_pb.Transform()
107 delta = (width - height) / (width * 2.0)
108 crop_xform.set_crop_left_x(delta)
109 crop_xform.set_crop_right_x(1.0 - delta)
110 elif width < height:
112 crop_xform = images_service_pb.Transform()
113 delta = (height - width) / (height * 2.0)
114 top_delta = max(0.0, delta - 0.25)
115 bottom_delta = 1.0 - (2.0 * delta) + top_delta
116 crop_xform.set_crop_top_y(top_delta)
117 crop_xform.set_crop_bottom_y(bottom_delta)
118 if crop_xform:
119 image = self._images_stub._Crop(image, crop_xform)
122 if resize is None:
123 if width > DEFAULT_SERVING_SIZE or height > DEFAULT_SERVING_SIZE:
124 resize = DEFAULT_SERVING_SIZE
127 if resize:
129 resize_xform = images_service_pb.Transform()
130 resize_xform.set_width(resize)
131 resize_xform.set_height(resize)
132 image = self._images_stub._Resize(image, resize_xform)
134 output_settings = images_service_pb.OutputSettings()
137 output_mime_type = images_service_pb.OutputSettings.JPEG
138 if original_mime_type in ['PNG', 'GIF']:
139 output_mime_type = images_service_pb.OutputSettings.PNG
140 output_settings.set_mime_type(output_mime_type)
141 return (self._images_stub._EncodeImage(image, output_settings),
142 self._mime_type_map[output_mime_type])
144 def _ParseOptions(self, options):
145 """Currently only support resize and crop options.
147 Args:
148 options: the url resize and crop option string.
150 Returns:
151 (resize, crop) options parsed from the string.
153 match = re.search('^s(\\d+)(-c)?', options)
154 resize = None
155 crop = False
156 if match:
157 if match.group(1):
158 resize = int(match.group(1))
159 if match.group(2):
160 crop = True
163 if resize and (resize > BlobImageDispatcher._size_limit or
164 resize < 0):
165 raise ValueError, 'Invalid resize'
166 return (resize, crop)
168 def _ParseUrl(self, url):
169 """Parse the URL into the blobkey and option string.
171 Args:
172 url: a url as a string.
174 Returns:
175 (blob_key, option) tuple parsed out of the URL.
177 path = urlparse.urlsplit(url)[2]
178 match = re.search('/_ah/img/([-\\w:]+)([=]*)([-\\w]+)?', path)
179 if not match or not match.group(1):
180 raise ValueError, 'Failed to parse image url.'
181 options = ''
182 blobkey = match.group(1)
183 if match.group(3):
184 if match.group(2):
185 blobkey = ''.join([blobkey, match.group(2)[1:]])
186 options = match.group(3)
187 elif match.group(2):
188 blobkey = ''.join([blobkey, match.group(2)])
189 return (blobkey, options)
192 def Dispatch(self,
193 request,
194 outfile,
195 base_env_dict=None):
196 """Handle GET image serving request.
198 This dispatcher handles image requests under the /_ah/img/ path.
199 The rest of the path should be a serialized blobkey used to retrieve
200 the image from blobstore.
202 Args:
203 request: The HTTP request.
204 outfile: The response file.
205 base_env_dict: Dictionary of CGI environment parameters if available.
206 Defaults to None.
208 try:
209 if base_env_dict and base_env_dict['REQUEST_METHOD'] != 'GET':
210 raise RuntimeError, 'BlobImage only handles GET requests.'
212 blobkey, options = self._ParseUrl(request.relative_url)
215 key = datastore.Key.from_path(BLOB_SERVING_URL_KIND,
216 blobkey,
217 namespace='')
218 try:
219 datastore.Get(key)
220 except datastore_errors.EntityNotFoundError:
221 logging.warning('The blobkey %s has not registered for image '
222 'serving. Please ensure get_serving_url is '
223 'called before attempting to serve blobs.', blobkey)
224 image, mime_type = self._TransformImage(blobkey, options)
225 output_dict = {'status': 200, 'content_type': mime_type,
226 'data': image}
227 outfile.write(BLOBIMAGE_RESPONSE_TEMPLATE % output_dict)
228 except ValueError:
229 logging.exception('ValueError while serving image.')
230 outfile.write('Status: 404\r\n')
231 except RuntimeError:
232 logging.exception('RuntimeError while serving image.')
233 outfile.write('Status: 400\r\n')
234 except:
237 logging.exception('Exception while serving image.')
238 outfile.write('Status: 500\r\n')
240 return BlobImageDispatcher(images_stub)