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.
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.
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'
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.
53 images_stub: an images_stub to perform the image resizing on blobs.
57 New dispatcher capable of dynamic image serving requests.
62 from google
.appengine
.tools
import old_dev_appserver
64 class BlobImageDispatcher(old_dev_appserver
.URLDispatcher
):
65 """Dispatcher that handles image serving requests."""
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
):
76 images_stub: an images_stub to perform the image resizing on blobs.
78 self
._images
_stub
= images_stub
80 def _TransformImage(self
, blob_key
, options
):
81 """Construct and execute transform request to the images stub.
84 blob_key: blob_key to the image to transform.
85 options: resize and crop option string to apply to the image.
88 The tranformed (if necessary) image bytes.
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
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
)
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
)
116 image
= self
._images
_stub
._Crop
(image
, crop_xform
)
120 if width
> DEFAULT_SERVING_SIZE
or height
> DEFAULT_SERVING_SIZE
:
121 resize
= DEFAULT_SERVING_SIZE
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.
145 options: the url resize and crop option string.
148 (resize, crop) options parsed from the string.
150 match
= re
.search('^s(\\d+)(-c)?', options
)
155 resize
= int(match
.group(1))
160 if resize
and (resize
> BlobImageDispatcher
._size
_limit
or
162 raise ValueError, 'Invalid resize'
163 return (resize
, crop
)
165 def _ParseUrl(self
, url
):
166 """Parse the URL into the blobkey and option string.
169 url: a url as a string.
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.'
179 blobkey
= match
.group(1)
182 blobkey
= ''.join([blobkey
, match
.group(2)[1:]])
183 options
= match
.group(3)
185 blobkey
= ''.join([blobkey
, match
.group(2)])
186 return (blobkey
, options
)
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.
200 request: The HTTP request.
201 outfile: The response file.
202 base_env_dict: Dictionary of CGI environment parameters if available.
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
,
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
,
224 outfile
.write(BLOBIMAGE_RESPONSE_TEMPLATE
% output_dict
)
226 logging
.exception('ValueError while serving image.')
227 outfile
.write('Status: 404\r\n')
229 logging
.exception('RuntimeError while serving image.')
230 outfile
.write('Status: 400\r\n')
234 logging
.exception('Exception while serving image.')
235 outfile
.write('Status: 500\r\n')
237 return BlobImageDispatcher(images_stub
)