Merge branch 'processor_updates' into develop

This commit is contained in:
Matthew Tretter 2012-02-18 00:20:59 -05:00
commit 00c1cd3f9e
6 changed files with 423 additions and 397 deletions

View file

@ -61,12 +61,12 @@ your spec, you can expose different versions of the original image::
from django.db import models
from imagekit.models.fields import ImageSpecField
from imagekit.processors import resize, Adjust
from imagekit.processors import ResizeToFill, Adjust
class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1),
resize.Fill(50, 50)], image_field='original_image',
ResizeToFill(50, 50)], image_field='original_image',
format='JPEG', options={'quality': 90})
The ``thumbnail`` property will now return a cropped image::
@ -77,7 +77,7 @@ The ``thumbnail`` property will now return a cropped image::
photo.original_image.width # > 1000
The original image is not modified; ``thumbnail`` is a new file that is the
result of running the ``imagekit.processors.resize.Fill`` processor on the
result of running the ``imagekit.processors.ResizeToFill`` processor on the
original.
The ``imagekit.processors`` module contains processors for many common

View file

@ -7,260 +7,7 @@ should be limited to image manipulations--they should be completely decoupled
from both the filesystem and the ORM.
"""
from imagekit.lib import Image, ImageColor, ImageEnhance
from imagekit.processors import resize, crop
RGBA_TRANSPARENCY_FORMATS = ['PNG']
PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF']
class ProcessorPipeline(list):
"""
A :class:`list` of other processors. This class allows any object that
knows how to deal with a single processor to deal with a list of them.
For example::
processed_image = ProcessorPipeline([ProcessorA(), ProcessorB()]).process(image)
"""
def process(self, img):
for proc in self:
img = proc.process(img)
return img
class Adjust(object):
"""
Performs color, brightness, contrast, and sharpness enhancements on the
image. See :mod:`PIL.ImageEnhance` for more imformation.
"""
def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0):
"""
:param color: A number between 0 and 1 that specifies the saturation
of the image. 0 corresponds to a completely desaturated image
(black and white) and 1 to the original color.
See :class:`PIL.ImageEnhance.Color`
:param brightness: A number representing the brightness; 0 results in
a completely black image whereas 1 corresponds to the brightness
of the original. See :class:`PIL.ImageEnhance.Brightness`
:param contrast: A number representing the contrast; 0 results in a
completely gray image whereas 1 corresponds to the contrast of
the original. See :class:`PIL.ImageEnhance.Contrast`
:param sharpness: A number representing the sharpness; 0 results in a
blurred image; 1 corresponds to the original sharpness; 2
results in a sharpened image. See
:class:`PIL.ImageEnhance.Sharpness`
"""
self.color = color
self.brightness = brightness
self.contrast = contrast
self.sharpness = sharpness
def process(self, img):
original = img = img.convert('RGBA')
for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']:
factor = getattr(self, name.lower())
if factor != 1.0:
try:
img = getattr(ImageEnhance, name)(img).enhance(factor)
except ValueError:
pass
else:
# PIL's Color and Contrast filters both convert the image
# to L mode, losing transparency info, so we put it back.
# See https://github.com/jdriscoll/django-imagekit/issues/64
if name in ('Color', 'Contrast'):
img = Image.merge('RGBA', img.split()[:3] +
original.split()[3:4])
return img
class Reflection(object):
"""
Creates an image with a reflection.
"""
background_color = '#FFFFFF'
size = 0.0
opacity = 0.6
def process(self, img):
# Convert bgcolor string to RGB value.
background_color = ImageColor.getrgb(self.background_color)
# Handle palleted images.
img = img.convert('RGB')
# Copy orignial image and flip the orientation.
reflection = img.copy().transpose(Image.FLIP_TOP_BOTTOM)
# Create a new image filled with the bgcolor the same size.
background = Image.new("RGB", img.size, background_color)
# Calculate our alpha mask.
start = int(255 - (255 * self.opacity)) # The start of our gradient.
steps = int(255 * self.size) # The number of intermedite values.
increment = (255 - start) / float(steps)
mask = Image.new('L', (1, 255))
for y in range(255):
if y < steps:
val = int(y * increment + start)
else:
val = 255
mask.putpixel((0, y), val)
alpha_mask = mask.resize(img.size)
# Merge the reflection onto our background color using the alpha mask.
reflection = Image.composite(background, reflection, alpha_mask)
# Crop the reflection.
reflection_height = int(img.size[1] * self.size)
reflection = reflection.crop((0, 0, img.size[0], reflection_height))
# Create new image sized to hold both the original image and
# the reflection.
composite = Image.new("RGB", (img.size[0], img.size[1] + reflection_height), background_color)
# Paste the orignal image and the reflection into the composite image.
composite.paste(img, (0, 0))
composite.paste(reflection, (0, img.size[1]))
# Return the image complete with reflection effect.
return composite
class Transpose(object):
"""
Rotates or flips the image.
"""
AUTO = 'auto'
FLIP_HORIZONTAL = Image.FLIP_LEFT_RIGHT
FLIP_VERTICAL = Image.FLIP_TOP_BOTTOM
ROTATE_90 = Image.ROTATE_90
ROTATE_180 = Image.ROTATE_180
ROTATE_270 = Image.ROTATE_270
methods = [AUTO]
_EXIF_ORIENTATION_STEPS = {
1: [],
2: [FLIP_HORIZONTAL],
3: [ROTATE_180],
4: [FLIP_VERTICAL],
5: [ROTATE_270, FLIP_HORIZONTAL],
6: [ROTATE_270],
7: [ROTATE_90, FLIP_HORIZONTAL],
8: [ROTATE_90],
}
def __init__(self, *args):
"""
Possible arguments:
- Transpose.AUTO
- Transpose.FLIP_HORIZONTAL
- Transpose.FLIP_VERTICAL
- Transpose.ROTATE_90
- Transpose.ROTATE_180
- Transpose.ROTATE_270
The order of the arguments dictates the order in which the
Transposition steps are taken.
If Transpose.AUTO is present, all other arguments are ignored, and
the processor will attempt to rotate the image according to the
EXIF Orientation data.
"""
super(Transpose, self).__init__()
if args:
self.methods = args
def process(self, img):
if self.AUTO in self.methods:
try:
orientation = img._getexif()[0x0112]
ops = self._EXIF_ORIENTATION_STEPS[orientation]
except (KeyError, TypeError, AttributeError):
ops = []
else:
ops = self.methods
for method in ops:
img = img.transpose(method)
return img
class AutoConvert(object):
"""A processor that does some common-sense conversions based on the target
format. This includes things like preserving transparency and quantizing.
This processors is used automatically by ``ImageSpecField`` and
``ProcessedImageField`` immediately before saving the image unless you
specify ``autoconvert=False``.
"""
def __init__(self, format):
self.format = format
def process(self, img):
matte = False
self.save_kwargs = {}
if img.mode == 'RGBA':
if self.format in RGBA_TRANSPARENCY_FORMATS:
pass
elif self.format in PALETTE_TRANSPARENCY_FORMATS:
# If you're going from a format with alpha transparency to one
# with palette transparency, transparency values will be
# snapped: pixels that are more opaque than not will become
# fully opaque; pixels that are more transparent than not will
# become fully transparent. This will not produce a good-looking
# result if your image contains varying levels of opacity; in
# that case, you'll probably want to use a processor to matte
# the image on a solid color. The reason we don't matte by
# default is because not doing so allows processors to treat
# RGBA-format images as a super-type of P-format images: if you
# have an RGBA-format image with only a single transparent
# color, and save it as a GIF, it will retain its transparency.
# In other words, a P-format image converted to an
# RGBA-formatted image by a processor and then saved as a
# P-format image will give the expected results.
alpha = img.split()[-1]
mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE,
colors=255)
img.paste(255, mask)
self.save_kwargs['transparency'] = 255
else:
# Simply converting an RGBA-format image to an RGB one creates a
# gross result, so we matte the image on a white background. If
# that's not what you want, that's fine: use a processor to deal
# with the transparency however you want. This is simply a
# sensible default that will always produce something that looks
# good. Or at least, it will look better than just a straight
# conversion.
matte = True
elif img.mode == 'P':
if self.format in PALETTE_TRANSPARENCY_FORMATS:
try:
self.save_kwargs['transparency'] = img.info['transparency']
except KeyError:
pass
elif self.format in RGBA_TRANSPARENCY_FORMATS:
# Currently PIL doesn't support any RGBA-mode formats that
# aren't also P-mode formats, so this will never happen.
img = img.convert('RGBA')
else:
matte = True
else:
img = img.convert('RGB')
# GIFs are always going to be in palette mode, so we can do a little
# optimization. Note that the RGBA sources also use adaptive
# quantization (above). Images that are already in P mode don't need
# any quantization because their colors are already limited.
if self.format == 'GIF':
img = img.convert('P', palette=Image.ADAPTIVE)
if matte:
img = img.convert('RGBA')
bg = Image.new('RGBA', img.size, (255, 255, 255))
bg.paste(img, img)
img = bg.convert('RGB')
if self.format == 'JPEG':
self.save_kwargs['optimize'] = True
return img
from .base import *
from .crop import *
from .resize import *

296
imagekit/processors/base.py Normal file
View file

@ -0,0 +1,296 @@
from imagekit.lib import Image, ImageColor, ImageEnhance
RGBA_TRANSPARENCY_FORMATS = ['PNG']
PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF']
class ProcessorPipeline(list):
"""
A :class:`list` of other processors. This class allows any object that
knows how to deal with a single processor to deal with a list of them.
For example::
processed_image = ProcessorPipeline([ProcessorA(), ProcessorB()]).process(image)
"""
def process(self, img):
for proc in self:
img = proc.process(img)
return img
class Adjust(object):
"""
Performs color, brightness, contrast, and sharpness enhancements on the
image. See :mod:`PIL.ImageEnhance` for more imformation.
"""
def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0):
"""
:param color: A number between 0 and 1 that specifies the saturation
of the image. 0 corresponds to a completely desaturated image
(black and white) and 1 to the original color.
See :class:`PIL.ImageEnhance.Color`
:param brightness: A number representing the brightness; 0 results in
a completely black image whereas 1 corresponds to the brightness
of the original. See :class:`PIL.ImageEnhance.Brightness`
:param contrast: A number representing the contrast; 0 results in a
completely gray image whereas 1 corresponds to the contrast of
the original. See :class:`PIL.ImageEnhance.Contrast`
:param sharpness: A number representing the sharpness; 0 results in a
blurred image; 1 corresponds to the original sharpness; 2
results in a sharpened image. See
:class:`PIL.ImageEnhance.Sharpness`
"""
self.color = color
self.brightness = brightness
self.contrast = contrast
self.sharpness = sharpness
def process(self, img):
original = img = img.convert('RGBA')
for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']:
factor = getattr(self, name.lower())
if factor != 1.0:
try:
img = getattr(ImageEnhance, name)(img).enhance(factor)
except ValueError:
pass
else:
# PIL's Color and Contrast filters both convert the image
# to L mode, losing transparency info, so we put it back.
# See https://github.com/jdriscoll/django-imagekit/issues/64
if name in ('Color', 'Contrast'):
img = Image.merge('RGBA', img.split()[:3] +
original.split()[3:4])
return img
class Reflection(object):
"""
Creates an image with a reflection.
"""
background_color = '#FFFFFF'
size = 0.0
opacity = 0.6
def process(self, img):
# Convert bgcolor string to RGB value.
background_color = ImageColor.getrgb(self.background_color)
# Handle palleted images.
img = img.convert('RGB')
# Copy orignial image and flip the orientation.
reflection = img.copy().transpose(Image.FLIP_TOP_BOTTOM)
# Create a new image filled with the bgcolor the same size.
background = Image.new("RGB", img.size, background_color)
# Calculate our alpha mask.
start = int(255 - (255 * self.opacity)) # The start of our gradient.
steps = int(255 * self.size) # The number of intermedite values.
increment = (255 - start) / float(steps)
mask = Image.new('L', (1, 255))
for y in range(255):
if y < steps:
val = int(y * increment + start)
else:
val = 255
mask.putpixel((0, y), val)
alpha_mask = mask.resize(img.size)
# Merge the reflection onto our background color using the alpha mask.
reflection = Image.composite(background, reflection, alpha_mask)
# Crop the reflection.
reflection_height = int(img.size[1] * self.size)
reflection = reflection.crop((0, 0, img.size[0], reflection_height))
# Create new image sized to hold both the original image and
# the reflection.
composite = Image.new("RGB", (img.size[0], img.size[1] + reflection_height), background_color)
# Paste the orignal image and the reflection into the composite image.
composite.paste(img, (0, 0))
composite.paste(reflection, (0, img.size[1]))
# Return the image complete with reflection effect.
return composite
class Transpose(object):
"""
Rotates or flips the image.
"""
AUTO = 'auto'
FLIP_HORIZONTAL = Image.FLIP_LEFT_RIGHT
FLIP_VERTICAL = Image.FLIP_TOP_BOTTOM
ROTATE_90 = Image.ROTATE_90
ROTATE_180 = Image.ROTATE_180
ROTATE_270 = Image.ROTATE_270
methods = [AUTO]
_EXIF_ORIENTATION_STEPS = {
1: [],
2: [FLIP_HORIZONTAL],
3: [ROTATE_180],
4: [FLIP_VERTICAL],
5: [ROTATE_270, FLIP_HORIZONTAL],
6: [ROTATE_270],
7: [ROTATE_90, FLIP_HORIZONTAL],
8: [ROTATE_90],
}
def __init__(self, *args):
"""
Possible arguments:
- Transpose.AUTO
- Transpose.FLIP_HORIZONTAL
- Transpose.FLIP_VERTICAL
- Transpose.ROTATE_90
- Transpose.ROTATE_180
- Transpose.ROTATE_270
The order of the arguments dictates the order in which the
Transposition steps are taken.
If Transpose.AUTO is present, all other arguments are ignored, and
the processor will attempt to rotate the image according to the
EXIF Orientation data.
"""
super(Transpose, self).__init__()
if args:
self.methods = args
def process(self, img):
if self.AUTO in self.methods:
try:
orientation = img._getexif()[0x0112]
ops = self._EXIF_ORIENTATION_STEPS[orientation]
except (KeyError, TypeError, AttributeError):
ops = []
else:
ops = self.methods
for method in ops:
img = img.transpose(method)
return img
class AutoConvert(object):
"""A processor that does some common-sense conversions based on the target
format. This includes things like preserving transparency and quantizing.
This processors is used automatically by ``ImageSpecField`` and
``ProcessedImageField`` immediately before saving the image unless you
specify ``autoconvert=False``.
"""
def __init__(self, format):
self.format = format
def process(self, img):
matte = False
self.save_kwargs = {}
self.rgba_ = img.mode == 'RGBA'
if self.rgba_:
if self.format in RGBA_TRANSPARENCY_FORMATS:
pass
elif self.format in PALETTE_TRANSPARENCY_FORMATS:
# If you're going from a format with alpha transparency to one
# with palette transparency, transparency values will be
# snapped: pixels that are more opaque than not will become
# fully opaque; pixels that are more transparent than not will
# become fully transparent. This will not produce a good-looking
# result if your image contains varying levels of opacity; in
# that case, you'll probably want to use a processor to matte
# the image on a solid color. The reason we don't matte by
# default is because not doing so allows processors to treat
# RGBA-format images as a super-type of P-format images: if you
# have an RGBA-format image with only a single transparent
# color, and save it as a GIF, it will retain its transparency.
# In other words, a P-format image converted to an
# RGBA-formatted image by a processor and then saved as a
# P-format image will give the expected results.
alpha = img.split()[-1]
mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE,
colors=255)
img.paste(255, mask)
self.save_kwargs['transparency'] = 255
else:
# Simply converting an RGBA-format image to an RGB one creates a
# gross result, so we matte the image on a white background. If
# that's not what you want, that's fine: use a processor to deal
# with the transparency however you want. This is simply a
# sensible default that will always produce something that looks
# good. Or at least, it will look better than just a straight
# conversion.
matte = True
elif img.mode == 'P':
if self.format in PALETTE_TRANSPARENCY_FORMATS:
try:
self.save_kwargs['transparency'] = img.info['transparency']
except KeyError:
pass
elif self.format in RGBA_TRANSPARENCY_FORMATS:
# Currently PIL doesn't support any RGBA-mode formats that
# aren't also P-mode formats, so this will never happen.
img = img.convert('RGBA')
else:
matte = True
else:
img = img.convert('RGB')
# GIFs are always going to be in palette mode, so we can do a little
# optimization. Note that the RGBA sources also use adaptive
# quantization (above). Images that are already in P mode don't need
# any quantization because their colors are already limited.
if self.format == 'GIF':
img = img.convert('P', palette=Image.ADAPTIVE)
if matte:
img = img.convert('RGBA')
bg = Image.new('RGBA', img.size, (255, 255, 255))
bg.paste(img, img)
img = bg.convert('RGB')
if self.format == 'JPEG':
self.save_kwargs['optimize'] = True
return img
class Anchor(object):
"""
Defines all the anchor points needed by the various processor classes.
"""
TOP_LEFT = 'tl'
TOP = 't'
TOP_RIGHT = 'tr'
BOTTOM_LEFT = 'bl'
BOTTOM = 'b'
BOTTOM_RIGHT = 'br'
CENTER = 'c'
LEFT = 'l'
RIGHT = 'r'
_ANCHOR_PTS = {
TOP_LEFT: (0, 0),
TOP: (0.5, 0),
TOP_RIGHT: (1, 0),
LEFT: (0, 0.5),
CENTER: (0.5, 0.5),
RIGHT: (1, 0.5),
BOTTOM_LEFT: (0, 1),
BOTTOM: (0.5, 1),
BOTTOM_RIGHT: (1, 1),
}
@staticmethod
def get_tuple(anchor):
"""Normalizes anchor values (strings or tuples) to tuples.
"""
# If the user passed in one of the string values, convert it to a
# percentage tuple.
if anchor in Anchor._ANCHOR_PTS.keys():
anchor = Anchor._ANCHOR_PTS[anchor]
return anchor

View file

@ -1,5 +1,6 @@
from ..lib import Image, ImageChops, ImageDraw, ImageStat
from .base import Anchor
from .utils import histogram_entropy
from ..lib import Image, ImageChops, ImageDraw, ImageStat
class Side(object):
@ -71,101 +72,31 @@ class TrimBorderColor(object):
return img
class BasicCrop(object):
"""Crops an image to the specified rectangular region.
"""
def __init__(self, x, y, width, height):
"""
:param x: The x position of the clipping box, in pixels.
:param y: The y position of the clipping box, in pixels.
:param width: The width position of the clipping box, in pixels.
:param height: The height position of the clipping box, in pixels.
"""
self.x = x
self.y = y
self.width = width
self.height = height
def process(self, img):
box = (self.x, self.y, self.x + self.width, self.y + self.height)
return img.crop(box)
class Crop(object):
"""
Crops an image , cropping it to the specified width and height
relative to the anchor.
Crops an image, cropping it to the specified width and height. You may
optionally provide either an anchor or x and y coordinates. This processor
functions exactly the same as ``ResizeCanvas`` except that it will never
enlarge the image.
"""
TOP_LEFT = 'tl'
TOP = 't'
TOP_RIGHT = 'tr'
BOTTOM_LEFT = 'bl'
BOTTOM = 'b'
BOTTOM_RIGHT = 'br'
CENTER = 'c'
LEFT = 'l'
RIGHT = 'r'
_ANCHOR_PTS = {
TOP_LEFT: (0, 0),
TOP: (0.5, 0),
TOP_RIGHT: (1, 0),
LEFT: (0, 0.5),
CENTER: (0.5, 0.5),
RIGHT: (1, 0.5),
BOTTOM_LEFT: (0, 1),
BOTTOM: (0.5, 1),
BOTTOM_RIGHT: (1, 1),
}
def __init__(self, width=None, height=None, anchor=None):
"""
:param width: The target width, in pixels.
:param height: The target height, in pixels.
:param anchor: Specifies which part of the image should be retained
when cropping. Valid values are:
- Crop.TOP_LEFT
- Crop.TOP
- Crop.TOP_RIGHT
- Crop.LEFT
- Crop.CENTER
- Crop.RIGHT
- Crop.BOTTOM_LEFT
- Crop.BOTTOM
- Crop.BOTTOM_RIGHT
You may also pass a tuple that indicates the percentages of excess
to be trimmed from each dimension. For example, ``(0, 0)``
corresponds to "top left", ``(0.5, 0.5)`` to "center" and ``(1, 1)``
to "bottom right". This is basically the same as using percentages
in CSS background positions.
"""
def __init__(self, width=None, height=None, anchor=None, x=None, y=None):
self.width = width
self.height = height
self.anchor = anchor
self.x = x
self.y = y
def process(self, img):
from .resize import ResizeCanvas
original_width, original_height = img.size
new_width, new_height = min(original_width, self.width), \
min(original_height, self.height)
trim_x, trim_y = original_width - new_width, \
original_height - new_height
# If the user passed in one of the string values, convert it to a
# percentage tuple.
anchor = self.anchor or Crop.CENTER
if anchor in Crop._ANCHOR_PTS.keys():
anchor = Crop._ANCHOR_PTS[anchor]
x = int(float(trim_x) * float(anchor[0]))
y = int(float(trim_y) * float(anchor[1]))
return BasicCrop(x, y, new_width, new_height).process(img)
return ResizeCanvas(new_width, new_height, anchor=self.anchor,
x=self.x, y=self.y).process(img)
class SmartCrop(object):

View file

@ -1,9 +1,9 @@
from imagekit.lib import Image
from . import crop
import warnings
from .base import Anchor
class BasicResize(object):
class Resize(object):
"""
Resizes an image to the specified width and height.
@ -25,7 +25,7 @@ class Cover(object):
"""
Resizes the image to the smallest possible size that will entirely cover the
provided dimensions. You probably won't be using this processor directly,
but it's used internally by ``Fill`` and ``SmartFill``.
but it's used internally by ``ResizeToFill`` and ``SmartResize``.
"""
def __init__(self, width, height):
@ -42,59 +42,39 @@ class Cover(object):
float(self.height) / original_height)
new_width, new_height = (int(original_width * ratio),
int(original_height * ratio))
return BasicResize(new_width, new_height).process(img)
return Resize(new_width, new_height).process(img)
class Fill(object):
class ResizeToFill(object):
"""
Resizes an image , cropping it to the exact specified width and height.
Resizes an image, cropping it to the exact specified width and height.
"""
TOP_LEFT = crop.Crop.TOP_LEFT
TOP = crop.Crop.TOP
TOP_RIGHT = crop.Crop.TOP_RIGHT
BOTTOM_LEFT = crop.Crop.BOTTOM_LEFT
BOTTOM = crop.Crop.BOTTOM
BOTTOM_RIGHT = crop.Crop.BOTTOM_RIGHT
CENTER = crop.Crop.CENTER
LEFT = crop.Crop.LEFT
RIGHT = crop.Crop.RIGHT
def __init__(self, width=None, height=None, anchor=None):
"""
:param width: The target width, in pixels.
:param height: The target height, in pixels.
:param anchor: Specifies which part of the image should be retained
when cropping. Valid values are:
- Fill.TOP_LEFT
- Fill.TOP
- Fill.TOP_RIGHT
- Fill.LEFT
- Fill.CENTER
- Fill.RIGHT
- Fill.BOTTOM_LEFT
- Fill.BOTTOM
- Fill.BOTTOM_RIGHT
when cropping.
"""
self.width = width
self.height = height
self.anchor = anchor
def process(self, img):
from .crop import Crop
img = Cover(self.width, self.height).process(img)
return crop.Crop(self.width, self.height,
return Crop(self.width, self.height,
anchor=self.anchor).process(img)
class SmartFill(object):
class SmartResize(object):
"""
The ``SmartFill`` processor is identical to ``Fill``, except that it uses
entropy to crop the image instead of a user-specified anchor point.
Internally, it simply runs the ``resize.Cover`` and ``crop.SmartCrop``
The ``SmartResize`` processor is identical to ``ResizeToFill``, except that
it uses entropy to crop the image instead of a user-specified anchor point.
Internally, it simply runs the ``ResizeToCover`` and ``SmartCrop``
processors in series.
"""
def __init__(self, width, height):
"""
@ -105,23 +85,104 @@ class SmartFill(object):
self.width, self.height = width, height
def process(self, img):
from .crop import SmartCrop
img = Cover(self.width, self.height).process(img)
return crop.SmartCrop(self.width, self.height).process(img)
return SmartCrop(self.width, self.height).process(img)
class Crop(Fill):
def __init__(self, *args, **kwargs):
warnings.warn('`imagekit.processors.resize.Crop` has been renamed to'
'`imagekit.processors.resize.Fill`.', DeprecationWarning)
super(Crop, self).__init__(*args, **kwargs)
class ResizeCanvas(object):
"""
Resizes the canvas, using the provided background color if the new size is
larger than the current image.
"""
def __init__(self, width, height, color=None, anchor=None, x=None, y=None):
"""
:param width: The target width, in pixels.
:param height: The target height, in pixels.
:param color: The background color to use for padding.
:param anchor: Specifies the position of the original image on the new
canvas. Valid values are:
- Anchor.TOP_LEFT
- Anchor.TOP
- Anchor.TOP_RIGHT
- Anchor.LEFT
- Anchor.CENTER
- Anchor.RIGHT
- Anchor.BOTTOM_LEFT
- Anchor.BOTTOM
- Anchor.BOTTOM_RIGHT
You may also pass a tuple that indicates the position in
percentages. For example, ``(0, 0)`` corresponds to "top left",
``(0.5, 0.5)`` to "center" and ``(1, 1)`` to "bottom right". This is
basically the same as using percentages in CSS background positions.
"""
if x is not None or y is not None:
if anchor:
raise Exception('You may provide either an anchor or x and y'
' coordinate, but not both.')
else:
self.x, self.y = x or 0, y or 0
self.anchor = None
else:
self.anchor = anchor or Anchor.CENTER
self.x = self.y = None
self.width = width
self.height = height
self.color = color or (255, 255, 255, 0)
def process(self, img):
original_width, original_height = img.size
if self.anchor:
anchor = Anchor.get_tuple(self.anchor)
trim_x, trim_y = self.width - original_width, \
self.height - original_height
x = int(float(trim_x) * float(anchor[0]))
y = int(float(trim_y) * float(anchor[1]))
else:
x, y = self.x, self.y
new_img = Image.new('RGBA', (self.width, self.height), self.color)
new_img.paste(img, (x, y))
return new_img
class Fit(object):
class AddBorder(object):
"""
Add a border of specific color and size to an image.
"""
def __init__(self, thickness, color=None):
"""
:param color: Color to use for the border
:param thickness: Thickness of the border. Can be either an int or
a 4-tuple of ints of the form (top, right, bottom, left).
"""
self.color = color
if isinstance(thickness, int):
self.top = self.right = self.bottom = self.left = thickness
else:
self.top, self.right, self.bottom, self.left = thickness
def process(self, img):
new_width = img.size[0] + self.left + self.right
new_height = img.size[1] + self.top + self.bottom
return ResizeCanvas(new_width, new_height, color=self.color,
x=self.left, y=self.top).process(img)
class ResizeToFit(object):
"""
Resizes an image to fit within the specified dimensions.
"""
def __init__(self, width=None, height=None, upscale=None, mat_color=None):
def __init__(self, width=None, height=None, upscale=None, mat_color=None, anchor=Anchor.CENTER):
"""
:param width: The maximum width of the desired image.
:param height: The maximum height of the desired image.
@ -136,6 +197,7 @@ class Fit(object):
self.height = height
self.upscale = upscale
self.mat_color = mat_color
self.anchor = anchor
def process(self, img):
cur_width, cur_height = img.size
@ -151,18 +213,8 @@ class Fit(object):
int(round(cur_height * ratio)))
if (cur_width > new_dimensions[0] or cur_height > new_dimensions[1]) or \
self.upscale:
img = BasicResize(new_dimensions[0],
img = Resize(new_dimensions[0],
new_dimensions[1]).process(img)
if self.mat_color:
new_img = Image.new('RGBA', (self.width, self.height), self.mat_color)
new_img.paste(img, ((self.width - img.size[0]) / 2, (self.height - img.size[1]) / 2))
img = new_img
img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img)
return img
class SmartCrop(crop.SmartCrop):
def __init__(self, *args, **kwargs):
warnings.warn('The SmartCrop processor has been moved to'
' `imagekit.processors.crop.SmartCrop`, where it belongs.',
DeprecationWarning)
super(SmartCrop, self).__init__(*args, **kwargs)

View file

@ -13,8 +13,8 @@ from imagekit import utils
from imagekit.lib import Image
from imagekit.models.fields import ImageSpecField
from imagekit.processors import Adjust
from imagekit.processors.resize import Fill
from imagekit.processors.crop import SmartCrop
from imagekit.processors import ResizeToFill
from imagekit.processors import SmartCrop
def generate_lenna():
@ -52,7 +52,7 @@ class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1),
Fill(50, 50)], image_field='original_image', format='JPEG',
ResizeToFill(50, 50)], image_field='original_image', format='JPEG',
options={'quality': 90})
smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2,