1.9.30 sync.
[gae.git] / python / google / appengine / tools / dev_appserver_blobimage.py
blob5130be15a52912dd46466694436ad16dcc7ce58e
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.
17 """Dispatcher for dynamic image serving requests.
19 Classes:
21 CreateBlobImageDispatcher:
22 Creates a dispatcher that will handle an image serving request. It will
23 fetch an image from blobstore and dynamically resize it.
24 """
28 import logging
29 import re
30 import urlparse
32 from google.appengine.api import datastore
33 from google.appengine.api import datastore_errors
34 from google.appengine.api.images import images_service_pb
36 BLOBIMAGE_URL_PATTERN = '/_ah/img(?:/.*)?'
38 BLOBIMAGE_RESPONSE_TEMPLATE = (
39 'Status: %(status)s\r\nContent-Type: %(content_type)s\r\n'
40 'Cache-Control: public, max-age=600, no-transform'
41 '\r\n\r\n%(data)s')
44 BLOB_SERVING_URL_KIND = '__BlobServingUrl__'
47 DEFAULT_SERVING_SIZE = 512
49 def CreateBlobImageDispatcher(images_stub):
50 """Function to create a dynamic image serving stub.
52 Args:
53 images_stub: an images_stub to perform the image resizing on blobs.
56 Returns:
57 New dispatcher capable of dynamic image serving requests.
58 """
62 from google.appengine.tools import old_dev_appserver
64 class BlobImageDispatcher(old_dev_appserver.URLDispatcher):
65 """Dispatcher that handles image serving requests."""
67 _size_limit = 1600
68 _mime_type_map = {images_service_pb.OutputSettings.JPEG: 'image/jpeg',
69 images_service_pb.OutputSettings.PNG: 'image/png',
70 images_service_pb.OutputSettings.WEBP: 'image/webp'}
72 def __init__(self, images_stub):
73 """Constructor.
75 Args:
76 images_stub: an images_stub to perform the image resizing on blobs.
77 """
78 self._images_stub = images_stub
80 def _TransformImage(self, blob_key, options):
81 """Construct and execute transform request to the images stub.
83 Args:
84 blob_key: blob_key to the image to transform.
85 options: resize and crop option string to apply to the image.
87 Returns:
88 The tranformed (if necessary) image bytes.
89 """
90 resize, crop = self._ParseOptions(options)
92 image_data = images_service_pb.ImageData()
93 image_data.set_blob_key(blob_key)
94 image = self._images_stub._OpenImageData(image_data)
95 original_mime_type = image.format
96 width, height = image.size
99 if crop:
100 crop_xform = None
101 if width > height:
103 crop_xform = images_service_pb.Transform()
104 delta = (width - height) / (width * 2.0)
105 crop_xform.set_crop_left_x(delta)
106 crop_xform.set_crop_right_x(1.0 - delta)
107 elif width < height:
109 crop_xform = images_service_pb.Transform()
110 delta = (height - width) / (height * 2.0)
111 top_delta = max(0.0, delta - 0.25)
112 bottom_delta = 1.0 - (2.0 * delta) + top_delta
113 crop_xform.set_crop_top_y(top_delta)
114 crop_xform.set_crop_bottom_y(bottom_delta)
115 if crop_xform:
116 image = self._images_stub._Crop(image, crop_xform)
119 if resize is None:
120 if width > DEFAULT_SERVING_SIZE or height > DEFAULT_SERVING_SIZE:
121 resize = DEFAULT_SERVING_SIZE
124 if resize:
126 resize_xform = images_service_pb.Transform()
127 resize_xform.set_width(resize)
128 resize_xform.set_height(resize)
129 image = self._images_stub._Resize(image, resize_xform)
131 output_settings = images_service_pb.OutputSettings()
134 output_mime_type = images_service_pb.OutputSettings.JPEG
135 if original_mime_type in ['PNG', 'GIF']:
136 output_mime_type = images_service_pb.OutputSettings.PNG
137 output_settings.set_mime_type(output_mime_type)
138 return (self._images_stub._EncodeImage(image, output_settings),
139 self._mime_type_map[output_mime_type])
141 def _ParseOptions(self, options):
142 """Currently only support resize and crop options.
144 Args:
145 options: the url resize and crop option string.
147 Returns:
148 (resize, crop) options parsed from the string.
150 match = re.search('^s(\\d+)(-c)?', options)
151 resize = None
152 crop = False
153 if match:
154 if match.group(1):
155 resize = int(match.group(1))
156 if match.group(2):
157 crop = True
160 if resize and (resize > BlobImageDispatcher._size_limit or
161 resize < 0):
162 raise ValueError, 'Invalid resize'
163 return (resize, crop)
165 def _ParseUrl(self, url):
166 """Parse the URL into the blobkey and option string.
168 Args:
169 url: a url as a string.
171 Returns:
172 (blob_key, option) tuple parsed out of the URL.
174 path = urlparse.urlsplit(url)[2]
175 match = re.search('/_ah/img/([-\\w:]+)([=]*)([-\\w]+)?', path)
176 if not match or not match.group(1):
177 raise ValueError, 'Failed to parse image url.'
178 options = ''
179 blobkey = match.group(1)
180 if match.group(3):
181 if match.group(2):
182 blobkey = ''.join([blobkey, match.group(2)[1:]])
183 options = match.group(3)
184 elif match.group(2):
185 blobkey = ''.join([blobkey, match.group(2)])
186 return (blobkey, options)
189 def Dispatch(self,
190 request,
191 outfile,
192 base_env_dict=None):
193 """Handle GET image serving request.
195 This dispatcher handles image requests under the /_ah/img/ path.
196 The rest of the path should be a serialized blobkey used to retrieve
197 the image from blobstore.
199 Args:
200 request: The HTTP request.
201 outfile: The response file.
202 base_env_dict: Dictionary of CGI environment parameters if available.
203 Defaults to None.
205 try:
206 if base_env_dict and base_env_dict['REQUEST_METHOD'] != 'GET':
207 raise RuntimeError, 'BlobImage only handles GET requests.'
209 blobkey, options = self._ParseUrl(request.relative_url)
212 key = datastore.Key.from_path(BLOB_SERVING_URL_KIND,
213 blobkey,
214 namespace='')
215 try:
216 datastore.Get(key)
217 except datastore_errors.EntityNotFoundError:
218 logging.warning('The blobkey %s has not registered for image '
219 'serving. Please ensure get_serving_url is '
220 'called before attempting to serve blobs.', blobkey)
221 image, mime_type = self._TransformImage(blobkey, options)
222 output_dict = {'status': 200, 'content_type': mime_type,
223 'data': image}
224 outfile.write(BLOBIMAGE_RESPONSE_TEMPLATE % output_dict)
225 except ValueError:
226 logging.exception('ValueError while serving image.')
227 outfile.write('Status: 404\r\n')
228 except RuntimeError:
229 logging.exception('RuntimeError while serving image.')
230 outfile.write('Status: 400\r\n')
231 except:
234 logging.exception('Exception while serving image.')
235 outfile.write('Status: 500\r\n')
237 return BlobImageDispatcher(images_stub)