App Engine Python SDK version 1.8.1
[gae.git] / python / google / appengine / api / images / images_stub.py
blobb899a832320bd9d533adf7709b1c4f5ac40789ca
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."""
29 import datetime
30 import logging
31 import re
32 import time
33 import StringIO
35 try:
36 import json as simplejson
37 except ImportError:
38 import simplejson
40 try:
41 import PIL
42 from PIL import _imaging
43 from PIL import Image
44 except ImportError:
45 import _imaging
46 # Try importing the 'Image' module directly. If that fails, try
47 # importing it from the 'PIL' package (this is necessary to also
48 # cover "pillow" package installations).
49 try:
50 import Image
51 except ImportError:
52 from PIL import Image
54 from google.appengine.api import apiproxy_stub
55 from google.appengine.api import apiproxy_stub_map
56 from google.appengine.api import datastore
57 from google.appengine.api import datastore_errors
58 from google.appengine.api import datastore_types
59 from google.appengine.api import images
60 from google.appengine.api.blobstore import blobstore_stub
61 from google.appengine.api.images import images_blob_stub
62 from google.appengine.api.images import images_service_pb
63 from google.appengine.runtime import apiproxy_errors
69 GS_INFO_KIND = "__GsFileInfo__"
71 BLOB_SERVING_URL_KIND = images_blob_stub.BLOB_SERVING_URL_KIND
73 MAX_REQUEST_SIZE = 32 << 20
76 _EXIF_ORIENTATION_TAG = 274
79 _EXIF_DATETIMEORIGINAL_TAG = 36867
82 _EXIF_TAGS = {
83 256: "ImageWidth",
84 257: "ImageLength",
85 271: "Make",
86 272: "Model",
87 _EXIF_ORIENTATION_TAG: "Orientation",
88 305: "Software",
89 306: "DateTime",
90 34855: "ISOSpeedRatings",
91 _EXIF_DATETIMEORIGINAL_TAG: "DateTimeOriginal",
92 36868: "DateTimeDigitized",
93 37383: "MeteringMode",
94 37385: "Flash",
95 41987: "WhiteBallance"}
98 def _ArgbToRgbaTuple(argb):
99 """Convert from a single ARGB value to a tuple containing RGBA.
101 Args:
102 argb: Signed 32 bit integer containing an ARGB value.
104 Returns:
105 RGBA tuple.
108 unsigned_argb = argb % 0x100000000
109 return ((unsigned_argb >> 16) & 0xFF,
110 (unsigned_argb >> 8) & 0xFF,
111 unsigned_argb & 0xFF,
112 (unsigned_argb >> 24) & 0xFF)
115 def _BackendPremultiplication(color):
116 """Apply premultiplication and unpremultiplication to match production.
118 Args:
119 color: color tuple as returned by _ArgbToRgbaTuple.
121 Returns:
122 RGBA tuple.
128 alpha = color[3]
129 rgb = color[0:3]
130 multiplied = [(x * (alpha + 1)) >> 8 for x in rgb]
131 if alpha:
132 alpha_inverse = 0xffffff / alpha
133 unmultiplied = [(x * alpha_inverse) >> 16 for x in multiplied]
134 else:
135 unmultiplied = [0] * 3
137 return tuple(unmultiplied + [alpha])
140 class ImagesServiceStub(apiproxy_stub.APIProxyStub):
141 """Stub version of images API to be used with the dev_appserver."""
143 def __init__(self, service_name="images", host_prefix=""):
144 """Preloads PIL to load all modules in the unhardened environment.
146 Args:
147 service_name: Service name expected for all calls.
148 host_prefix: the URL prefix (protocol://host:port) to preprend to
149 image urls on a call to GetUrlBase.
151 super(ImagesServiceStub, self).__init__(service_name,
152 max_request_size=MAX_REQUEST_SIZE)
153 self._blob_stub = images_blob_stub.ImagesBlobStub(host_prefix)
154 Image.init()
156 def _Dynamic_Composite(self, request, response):
157 """Implementation of ImagesService::Composite.
159 Based off documentation of the PIL library at
160 http://www.pythonware.com/library/pil/handbook/index.htm
162 Args:
163 request: ImagesCompositeRequest, contains image request info.
164 response: ImagesCompositeResponse, contains transformed image.
166 width = request.canvas().width()
167 height = request.canvas().height()
168 color = _ArgbToRgbaTuple(request.canvas().color())
171 color = _BackendPremultiplication(color)
172 canvas = Image.new("RGBA", (width, height), color)
173 sources = []
174 if (not request.canvas().width() or request.canvas().width() > 4000 or
175 not request.canvas().height() or request.canvas().height() > 4000):
176 raise apiproxy_errors.ApplicationError(
177 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
178 if not request.image_size():
179 raise apiproxy_errors.ApplicationError(
180 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
181 if not request.options_size():
182 raise apiproxy_errors.ApplicationError(
183 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
184 if request.options_size() > images.MAX_COMPOSITES_PER_REQUEST:
185 raise apiproxy_errors.ApplicationError(
186 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
187 for image in request.image_list():
188 sources.append(self._OpenImageData(image))
190 for options in request.options_list():
191 if (options.anchor() < images.TOP_LEFT or
192 options.anchor() > images.BOTTOM_RIGHT):
193 raise apiproxy_errors.ApplicationError(
194 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
195 if options.source_index() >= len(sources) or options.source_index() < 0:
196 raise apiproxy_errors.ApplicationError(
197 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
198 if options.opacity() < 0 or options.opacity() > 1:
199 raise apiproxy_errors.ApplicationError(
200 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
201 source = sources[options.source_index()]
202 x_anchor = (options.anchor() % 3) * 0.5
203 y_anchor = (options.anchor() / 3) * 0.5
204 x_offset = int(options.x_offset() + x_anchor * (width - source.size[0]))
205 y_offset = int(options.y_offset() + y_anchor * (height - source.size[1]))
206 if source.mode == "RGBA":
207 canvas.paste(source, (x_offset, y_offset), source)
208 else:
209 alpha = options.opacity() * 255
210 mask = Image.new("L", source.size, alpha)
211 canvas.paste(source, (x_offset, y_offset), mask)
212 response_value = self._EncodeImage(canvas, request.canvas().output())
213 response.mutable_image().set_content(response_value)
215 def _Dynamic_Histogram(self, request, response):
216 """Trivial implementation of ImagesService::Histogram.
218 Based off documentation of the PIL library at
219 http://www.pythonware.com/library/pil/handbook/index.htm
221 Args:
222 request: ImagesHistogramRequest, contains the image.
223 response: ImagesHistogramResponse, contains histogram of the image.
225 image = self._OpenImageData(request.image())
227 img_format = image.format
228 if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF", "WEBP"):
229 raise apiproxy_errors.ApplicationError(
230 images_service_pb.ImagesServiceError.NOT_IMAGE)
231 image = image.convert("RGBA")
232 red = [0] * 256
233 green = [0] * 256
234 blue = [0] * 256
239 for pixel in image.getdata():
240 red[int((pixel[0] * pixel[3]) / 255)] += 1
241 green[int((pixel[1] * pixel[3]) / 255)] += 1
242 blue[int((pixel[2] * pixel[3]) / 255)] += 1
243 histogram = response.mutable_histogram()
244 for value in red:
245 histogram.add_red(value)
246 for value in green:
247 histogram.add_green(value)
248 for value in blue:
249 histogram.add_blue(value)
251 def _Dynamic_Transform(self, request, response):
252 """Trivial implementation of ImagesService::Transform.
254 Based off documentation of the PIL library at
255 http://www.pythonware.com/library/pil/handbook/index.htm
257 Args:
258 request: ImagesTransformRequest, contains image request info.
259 response: ImagesTransformResponse, contains transformed image.
261 original_image = self._OpenImageData(request.image())
263 input_settings = request.input()
264 correct_orientation = (
265 input_settings.has_correct_exif_orientation() and
266 input_settings.correct_exif_orientation() ==
267 images_service_pb.InputSettings.CORRECT_ORIENTATION)
271 source_metadata = self._ExtractMetadata(
272 original_image, input_settings.parse_metadata())
273 if input_settings.parse_metadata():
274 logging.info(
275 "Once the application is deployed, a more powerful metadata "
276 "extraction will be performed which might return many more fields.")
278 new_image = self._ProcessTransforms(original_image,
279 request.transform_list(),
280 correct_orientation)
282 substitution_rgb = None
283 if input_settings.has_transparent_substitution_rgb():
284 substitution_rgb = input_settings.transparent_substitution_rgb()
285 response_value = self._EncodeImage(new_image,
286 request.output(),
287 substitution_rgb)
288 response.mutable_image().set_content(response_value)
289 response.set_source_metadata(source_metadata)
291 def _Dynamic_GetUrlBase(self, request, response):
292 self._blob_stub.GetUrlBase(request, response)
294 def _Dynamic_DeleteUrlBase(self, request, response):
295 self._blob_stub.DeleteUrlBase(request, response)
297 def _EncodeImage(self, image, output_encoding, substitution_rgb=None):
298 """Encode the given image and return it in string form.
300 Args:
301 image: PIL Image object, image to encode.
302 output_encoding: ImagesTransformRequest.OutputSettings object.
303 substitution_rgb: The color to use for transparent pixels if the output
304 format does not support transparency.
306 Returns:
307 str with encoded image information in given encoding format.
309 image_string = StringIO.StringIO()
311 image_encoding = "PNG"
313 if (output_encoding.mime_type() == images_service_pb.OutputSettings.WEBP):
314 image_encoding = "WEBP"
316 if (output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG):
317 image_encoding = "JPEG"
324 if substitution_rgb:
328 blue = substitution_rgb & 0xFF
329 green = (substitution_rgb >> 8) & 0xFF
330 red = (substitution_rgb >> 16) & 0xFF
331 background = Image.new("RGB", image.size, (red, green, blue))
332 background.paste(image, mask=image.split()[3])
333 image = background
334 else:
335 image = image.convert("RGB")
337 image.save(image_string, image_encoding)
338 return image_string.getvalue()
340 def _OpenImageData(self, image_data):
341 """Open image data from ImageData protocol buffer.
343 Args:
344 image_data: ImageData protocol buffer containing image data or blob
345 reference.
347 Returns:
348 Image containing the image data passed in or reference by blob-key.
350 Raises:
351 ApplicationError if both content and blob-key are provided.
352 NOTE: 'content' must always be set because it is a required field,
353 however, it must be the empty string when a blob-key is provided.
355 if image_data.content() and image_data.has_blob_key():
356 raise apiproxy_errors.ApplicationError(
357 images_service_pb.ImagesServiceError.INVALID_BLOB_KEY)
359 if image_data.has_blob_key():
360 image = self._OpenBlob(image_data.blob_key())
361 else:
362 image = self._OpenImage(image_data.content())
365 img_format = image.format
366 if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF", "WEBP"):
367 raise apiproxy_errors.ApplicationError(
368 images_service_pb.ImagesServiceError.NOT_IMAGE)
369 return image
371 def _OpenImage(self, image):
372 """Opens an image provided as a string.
374 Args:
375 image: image data to be opened
377 Raises:
378 apiproxy_errors.ApplicationError if the image cannot be opened or if it
379 is an unsupported format.
381 Returns:
382 Image containing the image data passed in.
384 if not image:
385 raise apiproxy_errors.ApplicationError(
386 images_service_pb.ImagesServiceError.NOT_IMAGE)
388 image = StringIO.StringIO(image)
389 try:
390 return Image.open(image)
391 except IOError:
393 raise apiproxy_errors.ApplicationError(
394 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
396 def _OpenBlob(self, blob_key):
397 """Create an Image from the blob data read from blob_key."""
399 try:
400 _ = datastore.Get(
401 blobstore_stub.BlobstoreServiceStub.ToDatastoreBlobKey(blob_key))
402 except datastore_errors.Error:
405 logging.exception("Blob with key %r does not exist", blob_key)
406 raise apiproxy_errors.ApplicationError(
407 images_service_pb.ImagesServiceError.UNSPECIFIED_ERROR)
409 blobstore_storage = apiproxy_stub_map.apiproxy.GetStub("blobstore")
412 try:
413 blob_file = blobstore_storage.storage.OpenBlob(blob_key)
414 except IOError:
415 logging.exception("Could not get file for blob_key %r", blob_key)
417 raise apiproxy_errors.ApplicationError(
418 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
420 try:
421 return Image.open(blob_file)
422 except IOError:
423 logging.exception("Could not open image %r for blob_key %r",
424 blob_file, blob_key)
426 raise apiproxy_errors.ApplicationError(
427 images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
429 def _ValidateCropArg(self, arg):
430 """Check an argument for the Crop transform.
432 Args:
433 arg: float, argument to Crop transform to check.
435 Raises:
436 apiproxy_errors.ApplicationError on problem with argument.
438 if not isinstance(arg, float):
439 raise apiproxy_errors.ApplicationError(
440 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
442 if not (0 <= arg <= 1.0):
443 raise apiproxy_errors.ApplicationError(
444 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
446 def _CalculateNewDimensions(self,
447 current_width,
448 current_height,
449 req_width,
450 req_height,
451 crop_to_fit,
452 allow_stretch):
453 """Get new resize dimensions keeping the current aspect ratio.
455 This uses the more restricting of the two requested values to determine
456 the new ratio. See also crop_to_fit.
458 Args:
459 current_width: int, current width of the image.
460 current_height: int, current height of the image.
461 req_width: int, requested new width of the image, 0 if unspecified.
462 req_height: int, requested new height of the image, 0 if unspecified.
463 crop_to_fit: bool, True if the less restricting dimension should be used.
464 allow_stretch: bool, True is aspect ratio should be ignored.
466 Raises:
467 apiproxy_errors.ApplicationError: if crop_to_fit is True either req_width
468 or req_height is 0.
470 Returns:
471 tuple (width, height) which are both ints of the new ratio.
475 width_ratio = float(req_width) / current_width
476 height_ratio = float(req_height) / current_height
478 if allow_stretch:
480 if not req_width or not req_height:
481 raise apiproxy_errors.ApplicationError(
482 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
483 return req_width, req_height
484 elif crop_to_fit:
486 if not req_width or not req_height:
487 raise apiproxy_errors.ApplicationError(
488 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
489 if width_ratio > height_ratio:
490 return req_width, int(width_ratio * current_height)
491 else:
492 return int(height_ratio * current_width), req_height
493 else:
496 if req_width == 0 or (width_ratio > height_ratio and req_height != 0):
498 return int(height_ratio * current_width), req_height
499 else:
501 return req_width, int(width_ratio * current_height)
503 def _Resize(self, image, transform):
504 """Use PIL to resize the given image with the given transform.
506 Args:
507 image: PIL.Image.Image object to resize.
508 transform: images_service_pb.Transform to use when resizing.
510 Returns:
511 PIL.Image.Image with transforms performed on it.
513 Raises:
514 BadRequestError if the resize data given is bad.
516 width = 0
517 height = 0
519 if transform.has_width():
520 width = transform.width()
521 if width < 0 or 4000 < width:
522 raise apiproxy_errors.ApplicationError(
523 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
525 if transform.has_height():
526 height = transform.height()
527 if height < 0 or 4000 < height:
528 raise apiproxy_errors.ApplicationError(
529 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
531 crop_to_fit = transform.crop_to_fit()
532 allow_stretch = transform.allow_stretch()
534 current_width, current_height = image.size
535 new_width, new_height = self._CalculateNewDimensions(current_width,
536 current_height,
537 width,
538 height,
539 crop_to_fit,
540 allow_stretch)
541 new_image = image.resize((new_width, new_height), Image.ANTIALIAS)
542 if crop_to_fit and (new_width > width or new_height > height):
544 left = int((new_width - width) * transform.crop_offset_x())
545 top = int((new_height - height) * transform.crop_offset_y())
546 right = left + width
547 bottom = top + height
548 new_image = new_image.crop((left, top, right, bottom))
550 return new_image
552 def _Rotate(self, image, transform):
553 """Use PIL to rotate the given image with the given transform.
555 Args:
556 image: PIL.Image.Image object to rotate.
557 transform: images_service_pb.Transform to use when rotating.
559 Returns:
560 PIL.Image.Image with transforms performed on it.
562 Raises:
563 BadRequestError if the rotate data given is bad.
565 degrees = transform.rotate()
566 if degrees < 0 or degrees % 90 != 0:
567 raise apiproxy_errors.ApplicationError(
568 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
569 degrees %= 360
572 degrees = 360 - degrees
573 return image.rotate(degrees)
575 def _Crop(self, image, transform):
576 """Use PIL to crop the given image with the given transform.
578 Args:
579 image: PIL.Image.Image object to crop.
580 transform: images_service_pb.Transform to use when cropping.
582 Returns:
583 PIL.Image.Image with transforms performed on it.
585 Raises:
586 BadRequestError if the crop data given is bad.
588 left_x = 0.0
589 top_y = 0.0
590 right_x = 1.0
591 bottom_y = 1.0
593 if transform.has_crop_left_x():
594 left_x = transform.crop_left_x()
595 self._ValidateCropArg(left_x)
597 if transform.has_crop_top_y():
598 top_y = transform.crop_top_y()
599 self._ValidateCropArg(top_y)
601 if transform.has_crop_right_x():
602 right_x = transform.crop_right_x()
603 self._ValidateCropArg(right_x)
605 if transform.has_crop_bottom_y():
606 bottom_y = transform.crop_bottom_y()
607 self._ValidateCropArg(bottom_y)
610 width, height = image.size
612 box = (int(round(left_x * width)),
613 int(round(top_y * height)),
614 int(round(right_x * width)),
615 int(round(bottom_y * height)))
617 return image.crop(box)
619 @staticmethod
620 def _GetExifFromImage(image):
621 if hasattr(image, "_getexif"):
627 try:
628 from PIL import TiffImagePlugin
629 return image._getexif()
630 except ImportError:
631 # We have not managed to get this to work in the SDK with Python
632 # 2.5, so just catch the ImportError and pretend there is no
633 # EXIF information of interest.
634 logging.info('Sorry, TiffImagePlugin does not work in this environment')
635 return None
637 @staticmethod
638 def _ExtractMetadata(image, parse_metadata):
639 """Extract EXIF metadata from the image.
641 Note that this is a much simplified version of metadata extraction. After
642 deployment applications have access to a more powerful parser that can
643 parse hundreds of fields from images.
645 Args:
646 image: PIL Image object.
647 parse_metadata: bool, True if metadata parsing has been requested. If
648 False the result will contain image dimensions.
649 Returns:
650 str, JSON encoded values with various metadata fields.
653 def ExifTimeToUnixtime(exif_time):
654 """Convert time in EXIF to unix time.
656 Args:
657 exif_time: str, the time from the EXIF block formated by EXIF standard.
658 E.g., "2011:02:20 10:23:12", seconds are optional.
660 Returns:
661 Integer, the time in unix fromat: seconds since the epoch.
663 regexp = re.compile(r"^([0-9]{4}):([0-9]{1,2}):([0-9]{1,2})"
664 " ([0-9]{1,2}):([0-9]{1,2})(?::([0-9]{1,2}))?")
665 match = regexp.match(exif_time)
666 if match is None: return None
667 try:
668 date = datetime.datetime(*map(int, filter(None, match.groups())))
669 except ValueError:
670 logging.info("Invalid date in EXIF: %s", exif_time)
671 return None
672 return int(time.mktime(date.timetuple()))
674 metadata_dict = (
675 parse_metadata and ImagesServiceStub._GetExifFromImage(image) or {})
677 metadata_dict[256], metadata_dict[257] = image.size
681 if _EXIF_DATETIMEORIGINAL_TAG in metadata_dict:
682 date_ms = ExifTimeToUnixtime(metadata_dict[_EXIF_DATETIMEORIGINAL_TAG])
683 if date_ms:
684 metadata_dict[_EXIF_DATETIMEORIGINAL_TAG] = date_ms
685 else:
686 del metadata_dict[_EXIF_DATETIMEORIGINAL_TAG]
687 metadata = dict([(_EXIF_TAGS[k], v)
688 for k, v in metadata_dict.iteritems()
689 if k in _EXIF_TAGS])
690 return simplejson.dumps(metadata)
692 def _CorrectOrientation(self, image, orientation):
693 """Use PIL to correct the image orientation based on its EXIF.
695 See JEITA CP-3451 at http://www.exif.org/specifications.html,
696 Exif 2.2, page 18.
698 Args:
699 image: source PIL.Image.Image object.
700 orientation: integer in range (1,8) inclusive, corresponding the image
701 orientation from EXIF.
703 Returns:
704 PIL.Image.Image with transforms performed on it. If no correction was
705 done, it returns the input image.
709 if orientation == 2:
710 image = image.transpose(Image.FLIP_LEFT_RIGHT)
711 elif orientation == 3:
712 image = image.rotate(180)
713 elif orientation == 4:
714 image = image.transpose(Image.FLIP_TOP_BOTTOM)
715 elif orientation == 5:
716 image = image.transpose(Image.FLIP_TOP_BOTTOM)
717 image = image.rotate(270)
718 elif orientation == 6:
719 image = image.rotate(270)
720 elif orientation == 7:
721 image = image.transpose(Image.FLIP_LEFT_RIGHT)
722 image = image.rotate(270)
723 elif orientation == 8:
724 image = image.rotate(90)
726 return image
728 def _ProcessTransforms(self, image, transforms, correct_orientation):
729 """Execute PIL operations based on transform values.
731 Args:
732 image: PIL.Image.Image instance, image to manipulate.
733 transforms: list of ImagesTransformRequest.Transform objects.
734 correct_orientation: True to indicate that image orientation should be
735 corrected based on its EXIF.
736 Returns:
737 PIL.Image.Image with transforms performed on it.
739 Raises:
740 BadRequestError if we are passed more than one of the same type of
741 transform.
743 new_image = image
744 if len(transforms) > images.MAX_TRANSFORMS_PER_REQUEST:
745 raise apiproxy_errors.ApplicationError(
746 images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
748 orientation = 1
749 if correct_orientation:
752 exif = self._GetExifFromImage(image)
753 if not exif or _EXIF_ORIENTATION_TAG not in exif:
754 correct_orientation = False
755 else:
756 orientation = exif[_EXIF_ORIENTATION_TAG]
758 width, height = new_image.size
759 if height > width:
760 orientation = 1
762 for transform in transforms:
770 if (correct_orientation and
771 not (transform.has_crop_left_x() or
772 transform.has_crop_top_y() or
773 transform.has_crop_right_x() or
774 transform.has_crop_bottom_y()) and
775 not transform.has_horizontal_flip() and
776 not transform.has_vertical_flip()):
777 new_image = self._CorrectOrientation(new_image, orientation)
778 correct_orientation = False
780 if transform.has_width() or transform.has_height():
782 new_image = self._Resize(new_image, transform)
784 elif transform.has_rotate():
786 new_image = self._Rotate(new_image, transform)
788 elif transform.has_horizontal_flip():
790 new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT)
792 elif transform.has_vertical_flip():
794 new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM)
796 elif (transform.has_crop_left_x() or
797 transform.has_crop_top_y() or
798 transform.has_crop_right_x() or
799 transform.has_crop_bottom_y()):
801 new_image = self._Crop(new_image, transform)
803 elif transform.has_autolevels():
806 logging.info("I'm Feeling Lucky autolevels will be visible once this "
807 "application is deployed.")
808 else:
809 logging.warn("Found no transformations found to perform.")
811 if correct_orientation:
814 new_image = self._CorrectOrientation(new_image, orientation)
815 correct_orientation = False
820 return new_image