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
class ImagesServiceStub(apiproxy_stub.APIProxyStub):
40
"""Stub version of images API to be used with the dev_appserver."""
42
def __init__(self, service_name='images'):
43
"""Preloads PIL to load all modules in the unhardened environment.
46
service_name: Service name expected for all calls.
48
super(ImagesServiceStub, self).__init__(service_name)
51
def _Dynamic_Transform(self, request, response):
52
"""Trivial implementation of ImagesService::Transform.
54
Based off documentation of the PIL library at
55
http://www.pythonware.com/library/pil/handbook/index.htm
58
request: ImagesTransformRequest, contains image request info.
59
response: ImagesTransformResponse, contains transformed image.
61
image = request.image().content()
63
raise apiproxy_errors.ApplicationError(
64
images_service_pb.ImagesServiceError.NOT_IMAGE)
66
image = StringIO.StringIO(image)
68
original_image = Image.open(image)
70
raise apiproxy_errors.ApplicationError(
71
images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
73
img_format = original_image.format
74
if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"):
75
raise apiproxy_errors.ApplicationError(
76
images_service_pb.ImagesServiceError.NOT_IMAGE)
78
new_image = self._ProcessTransforms(original_image,
79
request.transform_list())
81
response_value = self._EncodeImage(new_image, request.output())
82
response.mutable_image().set_content(response_value)
84
def _EncodeImage(self, image, output_encoding):
85
"""Encode the given image and return it in string form.
88
image: PIL Image object, image to encode.
89
output_encoding: ImagesTransformRequest.OutputSettings object.
92
str with encoded image information in given encoding format.
94
image_string = StringIO.StringIO()
96
image_encoding = "PNG"
98
if (output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG):
99
image_encoding = "JPEG"
101
image = image.convert("RGB")
103
image.save(image_string, image_encoding)
105
return image_string.getvalue()
107
def _ValidateCropArg(self, arg):
108
"""Check an argument for the Crop transform.
111
arg: float, argument to Crop transform to check.
114
apiproxy_errors.ApplicationError on problem with argument.
116
if not isinstance(arg, float):
117
raise apiproxy_errors.ApplicationError(
118
images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
120
if not (0 <= arg <= 1.0):
121
raise apiproxy_errors.ApplicationError(
122
images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
124
def _CalculateNewDimensions(self,
129
"""Get new resize dimensions keeping the current aspect ratio.
131
This uses the more restricting of the two requested values to determine
135
current_width: int, current width of the image.
136
current_height: int, current height of the image.
137
req_width: int, requested new width of the image.
138
req_height: int, requested new height of the image.
141
tuple (width, height) which are both ints of the new ratio.
144
width_ratio = float(req_width) / current_width
145
height_ratio = float(req_height) / current_height
147
if req_width == 0 or (width_ratio > height_ratio and req_height != 0):
148
return int(height_ratio * current_width), req_height
150
return req_width, int(width_ratio * current_height)
152
def _Resize(self, image, transform):
153
"""Use PIL to resize the given image with the given transform.
156
image: PIL.Image.Image object to resize.
157
transform: images_service_pb.Transform to use when resizing.
160
PIL.Image.Image with transforms performed on it.
163
BadRequestError if the resize data given is bad.
168
if transform.has_width():
169
width = transform.width()
170
if width < 0 or 4000 < width:
171
raise apiproxy_errors.ApplicationError(
172
images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
174
if transform.has_height():
175
height = transform.height()
176
if height < 0 or 4000 < height:
177
raise apiproxy_errors.ApplicationError(
178
images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
180
current_width, current_height = image.size
181
new_width, new_height = self._CalculateNewDimensions(current_width,
186
return image.resize((new_width, new_height), Image.ANTIALIAS)
188
def _Rotate(self, image, transform):
189
"""Use PIL to rotate the given image with the given transform.
192
image: PIL.Image.Image object to rotate.
193
transform: images_service_pb.Transform to use when rotating.
196
PIL.Image.Image with transforms performed on it.
199
BadRequestError if the rotate data given is bad.
201
degrees = transform.rotate()
202
if degrees < 0 or degrees % 90 != 0:
203
raise apiproxy_errors.ApplicationError(
204
images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
207
degrees = 360 - degrees
208
return image.rotate(degrees)
210
def _Crop(self, image, transform):
211
"""Use PIL to crop the given image with the given transform.
214
image: PIL.Image.Image object to crop.
215
transform: images_service_pb.Transform to use when cropping.
218
PIL.Image.Image with transforms performed on it.
221
BadRequestError if the crop data given is bad.
228
if transform.has_crop_left_x():
229
left_x = transform.crop_left_x()
230
self._ValidateCropArg(left_x)
232
if transform.has_crop_top_y():
233
top_y = transform.crop_top_y()
234
self._ValidateCropArg(top_y)
236
if transform.has_crop_right_x():
237
right_x = transform.crop_right_x()
238
self._ValidateCropArg(right_x)
240
if transform.has_crop_bottom_y():
241
bottom_y = transform.crop_bottom_y()
242
self._ValidateCropArg(bottom_y)
244
width, height = image.size
246
box = (int(transform.crop_left_x() * width),
247
int(transform.crop_top_y() * height),
248
int(transform.crop_right_x() * width),
249
int(transform.crop_bottom_y() * height))
251
return image.crop(box)
253
def _ProcessTransforms(self, image, transforms):
254
"""Execute PIL operations based on transform values.
257
image: PIL.Image.Image instance, image to manipulate.
258
trasnforms: list of ImagesTransformRequest.Transform objects.
261
PIL.Image.Image with transforms performed on it.
264
BadRequestError if we are passed more than one of the same type of
268
if len(transforms) > images.MAX_TRANSFORMS_PER_REQUEST:
269
raise apiproxy_errors.ApplicationError(
270
images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
271
for transform in transforms:
272
if transform.has_width() or transform.has_height():
273
new_image = self._Resize(new_image, transform)
275
elif transform.has_rotate():
276
new_image = self._Rotate(new_image, transform)
278
elif transform.has_horizontal_flip():
279
new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT)
281
elif transform.has_vertical_flip():
282
new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM)
284
elif (transform.has_crop_left_x() or
285
transform.has_crop_top_y() or
286
transform.has_crop_right_x() or
287
transform.has_crop_bottom_y()):
288
new_image = self._Crop(new_image, transform)
290
elif transform.has_autolevels():
291
logging.info("I'm Feeling Lucky autolevels will be visible once this "
292
"application is deployed.")
294
logging.warn("Found no transformations found to perform.")