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."""
27 from PIL
import _imaging
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.
43 argb: Signed 32 bit integer containing an ARGB value.
48 unsigned_argb
= argb
% 0x100000000
49 return ((unsigned_argb
>> 16) & 0xFF,
50 (unsigned_argb
>> 8) & 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.
62 service_name: Service name expected for all calls.
64 super(ImagesServiceStub
, self
).__init
__(service_name
)
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
74 request: ImagesCompositeRequest, contains image request info.
75 response: ImagesCompositeResponse, contains transformed image.
77 width
= request
.canvas().width()
78 height
= request
.canvas().height()
79 color
= _ArgbToRgbaTuple(request
.canvas().color())
80 canvas
= Image
.new("RGBA", (width
, height
), color
)
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
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")
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()
145 histogram
.add_red(value
)
147 histogram
.add_green(value
)
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
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.
173 image: PIL Image object, image to encode.
174 output_encoding: ImagesTransformRequest.OutputSettings object.
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.
196 image: image data to be opened
199 apiproxy_errors.ApplicationError if the image cannot be opened or if it
200 is an unsupported format.
203 Image containing the image data passed in.
206 raise apiproxy_errors
.ApplicationError(
207 images_service_pb
.ImagesServiceError
.NOT_IMAGE
)
209 image
= StringIO
.StringIO(image
)
211 image
= Image
.open(image
)
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
)
222 def _ValidateCropArg(self
, arg
):
223 """Check an argument for the Crop transform.
226 arg: float, argument to Crop transform to check.
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
,
244 """Get new resize dimensions keeping the current aspect ratio.
246 This uses the more restricting of the two requested values to determine
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.
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
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.
271 image: PIL.Image.Image object to resize.
272 transform: images_service_pb.Transform to use when resizing.
275 PIL.Image.Image with transforms performed on it.
278 BadRequestError if the resize data given is bad.
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
,
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.
307 image: PIL.Image.Image object to rotate.
308 transform: images_service_pb.Transform to use when rotating.
311 PIL.Image.Image with transforms performed on it.
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
)
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.
329 image: PIL.Image.Image object to crop.
330 transform: images_service_pb.Transform to use when cropping.
333 PIL.Image.Image with transforms performed on it.
336 BadRequestError if the crop data given is bad.
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.
372 image: PIL.Image.Image instance, image to manipulate.
373 trasnforms: list of ImagesTransformRequest.Transform objects.
376 PIL.Image.Image with transforms performed on it.
379 BadRequestError if we are passed more than one of the same type of
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.")
409 logging
.warn("Found no transformations found to perform.")