diff --git a/CHANGES.txt b/CHANGES.txt index 13c3e38..62c0380 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/pilbox/app.py b/pilbox/app.py index d8d0d0d..519b521 100755 --- a/pilbox/app.py +++ b/pilbox/app.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, with_statement +import json import logging import socket @@ -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"] @@ -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() @@ -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: @@ -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: diff --git a/pilbox/image.py b/pilbox/image.py index bb23846..96bf17e 100644 --- a/pilbox/image.py +++ b/pilbox/image.py @@ -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): @@ -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: diff --git a/pilbox/test/data/test_clip_enlarging.jpg b/pilbox/test/data/test_clip_enlarging.jpg new file mode 100644 index 0000000..bd81b09 Binary files /dev/null and b/pilbox/test/data/test_clip_enlarging.jpg differ diff --git a/pilbox/test/image_test.py b/pilbox/test/image_test.py index 72fb4c9..9113782 100644 --- a/pilbox/test/image_test.py +++ b/pilbox/test/image_test.py @@ -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( diff --git a/setup.py b/setup.py index bb180da..b06c123 100644 --- a/setup.py +++ b/setup.py @@ -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=[