|
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 |