diff options
Diffstat (limited to 'google_appengine/google/appengine/api/images/images_stub.py')
-rwxr-xr-x | google_appengine/google/appengine/api/images/images_stub.py | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/google_appengine/google/appengine/api/images/images_stub.py b/google_appengine/google/appengine/api/images/images_stub.py new file mode 100755 index 0000000..d89f47e --- /dev/null +++ b/google_appengine/google/appengine/api/images/images_stub.py @@ -0,0 +1,411 @@ +#!/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. +# + +"""Stub version of the images API.""" + + + +import logging +import StringIO + +try: + import PIL + from PIL import _imaging + from PIL import Image +except ImportError: + import _imaging + import Image + +from google.appengine.api import apiproxy_stub +from google.appengine.api import images +from google.appengine.api.images import images_service_pb +from google.appengine.runtime import apiproxy_errors + + +def _ArgbToRgbaTuple(argb): + """Convert from a single ARGB value to a tuple containing RGBA. + + Args: + argb: Signed 32 bit integer containing an ARGB value. + + Returns: + RGBA tuple. + """ + unsigned_argb = argb % 0x100000000 + return ((unsigned_argb >> 16) & 0xFF, + (unsigned_argb >> 8) & 0xFF, + unsigned_argb & 0xFF, + (unsigned_argb >> 24) & 0xFF) + + +class ImagesServiceStub(apiproxy_stub.APIProxyStub): + """Stub version of images API to be used with the dev_appserver.""" + + def __init__(self, service_name='images'): + """Preloads PIL to load all modules in the unhardened environment. + + Args: + service_name: Service name expected for all calls. + """ + super(ImagesServiceStub, self).__init__(service_name) + Image.init() + + def _Dynamic_Composite(self, request, response): + """Implementation of ImagesService::Composite. + + Based off documentation of the PIL library at + http://www.pythonware.com/library/pil/handbook/index.htm + + Args: + request: ImagesCompositeRequest, contains image request info. + response: ImagesCompositeResponse, contains transformed image. + """ + width = request.canvas().width() + height = request.canvas().height() + color = _ArgbToRgbaTuple(request.canvas().color()) + canvas = Image.new("RGBA", (width, height), color) + sources = [] + if (not request.canvas().width() or request.canvas().width() > 4000 or + not request.canvas().height() or request.canvas().height() > 4000): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + if not request.image_size(): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + if not request.options_size(): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + if request.options_size() > images.MAX_COMPOSITES_PER_REQUEST: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + for image in request.image_list(): + sources.append(self._OpenImage(image.content())) + + for options in request.options_list(): + if (options.anchor() < images.TOP_LEFT or + options.anchor() > images.BOTTOM_RIGHT): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + if options.source_index() >= len(sources) or options.source_index() < 0: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + if options.opacity() < 0 or options.opacity() > 1: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + source = sources[options.source_index()] + x_anchor = (options.anchor() % 3) * 0.5 + y_anchor = (options.anchor() / 3) * 0.5 + x_offset = int(options.x_offset() + x_anchor * (width - source.size[0])) + y_offset = int(options.y_offset() + y_anchor * (height - source.size[1])) + alpha = options.opacity() * 255 + mask = Image.new("L", source.size, alpha) + canvas.paste(source, (x_offset, y_offset), mask) + response_value = self._EncodeImage(canvas, request.canvas().output()) + response.mutable_image().set_content(response_value) + + def _Dynamic_Histogram(self, request, response): + """Trivial implementation of ImagesService::Histogram. + + Based off documentation of the PIL library at + http://www.pythonware.com/library/pil/handbook/index.htm + + Args: + request: ImagesHistogramRequest, contains the image. + response: ImagesHistogramResponse, contains histogram of the image. + """ + image = self._OpenImage(request.image().content()) + img_format = image.format + if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.NOT_IMAGE) + image = image.convert("RGBA") + red = [0] * 256 + green = [0] * 256 + blue = [0] * 256 + for pixel in image.getdata(): + red[int((pixel[0] * pixel[3]) / 255)] += 1 + green[int((pixel[1] * pixel[3]) / 255)] += 1 + blue[int((pixel[2] * pixel[3]) / 255)] += 1 + histogram = response.mutable_histogram() + for value in red: + histogram.add_red(value) + for value in green: + histogram.add_green(value) + for value in blue: + histogram.add_blue(value) + + def _Dynamic_Transform(self, request, response): + """Trivial implementation of ImagesService::Transform. + + Based off documentation of the PIL library at + http://www.pythonware.com/library/pil/handbook/index.htm + + Args: + request: ImagesTransformRequest, contains image request info. + response: ImagesTransformResponse, contains transformed image. + """ + original_image = self._OpenImage(request.image().content()) + + new_image = self._ProcessTransforms(original_image, + request.transform_list()) + + response_value = self._EncodeImage(new_image, request.output()) + response.mutable_image().set_content(response_value) + + def _EncodeImage(self, image, output_encoding): + """Encode the given image and return it in string form. + + Args: + image: PIL Image object, image to encode. + output_encoding: ImagesTransformRequest.OutputSettings object. + + Returns: + str with encoded image information in given encoding format. + """ + image_string = StringIO.StringIO() + + image_encoding = "PNG" + + if (output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG): + image_encoding = "JPEG" + + image = image.convert("RGB") + + image.save(image_string, image_encoding) + + return image_string.getvalue() + + def _OpenImage(self, image): + """Opens an image provided as a string. + + Args: + image: image data to be opened + + Raises: + apiproxy_errors.ApplicationError if the image cannot be opened or if it + is an unsupported format. + + Returns: + Image containing the image data passed in. + """ + if not image: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.NOT_IMAGE) + + image = StringIO.StringIO(image) + try: + image = Image.open(image) + except IOError: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_IMAGE_DATA) + + img_format = image.format + if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.NOT_IMAGE) + return image + + def _ValidateCropArg(self, arg): + """Check an argument for the Crop transform. + + Args: + arg: float, argument to Crop transform to check. + + Raises: + apiproxy_errors.ApplicationError on problem with argument. + """ + if not isinstance(arg, float): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + + if not (0 <= arg <= 1.0): + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + + def _CalculateNewDimensions(self, + current_width, + current_height, + req_width, + req_height): + """Get new resize dimensions keeping the current aspect ratio. + + This uses the more restricting of the two requested values to determine + the new ratio. + + Args: + current_width: int, current width of the image. + current_height: int, current height of the image. + req_width: int, requested new width of the image. + req_height: int, requested new height of the image. + + Returns: + tuple (width, height) which are both ints of the new ratio. + """ + + width_ratio = float(req_width) / current_width + height_ratio = float(req_height) / current_height + + if req_width == 0 or (width_ratio > height_ratio and req_height != 0): + return int(height_ratio * current_width), req_height + else: + return req_width, int(width_ratio * current_height) + + def _Resize(self, image, transform): + """Use PIL to resize the given image with the given transform. + + Args: + image: PIL.Image.Image object to resize. + transform: images_service_pb.Transform to use when resizing. + + Returns: + PIL.Image.Image with transforms performed on it. + + Raises: + BadRequestError if the resize data given is bad. + """ + width = 0 + height = 0 + + if transform.has_width(): + width = transform.width() + if width < 0 or 4000 < width: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + + if transform.has_height(): + height = transform.height() + if height < 0 or 4000 < height: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + + current_width, current_height = image.size + new_width, new_height = self._CalculateNewDimensions(current_width, + current_height, + width, + height) + + return image.resize((new_width, new_height), Image.ANTIALIAS) + + def _Rotate(self, image, transform): + """Use PIL to rotate the given image with the given transform. + + Args: + image: PIL.Image.Image object to rotate. + transform: images_service_pb.Transform to use when rotating. + + Returns: + PIL.Image.Image with transforms performed on it. + + Raises: + BadRequestError if the rotate data given is bad. + """ + degrees = transform.rotate() + if degrees < 0 or degrees % 90 != 0: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + degrees %= 360 + + degrees = 360 - degrees + return image.rotate(degrees) + + def _Crop(self, image, transform): + """Use PIL to crop the given image with the given transform. + + Args: + image: PIL.Image.Image object to crop. + transform: images_service_pb.Transform to use when cropping. + + Returns: + PIL.Image.Image with transforms performed on it. + + Raises: + BadRequestError if the crop data given is bad. + """ + left_x = 0.0 + top_y = 0.0 + right_x = 1.0 + bottom_y = 1.0 + + if transform.has_crop_left_x(): + left_x = transform.crop_left_x() + self._ValidateCropArg(left_x) + + if transform.has_crop_top_y(): + top_y = transform.crop_top_y() + self._ValidateCropArg(top_y) + + if transform.has_crop_right_x(): + right_x = transform.crop_right_x() + self._ValidateCropArg(right_x) + + if transform.has_crop_bottom_y(): + bottom_y = transform.crop_bottom_y() + self._ValidateCropArg(bottom_y) + + width, height = image.size + + box = (int(transform.crop_left_x() * width), + int(transform.crop_top_y() * height), + int(transform.crop_right_x() * width), + int(transform.crop_bottom_y() * height)) + + return image.crop(box) + + def _ProcessTransforms(self, image, transforms): + """Execute PIL operations based on transform values. + + Args: + image: PIL.Image.Image instance, image to manipulate. + trasnforms: list of ImagesTransformRequest.Transform objects. + + Returns: + PIL.Image.Image with transforms performed on it. + + Raises: + BadRequestError if we are passed more than one of the same type of + transform. + """ + new_image = image + if len(transforms) > images.MAX_TRANSFORMS_PER_REQUEST: + raise apiproxy_errors.ApplicationError( + images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA) + for transform in transforms: + if transform.has_width() or transform.has_height(): + new_image = self._Resize(new_image, transform) + + elif transform.has_rotate(): + new_image = self._Rotate(new_image, transform) + + elif transform.has_horizontal_flip(): + new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT) + + elif transform.has_vertical_flip(): + new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM) + + elif (transform.has_crop_left_x() or + transform.has_crop_top_y() or + transform.has_crop_right_x() or + transform.has_crop_bottom_y()): + new_image = self._Crop(new_image, transform) + + elif transform.has_autolevels(): + logging.info("I'm Feeling Lucky autolevels will be visible once this " + "application is deployed.") + else: + logging.warn("Found no transformations found to perform.") + + return new_image |