|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
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 |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
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. |
|
16 # |
|
17 |
|
18 """Image manipulation API. |
|
19 |
|
20 Classes defined in this module: |
|
21 Image: class used to encapsulate image information and transformations for |
|
22 that image. |
|
23 |
|
24 The current manipulations that are available are resize, rotate, |
|
25 horizontal_flip, vertical_flip, crop and im_feeling_lucky. |
|
26 |
|
27 It should be noted that each transform can only be called once per image |
|
28 per execute_transforms() call. |
|
29 """ |
|
30 |
|
31 |
|
32 |
|
33 from google.appengine.api import apiproxy_stub_map |
|
34 from google.appengine.api.images import images_service_pb |
|
35 from google.appengine.runtime import apiproxy_errors |
|
36 |
|
37 |
|
38 JPEG = images_service_pb.OutputSettings.JPEG |
|
39 PNG = images_service_pb.OutputSettings.PNG |
|
40 |
|
41 OUTPUT_ENCODING_TYPES = frozenset([JPEG, PNG]) |
|
42 |
|
43 |
|
44 class Error(Exception): |
|
45 """Base error class for this module.""" |
|
46 |
|
47 |
|
48 class TransformationError(Error): |
|
49 """Error while attempting to transform the image.""" |
|
50 |
|
51 |
|
52 class BadRequestError(Error): |
|
53 """The parameters given had something wrong with them.""" |
|
54 |
|
55 |
|
56 class NotImageError(Error): |
|
57 """The image data given is not recognizable as an image.""" |
|
58 |
|
59 |
|
60 class BadImageError(Error): |
|
61 """The image data given is corrupt.""" |
|
62 |
|
63 |
|
64 class LargeImageError(Error): |
|
65 """The image data given is too large to process.""" |
|
66 |
|
67 |
|
68 class Image(object): |
|
69 """Image object to manipulate.""" |
|
70 |
|
71 def __init__(self, image_data): |
|
72 """Constructor. |
|
73 |
|
74 Args: |
|
75 image_data: str, image data in string form. |
|
76 |
|
77 Raises: |
|
78 NotImageError if the given data is empty. |
|
79 """ |
|
80 if not image_data: |
|
81 raise NotImageError("Empty image data.") |
|
82 |
|
83 self._image_data = image_data |
|
84 self._transforms = [] |
|
85 self._transform_map = {} |
|
86 |
|
87 def _check_transform_limits(self, transform): |
|
88 """Ensure some simple limits on the number of transforms allowed. |
|
89 |
|
90 Args: |
|
91 transform: images_service_pb.ImagesServiceTransform, enum of the |
|
92 trasnform called. |
|
93 |
|
94 Raises: |
|
95 BadRequestError if the transform has already been requested for the image. |
|
96 """ |
|
97 if not images_service_pb.ImagesServiceTransform.Type_Name(transform): |
|
98 raise BadRequestError("'%s' is not a valid transform." % transform) |
|
99 |
|
100 if transform in self._transform_map: |
|
101 transform_name = images_service_pb.ImagesServiceTransform.Type_Name( |
|
102 transform) |
|
103 raise BadRequestError("A '%s' transform has already been " |
|
104 "requested on this image." % transform_name) |
|
105 self._transform_map[transform] = True |
|
106 |
|
107 def resize(self, width=0, height=0): |
|
108 """Resize the image maintaining the aspect ratio. |
|
109 |
|
110 If both width and height are specified, the more restricting of the two |
|
111 values will be used when resizing the photo. The maximum dimension allowed |
|
112 for both width and height is 4000 pixels. |
|
113 |
|
114 Args: |
|
115 width: int, width (in pixels) to change the image width to. |
|
116 height: int, height (in pixels) to change the image height to. |
|
117 |
|
118 Raises: |
|
119 TypeError when width or height is not either 'int' or 'long' types. |
|
120 BadRequestError when there is something wrong with the given height or |
|
121 width or if a Resize has already been requested on this image. |
|
122 """ |
|
123 if (not isinstance(width, (int, long)) or |
|
124 not isinstance(height, (int, long))): |
|
125 raise TypeError("Width and height must be integers.") |
|
126 if width < 0 or height < 0: |
|
127 raise BadRequestError("Width and height must be >= 0.") |
|
128 |
|
129 if not width and not height: |
|
130 raise BadRequestError("At least one of width or height must be > 0.") |
|
131 |
|
132 if width > 4000 or height > 4000: |
|
133 raise BadRequestError("Both width and height must be < 4000.") |
|
134 |
|
135 self._check_transform_limits( |
|
136 images_service_pb.ImagesServiceTransform.RESIZE) |
|
137 |
|
138 transform = images_service_pb.Transform() |
|
139 transform.set_width(width) |
|
140 transform.set_height(height) |
|
141 |
|
142 self._transforms.append(transform) |
|
143 |
|
144 def rotate(self, degrees): |
|
145 """Rotate an image a given number of degrees clockwise. |
|
146 |
|
147 Args: |
|
148 degrees: int, must be a multiple of 90. |
|
149 |
|
150 Raises: |
|
151 TypeError when degrees is not either 'int' or 'long' types. |
|
152 BadRequestError when there is something wrong with the given degrees or |
|
153 if a Rotate trasnform has already been requested. |
|
154 """ |
|
155 if not isinstance(degrees, (int, long)): |
|
156 raise TypeError("Degrees must be integers.") |
|
157 |
|
158 if degrees % 90 != 0: |
|
159 raise BadRequestError("degrees argument must be multiple of 90.") |
|
160 |
|
161 degrees = degrees % 360 |
|
162 |
|
163 self._check_transform_limits( |
|
164 images_service_pb.ImagesServiceTransform.ROTATE) |
|
165 |
|
166 transform = images_service_pb.Transform() |
|
167 transform.set_rotate(degrees) |
|
168 |
|
169 self._transforms.append(transform) |
|
170 |
|
171 def horizontal_flip(self): |
|
172 """Flip the image horizontally. |
|
173 |
|
174 Raises: |
|
175 BadRequestError if a HorizontalFlip has already been requested on the |
|
176 image. |
|
177 """ |
|
178 self._check_transform_limits( |
|
179 images_service_pb.ImagesServiceTransform.HORIZONTAL_FLIP) |
|
180 |
|
181 transform = images_service_pb.Transform() |
|
182 transform.set_horizontal_flip(True) |
|
183 |
|
184 self._transforms.append(transform) |
|
185 |
|
186 def vertical_flip(self): |
|
187 """Flip the image vertically. |
|
188 |
|
189 Raises: |
|
190 BadRequestError if a HorizontalFlip has already been requested on the |
|
191 image. |
|
192 """ |
|
193 self._check_transform_limits( |
|
194 images_service_pb.ImagesServiceTransform.VERTICAL_FLIP) |
|
195 transform = images_service_pb.Transform() |
|
196 transform.set_vertical_flip(True) |
|
197 |
|
198 self._transforms.append(transform) |
|
199 |
|
200 def _validate_crop_arg(self, val, val_name): |
|
201 """Validate the given value of a Crop() method argument. |
|
202 |
|
203 Args: |
|
204 val: float, value of the argument. |
|
205 val_name: str, name of the argument. |
|
206 |
|
207 Raises: |
|
208 TypeError if the args are not of type 'float'. |
|
209 BadRequestError when there is something wrong with the given bounding box. |
|
210 """ |
|
211 if type(val) != float: |
|
212 raise TypeError("arg '%s' must be of type 'float'." % val_name) |
|
213 |
|
214 if not (0 <= val <= 1.0): |
|
215 raise BadRequestError("arg '%s' must be between 0.0 and 1.0 " |
|
216 "(inclusive)" % val_name) |
|
217 |
|
218 def crop(self, left_x, top_y, right_x, bottom_y): |
|
219 """Crop the image. |
|
220 |
|
221 The four arguments are the scaling numbers to describe the bounding box |
|
222 which will crop the image. The upper left point of the bounding box will |
|
223 be at (left_x*image_width, top_y*image_height) the lower right point will |
|
224 be at (right_x*image_width, bottom_y*image_height). |
|
225 |
|
226 Args: |
|
227 left_x: float value between 0.0 and 1.0 (inclusive). |
|
228 top_y: float value between 0.0 and 1.0 (inclusive). |
|
229 right_x: float value between 0.0 and 1.0 (inclusive). |
|
230 bottom_y: float value between 0.0 and 1.0 (inclusive). |
|
231 |
|
232 Raises: |
|
233 TypeError if the args are not of type 'float'. |
|
234 BadRequestError when there is something wrong with the given bounding box |
|
235 or if there has already been a crop transform requested for this image. |
|
236 """ |
|
237 self._validate_crop_arg(left_x, "left_x") |
|
238 self._validate_crop_arg(top_y, "top_y") |
|
239 self._validate_crop_arg(right_x, "right_x") |
|
240 self._validate_crop_arg(bottom_y, "bottom_y") |
|
241 |
|
242 if left_x >= right_x: |
|
243 raise BadRequestError("left_x must be less than right_x") |
|
244 if top_y >= bottom_y: |
|
245 raise BadRequestError("top_y must be less than bottom_y") |
|
246 |
|
247 self._check_transform_limits(images_service_pb.ImagesServiceTransform.CROP) |
|
248 |
|
249 transform = images_service_pb.Transform() |
|
250 transform.set_crop_left_x(left_x) |
|
251 transform.set_crop_top_y(top_y) |
|
252 transform.set_crop_right_x(right_x) |
|
253 transform.set_crop_bottom_y(bottom_y) |
|
254 |
|
255 self._transforms.append(transform) |
|
256 |
|
257 def im_feeling_lucky(self): |
|
258 """Automatically adjust image contrast and color levels. |
|
259 |
|
260 This is similar to the "I'm Feeling Lucky" button in Picasa. |
|
261 |
|
262 Raises: |
|
263 BadRequestError if this transform has already been requested for this |
|
264 image. |
|
265 """ |
|
266 self._check_transform_limits( |
|
267 images_service_pb.ImagesServiceTransform.IM_FEELING_LUCKY) |
|
268 transform = images_service_pb.Transform() |
|
269 transform.set_autolevels(True) |
|
270 |
|
271 self._transforms.append(transform) |
|
272 |
|
273 def execute_transforms(self, output_encoding=PNG): |
|
274 """Perform transformations on given image. |
|
275 |
|
276 Args: |
|
277 output_encoding: A value from OUTPUT_ENCODING_TYPES. |
|
278 |
|
279 Returns: |
|
280 str, image data after the transformations have been performed on it. |
|
281 |
|
282 Raises: |
|
283 BadRequestError when there is something wrong with the request |
|
284 specifications. |
|
285 NotImageError when the image data given is not an image. |
|
286 BadImageError when the image data given is corrupt. |
|
287 LargeImageError when the image data given is too large to process. |
|
288 TransformtionError when something errors during image manipulation. |
|
289 Error when something unknown, but bad, happens. |
|
290 """ |
|
291 if output_encoding not in OUTPUT_ENCODING_TYPES: |
|
292 raise BadRequestError("Output encoding type not in recognized set " |
|
293 "%s" % OUTPUT_ENCODING_TYPES) |
|
294 |
|
295 if not self._transforms: |
|
296 raise BadRequestError("Must specify at least one transformation.") |
|
297 |
|
298 request = images_service_pb.ImagesTransformRequest() |
|
299 response = images_service_pb.ImagesTransformResponse() |
|
300 |
|
301 request.mutable_image().set_content(self._image_data) |
|
302 |
|
303 for transform in self._transforms: |
|
304 request.add_transform().CopyFrom(transform) |
|
305 |
|
306 request.mutable_output().set_mime_type(output_encoding) |
|
307 |
|
308 try: |
|
309 apiproxy_stub_map.MakeSyncCall("images", |
|
310 "Transform", |
|
311 request, |
|
312 response) |
|
313 except apiproxy_errors.ApplicationError, e: |
|
314 if (e.application_error == |
|
315 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA): |
|
316 raise BadRequestError() |
|
317 elif (e.application_error == |
|
318 images_service_pb.ImagesServiceError.NOT_IMAGE): |
|
319 raise NotImageError() |
|
320 elif (e.application_error == |
|
321 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA): |
|
322 raise BadImageError() |
|
323 elif (e.application_error == |
|
324 images_service_pb.ImagesServiceError.IMAGE_TOO_LARGE): |
|
325 raise LargeImageError() |
|
326 elif (e.application_error == |
|
327 images_service_pb.ImagesServiceError.UNSPECIFIED_ERROR): |
|
328 raise TransformationError() |
|
329 else: |
|
330 raise Error() |
|
331 |
|
332 self._image_data = response.image().content() |
|
333 self._transforms = [] |
|
334 self._transform_map.clear() |
|
335 return self._image_data |
|
336 |
|
337 |
|
338 def resize(image_data, width=0, height=0, output_encoding=PNG): |
|
339 """Resize a given image file maintaining the aspect ratio. |
|
340 |
|
341 If both width and height are specified, the more restricting of the two |
|
342 values will be used when resizing the photo. The maximum dimension allowed |
|
343 for both width and height is 4000 pixels. |
|
344 |
|
345 Args: |
|
346 image_data: str, source image data. |
|
347 width: int, width (in pixels) to change the image width to. |
|
348 height: int, height (in pixels) to change the image height to. |
|
349 output_encoding: a value from OUTPUT_ENCODING_TYPES. |
|
350 |
|
351 Raises: |
|
352 TypeError when width or height not either 'int' or 'long' types. |
|
353 BadRequestError when there is something wrong with the given height or |
|
354 width or if a Resize has already been requested on this image. |
|
355 Error when something went wrong with the call. See Image.ExecuteTransforms |
|
356 for more details. |
|
357 """ |
|
358 image = Image(image_data) |
|
359 image.resize(width, height) |
|
360 return image.execute_transforms(output_encoding=output_encoding) |
|
361 |
|
362 |
|
363 def rotate(image_data, degrees, output_encoding=PNG): |
|
364 """Rotate a given image a given number of degrees clockwise. |
|
365 |
|
366 Args: |
|
367 image_data: str, source image data. |
|
368 degrees: value from ROTATE_DEGREE_VALUES. |
|
369 output_encoding: a value from OUTPUT_ENCODING_TYPES. |
|
370 |
|
371 Raises: |
|
372 TypeError when degrees is not either 'int' or 'long' types. |
|
373 BadRequestError when there is something wrong with the given degrees or |
|
374 if a Rotate trasnform has already been requested. |
|
375 Error when something went wrong with the call. See Image.ExecuteTransforms |
|
376 for more details. |
|
377 """ |
|
378 image = Image(image_data) |
|
379 image.rotate(degrees) |
|
380 return image.execute_transforms(output_encoding=output_encoding) |
|
381 |
|
382 |
|
383 def horizontal_flip(image_data, output_encoding=PNG): |
|
384 """Flip the image horizontally. |
|
385 |
|
386 Args: |
|
387 image_data: str, source image data. |
|
388 output_encoding: a value from OUTPUT_ENCODING_TYPES. |
|
389 |
|
390 Raises: |
|
391 Error when something went wrong with the call. See Image.ExecuteTransforms |
|
392 for more details. |
|
393 """ |
|
394 image = Image(image_data) |
|
395 image.horizontal_flip() |
|
396 return image.execute_transforms(output_encoding=output_encoding) |
|
397 |
|
398 |
|
399 def vertical_flip(image_data, output_encoding=PNG): |
|
400 """Flip the image vertically. |
|
401 |
|
402 Args: |
|
403 image_data: str, source image data. |
|
404 output_encoding: a value from OUTPUT_ENCODING_TYPES. |
|
405 |
|
406 Raises: |
|
407 Error when something went wrong with the call. See Image.ExecuteTransforms |
|
408 for more details. |
|
409 """ |
|
410 image = Image(image_data) |
|
411 image.vertical_flip() |
|
412 return image.execute_transforms(output_encoding=output_encoding) |
|
413 |
|
414 |
|
415 def crop(image_data, left_x, top_y, right_x, bottom_y, output_encoding=PNG): |
|
416 """Crop the given image. |
|
417 |
|
418 The four arguments are the scaling numbers to describe the bounding box |
|
419 which will crop the image. The upper left point of the bounding box will |
|
420 be at (left_x*image_width, top_y*image_height) the lower right point will |
|
421 be at (right_x*image_width, bottom_y*image_height). |
|
422 |
|
423 Args: |
|
424 image_data: str, source image data. |
|
425 left_x: float value between 0.0 and 1.0 (inclusive). |
|
426 top_y: float value between 0.0 and 1.0 (inclusive). |
|
427 right_x: float value between 0.0 and 1.0 (inclusive). |
|
428 bottom_y: float value between 0.0 and 1.0 (inclusive). |
|
429 output_encoding: a value from OUTPUT_ENCODING_TYPES. |
|
430 |
|
431 Raises: |
|
432 TypeError if the args are not of type 'float'. |
|
433 BadRequestError when there is something wrong with the given bounding box |
|
434 or if there has already been a crop transform requested for this image. |
|
435 Error when something went wrong with the call. See Image.ExecuteTransforms |
|
436 for more details. |
|
437 """ |
|
438 image = Image(image_data) |
|
439 image.crop(left_x, top_y, right_x, bottom_y) |
|
440 return image.execute_transforms(output_encoding=output_encoding) |
|
441 |
|
442 |
|
443 def im_feeling_lucky(image_data, output_encoding=PNG): |
|
444 """Automatically adjust image levels. |
|
445 |
|
446 This is similar to the "I'm Feeling Lucky" button in Picasa. |
|
447 |
|
448 Args: |
|
449 image_data: str, source image data. |
|
450 output_encoding: a value from OUTPUT_ENCODING_TYPES. |
|
451 |
|
452 Raises: |
|
453 Error when something went wrong with the call. See Image.ExecuteTransforms |
|
454 for more details. |
|
455 """ |
|
456 image = Image(image_data) |
|
457 image.im_feeling_lucky() |
|
458 return image.execute_transforms(output_encoding=output_encoding) |