Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adding support for trim operations. Closes #116

Trim operations are now supported adding /trim to the URL.
Usage: /trim:[reference pixel]:[tolerance]
Both parameters are optional.
reference pixel: top-left or bottom-right (Default: top-left)
tolerance: Integer value used as tolerance (Default: 0)

The euclidian distance between the colors of the reference pixel
and the surrounding pixels is used. If the distance is within the
tolerance they'll get trimmed. For a RGB image the tolerance would
be within the range 0-442
  • Loading branch information...
commit 42e237474ee079121d0a0b5bf099a1a34d60dedd 1 parent f7b4afa
@cezarsa cezarsa authored
View
7 thumbor/context.py
@@ -62,6 +62,7 @@ class RequestParameters:
def __init__(self,
debug=False,
meta=False,
+ trim=None,
crop_left=None,
crop_top=None,
crop_right=None,
@@ -88,6 +89,12 @@ def __init__(self,
self.debug = bool(debug)
self.meta = bool(meta)
+ self.trim = trim
+ if trim is not None:
+ trim_parts = trim.split(':')
+ self.trim_pos = trim_parts[1] if len(trim_parts) > 1 else "top-left"
+ self.trim_tolerance = int(trim_parts[2]) if len(trim_parts) > 2 else 0
+
if crop is not None:
self.crop = crop
else:
View
103 thumbor/ext/filters/_bounding_box.c
@@ -0,0 +1,103 @@
+#include "filter.h"
+
+typedef struct {
+ int left;
+ int top;
+ int right;
+ int bottom;
+} box;
+
+typedef struct {
+ int width;
+ int height;
+ int stride;
+ unsigned char *image;
+} bitmap;
+
+inline double color_distance(unsigned char *c1, unsigned char *c2, int stride) {
+ double sum = 0;
+ int i;
+ for (i = 0; i < stride; ++i) {
+ sum += ((int)c1[i] - (int)c2[i]) * ((int)c1[i] - (int)c2[i]);
+ }
+ return sqrt(sum);
+}
+
+static box
+find_bounding_box(bitmap *image_data, char *reference_mode, int tolerance) {
+ box b = {
+ image_data->width, image_data->height, 0, 0
+ };
+ unsigned char *pixel, *reference_pixel;
+ int x, xi, y, stride = image_data->stride;
+
+ if (strcmp(reference_mode, "top-left") == 0) {
+ reference_pixel = image_data->image;
+ } else { // bottom-right
+ reference_pixel = image_data->image + (image_data->width * stride * (image_data->height - 1)) + (stride * (image_data->width - 1));
+ }
+
+ for (y = 0; y < image_data->height; ++y) {
+ for (x = 0; x < image_data->width; ++x) {
+ pixel = image_data->image + (image_data->width * stride * y) + (stride * x);
+ if (color_distance(pixel, reference_pixel, stride) > tolerance) {
+ if (x < b.left) {
+ b.left = x;
+ }
+ if (y < b.top) {
+ b.top = y;
+ }
+ b.bottom = y;
+ break;
+ }
+ }
+ int min_right = x > b.right ? x : b.right;
+ for (xi = image_data->width - 1; xi > min_right; --xi) {
+ pixel = image_data->image + (image_data->width * stride * y) + (stride * xi);
+ if (color_distance(pixel, reference_pixel, stride) > tolerance) {
+ if (xi > b.right) {
+ b.right = xi;
+ }
+ break;
+ }
+ }
+ }
+
+ return b;
+}
+
+static PyObject*
+_bounding_box_apply(PyObject *self, PyObject *args)
+{
+ PyObject *buffer_py = NULL, *image_mode = NULL, *width_py = NULL, *height_py = NULL, *reference_mode_py = NULL, *tolerance_py = NULL;
+
+ if (!PyArg_UnpackTuple(args, "apply", 6, 6, &image_mode, &width_py, &height_py, &reference_mode_py, &tolerance_py, &buffer_py)) {
+ return NULL;
+ }
+
+ char *image_mode_str = PyString_AsString(image_mode);
+ char *reference_mode = PyString_AsString(reference_mode_py);
+ unsigned char *buffer = (unsigned char *) PyString_AsString(buffer_py);
+ int width = (int) PyInt_AsLong(width_py),
+ height = (int) PyInt_AsLong(height_py);
+ int num_bytes = bytes_per_pixel(image_mode_str);
+ int tolerance = (int) PyInt_AsLong(tolerance_py);
+
+ bitmap bitmap = {
+ width,
+ height,
+ num_bytes,
+ buffer
+ };
+
+ box b = find_bounding_box(&bitmap, reference_mode, tolerance);
+
+ return Py_BuildValue("iiii", b.left, b.top, b.right, b.bottom);
+}
+
+FILTER_MODULE(_bounding_box,
+ "apply(image_mode, width, height, reference_mode, tolerance, buffer) -> (left, top, right, bottom)\n"
+ "Calculates the bounding box necessary to trim an image based on the color of "
+ "one of the corners and the euclidian distance between the colors within a "
+ "specified tolerance."
+)
View
25 thumbor/transformer.py
@@ -11,6 +11,15 @@
import math
from thumbor.point import FocalPoint
+from thumbor.utils import logger
+
+trim_enabled = True
+try:
+ from thumbor.ext.filters import _bounding_box
+except ImportError:
+ logger.warn("Error importing bounding_box filter, trimming won't work")
+ trim_enabled = False
+
class Transformer(object):
def __init__(self, context):
@@ -74,8 +83,24 @@ def adjust_focal_points(self):
def transform(self, callback):
self.done_callback = callback
self.reorientate()
+ self.trim()
self.smart_detect()
+ def trim(self):
+ if self.context.request.trim is None or not trim_enabled:
+ return
+ box = _bounding_box.apply(self.engine.get_image_mode(), self.engine.size[0], self.engine.size[1], self.context.request.trim_pos, self.context.request.trim_tolerance, self.engine.get_image_data())
+ if box[2] < box[0] or box[3] < box[1]:
+ logger.warn("Ignoring trim, there wouldn't be any image left, check the tolerance.")
+ return
+
+ self.engine.crop(box[0], box[1], box[2] + 1, box[3] + 1)
+ if self.context.request.should_crop:
+ self.context.request.crop['left'] -= box[0]
+ self.context.request.crop['top'] -= box[1]
+ self.context.request.crop['right'] -= box[0]
+ self.context.request.crop['bottom'] -= box[1]
+
def reorientate(self):
if self.context.config.RESPECT_ORIENTATION:
engine = self.context.modules.engine
View
7 thumbor/url.py
@@ -15,6 +15,7 @@ class Url(object):
unsafe_or_hash = r'(?:(?:(?P<unsafe>unsafe)|(?P<hash>[^/]{28,}?))/)?'
debug = '(?:(?P<debug>debug)/)?'
meta = '(?:(?P<meta>meta)/)?'
+ trim = '(?:(?P<trim>trim(?::(?:top-left|bottom-right))?(?::\d+)?)/)?'
crop = '(?:(?P<crop_left>\d+)x(?P<crop_top>\d+):(?P<crop_right>\d+)x(?P<crop_bottom>\d+)/)?'
fit_in = '(?:(?P<adaptive>adaptive-)?(?P<fit_in>fit-in)/)?'
dimensions = '(?:(?P<horizontal_flip>-)?(?P<width>(?:\d+|orig))?x(?P<vertical_flip>-)?(?P<height>(?:\d+|orig))?/)?'
@@ -33,6 +34,7 @@ def regex(cls):
reg.append(cls.unsafe_or_hash)
reg.append(cls.debug)
reg.append(cls.meta)
+ reg.append(cls.trim)
reg.append(cls.crop)
reg.append(cls.fit_in)
reg.append(cls.dimensions)
@@ -62,6 +64,7 @@ def parse(cls, url):
values = {
'debug': result['debug'] == 'debug',
'meta': result['meta'] == 'meta',
+ 'trim': result['trim'],
'crop': {
'left': int_or_0(result['crop_left']),
'top': int_or_0(result['crop_top']),
@@ -90,6 +93,7 @@ def generate_options(cls,
height=0,
smart=False,
meta=False,
+ trim=None,
adaptive=False,
fit_in=False,
horizontal_flip=False,
@@ -110,6 +114,9 @@ def generate_options(cls,
if meta:
url.append('meta')
+ if trim:
+ url.append(trim)
+
crop = crop_left or crop_top or crop_right or crop_bottom
if crop:
url.append('%sx%s:%sx%s' % (

4 comments on commit 42e2374

@heynemann
Owner

Cool, now on to change libthumbor, rubythumbor, thumboree...

@dhardy92
Collaborator

Hmm why not a filter ? as the opposite of fill()

@heynemann
Owner
@dhardy92
Collaborator

OK. It makes sens to me
I didn't catch it.

Please sign in to comment.
Something went wrong with that request. Please try again.