blob: a8b3c790e5effcd2328d264089b760192eb6807e [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Dispatcher for dynamic image serving requests.
Classes:
CreateBlobImageDispatcher:
Creates a dispatcher that will handle an image serving request. It will
fetch an image from blobstore and dynamically resize it.
"""
import logging
import re
import urlparse
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api.images import images_service_pb
BLOBIMAGE_URL_PATTERN = '/_ah/img(?:/.*)?'
BLOBIMAGE_RESPONSE_TEMPLATE = (
'Status: %(status)s\r\nContent-Type: %(content_type)s\r\n'
'Cache-Control: public, max-age=600, no-transform'
'\r\n\r\n%(data)s')
BLOB_SERVING_URL_KIND = '__BlobServingUrl__'
DEFAULT_SERVING_SIZE = 512
def CreateBlobImageDispatcher(images_stub):
"""Function to create a dynamic image serving stub.
Args:
images_stub: an images_stub to perform the image resizing on blobs.
Returns:
New dispatcher capable of dynamic image serving requests.
"""
from google.appengine.tools import old_dev_appserver
class BlobImageDispatcher(old_dev_appserver.URLDispatcher):
"""Dispatcher that handles image serving requests."""
_size_limit = 1600
_mime_type_map = {images_service_pb.OutputSettings.JPEG: 'image/jpeg',
images_service_pb.OutputSettings.PNG: 'image/png',
images_service_pb.OutputSettings.WEBP: 'image/webp'}
def __init__(self, images_stub):
"""Constructor.
Args:
images_stub: an images_stub to perform the image resizing on blobs.
"""
self._images_stub = images_stub
def _TransformImage(self, blob_key, options):
"""Construct and execute transform request to the images stub.
Args:
blob_key: blob_key to the image to transform.
options: resize and crop option string to apply to the image.
Returns:
The tranformed (if necessary) image bytes.
"""
resize, crop = self._ParseOptions(options)
image_data = images_service_pb.ImageData()
image_data.set_blob_key(blob_key)
image = self._images_stub._OpenImageData(image_data)
original_mime_type = image.format
width, height = image.size
if crop:
crop_xform = None
if width > height:
crop_xform = images_service_pb.Transform()
delta = (width - height) / (width * 2.0)
crop_xform.set_crop_left_x(delta)
crop_xform.set_crop_right_x(1.0 - delta)
elif width < height:
crop_xform = images_service_pb.Transform()
delta = (height - width) / (height * 2.0)
top_delta = max(0.0, delta - 0.25)
bottom_delta = 1.0 - (2.0 * delta) + top_delta
crop_xform.set_crop_top_y(top_delta)
crop_xform.set_crop_bottom_y(bottom_delta)
if crop_xform:
image = self._images_stub._Crop(image, crop_xform)
if resize is None:
if width > DEFAULT_SERVING_SIZE or height > DEFAULT_SERVING_SIZE:
resize = DEFAULT_SERVING_SIZE
if resize:
resize_xform = images_service_pb.Transform()
resize_xform.set_width(resize)
resize_xform.set_height(resize)
image = self._images_stub._Resize(image, resize_xform)
output_settings = images_service_pb.OutputSettings()
output_mime_type = images_service_pb.OutputSettings.JPEG
if original_mime_type in ['PNG', 'GIF']:
output_mime_type = images_service_pb.OutputSettings.PNG
output_settings.set_mime_type(output_mime_type)
return (self._images_stub._EncodeImage(image, output_settings),
self._mime_type_map[output_mime_type])
def _ParseOptions(self, options):
"""Currently only support resize and crop options.
Args:
options: the url resize and crop option string.
Returns:
(resize, crop) options parsed from the string.
"""
match = re.search('^s(\\d+)(-c)?', options)
resize = None
crop = False
if match:
if match.group(1):
resize = int(match.group(1))
if match.group(2):
crop = True
if resize and (resize > BlobImageDispatcher._size_limit or
resize < 0):
raise ValueError, 'Invalid resize'
return (resize, crop)
def _ParseUrl(self, url):
"""Parse the URL into the blobkey and option string.
Args:
url: a url as a string.
Returns:
(blob_key, option) tuple parsed out of the URL.
"""
path = urlparse.urlsplit(url)[2]
match = re.search('/_ah/img/([-\\w:]+)([=]*)([-\\w]+)?', path)
if not match or not match.group(1):
raise ValueError, 'Failed to parse image url.'
options = ''
blobkey = match.group(1)
if match.group(3):
if match.group(2):
blobkey = ''.join([blobkey, match.group(2)[1:]])
options = match.group(3)
elif match.group(2):
blobkey = ''.join([blobkey, match.group(2)])
return (blobkey, options)
def Dispatch(self,
request,
outfile,
base_env_dict=None):
"""Handle GET image serving request.
This dispatcher handles image requests under the /_ah/img/ path.
The rest of the path should be a serialized blobkey used to retrieve
the image from blobstore.
Args:
request: The HTTP request.
outfile: The response file.
base_env_dict: Dictionary of CGI environment parameters if available.
Defaults to None.
"""
try:
if base_env_dict and base_env_dict['REQUEST_METHOD'] != 'GET':
raise RuntimeError, 'BlobImage only handles GET requests.'
blobkey, options = self._ParseUrl(request.relative_url)
key = datastore.Key.from_path(BLOB_SERVING_URL_KIND,
blobkey,
namespace='')
try:
datastore.Get(key)
except datastore_errors.EntityNotFoundError:
logging.warning('The blobkey %s has not registered for image '
'serving. Please ensure get_serving_url is '
'called before attempting to serve blobs.', blobkey)
image, mime_type = self._TransformImage(blobkey, options)
output_dict = {'status': 200, 'content_type': mime_type,
'data': image}
outfile.write(BLOBIMAGE_RESPONSE_TEMPLATE % output_dict)
except ValueError:
logging.exception('ValueError while serving image.')
outfile.write('Status: 404\r\n')
except RuntimeError:
logging.exception('RuntimeError while serving image.')
outfile.write('Status: 400\r\n')
except:
logging.exception('Exception while serving image.')
outfile.write('Status: 500\r\n')
return BlobImageDispatcher(images_stub)