Update Google App Engine to 1.2.2 in thirdparty folder.
[Melange.git] / thirdparty / google_appengine / google / appengine / api / images / images_stub.py
blobd89f47eac55dce803c8cbc1fe9f45cfc7a235336
1 #!/usr/bin/env python
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.
18 """Stub version of the images API."""
22 import logging
23 import StringIO
25 try:
26 import PIL
27 from PIL import _imaging
28 from PIL import Image
29 except ImportError:
30 import _imaging
31 import Image
33 from google.appengine.api import apiproxy_stub
34 from google.appengine.api import images
35 from google.appengine.api.images import images_service_pb
36 from google.appengine.runtime import apiproxy_errors
39 def _ArgbToRgbaTuple(argb):
40 """Convert from a single ARGB value to a tuple containing RGBA.
42 Args:
43 argb: Signed 32 bit integer containing an ARGB value.
45 Returns:
46 RGBA tuple.
47 """
48 unsigned_argb = argb % 0x100000000
49 return ((unsigned_argb >> 16) & 0xFF,
50 (unsigned_argb >> 8) & 0xFF,
51 unsigned_argb & 0xFF,
52 (unsigned_argb >> 24) & 0xFF)
55 class ImagesServiceStub(apiproxy_stub.APIProxyStub):
56 """Stub version of images API to be used with the dev_appserver."""
58 def __init__(self, service_name='images'):
59 """Preloads PIL to load all modules in the unhardened environment.
61 Args:
62 service_name: Service name expected for all calls.
63 """
64 super(ImagesServiceStub, self).__init__(service_name)
65 Image.init()
67 def _Dynamic_Composite(self, request, response):
68 """Implementation of ImagesService::Composite.
70 Based off documentation of the PIL library at
71 http://www.pythonware.com/library/pil/handbook/index.htm
73 Args:
74 request: ImagesCompositeRequest, contains image request info.
75 response: ImagesCompositeResponse, contains transformed image.
76 """
77 width = request.canvas().width()
78 height = request.canvas().height()
79 color = _ArgbToRgbaTuple(request.canvas().color())
80 canvas = Image.new("RGBA", (width, height), color)
81 sources = []
82 if (not request.canvas().width() or request.canvas().width() > 4000 or
83 not request.canvas().height() or request.canvas().height() > 4000):
84 raise apiproxy_errors.ApplicationError(
85 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
86 if not request.image_size():
87 raise apiproxy_errors.ApplicationError(
88 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
89 if not request.options_size():
90 raise apiproxy_errors.ApplicationError(
91 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
92 if request.options_size() > images.MAX_COMPOSITES_PER_REQUEST:
93 raise apiproxy_errors.ApplicationError(
94 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
95 for image in request.image_list():
96 sources.append(self._OpenImage(image.content()))
98 for options in request.options_list():
99 if (options.anchor() < images.TOP_LEFT or
100 options.anchor() > images.BOTTOM_RIGHT):
101 raise apiproxy_errors.ApplicationError(
102 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
103 if options.source_index() >= len(sources) or options.source_index() < 0:
104 raise apiproxy_errors.ApplicationError(
105 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
106 if options.opacity() < 0 or options.opacity() > 1:
107 raise apiproxy_errors.ApplicationError(
108 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
109 source = sources[options.source_index()]
110 x_anchor = (options.anchor() % 3) * 0.5
111 y_anchor = (options.anchor() / 3) * 0.5
112 x_offset = int(options.x_offset() + x_anchor * (width - source.size[0]))
113 y_offset = int(options.y_offset() + y_anchor * (height - source.size[1]))
114 alpha = options.opacity() * 255
115 mask = Image.new("L", source.size, alpha)
116 canvas.paste(source, (x_offset, y_offset), mask)
117 response_value = self._EncodeImage(canvas, request.canvas().output())
118 response.mutable_image().set_content(response_value)
120 def _Dynamic_Histogram(self, request, response):
121 """Trivial implementation of ImagesService::Histogram.
123 Based off documentation of the PIL library at
124 http://www.pythonware.com/library/pil/handbook/index.htm
126 Args:
127 request: ImagesHistogramRequest, contains the image.
128 response: ImagesHistogramResponse, contains histogram of the image.
130 image = self._OpenImage(request.image().content())
131 img_format = image.format
132 if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"):
133 raise apiproxy_errors.ApplicationError(
134 images_service_pb.ImagesServiceError.NOT_IMAGE)
135 image = image.convert("RGBA")
136 red = [0] * 256
137 green = [0] * 256
138 blue = [0] * 256
139 for pixel in image.getdata():
140 red[int((pixel[0] * pixel[3]) / 255)] += 1
141 green[int((pixel[1] * pixel[3]) / 255)] += 1
142 blue[int((pixel[2] * pixel[3]) / 255)] += 1
143 histogram = response.mutable_histogram()
144 for value in red:
145 histogram.add_red(value)
146 for value in green:
147 histogram.add_green(value)
148 for value in blue:
149 histogram.add_blue(value)
151 def _Dynamic_Transform(self, request, response):
152 """Trivial implementation of ImagesService::Transform.
154 Based off documentation of the PIL library at
155 http://www.pythonware.com/library/pil/handbook/index.htm
157 Args:
158 request: ImagesTransformRequest, contains image request info.
159 response: ImagesTransformResponse, contains transformed image.
161 original_image = self._OpenImage(request.image().content())
163 new_image = self._ProcessTransforms(original_image,
164 request.transform_list())
166 response_value = self._EncodeImage(new_image, request.output())
167 response.mutable_image().set_content(response_value)
169 def _EncodeImage(self, image, output_encoding):
170 """Encode the given image and return it in string form.
172 Args:
173 image: PIL Image object, image to encode.
174 output_encoding: ImagesTransformRequest.OutputSettings object.
176 Returns:
177 str with encoded image information in given encoding format.
179 image_string = StringIO.StringIO()
181 image_encoding = "PNG"
183 if (output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG):
184 image_encoding = "JPEG"
186 image = image.convert("RGB")
188 image.save(image_string, image_encoding)
190 return image_string.getvalue()
192 def _OpenImage(self, image):
193 """Opens an image provided as a string.
195 Args:
196 image: image data to be opened
198 Raises:
199 apiproxy_errors.ApplicationError if the image cannot be opened or if it
200 is an unsupported format.
202 Returns:
203 Image containing the image data passed in.
205 if not image:
206 raise apiproxy_errors.ApplicationError(
207 images_service_pb.ImagesServiceError.NOT_IMAGE)
209 image = StringIO.StringIO(image)
210 try:
211 image = Image.open(image)
212 except IOError:
213 raise apiproxy_errors.ApplicationError(
214 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
216 img_format = image.format
217 if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"):
218 raise apiproxy_errors.ApplicationError(
219 images_service_pb.ImagesServiceError.NOT_IMAGE)
220 return image
222 def _ValidateCropArg(self, arg):
223 """Check an argument for the Crop transform.
225 Args:
226 arg: float, argument to Crop transform to check.
228 Raises:
229 apiproxy_errors.ApplicationError on problem with argument.
231 if not isinstance(arg, float):
232 raise apiproxy_errors.ApplicationError(
233 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
235 if not (0 <= arg <= 1.0):
236 raise apiproxy_errors.ApplicationError(
237 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
239 def _CalculateNewDimensions(self,
240 current_width,
241 current_height,
242 req_width,
243 req_height):
244 """Get new resize dimensions keeping the current aspect ratio.
246 This uses the more restricting of the two requested values to determine
247 the new ratio.
249 Args:
250 current_width: int, current width of the image.
251 current_height: int, current height of the image.
252 req_width: int, requested new width of the image.
253 req_height: int, requested new height of the image.
255 Returns:
256 tuple (width, height) which are both ints of the new ratio.
259 width_ratio = float(req_width) / current_width
260 height_ratio = float(req_height) / current_height
262 if req_width == 0 or (width_ratio > height_ratio and req_height != 0):
263 return int(height_ratio * current_width), req_height
264 else:
265 return req_width, int(width_ratio * current_height)
267 def _Resize(self, image, transform):
268 """Use PIL to resize the given image with the given transform.
270 Args:
271 image: PIL.Image.Image object to resize.
272 transform: images_service_pb.Transform to use when resizing.
274 Returns:
275 PIL.Image.Image with transforms performed on it.
277 Raises:
278 BadRequestError if the resize data given is bad.
280 width = 0
281 height = 0
283 if transform.has_width():
284 width = transform.width()
285 if width < 0 or 4000 < width:
286 raise apiproxy_errors.ApplicationError(
287 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
289 if transform.has_height():
290 height = transform.height()
291 if height < 0 or 4000 < height:
292 raise apiproxy_errors.ApplicationError(
293 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
295 current_width, current_height = image.size
296 new_width, new_height = self._CalculateNewDimensions(current_width,
297 current_height,
298 width,
299 height)
301 return image.resize((new_width, new_height), Image.ANTIALIAS)
303 def _Rotate(self, image, transform):
304 """Use PIL to rotate the given image with the given transform.
306 Args:
307 image: PIL.Image.Image object to rotate.
308 transform: images_service_pb.Transform to use when rotating.
310 Returns:
311 PIL.Image.Image with transforms performed on it.
313 Raises:
314 BadRequestError if the rotate data given is bad.
316 degrees = transform.rotate()
317 if degrees < 0 or degrees % 90 != 0:
318 raise apiproxy_errors.ApplicationError(
319 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
320 degrees %= 360
322 degrees = 360 - degrees
323 return image.rotate(degrees)
325 def _Crop(self, image, transform):
326 """Use PIL to crop the given image with the given transform.
328 Args:
329 image: PIL.Image.Image object to crop.
330 transform: images_service_pb.Transform to use when cropping.
332 Returns:
333 PIL.Image.Image with transforms performed on it.
335 Raises:
336 BadRequestError if the crop data given is bad.
338 left_x = 0.0
339 top_y = 0.0
340 right_x = 1.0
341 bottom_y = 1.0
343 if transform.has_crop_left_x():
344 left_x = transform.crop_left_x()
345 self._ValidateCropArg(left_x)
347 if transform.has_crop_top_y():
348 top_y = transform.crop_top_y()
349 self._ValidateCropArg(top_y)
351 if transform.has_crop_right_x():
352 right_x = transform.crop_right_x()
353 self._ValidateCropArg(right_x)
355 if transform.has_crop_bottom_y():
356 bottom_y = transform.crop_bottom_y()
357 self._ValidateCropArg(bottom_y)
359 width, height = image.size
361 box = (int(transform.crop_left_x() * width),
362 int(transform.crop_top_y() * height),
363 int(transform.crop_right_x() * width),
364 int(transform.crop_bottom_y() * height))
366 return image.crop(box)
368 def _ProcessTransforms(self, image, transforms):
369 """Execute PIL operations based on transform values.
371 Args:
372 image: PIL.Image.Image instance, image to manipulate.
373 trasnforms: list of ImagesTransformRequest.Transform objects.
375 Returns:
376 PIL.Image.Image with transforms performed on it.
378 Raises:
379 BadRequestError if we are passed more than one of the same type of
380 transform.
382 new_image = image
383 if len(transforms) > images.MAX_TRANSFORMS_PER_REQUEST:
384 raise apiproxy_errors.ApplicationError(
385 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
386 for transform in transforms:
387 if transform.has_width() or transform.has_height():
388 new_image = self._Resize(new_image, transform)
390 elif transform.has_rotate():
391 new_image = self._Rotate(new_image, transform)
393 elif transform.has_horizontal_flip():
394 new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT)
396 elif transform.has_vertical_flip():
397 new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM)
399 elif (transform.has_crop_left_x() or
400 transform.has_crop_top_y() or
401 transform.has_crop_right_x() or
402 transform.has_crop_bottom_y()):
403 new_image = self._Crop(new_image, transform)
405 elif transform.has_autolevels():
406 logging.info("I'm Feeling Lucky autolevels will be visible once this "
407 "application is deployed.")
408 else:
409 logging.warn("Found no transformations found to perform.")
411 return new_image