A Custom Cropping Engine With sorl-thumbnail
Sorl-thumbnail has a sensible crop function in the default engine, but sometimes you need a little more control over the results. This post provides a custom cropping PIL engine that can be used to return specific, i.e. from (x1, y1) to (x2, y2), cropped thumbnails
Update
This is an old post. I haven't used Sorl in quite a while so this code may be out-of-date
I was recently using sorl-thumbnail
and needed to create cropped thumbnails. I wanted to be able to specify exactly where I wanted to crop from as well as scale the dimensions of the resulting cropped thumbnail. For example “create crop from pixels 10,10
to 500,750
“,
Unfortunately the (sensible) default behaviour of the PIL
engine in sorl-thumbnail
first scales the image, and then performs a CSS-esque crop to the resulting scaled image. Essentially the reverse of what I wanted.
The following is a custom sorl-thumbnail
engine that can be hot-swapped (or used by default if you prefer), allowing you to create nicely cropped and scaled thumbnails.
from sorl.thumbnail.engines import pil_engine
from sorl.thumbnail import parsers
class CustomCroppingEngine(pil_engine.Engine):
"""
A custom sorl.thumbnail engine (using PIL) that first crops
an image according to 4 pixel/percentage values in the source
image, then scales that crop down to the size specified in the
geometry. This is in contrast to sorl.thumbnail's default engine
which first scales the image down to the specified geometry
and applies the crop afterward.
"""
def create(self, image, geometry, options):
image = self.orientation(image, geometry, options)
image = self.colorspace(image, geometry, options)
image = self.crop(image, geometry, options)
image = self.scale(image, geometry, options)
return image
def _crop_parse(self, crop, xy_image, xy_window):
"""
Convert the crop string passed by the user to accurate
cropping values (this is adapter from the default
sorl.thumbnail.parsers.parse_crop)
"""
crops = crop.split(' ')
if len(crops) != 4:
raise parsers.ThumbnailParseError(
'Unrecognized crop option: %s' % crop)
x1, y1, x2, y2 = crops
def get_offset(crop, epsilon):
m = parsers.bgpos_pat.match(crop)
if not m:
raise parsers.ThumbnailParseError(
'Unrecognized crop option: %s' % crop)
# we only take ints in the regexp
value = int(m.group('value'))
unit = m.group('unit')
if unit == '%':
value = epsilon * value / 100.0
return int(max(0, min(value, epsilon)))
x1 = get_offset(x1, xy_image[0])
y1 = get_offset(y1, xy_image[0])
x2 = get_offset(x2, xy_image[1])
y2 = get_offset(y2, xy_image[1])
return x1, y1, x2, y2
def crop(self, image, geometry, options):
crop = options['crop']
if not crop or crop == 'noop':
return image
x_image, y_image = self.get_image_size(image)
x1,y1,x2,y2 = self._crop_parse(
crop, (x_image, y_image), geometry)
return self._crop(image, x1, y1, x2, y2)
def _crop(self, image, x1, y1, x2, y2):
return image.crop((x1, y1, x2, y2))
this can now be used dynamically in your application:
from sorl.thumbnail import get_thumbnail, default
from myapp.engine import CustomCroppingEngine
def crop(img, x1, y1, x2, y2, crop_width, crop_height):
# Swap engines
old_engine = default.engine
default.engine = CustomCroppingEngine()
crop = get_thumbnail(
self.source,
"%sx%s" % (crop_width, crop_height),
crop="%s%% %s%% %s%% %s%%" % (int(x1), int(y1), int(x2), int(y2)))
# Replace normal engine
default.engine = old_engine
return crop
alternatively you can make permanent use of this engine by setting settings.THUMBNAIL_ENGINE