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.
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.
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'
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.
56 images_stub: an images_stub to perform the image resizing on blobs.
60 New dispatcher capable of dynamic image serving requests.
65 from google
.appengine
.tools
import dev_appserver
67 class BlobImageDispatcher(dev_appserver
.URLDispatcher
):
68 """Dispatcher that handles image serving requests."""
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
):
79 images_stub: an images_stub to perform the image resizing on blobs.
81 self
._images
_stub
= images_stub
83 def _TransformImage(self
, blob_key
, options
):
84 """Construct and execute transform request to the images stub.
87 blob_key: blob_key to the image to transform.
88 options: resize and crop option string to apply to the image.
91 The tranformed (if necessary) image bytes.
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
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
)
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
)
119 image
= self
._images
_stub
._Crop
(image
, crop_xform
)
123 if width
> DEFAULT_SERVING_SIZE
or height
> DEFAULT_SERVING_SIZE
:
124 resize
= DEFAULT_SERVING_SIZE
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.
148 options: the url resize and crop option string.
151 (resize, crop) options parsed from the string.
153 match
= re
.search('^s(\\d+)(-c)?', options
)
158 resize
= int(match
.group(1))
163 if resize
and (resize
> BlobImageDispatcher
._size
_limit
or
165 raise ValueError, 'Invalid resize'
166 return (resize
, crop
)
168 def _ParseUrl(self
, url
):
169 """Parse the URL into the blobkey and option string.
172 url: a url as a string.
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.'
182 blobkey
= match
.group(1)
185 blobkey
= ''.join([blobkey
, match
.group(2)[1:]])
186 options
= match
.group(3)
188 blobkey
= ''.join([blobkey
, match
.group(2)])
189 return (blobkey
, options
)
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.
203 request: The HTTP request.
204 outfile: The response file.
205 base_env_dict: Dictionary of CGI environment parameters if available.
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
,
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
,
227 outfile
.write(BLOBIMAGE_RESPONSE_TEMPLATE
% output_dict
)
229 logging
.exception('ValueError while serving image.')
230 outfile
.write('Status: 404\r\n')
232 logging
.exception('RuntimeError while serving image.')
233 outfile
.write('Status: 400\r\n')
237 logging
.exception('Exception while serving image.')
238 outfile
.write('Status: 500\r\n')
240 return BlobImageDispatcher(images_stub
)