thirdparty/google_appengine/google/appengine/api/images/__init__.py
changeset 109 620f9b141567
child 828 f5fd65cc3bf3
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 """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)