thirdparty/google_appengine/google/appengine/api/images/images_stub.py
changeset 109 620f9b141567
child 686 df109be0567c
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     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 """Stub version of the images API."""
       
    19 
       
    20 
       
    21 
       
    22 import logging
       
    23 import StringIO
       
    24 
       
    25 import PIL
       
    26 from PIL import _imaging
       
    27 from PIL import Image
       
    28 
       
    29 from google.appengine.api import images
       
    30 from google.appengine.api.images import images_service_pb
       
    31 from google.appengine.runtime import apiproxy_errors
       
    32 
       
    33 
       
    34 class ImagesServiceStub(object):
       
    35   """Stub version of images API to be used with the dev_appserver."""
       
    36 
       
    37   def __init__(self):
       
    38     """Preloads PIL to load all modules in the unhardened environment."""
       
    39     Image.init()
       
    40 
       
    41   def MakeSyncCall(self, service, call, request, response):
       
    42     """Main entry point.
       
    43 
       
    44     Args:
       
    45       service: str, must be 'images'.
       
    46       call: str, name of the RPC to make, must be part of ImagesService.
       
    47       request: pb object, corresponding args to the 'call' argument.
       
    48       response: pb object, return value for the 'call' argument.
       
    49     """
       
    50     assert service == "images"
       
    51     assert request.IsInitialized()
       
    52 
       
    53     attr = getattr(self, "_Dynamic_" + call)
       
    54     attr(request, response)
       
    55 
       
    56   def _Dynamic_Transform(self, request, response):
       
    57     """Trivial implementation of ImagesService::Transform.
       
    58 
       
    59     Based off documentation of the PIL library at
       
    60     http://www.pythonware.com/library/pil/handbook/index.htm
       
    61 
       
    62     Args:
       
    63       request: ImagesTransformRequest, contains image request info.
       
    64       response: ImagesTransformResponse, contains transformed image.
       
    65     """
       
    66     image = request.image().content()
       
    67     if not image:
       
    68       raise apiproxy_errors.ApplicationError(
       
    69           images_service_pb.ImagesServiceError.NOT_IMAGE)
       
    70 
       
    71     image = StringIO.StringIO(image)
       
    72     try:
       
    73       original_image = Image.open(image)
       
    74     except IOError:
       
    75       raise apiproxy_errors.ApplicationError(
       
    76           images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)
       
    77 
       
    78     img_format = original_image.format
       
    79     if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"):
       
    80       raise apiproxy_errors.ApplicationError(
       
    81           images_service_pb.ImagesServiceError.NOT_IMAGE)
       
    82 
       
    83     new_image = self._ProcessTransforms(original_image,
       
    84                                         request.transform_list())
       
    85 
       
    86     response_value = self._EncodeImage(new_image, request.output())
       
    87     response.mutable_image().set_content(response_value)
       
    88 
       
    89   def _EncodeImage(self, image, output_encoding):
       
    90     """Encode the given image and return it in string form.
       
    91 
       
    92     Args:
       
    93       image: PIL Image object, image to encode.
       
    94       output_encoding: ImagesTransformRequest.OutputSettings object.
       
    95 
       
    96     Returns:
       
    97       str with encoded image information in given encoding format.
       
    98     """
       
    99     image_string = StringIO.StringIO()
       
   100 
       
   101     image_encoding = "PNG"
       
   102 
       
   103     if (output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG):
       
   104       image_encoding = "JPEG"
       
   105 
       
   106       image = image.convert("RGB")
       
   107 
       
   108     image.save(image_string, image_encoding)
       
   109 
       
   110     return image_string.getvalue()
       
   111 
       
   112   def _ValidateCropArg(self, arg):
       
   113     """Check an argument for the Crop transform.
       
   114 
       
   115     Args:
       
   116       arg: float, argument to Crop transform to check.
       
   117 
       
   118     Raises:
       
   119       apiproxy_errors.ApplicationError on problem with argument.
       
   120     """
       
   121     if not isinstance(arg, float):
       
   122       raise apiproxy_errors.ApplicationError(
       
   123           images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
       
   124 
       
   125     if not (0 <= arg <= 1.0):
       
   126       raise apiproxy_errors.ApplicationError(
       
   127           images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
       
   128 
       
   129   def _CalculateNewDimensions(self,
       
   130                               current_width,
       
   131                               current_height,
       
   132                               req_width,
       
   133                               req_height):
       
   134     """Get new resize dimensions keeping the current aspect ratio.
       
   135 
       
   136     This uses the more restricting of the two requested values to determine
       
   137     the new ratio.
       
   138 
       
   139     Args:
       
   140       current_width: int, current width of the image.
       
   141       current_height: int, current height of the image.
       
   142       req_width: int, requested new width of the image.
       
   143       req_height: int, requested new height of the image.
       
   144 
       
   145     Returns:
       
   146       tuple (width, height) which are both ints of the new ratio.
       
   147     """
       
   148 
       
   149     width_ratio = float(req_width) / current_width
       
   150     height_ratio = float(req_height) / current_height
       
   151 
       
   152     if req_width == 0 or (width_ratio > height_ratio and req_height != 0):
       
   153       return int(height_ratio * current_width), req_height
       
   154     else:
       
   155       return req_width, int(width_ratio * current_height)
       
   156 
       
   157   def _Resize(self, image, transform):
       
   158     """Use PIL to resize the given image with the given transform.
       
   159 
       
   160     Args:
       
   161       image: PIL.Image.Image object to resize.
       
   162       transform: images_service_pb.Transform to use when resizing.
       
   163 
       
   164     Returns:
       
   165       PIL.Image.Image with transforms performed on it.
       
   166 
       
   167     Raises:
       
   168       BadRequestError if the resize data given is bad.
       
   169     """
       
   170     width = 0
       
   171     height = 0
       
   172 
       
   173     if transform.has_width():
       
   174       width = transform.width()
       
   175       if width < 0 or 4000 < width:
       
   176         raise apiproxy_errors.ApplicationError(
       
   177             images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
       
   178 
       
   179     if transform.has_height():
       
   180       height = transform.height()
       
   181       if height < 0 or 4000 < height:
       
   182         raise apiproxy_errors.ApplicationError(
       
   183             images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
       
   184 
       
   185     current_width, current_height = image.size
       
   186     new_width, new_height = self._CalculateNewDimensions(current_width,
       
   187                                                          current_height,
       
   188                                                          width,
       
   189                                                          height)
       
   190 
       
   191     return image.resize((new_width, new_height), Image.ANTIALIAS)
       
   192 
       
   193   def _Rotate(self, image, transform):
       
   194     """Use PIL to rotate the given image with the given transform.
       
   195 
       
   196     Args:
       
   197       image: PIL.Image.Image object to rotate.
       
   198       transform: images_service_pb.Transform to use when rotating.
       
   199 
       
   200     Returns:
       
   201       PIL.Image.Image with transforms performed on it.
       
   202 
       
   203     Raises:
       
   204       BadRequestError if the rotate data given is bad.
       
   205     """
       
   206     degrees = transform.rotate()
       
   207     if degrees < 0 or degrees % 90 != 0:
       
   208       raise apiproxy_errors.ApplicationError(
       
   209           images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
       
   210     degrees %= 360
       
   211 
       
   212     degrees = 360 - degrees
       
   213     return image.rotate(degrees)
       
   214 
       
   215   def _Crop(self, image, transform):
       
   216     """Use PIL to crop the given image with the given transform.
       
   217 
       
   218     Args:
       
   219       image: PIL.Image.Image object to crop.
       
   220       transform: images_service_pb.Transform to use when cropping.
       
   221 
       
   222     Returns:
       
   223       PIL.Image.Image with transforms performed on it.
       
   224 
       
   225     Raises:
       
   226       BadRequestError if the crop data given is bad.
       
   227     """
       
   228     left_x = 0.0
       
   229     top_y = 0.0
       
   230     right_x = 1.0
       
   231     bottom_y = 1.0
       
   232 
       
   233     if transform.has_crop_left_x():
       
   234       left_x = transform.crop_left_x()
       
   235       self._ValidateCropArg(left_x)
       
   236 
       
   237     if transform.has_crop_top_y():
       
   238       top_y = transform.crop_top_y()
       
   239       self._ValidateCropArg(top_y)
       
   240 
       
   241     if transform.has_crop_right_x():
       
   242       right_x = transform.crop_right_x()
       
   243       self._ValidateCropArg(right_x)
       
   244 
       
   245     if transform.has_crop_bottom_y():
       
   246       bottom_y = transform.crop_bottom_y()
       
   247       self._ValidateCropArg(bottom_y)
       
   248 
       
   249     width, height = image.size
       
   250 
       
   251     box = (int(transform.crop_left_x() * width),
       
   252            int(transform.crop_top_y() * height),
       
   253            int(transform.crop_right_x() * width),
       
   254            int(transform.crop_bottom_y() * height))
       
   255 
       
   256     return image.crop(box)
       
   257 
       
   258   def _CheckTransformCount(self, transform_map, req_transform):
       
   259     """Check that the requested transform hasn't already been set in map.
       
   260 
       
   261     Args:
       
   262       transform_map: {images_service_pb.ImagesServiceTransform: boolean}, map
       
   263         to use to determine if the requested transform has been called.
       
   264       req_transform: images_service_pb.ImagesServiceTransform, the requested
       
   265         transform.
       
   266 
       
   267     Raises:
       
   268       BadRequestError if we are passed more than one of the same type of
       
   269       transform.
       
   270     """
       
   271     if req_transform in transform_map:
       
   272       raise apiproxy_errors.ApplicationError(
       
   273           images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
       
   274     transform_map[req_transform] = True
       
   275 
       
   276   def _ProcessTransforms(self, image, transforms):
       
   277     """Execute PIL operations based on transform values.
       
   278 
       
   279     Args:
       
   280       image: PIL.Image.Image instance, image to manipulate.
       
   281       trasnforms: list of ImagesTransformRequest.Transform objects.
       
   282 
       
   283     Returns:
       
   284       PIL.Image.Image with transforms performed on it.
       
   285 
       
   286     Raises:
       
   287       BadRequestError if we are passed more than one of the same type of
       
   288       transform.
       
   289     """
       
   290     new_image = image
       
   291     transform_map = {}
       
   292     for transform in transforms:
       
   293       if transform.has_width() or transform.has_height():
       
   294         self._CheckTransformCount(
       
   295             transform_map,
       
   296             images_service_pb.ImagesServiceTransform.RESIZE
       
   297         )
       
   298 
       
   299         new_image = self._Resize(new_image, transform)
       
   300 
       
   301       elif transform.has_rotate():
       
   302         self._CheckTransformCount(
       
   303             transform_map,
       
   304             images_service_pb.ImagesServiceTransform.ROTATE
       
   305         )
       
   306 
       
   307         new_image = self._Rotate(new_image, transform)
       
   308 
       
   309       elif transform.has_horizontal_flip():
       
   310         self._CheckTransformCount(
       
   311             transform_map,
       
   312             images_service_pb.ImagesServiceTransform.HORIZONTAL_FLIP
       
   313         )
       
   314 
       
   315         new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT)
       
   316 
       
   317       elif transform.has_vertical_flip():
       
   318         self._CheckTransformCount(
       
   319             transform_map,
       
   320             images_service_pb.ImagesServiceTransform.VERTICAL_FLIP
       
   321         )
       
   322 
       
   323         new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM)
       
   324 
       
   325       elif (transform.has_crop_left_x() or
       
   326           transform.has_crop_top_y() or
       
   327           transform.has_crop_right_x() or
       
   328           transform.has_crop_bottom_y()):
       
   329         self._CheckTransformCount(
       
   330             transform_map,
       
   331             images_service_pb.ImagesServiceTransform.CROP
       
   332         )
       
   333 
       
   334         new_image = self._Crop(new_image, transform)
       
   335 
       
   336       elif transform.has_autolevels():
       
   337         self._CheckTransformCount(
       
   338             transform_map,
       
   339             images_service_pb.ImagesServiceTransform.IM_FEELING_LUCKY
       
   340         )
       
   341         logging.info("I'm Feeling Lucky autolevels will be visible once this "
       
   342                      "application is deployed.")
       
   343       else:
       
   344         logging.warn("Found no transformations found to perform.")
       
   345 
       
   346     return new_image