summaryrefslogtreecommitdiffstats
path: root/google_appengine/google/appengine/api/images/images_stub.py
diff options
context:
space:
mode:
Diffstat (limited to 'google_appengine/google/appengine/api/images/images_stub.py')
-rwxr-xr-xgoogle_appengine/google/appengine/api/images/images_stub.py411
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