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