App Engine Python SDK version 1.9.12
[gae.git] / python / google / appengine / api / images / images_stub.py
blob7db54c66b0e7ddf23c3d568c31299d8712ac0718
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.
21 """Stub version of the images API."""
28 import datetime
29 import logging
30 import re
31 import StringIO
32 import time
36 try:
37 import json as simplejson
38 except ImportError:
39 import simplejson
41 try:
42 import PIL
43 from PIL import _imaging
44 from PIL import Image
45 except ImportError:
46 import _imaging
47 # Try importing the 'Image' module directly. If that fails, try
48 # importing it from the 'PIL' package (this is necessary to also
49 # cover "pillow" package installations).
50 try:
51 import Image
52 except ImportError:
53 from PIL import Image
55 from google.appengine.api import apiproxy_stub
56 from google.appengine.api import apiproxy_stub_map
57 from google.appengine.api import datastore
58 from google.appengine.api import datastore_errors
59 from google.appengine.api import datastore_types
60 from google.appengine.api import images
61 from google.appengine.api.blobstore import blobstore_stub
62 from google.appengine.api.images import images_blob_stub
63 from google.appengine.api.images import images_service_pb
64 from google.appengine.runtime import apiproxy_errors
68 BLOB_SERVING_URL_KIND = images_blob_stub.BLOB_SERVING_URL_KIND
69 BMP = 'BMP'
70 GIF = 'GIF'
71 GS_INFO_KIND = '__GsFileInfo__'
72 ICO = 'ICO'
73 JPEG = 'JPEG'
74 MAX_REQUEST_SIZE = 32 << 20 # 32MB
75 PNG = 'PNG'
76 RGB = 'RGB'
77 RGBA = 'RGBA'
78 TIFF = 'TIFF'
79 WEBP = 'WEBP'
81 FORMAT_LIST = [BMP, GIF, ICO, JPEG, PNG, TIFF, WEBP]
82 EXIF_TIME_REGEX = re.compile(r'^([0-9]{4}):([0-9]{1,2}):([0-9]{1,2})'
83 ' ([0-9]{1,2}):([0-9]{1,2})(?::([0-9]{1,2}))?')
86 # Orientation tag id in EXIF.
87 _EXIF_ORIENTATION_TAG = 274
89 # DateTimeOriginal tag in EXIF.
90 _EXIF_DATETIMEORIGINAL_TAG = 36867
92 # Subset of EXIF tags. The stub is only able to extract these fields.
93 _EXIF_TAGS = {
94 256: 'ImageWidth',
95 257: 'ImageLength',
96 271: 'Make',
97 272: 'Model',
98 _EXIF_ORIENTATION_TAG: 'Orientation',
99 305: 'Software',
100 306: 'DateTime',
101 34855: 'ISOSpeedRatings',
102 _EXIF_DATETIMEORIGINAL_TAG: 'DateTimeOriginal',
103 36868: 'DateTimeDigitized',
104 37383: 'MeteringMode',
105 37385: 'Flash',
106 41987: 'WhiteBalance'}
109 def _ArgbToRgbaTuple(argb):
110 """Convert from a single ARGB value to a tuple containing RGBA.
112 Args:
113 argb: Signed 32 bit integer containing an ARGB value.
115 Returns:
116 RGBA tuple.
119 unsigned_argb = argb % 0x100000000
120 return ((unsigned_argb >> 16) & 0xFF,
121 (unsigned_argb >> 8) & 0xFF,
122 unsigned_argb & 0xFF,
123 (unsigned_argb >> 24) & 0xFF)
126 def _BackendPremultiplication(color):
127 """Apply premultiplication and unpremultiplication to match production.
129 Args:
130 color: color tuple as returned by _ArgbToRgbaTuple.
132 Returns:
133 RGBA tuple.
139 alpha = color[3]
140 rgb = color[0:3]
141 multiplied = [(x * (alpha + 1)) >> 8 for x in rgb]
142 if alpha:
143 alpha_inverse = 0xffffff / alpha
144 unmultiplied = [(x * alpha_inverse) >> 16 for x in multiplied]
145 else:
146 unmultiplied = [0] * 3
148 return tuple(unmultiplied + [alpha])
151 class ImagesServiceStub(apiproxy_stub.APIProxyStub):
152 """Stub version of images API to be used with the dev_appserver."""
154 def __init__(self, service_name='images', host_prefix=''):
155 """Preloads PIL to load all modules in the unhardened environment.
157 Args:
158 service_name: Service name expected for all calls.
159 host_prefix: the URL prefix (protocol://host:port) to preprend to
160 image urls on a call to GetUrlBase.
162 super(ImagesServiceStub, self).__init__(
163 service_name, max_request_size=MAX_REQUEST_SIZE)
164 self._blob_stub = images_blob_stub.ImagesBlobStub(host_prefix)
165 Image.init()
167 def _Dynamic_Composite(self, request, response):
168 """Implementation of ImagesService::Composite.
170 Based off documentation of the PIL library at
171 http://www.pythonware.com/library/pil/handbook/index.htm
173 Args:
174 request: ImagesCompositeRequest - Contains image request info.
175 response: ImagesCompositeResponse - Contains transformed image.
177 Raises:
178 ApplicationError: Bad data was provided, likely data about the dimensions.
180 if (not request.canvas().width() or not request.canvas().height() or
181 not request.image_size() or not request.options_size()):
182 raise apiproxy_errors.ApplicationError(
183 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
184 if (request.canvas().width() > 4000 or
185 request.canvas().height() > 4000 or
186 request.options_size() > images.MAX_COMPOSITES_PER_REQUEST):
187 raise apiproxy_errors.ApplicationError(
188 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
190 width = request.canvas().width()
191 height = request.canvas().height()
192 color = _ArgbToRgbaTuple(request.canvas().color())
195 color = _BackendPremultiplication(color)
196 canvas = Image.new(RGBA, (width, height), color)
197 sources = []
198 for image in request.image_list():
199 sources.append(self._OpenImageData(image))
201 for options in request.options_list():
202 if (options.anchor() < images.TOP_LEFT or
203 options.anchor() > images.BOTTOM_RIGHT):
204 raise apiproxy_errors.ApplicationError(
205 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
206 if options.source_index() >= len(sources) or options.source_index() < 0:
207 raise apiproxy_errors.ApplicationError(
208 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
209 if options.opacity() < 0 or options.opacity() > 1:
210 raise apiproxy_errors.ApplicationError(
211 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
212 source = sources[options.source_index()]
213 x_anchor = (options.anchor() % 3) * 0.5
214 y_anchor = (options.anchor() / 3) * 0.5
215 x_offset = int(options.x_offset() + x_anchor * (width - source.size[0]))
216 y_offset = int(options.y_offset() + y_anchor * (height - source.size[1]))
217 if source.mode == RGBA:
218 canvas.paste(source, (x_offset, y_offset), source)
219 else:
220 alpha = options.opacity() * 255
221 mask = Image.new('L', source.size, alpha)
222 canvas.paste(source, (x_offset, y_offset), mask)
223 response_value = self._EncodeImage(canvas, request.canvas().output())
224 response.mutable_image().set_content(response_value)
226 def _Dynamic_Histogram(self, request, response):
227 """Trivial implementation of an API.
229 Based off documentation of the PIL library at
230 http://www.pythonware.com/library/pil/handbook/index.htm
232 Args:
233 request: ImagesHistogramRequest - Contains the image.
234 response: ImagesHistogramResponse - Contains histogram of the image.
236 Raises:
237 ApplicationError: Image was of an unsupported format.
239 image = self._OpenImageData(request.image())
241 img_format = image.format
242 if img_format not in FORMAT_LIST:
243 raise apiproxy_errors.ApplicationError(
244 images_service_pb.ImagesServiceError.NOT_IMAGE)
245 image = image.convert(RGBA)
246 red = [0] * 256
247 green = [0] * 256
248 blue = [0] * 256
253 for pixel in image.getdata():
254 red[int((pixel[0] * pixel[3]) / 255)] += 1
255 green[int((pixel[1] * pixel[3]) / 255)] += 1
256 blue[int((pixel[2] * pixel[3]) / 255)] += 1
257 histogram = response.mutable_histogram()
258 for value in red:
259 histogram.add_red(value)
260 for value in green:
261 histogram.add_green(value)
262 for value in blue:
263 histogram.add_blue(value)
265 def _Dynamic_Transform(self, request, response):
266 """Trivial implementation of ImagesService::Transform.
268 Based off documentation of the PIL library at
269 http://www.pythonware.com/library/pil/handbook/index.htm
271 Args:
272 request: ImagesTransformRequest, contains image request info.
273 response: ImagesTransformResponse, contains transformed image.
275 original_image = self._OpenImageData(request.image())
277 input_settings = request.input()
278 correct_orientation = (
279 input_settings.has_correct_exif_orientation() and
280 input_settings.correct_exif_orientation() ==
281 images_service_pb.InputSettings.CORRECT_ORIENTATION)
285 source_metadata = self._ExtractMetadata(
286 original_image, input_settings.parse_metadata())
287 if input_settings.parse_metadata():
288 logging.info(
289 'Once the application is deployed, a more powerful metadata '
290 'extraction will be performed which might return many more fields.')
292 new_image = self._ProcessTransforms(
293 original_image, request.transform_list(), correct_orientation)
295 substitution_rgb = None
296 if input_settings.has_transparent_substitution_rgb():
297 substitution_rgb = input_settings.transparent_substitution_rgb()
298 response_value = self._EncodeImage(
299 new_image, request.output(), substitution_rgb)
300 response.mutable_image().set_content(response_value)
301 response.set_source_metadata(source_metadata)
303 def _Dynamic_GetUrlBase(self, request, response):
304 self._blob_stub.GetUrlBase(request, response)
306 def _Dynamic_DeleteUrlBase(self, request, response):
307 self._blob_stub.DeleteUrlBase(request, response)
309 def _EncodeImage(self, image, output_encoding, substitution_rgb=None):
310 """Encode the given image and return it in string form.
312 Args:
313 image: PIL Image object, image to encode.
314 output_encoding: ImagesTransformRequest.OutputSettings object.
315 substitution_rgb: The color to use for transparent pixels if the output
316 format does not support transparency.
318 Returns:
319 str - Encoded image information in given encoding format. Default is PNG.
321 image_string = StringIO.StringIO()
322 image_encoding = PNG
324 if output_encoding.mime_type() == images_service_pb.OutputSettings.WEBP:
325 image_encoding = WEBP
327 if output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG:
328 image_encoding = JPEG
335 if substitution_rgb:
339 blue = substitution_rgb & 0xFF
340 green = (substitution_rgb >> 8) & 0xFF
341 red = (substitution_rgb >> 16) & 0xFF
342 background = Image.new(RGB, image.size, (red, green, blue))
343 background.paste(image, mask=image.split()[3])
344 image = background
345 else:
346 image = image.convert(RGB)
348 image.save(image_string, image_encoding)
349 return image_string.getvalue()
351 def _OpenImageData(self, image_data):
352 """Open image data from ImageData protocol buffer.
354 Args:
355 image_data: ImageData protocol buffer containing image data or blob
356 reference.
358 Returns:
359 Image containing the image data passed in or reference by blob-key.
361 Raises:
362 ApplicationError: Both content and blob-key are provided.
363 NOTE: 'content' must always be set because it is a required field,
364 however, it must be the empty string when a blob-key is provided.
366 if image_data.content() and image_data.has_blob_key():
367 raise apiproxy_errors.ApplicationError(
368 images_service_pb.ImagesServiceError.INVALID_BLOB_KEY)
370 if image_data.has_blob_key():
371 image = self._OpenBlob(image_data.blob_key())
372 else:
373 image = self._OpenImage(image_data.content())
376 img_format = image.format
377 if img_format not in FORMAT_LIST:
378 raise apiproxy_errors.ApplicationError(
379 images_service_pb.ImagesServiceError.NOT_IMAGE)
380 return image
382 def _OpenImage(self, image):
383 """Opens an image provided as a string.
385 Args:
386 image: Image data to be opened.
388 Raises:
389 ApplicationError: Image could not be opened or was an unsupported format.
391 Returns:
392 Image containing the image data passed in.
394 if not image:
395 raise apiproxy_errors.ApplicationError(
396 images_service_pb.ImagesServiceError.NOT_IMAGE)
398 image = StringIO.StringIO(image)
399 try:
400 return Image.open(image)
401 except IOError:
403 raise apiproxy_errors.ApplicationError(
404 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
406 def _OpenBlob(self, blob_key):
407 """Create an Image from the blob data read from blob_key."""
409 try:
410 _ = datastore.Get(
411 blobstore_stub.BlobstoreServiceStub.ToDatastoreBlobKey(blob_key))
412 except datastore_errors.Error:
415 logging.exception('Blob with key %r does not exist', blob_key)
416 raise apiproxy_errors.ApplicationError(
417 images_service_pb.ImagesServiceError.UNSPECIFIED_ERROR)
419 blobstore_storage = apiproxy_stub_map.apiproxy.GetStub('blobstore')
422 try:
423 blob_file = blobstore_storage.storage.OpenBlob(blob_key)
424 except IOError:
425 logging.exception('Could not get file for blob_key %r', blob_key)
427 raise apiproxy_errors.ApplicationError(
428 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
430 try:
431 return Image.open(blob_file)
432 except IOError:
433 logging.exception('Could not open image %r for blob_key %r',
434 blob_file, blob_key)
436 raise apiproxy_errors.ApplicationError(
437 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
439 def _ValidateCropArg(self, arg):
440 """Check an argument for the Crop transform.
442 Args:
443 arg: float - Argument to Crop transform to check.
445 Raises:
446 ApplicationError: There was a problem with the provided argument.
448 if not isinstance(arg, float):
449 raise apiproxy_errors.ApplicationError(
450 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
452 if 0 > arg or arg > 1.0:
453 raise apiproxy_errors.ApplicationError(
454 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
456 def _CalculateNewDimensions(self,
457 current_width,
458 current_height,
459 req_width,
460 req_height,
461 crop_to_fit,
462 allow_stretch):
463 """Get new resize dimensions keeping the current aspect ratio.
465 This uses the more restricting of the two requested values to determine
466 the new ratio. See also crop_to_fit.
468 Args:
469 current_width: int, current width of the image.
470 current_height: int, current height of the image.
471 req_width: int, requested new width of the image, 0 if unspecified.
472 req_height: int, requested new height of the image, 0 if unspecified.
473 crop_to_fit: bool, True if the less restricting dimension should be used.
474 allow_stretch: bool, True is aspect ratio should be ignored.
476 Raises:
477 apiproxy_errors.ApplicationError: if crop_to_fit is True either req_width
478 or req_height is 0.
480 Returns:
481 tuple (width, height) ints of the new dimensions.
485 width_ratio = float(req_width) / current_width
486 height_ratio = float(req_height) / current_height
488 height = req_height
489 width = req_width
490 if allow_stretch or crop_to_fit:
492 if not req_width or not req_height:
493 raise apiproxy_errors.ApplicationError(
494 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
495 if not allow_stretch:
496 if width_ratio > height_ratio:
497 height = int(width_ratio * current_height)
498 else:
499 width = int(height_ratio * current_width)
500 else:
504 if not req_width or (width_ratio > height_ratio and req_height):
506 width = int(height_ratio * current_width)
507 else:
509 height = int(width_ratio * current_height)
510 return width, height
512 def _Resize(self, image, transform):
513 """Use PIL to resize the given image with the given transform.
515 Args:
516 image: PIL.Image.Image object to resize.
517 transform: images_service_pb.Transform to use when resizing.
519 Returns:
520 PIL.Image.Image with transforms performed on it.
522 Raises:
523 ApplicationError: The resize data given was bad.
525 width = 0
526 height = 0
528 if transform.has_width():
529 width = transform.width()
530 if width < 0 or 4000 < width:
531 raise apiproxy_errors.ApplicationError(
532 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
534 if transform.has_height():
535 height = transform.height()
536 if height < 0 or 4000 < height:
537 raise apiproxy_errors.ApplicationError(
538 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
540 crop_to_fit = transform.crop_to_fit()
541 allow_stretch = transform.allow_stretch()
543 current_width, current_height = image.size
544 new_width, new_height = self._CalculateNewDimensions(
545 current_width, current_height, width, height, crop_to_fit,
546 allow_stretch)
547 new_image = image.resize((new_width, new_height), Image.ANTIALIAS)
548 if crop_to_fit and (new_width > width or new_height > height):
550 left = int((new_width - width) * transform.crop_offset_x())
551 top = int((new_height - height) * transform.crop_offset_y())
552 right = left + width
553 bottom = top + height
554 new_image = new_image.crop((left, top, right, bottom))
556 return new_image
558 def _Rotate(self, image, transform):
559 """Use PIL to rotate the given image with the given transform.
561 Args:
562 image: PIL.Image.Image object to rotate.
563 transform: images_service_pb.Transform to use when rotating.
565 Returns:
566 PIL.Image.Image with transforms performed on it.
568 Raises:
569 ApplicationError: Given data for the rotate was bad.
571 degrees = transform.rotate()
572 if degrees < 0 or degrees % 90 != 0:
573 raise apiproxy_errors.ApplicationError(
574 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
575 degrees %= 360
578 degrees = 360 - degrees
579 return image.rotate(degrees)
581 def _Crop(self, image, transform):
582 """Use PIL to crop the given image with the given transform.
584 Args:
585 image: PIL.Image.Image object to crop.
586 transform: images_service_pb.Transform to use when cropping.
588 Returns:
589 PIL.Image.Image with transforms performed on it.
591 Raises:
592 BadRequestError if the crop data given is bad.
594 left_x = 0.0
595 top_y = 0.0
596 right_x = 1.0
597 bottom_y = 1.0
599 if transform.has_crop_left_x():
600 left_x = transform.crop_left_x()
601 self._ValidateCropArg(left_x)
603 if transform.has_crop_top_y():
604 top_y = transform.crop_top_y()
605 self._ValidateCropArg(top_y)
607 if transform.has_crop_right_x():
608 right_x = transform.crop_right_x()
609 self._ValidateCropArg(right_x)
611 if transform.has_crop_bottom_y():
612 bottom_y = transform.crop_bottom_y()
613 self._ValidateCropArg(bottom_y)
616 width, height = image.size
618 box = (int(round(left_x * width)),
619 int(round(top_y * height)),
620 int(round(right_x * width)),
621 int(round(bottom_y * height)))
623 return image.crop(box)
625 @staticmethod
626 def _GetExifFromImage(image):
627 if hasattr(image, '_getexif'):
633 try:
635 from PIL import TiffImagePlugin
637 return image._getexif()
638 except ImportError:
639 # We have not managed to get this to work in the SDK with Python
640 # 2.5, so just catch the ImportError and pretend there is no
641 # EXIF information of interest.
642 logging.info('Sorry, TiffImagePlugin does not work in this environment')
643 return None
645 @staticmethod
646 def _ExtractMetadata(image, parse_metadata):
647 """Extract EXIF metadata from the image.
649 Note that this is a much simplified version of metadata extraction. After
650 deployment applications have access to a more powerful parser that can
651 parse hundreds of fields from images.
653 Args:
654 image: PIL Image object.
655 parse_metadata: bool, True if metadata parsing has been requested. If
656 False the result will contain image dimensions.
657 Returns:
658 str - JSON encoded values with various metadata fields.
661 def ExifTimeToUnixtime(exif_time):
662 """Convert time in EXIF to unix time.
664 Args:
665 exif_time: str - Time from the EXIF block formated by EXIF standard.
666 Seconds are optional. (Example: '2011:02:20 10:23:12')
668 Returns:
669 Integer, the time in unix fromat: seconds since the epoch.
671 match = EXIF_TIME_REGEX.match(exif_time)
672 if not match:
673 return None
674 try:
675 date = datetime.datetime(*map(int, filter(None, match.groups())))
676 except ValueError:
677 logging.info('Invalid date in EXIF: %s', exif_time)
678 return None
679 return int(time.mktime(date.timetuple()))
681 metadata_dict = (
682 parse_metadata and ImagesServiceStub._GetExifFromImage(image) or {})
684 metadata_dict[256], metadata_dict[257] = image.size
688 if _EXIF_DATETIMEORIGINAL_TAG in metadata_dict:
689 date_ms = ExifTimeToUnixtime(metadata_dict[_EXIF_DATETIMEORIGINAL_TAG])
690 if date_ms:
691 metadata_dict[_EXIF_DATETIMEORIGINAL_TAG] = date_ms
692 else:
693 del metadata_dict[_EXIF_DATETIMEORIGINAL_TAG]
694 metadata = dict(
695 [(_EXIF_TAGS[k], v) for k, v in metadata_dict.iteritems()
696 if k in _EXIF_TAGS])
697 return simplejson.dumps(metadata)
699 def _CorrectOrientation(self, image, orientation):
700 """Use PIL to correct the image orientation based on its EXIF.
702 See JEITA CP-3451 at http://www.exif.org/specifications.html,
703 Exif 2.2, page 18.
705 Args:
706 image: source PIL.Image.Image object.
707 orientation: integer in range (1,8) inclusive, corresponding the image
708 orientation from EXIF.
710 Returns:
711 PIL.Image.Image with transforms performed on it. If no correction was
712 done, it returns the input image.
716 if orientation == 2:
717 image = image.transpose(Image.FLIP_LEFT_RIGHT)
718 elif orientation == 3:
719 image = image.rotate(180)
720 elif orientation == 4:
721 image = image.transpose(Image.FLIP_TOP_BOTTOM)
722 elif orientation == 5:
723 image = image.transpose(Image.FLIP_TOP_BOTTOM)
724 image = image.rotate(270)
725 elif orientation == 6:
726 image = image.rotate(270)
727 elif orientation == 7:
728 image = image.transpose(Image.FLIP_LEFT_RIGHT)
729 image = image.rotate(270)
730 elif orientation == 8:
731 image = image.rotate(90)
733 return image
735 def _ProcessTransforms(self, image, transforms, correct_orientation):
736 """Execute PIL operations based on transform values.
738 Args:
739 image: PIL.Image.Image instance, image to manipulate.
740 transforms: list of ImagesTransformRequest.Transform objects.
741 correct_orientation: True to indicate that image orientation should be
742 corrected based on its EXIF.
743 Returns:
744 PIL.Image.Image with transforms performed on it.
746 Raises:
747 ApplicationError: More than one of the same type of transform was present.
749 new_image = image
750 if len(transforms) > images.MAX_TRANSFORMS_PER_REQUEST:
751 raise apiproxy_errors.ApplicationError(
752 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
754 orientation = 1
755 if correct_orientation:
758 exif = self._GetExifFromImage(image)
759 if not exif or _EXIF_ORIENTATION_TAG not in exif:
760 correct_orientation = False
761 else:
762 orientation = exif[_EXIF_ORIENTATION_TAG]
764 width, height = new_image.size
765 if height > width:
766 orientation = 1
768 for transform in transforms:
776 if (correct_orientation and
777 not (transform.has_crop_left_x() or
778 transform.has_crop_top_y() or
779 transform.has_crop_right_x() or
780 transform.has_crop_bottom_y()) and
781 not transform.has_horizontal_flip() and
782 not transform.has_vertical_flip()):
783 new_image = self._CorrectOrientation(new_image, orientation)
784 correct_orientation = False
786 if transform.has_width() or transform.has_height():
788 new_image = self._Resize(new_image, transform)
790 elif transform.has_rotate():
792 new_image = self._Rotate(new_image, transform)
794 elif transform.has_horizontal_flip():
796 new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT)
798 elif transform.has_vertical_flip():
800 new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM)
802 elif (transform.has_crop_left_x() or
803 transform.has_crop_top_y() or
804 transform.has_crop_right_x() or
805 transform.has_crop_bottom_y()):
807 new_image = self._Crop(new_image, transform)
809 elif transform.has_autolevels():
812 logging.info('I\'m Feeling Lucky autolevels will be visible once this '
813 'application is deployed.')
814 else:
815 logging.warn('Found no transformations found to perform.')
817 if correct_orientation:
820 new_image = self._CorrectOrientation(new_image, orientation)
821 correct_orientation = False
826 return new_image