#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Stub version of the images API."""



import logging
import StringIO

try:
  import PIL
  from PIL import _imaging
  from PIL import Image
except ImportError:
  import _imaging
  import Image

from google.appengine.api import apiproxy_stub
from google.appengine.api import images
from google.appengine.api.images import images_service_pb
from google.appengine.runtime import apiproxy_errors


def _ArgbToRgbaTuple(argb):
  """Convert from a single ARGB value to a tuple containing RGBA.

  Args:
    argb: Signed 32 bit integer containing an ARGB value.

  Returns:
    RGBA tuple.
  """
  unsigned_argb = argb % 0x100000000
  return ((unsigned_argb >> 16) & 0xFF,
          (unsigned_argb >> 8) & 0xFF,
          unsigned_argb & 0xFF,
          (unsigned_argb >> 24) & 0xFF)


class ImagesServiceStub(apiproxy_stub.APIProxyStub):
  """Stub version of images API to be used with the dev_appserver."""

  def __init__(self, service_name='images'):
    """Preloads PIL to load all modules in the unhardened environment.

    Args:
      service_name: Service name expected for all calls.
    """
    super(ImagesServiceStub, self).__init__(service_name)
    Image.init()

  def _Dynamic_Composite(self, request, response):
    """Implementation of ImagesService::Composite.

    Based off documentation of the PIL library at
    http://www.pythonware.com/library/pil/handbook/index.htm

    Args:
      request: ImagesCompositeRequest, contains image request info.
      response: ImagesCompositeResponse, contains transformed image.
    """
    width = request.canvas().width()
    height = request.canvas().height()
    color = _ArgbToRgbaTuple(request.canvas().color())
    canvas = Image.new("RGBA", (width, height), color)
    sources = []
    if (not request.canvas().width() or request.canvas().width() > 4000 or
        not request.canvas().height() or request.canvas().height() > 4000):
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
    if not request.image_size():
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
    if not request.options_size():
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
    if request.options_size() > images.MAX_COMPOSITES_PER_REQUEST:
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
    for image in request.image_list():
      sources.append(self._OpenImage(image.content()))

    for options in request.options_list():
      if (options.anchor() < images.TOP_LEFT or
          options.anchor() > images.BOTTOM_RIGHT):
        raise apiproxy_errors.ApplicationError(
            images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
      if options.source_index() >= len(sources) or options.source_index() < 0:
        raise apiproxy_errors.ApplicationError(
            images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
      if options.opacity() < 0 or options.opacity() > 1:
        raise apiproxy_errors.ApplicationError(
            images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
      source = sources[options.source_index()]
      x_anchor = (options.anchor() % 3) * 0.5
      y_anchor = (options.anchor() / 3) * 0.5
      x_offset = int(options.x_offset() + x_anchor * (width - source.size[0]))
      y_offset = int(options.y_offset() + y_anchor * (height - source.size[1]))
      alpha = options.opacity() * 255
      mask = Image.new("L", source.size, alpha)
      canvas.paste(source, (x_offset, y_offset), mask)
    response_value = self._EncodeImage(canvas, request.canvas().output())
    response.mutable_image().set_content(response_value)

  def _Dynamic_Histogram(self, request, response):
    """Trivial implementation of ImagesService::Histogram.

    Based off documentation of the PIL library at
    http://www.pythonware.com/library/pil/handbook/index.htm

    Args:
      request: ImagesHistogramRequest, contains the image.
      response: ImagesHistogramResponse, contains histogram of the image.
    """
    image = self._OpenImage(request.image().content())
    img_format = image.format
    if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"):
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.NOT_IMAGE)
    image = image.convert("RGBA")
    red = [0] * 256
    green = [0] * 256
    blue = [0] * 256
    for pixel in image.getdata():
      red[int((pixel[0] * pixel[3]) / 255)] += 1
      green[int((pixel[1] * pixel[3]) / 255)] += 1
      blue[int((pixel[2] * pixel[3]) / 255)] += 1
    histogram = response.mutable_histogram()
    for value in red:
      histogram.add_red(value)
    for value in green:
      histogram.add_green(value)
    for value in blue:
      histogram.add_blue(value)

  def _Dynamic_Transform(self, request, response):
    """Trivial implementation of ImagesService::Transform.

    Based off documentation of the PIL library at
    http://www.pythonware.com/library/pil/handbook/index.htm

    Args:
      request: ImagesTransformRequest, contains image request info.
      response: ImagesTransformResponse, contains transformed image.
    """
    original_image = self._OpenImage(request.image().content())

    new_image = self._ProcessTransforms(original_image,
                                        request.transform_list())

    response_value = self._EncodeImage(new_image, request.output())
    response.mutable_image().set_content(response_value)

  def _EncodeImage(self, image, output_encoding):
    """Encode the given image and return it in string form.

    Args:
      image: PIL Image object, image to encode.
      output_encoding: ImagesTransformRequest.OutputSettings object.

    Returns:
      str with encoded image information in given encoding format.
    """
    image_string = StringIO.StringIO()

    image_encoding = "PNG"

    if (output_encoding.mime_type() == images_service_pb.OutputSettings.JPEG):
      image_encoding = "JPEG"

      image = image.convert("RGB")

    image.save(image_string, image_encoding)

    return image_string.getvalue()

  def _OpenImage(self, image):
    """Opens an image provided as a string.

    Args:
      image: image data to be opened

    Raises:
      apiproxy_errors.ApplicationError if the image cannot be opened or if it
      is an unsupported format.

    Returns:
      Image containing the image data passed in.
    """
    if not image:
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.NOT_IMAGE)

    image = StringIO.StringIO(image)
    try:
      image = Image.open(image)
    except IOError:
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_IMAGE_DATA)

    img_format = image.format
    if img_format not in ("BMP", "GIF", "ICO", "JPEG", "PNG", "TIFF"):
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.NOT_IMAGE)
    return image

  def _ValidateCropArg(self, arg):
    """Check an argument for the Crop transform.

    Args:
      arg: float, argument to Crop transform to check.

    Raises:
      apiproxy_errors.ApplicationError on problem with argument.
    """
    if not isinstance(arg, float):
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)

    if not (0 <= arg <= 1.0):
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)

  def _CalculateNewDimensions(self,
                              current_width,
                              current_height,
                              req_width,
                              req_height):
    """Get new resize dimensions keeping the current aspect ratio.

    This uses the more restricting of the two requested values to determine
    the new ratio.

    Args:
      current_width: int, current width of the image.
      current_height: int, current height of the image.
      req_width: int, requested new width of the image.
      req_height: int, requested new height of the image.

    Returns:
      tuple (width, height) which are both ints of the new ratio.
    """

    width_ratio = float(req_width) / current_width
    height_ratio = float(req_height) / current_height

    if req_width == 0 or (width_ratio > height_ratio and req_height != 0):
      return int(height_ratio * current_width), req_height
    else:
      return req_width, int(width_ratio * current_height)

  def _Resize(self, image, transform):
    """Use PIL to resize the given image with the given transform.

    Args:
      image: PIL.Image.Image object to resize.
      transform: images_service_pb.Transform to use when resizing.

    Returns:
      PIL.Image.Image with transforms performed on it.

    Raises:
      BadRequestError if the resize data given is bad.
    """
    width = 0
    height = 0

    if transform.has_width():
      width = transform.width()
      if width < 0 or 4000 < width:
        raise apiproxy_errors.ApplicationError(
            images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)

    if transform.has_height():
      height = transform.height()
      if height < 0 or 4000 < height:
        raise apiproxy_errors.ApplicationError(
            images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)

    current_width, current_height = image.size
    new_width, new_height = self._CalculateNewDimensions(current_width,
                                                         current_height,
                                                         width,
                                                         height)

    return image.resize((new_width, new_height), Image.ANTIALIAS)

  def _Rotate(self, image, transform):
    """Use PIL to rotate the given image with the given transform.

    Args:
      image: PIL.Image.Image object to rotate.
      transform: images_service_pb.Transform to use when rotating.

    Returns:
      PIL.Image.Image with transforms performed on it.

    Raises:
      BadRequestError if the rotate data given is bad.
    """
    degrees = transform.rotate()
    if degrees < 0 or degrees % 90 != 0:
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
    degrees %= 360

    degrees = 360 - degrees
    return image.rotate(degrees)

  def _Crop(self, image, transform):
    """Use PIL to crop the given image with the given transform.

    Args:
      image: PIL.Image.Image object to crop.
      transform: images_service_pb.Transform to use when cropping.

    Returns:
      PIL.Image.Image with transforms performed on it.

    Raises:
      BadRequestError if the crop data given is bad.
    """
    left_x = 0.0
    top_y = 0.0
    right_x = 1.0
    bottom_y = 1.0

    if transform.has_crop_left_x():
      left_x = transform.crop_left_x()
      self._ValidateCropArg(left_x)

    if transform.has_crop_top_y():
      top_y = transform.crop_top_y()
      self._ValidateCropArg(top_y)

    if transform.has_crop_right_x():
      right_x = transform.crop_right_x()
      self._ValidateCropArg(right_x)

    if transform.has_crop_bottom_y():
      bottom_y = transform.crop_bottom_y()
      self._ValidateCropArg(bottom_y)

    width, height = image.size

    box = (int(transform.crop_left_x() * width),
           int(transform.crop_top_y() * height),
           int(transform.crop_right_x() * width),
           int(transform.crop_bottom_y() * height))

    return image.crop(box)

  def _ProcessTransforms(self, image, transforms):
    """Execute PIL operations based on transform values.

    Args:
      image: PIL.Image.Image instance, image to manipulate.
      trasnforms: list of ImagesTransformRequest.Transform objects.

    Returns:
      PIL.Image.Image with transforms performed on it.

    Raises:
      BadRequestError if we are passed more than one of the same type of
      transform.
    """
    new_image = image
    if len(transforms) > images.MAX_TRANSFORMS_PER_REQUEST:
      raise apiproxy_errors.ApplicationError(
          images_service_pb.ImagesServiceError.BAD_TRANSFORM_DATA)
    for transform in transforms:
      if transform.has_width() or transform.has_height():
        new_image = self._Resize(new_image, transform)

      elif transform.has_rotate():
        new_image = self._Rotate(new_image, transform)

      elif transform.has_horizontal_flip():
        new_image = new_image.transpose(Image.FLIP_LEFT_RIGHT)

      elif transform.has_vertical_flip():
        new_image = new_image.transpose(Image.FLIP_TOP_BOTTOM)

      elif (transform.has_crop_left_x() or
          transform.has_crop_top_y() or
          transform.has_crop_right_x() or
          transform.has_crop_bottom_y()):
        new_image = self._Crop(new_image, transform)

      elif transform.has_autolevels():
        logging.info("I'm Feeling Lucky autolevels will be visible once this "
                     "application is deployed.")
      else:
        logging.warn("Found no transformations found to perform.")

    return new_image
