Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@
* 1.3.0: Increased Pillow to 4.1.0 and Tornado to 4.5.1
* 1.3.1: Fix pilbox.image CLI for python 3.0
* 1.3.2: Fix GIF P-mode to JPEG conversion
* 1.3.2.1: Allow clip mode to enlarge pictures
* 1.3.2.2: Create /info endpoint for image analysis
102 changes: 81 additions & 21 deletions pilbox/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import absolute_import, division, with_statement

import json
import logging
import socket

Expand Down Expand Up @@ -138,10 +139,88 @@ def __init__(self, **kwargs):
tornado.web.Application.__init__(self, self.get_handlers(), **settings)

def get_handlers(self):
return [(r"/", ImageHandler)]
return [
(r"/", ImageHandler),
(r"/info", ImageInfoHandler),
(r"/ping", AlertHandler),
]


class ImageHandler(tornado.web.RequestHandler):
class AlertHandler(tornado.web.RequestHandler):

def get(self):
self.write("pong")


class BaseImageHandler(tornado.web.RequestHandler):

@tornado.gen.coroutine
def fetch_image(self):
url = self.get_argument("url")
if self.settings.get("implicit_base_url") \
and urlparse(url).hostname is None:
url = urljoin(self.settings.get("implicit_base_url"), url)

client = tornado.httpclient.AsyncHTTPClient(
max_clients=self.settings.get("max_requests"))
try:
resp = yield client.fetch(
url,
request_timeout=self.settings.get("timeout"),
validate_cert=self.settings.get("validate_cert"),
proxy_host=self.settings.get("proxy_host"),
proxy_port=self.settings.get("proxy_port"))
raise tornado.gen.Return(resp)
except (socket.gaierror, tornado.httpclient.HTTPError) as e:
logger.warn("Fetch error for %s: %s",
self.get_argument("url"),
str(e))
raise errors.FetchError()

def get_argument(self, name, default=None, strip=True):
return super(BaseImageHandler, self).get_argument(name, default, strip)

def _validate_signature(self):
key = self.settings.get("client_key")
if key and not verify_signature(key, urlparse(self.request.uri).query):
raise errors.SignatureError("Invalid signature")

def write_error(self, status_code, **kwargs):
err = kwargs["exc_info"][1] if "exc_info" in kwargs else None
if isinstance(err, errors.PilboxError):
self.set_header("Content-Type", "application/json")
resp = dict(status_code=status_code,
error_code=err.get_code(),
error=err.log_message)
self.finish(tornado.escape.json_encode(resp))
else:
super(ImageHandler, self).write_error(status_code, **kwargs)


class ImageInfoHandler(BaseImageHandler):

@tornado.gen.coroutine
def get(self):
self._validate_signature()
resp = yield self.fetch_image()
image = Image(resp.buffer)
self.render_info(image)

def render_info(self, image):
info = {
'width': image.img.size[0],
'height': image.img.size[1],
}
info = json.dumps(info)
self._set_headers()
self.write(info)

def _set_headers(self):
self.set_header("Content-Type", "application/json")
self.set_header("charset", "utf-8")


class ImageHandler(BaseImageHandler):
FORWARD_HEADERS = ["Cache-Control", "Expires", "Last-Modified"]
OPERATIONS = ["region", "resize", "rotate", "noop"]

Expand All @@ -160,9 +239,6 @@ def get(self):
resp = yield self.fetch_image()
self.render_image(resp)

def get_argument(self, name, default=None, strip=True):
return super(ImageHandler, self).get_argument(name, default, strip)

def validate_request(self):
self._validate_operation()
self._validate_url()
Expand Down Expand Up @@ -220,17 +296,6 @@ def render_image(self, resp):
self.write(block)
outfile.close()

def write_error(self, status_code, **kwargs):
err = kwargs["exc_info"][1] if "exc_info" in kwargs else None
if isinstance(err, errors.PilboxError):
self.set_header("Content-Type", "application/json")
resp = dict(status_code=status_code,
error_code=err.get_code(),
error=err.log_message)
self.finish(tornado.escape.json_encode(resp))
else:
super(ImageHandler, self).write_error(status_code, **kwargs)

def _process_response(self, resp):
ops = self._get_operations()
if "noop" in ops:
Expand Down Expand Up @@ -328,11 +393,6 @@ def _validate_client(self):
if client and self.get_argument("client") != client:
raise errors.ClientError("Invalid client")

def _validate_signature(self):
key = self.settings.get("client_key")
if key and not verify_signature(key, urlparse(self.request.uri).query):
raise errors.SignatureError("Invalid signature")

def _validate_host(self):
hosts = self.settings.get("allowed_hosts", [])
if hosts and urlparse(self.get_argument("url")).hostname not in hosts:
Expand Down
12 changes: 12 additions & 0 deletions pilbox/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ def _adapt(self, size, opts):
self._fill(size, opts)

def _clip(self, size, opts):
if self.img.size < size:
size = self._get_size_for_clip_resize(size)
self._crop(size, opts)
self.img.thumbnail(size, opts["pil"]["filter"])

def _background(self, fmt, color):
Expand All @@ -320,6 +323,15 @@ def _background(self, fmt, color):
img.paste(self.img, mask=mask)
self.img = img

def _get_size_for_clip_resize(self, size):
current_width, current_height = self.img.size[0], self.img.size[1]
expected_width, expected_height = size[0], size[1]
if current_width > current_height:
size = (expected_width, None)
else:
size = (None, expected_height)
return self._get_size(*size)

def _crop(self, size, opts):
if opts["position"] == "face":
if cv is None:
Expand Down
Binary file added pilbox/test/data/test_clip_enlarging.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions pilbox/test/image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,18 @@ def _assert_expected_exif(self, case):
% (case["source_path"], case["expected_path"])
self.assertEqual(rv.read(), expected.read(), msg)

def test_clip_larger_images(self):
path = os.path.join(DATADIR, "test_clip_enlarging.jpg")
with open(path, "rb") as f:
img = Image(f).resize(500, 500, mode='clip')
assert img.img.size == (500, 430)

def test_clip_smaller_images(self):
path = os.path.join(DATADIR, "test_clip_enlarging.jpg")
with open(path, "rb") as f:
img = Image(f).resize(200, 200, mode='clip')
assert img.img.size == (200, 172)


def _get_simple_criteria_combinations():
return _make_combinations(
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def run(self):


setup(name='pilbox',
version='1.3.2',
version='1.3.2.2',
description='Pilbox is an image processing application server built on the Tornado web framework using the Pillow Imaging Library',
long_description=readme,
classifiers=[
Expand Down