Merge branch 'release/1.1.0'

* release/1.1.0: (28 commits)
  Adding (crude) changelog information for 1.1.0.
  Bumping the version number.
  Removing Pillow from requirements.txt and adding a note about PIL/Pillow to the README. References #72.
  Being a bit friendlier to Python 2.5 users... or is it 2.4. Fixes #82.
  Change handling of spec deletion
  No need to check for None
  Catch KeyError on image.info dictionary
  Using difference instead of subtract
  Indentation tweak
  Makes evaluation of `source_file` lazy
  Test illustrating #75, #74
  Separate create_photo method
  avoid Transpose to crash when exif data doesn't exists
  Allows `None` value for `processors` argument
  Remove _Resize class
  Adds crop module to docs
  Explicitly import crop module
  Renames processor to `TrimBorderColor`
  Gives precedence to user options
  Replaces `quality` argument with `options` dict
  ...
This commit is contained in:
Bryan Veloso 2011-12-22 22:18:31 -08:00
commit 0656292bef
18 changed files with 324 additions and 241 deletions

View file

@ -6,6 +6,7 @@ Maintainers
~~~~~~~~~~~
* `Bryan Veloso`_
* `Matthew Tretter`_
* `Chris Drackett`_
* `Greg Newman`_
@ -14,11 +15,11 @@ Contributors
* `Josh Ourisman`_
* `Jonathan Slenders`_
* `Matthew Tretter`_
* `Eric Eldredge`_
* `Chris McKenzie`_
* `Markus Kaiserswerth`_
* `Ryan Bagwell`_
* `Alexander Bohn`_
.. _Justin Driscoll: http://github.com/jdriscoll
@ -33,3 +34,4 @@ Contributors
.. _Chris McKenzie: http://github.com/kenzic
.. _Ryan Bagwell: http://github.com/ryanbagwell
.. _Markus Kaiserswerth: http://github.com/mkai
.. _Alexander Bohn: http://github.com/fish2000

View file

@ -6,9 +6,19 @@ like different sizes (e.g. thumbnails) and black and white versions.
Installation
------------
1. ``pip install django-imagekit``
1. Install `PIL`_ or `Pillow`_. If you're using `ImageField`s in Django, you
should have already done this.
2. ``pip install django-imagekit``
(or clone the source and put the imagekit module on your path)
2. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py
3. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py
.. note:: If you've never seen Pillow before, it considers itself a
more-frequently updated "friendly" fork of PIL that's compatible with
setuptools. As such, it shares the same namespace as PIL does and is a
drop-in replacement.
.. _`PIL`: http://pypi.python.org/pypi/PIL
.. _`Pillow`: http://pypi.python.org/pypi/Pillow
Adding Specs to a Model
@ -23,7 +33,7 @@ of a model class::
class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
formatted_image = ImageSpec(image_field='original_image', format='JPEG',
quality=90)
options={'quality': 90})
Accessing the spec through a model instance will create the image and return
an ImageFile-like object (just like with a normal
@ -51,7 +61,7 @@ your spec, you can expose different versions of the original image::
original_image = models.ImageField(upload_to='photos')
thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1),
resize.Crop(50, 50)], image_field='original_image',
format='JPEG', quality=90)
format='JPEG', options={'quality': 90})
The ``thumbnail`` property will now return a cropped image::
@ -77,7 +87,7 @@ implement a ``process()`` method::
class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
watermarked_image = ImageSpec([Watermark()], image_field='original_image',
format='JPEG', quality=90)
format='JPEG', options={'quality': 90})
Admin

View file

@ -18,6 +18,9 @@ API Reference
.. automodule:: imagekit.processors.resize
:members:
.. automodule:: imagekit.processors.crop
:members:
:mod:`admin` Module
--------------------

View file

@ -1,16 +1,30 @@
Changelog
=========
v1.1.0
------
- A ``SmartCrop`` resize processor was added. This allows an image to be
cropped based on the amount of entropy in the target image's histogram.
- The ``quality`` argument was removed in favor of an ``options`` dictionary.
This is a more general solution which grants access to PIL's format-specific
options (including "quality", "progressive", and "optimize" for JPEGs).
- The ``TrimColor`` processor was renamed to ``TrimBorderColor``.
- The private ``_Resize`` class has been removed.
v1.0.3
------
- ``ImageSpec._create()`` was renamed ``ImageSpec.generate()`` and is now
available in the public API.
- Added an ``AutoConvert`` processor to encapsulate the trasnparency
- Added an ``AutoConvert`` processor to encapsulate the transparency
handling logic.
- Refactored trasnaprency handling to be smarter, handleing a lot more of
- Refactored transparency handling to be smarter, handling a lot more of
the situations in which one would convert to or from formats that support
transparency.

View file

@ -48,9 +48,9 @@ copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett &
# built documents.
#
# The short X.Y version.
version = '1.0.3'
version = '1.1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0.3'
release = '1.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -1,141 +0,0 @@
.. _ref-tutorial:
ImageKit in 7 Steps
===================
Step 1
******
::
$ pip install django-imagekit
(or clone the source and put the imagekit module on your path)
Step 2
******
Add ImageKit to your models.
::
# myapp/models.py
from django.db import models
from imagekit.models import ImageModel
class Photo(ImageModel):
name = models.CharField(max_length=100)
original_image = models.ImageField(upload_to='photos')
num_views = models.PositiveIntegerField(editable=False, default=0)
class IKOptions:
# This inner class is where we define the ImageKit options for the model
spec_module = 'myapp.specs'
cache_dir = 'photos'
image_field = 'original_image'
save_count_as = 'num_views'
Step 3
******
Create your specifications.
::
# myapp/specs.py
from imagekit.specs import ImageSpec
from imagekit import processors
# first we define our thumbnail resize processor
class ResizeThumb(processors.Resize):
width = 100
height = 75
crop = True
# now we define a display size resize processor
class ResizeDisplay(processors.Resize):
width = 600
# now let's create an adjustment processor to enhance the image at small sizes
class EnchanceThumb(processors.Adjustment):
contrast = 1.2
sharpness = 1.1
# now we can define our thumbnail spec
class Thumbnail(ImageSpec):
access_as = 'thumbnail_image'
pre_cache = True
processors = [ResizeThumb, EnchanceThumb]
# and our display spec
class Display(ImageSpec):
increment_count = True
processors = [ResizeDisplay]
Step 4
******
Flush the cache and pre-generate thumbnails (ImageKit has to be added to ``INSTALLED_APPS`` for management command to work).
::
$ python manage.py ikflush myapp
Step 5
******
Use your new model in templates.
::
<div class="original">
<img src="{{ photo.original_image.url }}" alt="{{ photo.name }}">
</div>
<div class="display">
<img src="{{ photo.display.url }}" alt="{{ photo.name }}">
</div>
<div class="thumbs">
{% for p in photos %}
<img src="{{ p.thumbnail_image.url }}" alt="{{ p.name }}">
{% endfor %}
</div>
Step 6
******
Play with the API.
::
>>> from myapp.models import Photo
>>> p = Photo.objects.all()[0]
<Photo: MyPhoto>
>>> p.display.url
u'/static/photos/myphoto_display.jpg'
>>> p.display.width
600
>>> p.display.height
420
>>> p.display.image
<JpegImagePlugin.JpegImageFile instance at 0xf18990>
>>> p.display.file
<File: /path/to/media/photos/myphoto_display.jpg>
>>> p.display.spec
<class 'myapp.specs.Display'>
Step 7
******
Enjoy a nice beverage.
::
from refrigerator import beer
beer.enjoy()

View file

@ -1,4 +1,4 @@
__title__ = 'django-imagekit'
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge'
__version__ = (1, 0, 3, 'final', 0)
__version__ = (1, 1, 0, 'final', 0)
__license__ = 'BSD'

View file

@ -1,13 +1,17 @@
# Required PIL classes may or may not be available from the root namespace
# depending on the installation method used.
try:
from PIL import Image, ImageColor, ImageEnhance, ImageFile, ImageFilter
from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageFile, \
ImageFilter, ImageDraw, ImageStat
except ImportError:
try:
import Image
import ImageColor
import ImageChops
import ImageEnhance
import ImageFile
import ImageFilter
import ImageDraw
import ImageStat
except ImportError:
raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')

View file

@ -27,8 +27,7 @@ def flush_cache(apps, options):
print 'Flushing cache for "%s.%s"' % (app_label, model.__name__)
for obj in model.objects.order_by('-pk'):
for spec_file in get_spec_files(obj):
if spec_file is not None:
spec_file.delete(save=False)
spec_file.delete(save=False)
if spec_file.field.pre_cache:
spec_file.generate(False)
else:

View file

@ -15,15 +15,15 @@ from imagekit.processors import ProcessorPipeline, AutoConvert
class _ImageSpecMixin(object):
def __init__(self, processors=None, quality=70, format=None,
def __init__(self, processors=None, format=None, options={},
autoconvert=True):
self.processors = processors
self.quality = quality
self.format = format
self.options = options
self.autoconvert = autoconvert
def process(self, image, file):
processors = ProcessorPipeline(self.processors)
processors = ProcessorPipeline(self.processors or [])
return processors.process(image.copy())
@ -35,16 +35,19 @@ class ImageSpec(_ImageSpecMixin):
"""
_upload_to_attr = 'cache_to'
def __init__(self, processors=None, quality=70, format=None,
def __init__(self, processors=None, format=None, options={},
image_field=None, pre_cache=False, storage=None, cache_to=None,
autoconvert=True):
"""
:param processors: A list of processors to run on the original image.
:param quality: The quality of the output image. This option is only
used for the JPEG format.
:param format: The format of the output file. If not provided,
ImageSpec will try to guess the appropriate format based on the
extension of the filename and the format of the input image.
:param options: A dictionary that will be passed to PIL's
``Image.save()`` method as keyword arguments. Valid options vary
between formats, but some examples include ``quality``,
``optimize``, and ``progressive`` for JPEGs. See the PIL
documentation for others.
:param image_field: The name of the model property that contains the
original image.
:param pre_cache: A boolean that specifies whether the image should
@ -71,8 +74,8 @@ class ImageSpec(_ImageSpecMixin):
"""
_ImageSpecMixin.__init__(self, processors, quality=quality,
format=format, autoconvert=autoconvert)
_ImageSpecMixin.__init__(self, processors, format=format,
options=options, autoconvert=autoconvert)
self.image_field = image_field
self.pre_cache = pre_cache
self.storage = storage
@ -89,12 +92,10 @@ class ImageSpec(_ImageSpecMixin):
# Connect to the signals only once for this class.
uid = '%s.%s' % (cls.__module__, cls.__name__)
post_save.connect(_post_save_handler,
sender=cls,
dispatch_uid='%s_save' % uid)
post_delete.connect(_post_delete_handler,
sender=cls,
dispatch_uid='%s.delete' % uid)
post_save.connect(_post_save_handler, sender=cls,
dispatch_uid='%s_save' % uid)
post_delete.connect(_post_delete_handler, sender=cls,
dispatch_uid='%s.delete' % uid)
def _get_suggested_extension(name, format):
@ -125,6 +126,7 @@ class _ImageSpecFileMixin(object):
img = open_image(content)
original_format = img.format
img = self.field.process(img, self)
options = dict(self.field.options or {})
# Determine the format.
format = self.field.format
@ -139,29 +141,45 @@ class _ImageSpecFileMixin(object):
pass
format = format or img.format or original_format or 'JPEG'
if format == 'JPEG':
img_to_fobj_kwargs = dict(quality=int(self.field.quality),
optimize=True)
else:
img_to_fobj_kwargs = {}
# Run the AutoConvert processor
if getattr(self.field, 'autoconvert', True):
autoconvert_processor = AutoConvert(format)
img = autoconvert_processor.process(img)
img_to_fobj_kwargs.update(autoconvert_processor.save_kwargs)
options = dict(autoconvert_processor.save_kwargs.items() + \
options.items())
imgfile = img_to_fobj(img, format, **img_to_fobj_kwargs)
imgfile = img_to_fobj(img, format, **options)
content = ContentFile(imgfile.read())
return img, content
class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile):
def __init__(self, instance, field, attname, source_file):
def __init__(self, instance, field, attname):
ImageFieldFile.__init__(self, instance, field, None)
self.storage = field.storage or source_file.storage
self.attname = attname
self.source_file = source_file
self.storage = self.field.storage or self.source_file.storage
@property
def source_file(self):
field_name = getattr(self.field, 'image_field', None)
if field_name:
field_file = getattr(self.instance, field_name)
else:
image_fields = [getattr(self.instance, f.attname) for f in \
self.instance.__class__._meta.fields if \
isinstance(f, models.ImageField)]
if len(image_fields) == 0:
raise Exception('{0} does not define any ImageFields, so your '
'{1} ImageSpec has no image to act on.'.format(
self.instance.__class__.__name__, self.attname))
elif len(image_fields) > 1:
raise Exception('{0} defines multiple ImageFields, but you have '
'not specified an image_field for your {1} '
'ImageSpec.'.format(self.instance.__class__.__name__,
self.attname))
else:
field_file = image_fields[0]
return field_file
def _require_file(self):
if not self.source_file:
@ -220,10 +238,11 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile):
self.close()
del self.file
try:
self.storage.delete(self.name)
except (NotImplementedError, IOError):
pass
if self.name and self.storage.exists(self.name):
try:
self.storage.delete(self.name)
except NotImplementedError:
pass
# Delete the filesize cache.
if hasattr(self, '_size'):
@ -291,33 +310,11 @@ class _ImageSpecDescriptor(object):
self.attname = attname
self.field = field
def _get_image_field_file(self, instance):
field_name = getattr(self.field, 'image_field', None)
if field_name:
field = getattr(instance, field_name)
else:
image_fields = [getattr(instance, f.attname) for f in \
instance.__class__._meta.fields if \
isinstance(f, models.ImageField)]
if len(image_fields) == 0:
raise Exception('{0} does not define any ImageFields, so your '
'{1} ImageSpec has no image to act on.'.format(
instance.__class__.__name__, self.attname))
elif len(image_fields) > 1:
raise Exception('{0} defines multiple ImageFields, but you have '
'not specified an image_field for your {1} '
'ImageSpec.'.format(instance.__class__.__name__,
self.attname))
else:
field = image_fields[0]
return field
def __get__(self, instance, owner):
if instance is None:
return self.field
else:
img_spec_file = ImageSpecFile(instance, self.field,
self.attname, self._get_image_field_file(instance))
img_spec_file = ImageSpecFile(instance, self.field, self.attname)
setattr(instance, self.attname, img_spec_file)
return img_spec_file
@ -358,18 +355,22 @@ class ProcessedImageField(models.ImageField, _ImageSpecMixin):
_upload_to_attr = 'upload_to'
attr_class = ProcessedImageFieldFile
def __init__(self, processors=None, quality=70, format=None,
def __init__(self, processors=None, format=None, options={},
verbose_name=None, name=None, width_field=None, height_field=None,
autoconvert=True, **kwargs):
"""
The ProcessedImageField constructor accepts all of the arguments that
the :class:`django.db.models.ImageField` constructor accepts, as well
as the ``processors``, ``format``, and ``quality`` arguments of
as the ``processors``, ``format``, and ``options`` arguments of
:class:`imagekit.models.ImageSpec`.
"""
_ImageSpecMixin.__init__(self, processors, quality=quality,
format=format, autoconvert=autoconvert)
if 'quality' in kwargs:
raise Exception('The "quality" keyword argument has been'
""" deprecated. Use `options={'quality': %s}` instead.""" \
% kwargs['quality'])
_ImageSpecMixin.__init__(self, processors, format=format,
options=options, autoconvert=autoconvert)
models.ImageField.__init__(self, verbose_name, name, width_field,
height_field, **kwargs)

View file

@ -8,7 +8,7 @@ from both the filesystem and the ORM.
"""
from imagekit.lib import Image, ImageColor, ImageEnhance
from imagekit.processors import resize
from imagekit.processors import resize, crop
RGBA_TRANSPARENCY_FORMATS = ['PNG']
@ -167,7 +167,7 @@ class Transpose(object):
try:
orientation = img._getexif()[0x0112]
ops = self._EXIF_ORIENTATION_STEPS[orientation]
except (TypeError, AttributeError):
except (KeyError, TypeError, AttributeError):
ops = []
else:
ops = self.methods
@ -187,10 +187,10 @@ class AutoConvert(object):
def __init__(self, format):
self.format = format
self.save_kwargs = {}
def process(self, img):
matte = False
self.save_kwargs = {}
if img.mode == 'RGBA':
if self.format in RGBA_TRANSPARENCY_FORMATS:
pass
@ -227,7 +227,10 @@ class AutoConvert(object):
matte = True
elif img.mode == 'P':
if self.format in PALETTE_TRANSPARENCY_FORMATS:
self.save_kwargs['transparency'] = img.info['transparency']
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.
@ -250,4 +253,7 @@ class AutoConvert(object):
bg.paste(img, img)
img = bg.convert('RGB')
if self.format == 'JPEG':
self.save_kwargs['optimize'] = True
return img

View file

@ -0,0 +1,71 @@
from ..lib import Image, ImageChops, ImageDraw, ImageStat
class Side(object):
TOP = 't'
RIGHT = 'r'
BOTTOM = 'b'
LEFT = 'l'
ALL = (TOP, RIGHT, BOTTOM, LEFT)
def crop(img, bbox, sides=Side.ALL):
bbox = (
bbox[0] if Side.LEFT in sides else 0,
bbox[1] if Side.TOP in sides else 0,
bbox[2] if Side.RIGHT in sides else img.size[0],
bbox[3] if Side.BOTTOM in sides else img.size[1],
)
return img.crop(bbox)
def detect_border_color(img):
mask = Image.new('1', img.size, 1)
w, h = img.size[0] - 2, img.size[1] - 2
if w > 0 and h > 0:
draw = ImageDraw.Draw(mask)
draw.rectangle([1, 1, w, h], 0)
return ImageStat.Stat(img.convert('RGBA').histogram(mask)).median
class TrimBorderColor(object):
"""Trims a color from the sides of an image.
"""
def __init__(self, color=None, tolerance=0.3, sides=Side.ALL):
"""
:param color: The color to trim from the image, in a 4-tuple RGBA value,
where each component is an integer between 0 and 255, inclusive. If
no color is provided, the processor will attempt to detect the
border color automatically.
:param tolerance: A number between 0 and 1 where 0. Zero is the least
tolerant and one is the most.
:param sides: A list of sides that should be trimmed. Possible values
are provided by the :class:`Side` enum class.
"""
self.color = color
self.sides = sides
self.tolerance = tolerance
def process(self, img):
source = img.convert('RGBA')
border_color = self.color or tuple(detect_border_color(source))
bg = Image.new('RGBA', img.size, border_color)
diff = ImageChops.difference(source, bg)
if self.tolerance not in (0, 1):
# If tolerance is zero, we've already done the job. A tolerance of
# one would mean to trim EVERY color, and since that would result
# in a zero-sized image, we just ignore it.
if not 0 <= self.tolerance <= 1:
raise ValueError('%s is an invalid tolerance. Acceptable values'
' are between 0 and 1 (inclusive).' % self.tolerance)
tmp = ImageChops.constant(diff, int(self.tolerance * 255)) \
.convert('RGBA')
diff = ImageChops.subtract(diff, tmp)
bbox = diff.getbbox()
if bbox:
img = crop(img, bbox, self.sides)
return img

View file

@ -1,21 +1,9 @@
import math
from imagekit.lib import Image
class _Resize(object):
width = None
height = None
def __init__(self, width=None, height=None):
if width is not None:
self.width = width
if height is not None:
self.height = height
def process(self, img):
raise NotImplementedError('process must be overridden by subclasses.')
class Crop(_Resize):
class Crop(object):
"""
Resizes an image , cropping it to the specified width and height.
@ -60,7 +48,8 @@ class Crop(_Resize):
- Crop.BOTTOM_RIGHT
"""
super(Crop, self).__init__(width, height)
self.width = width
self.height = height
self.anchor = anchor
def process(self, img):
@ -86,7 +75,7 @@ class Crop(_Resize):
return img
class Fit(_Resize):
class Fit(object):
"""
Resizes an image to fit within the specified dimensions.
@ -100,7 +89,8 @@ class Fit(_Resize):
dimensions.
"""
super(Fit, self).__init__(width, height)
self.width = width
self.height = height
self.upscale = upscale
def process(self, img):
@ -121,3 +111,87 @@ class Fit(_Resize):
return img
img = img.resize(new_dimensions, Image.ANTIALIAS)
return img
def histogram_entropy(im):
"""
Calculate the entropy of an images' histogram. Used for "smart cropping" in easy-thumbnails;
see: https://raw.github.com/SmileyChris/easy-thumbnails/master/easy_thumbnails/utils.py
"""
if not isinstance(im, Image.Image):
return 0 # Fall back to a constant entropy.
histogram = im.histogram()
hist_ceil = float(sum(histogram))
histonorm = [histocol / hist_ceil for histocol in histogram]
return -sum([p * math.log(p, 2) for p in histonorm if p != 0])
class SmartCrop(object):
"""
Crop an image 'smartly' -- based on smart crop implementation from easy-thumbnails:
https://github.com/SmileyChris/easy-thumbnails/blob/master/easy_thumbnails/processors.py#L193
Smart cropping whittles away the parts of the image with the least entropy.
"""
def __init__(self, width=None, height=None):
self.width = width
self.height = height
def compare_entropy(self, start_slice, end_slice, slice, difference):
"""
Calculate the entropy of two slices (from the start and end of an axis),
returning a tuple containing the amount that should be added to the start
and removed from the end of the axis.
"""
start_entropy = histogram_entropy(start_slice)
end_entropy = histogram_entropy(end_slice)
if end_entropy and abs(start_entropy / end_entropy - 1) < 0.01:
# Less than 1% difference, remove from both sides.
if difference >= slice * 2:
return slice, slice
half_slice = slice // 2
return half_slice, slice - half_slice
if start_entropy > end_entropy:
return 0, slice
else:
return slice, 0
def process(self, img):
source_x, source_y = img.size
diff_x = int(source_x - min(source_x, self.width))
diff_y = int(source_y - min(source_y, self.height))
left = top = 0
right, bottom = source_x, source_y
while diff_x:
slice = min(diff_x, max(diff_x // 5, 10))
start = img.crop((left, 0, left + slice, source_y))
end = img.crop((right - slice, 0, right, source_y))
add, remove = self.compare_entropy(start, end, slice, diff_x)
left += add
right -= remove
diff_x = diff_x - add - remove
while diff_y:
slice = min(diff_y, max(diff_y // 5, 10))
start = img.crop((0, top, source_x, top + slice))
end = img.crop((0, bottom - slice, source_x, bottom))
add, remove = self.compare_entropy(start, end, slice, diff_y)
top += add
bottom -= remove
diff_y = diff_y - add - remove
box = (left, top, right, bottom)
img = img.crop(box)
return img

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
Django >= 1.3.1

BIN
tests/core/assets/Lenna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View file

@ -1,3 +1,5 @@
from __future__ import with_statement
import os
import tempfile
@ -9,13 +11,19 @@ from imagekit import utils
from imagekit.lib import Image
from imagekit.models import ImageSpec
from imagekit.processors import Adjust
from imagekit.processors.resize import Crop
from imagekit.processors.resize import Crop, SmartCrop
class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)],
image_field='original_image', format='JPEG', quality=90)
image_field='original_image', format='JPEG',
options={'quality': 90})
smartcropped_thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), SmartCrop(50, 50)],
image_field='original_image', format='JPEG',
options={'quality': 90})
class IKTest(TestCase):
@ -25,14 +33,42 @@ class IKTest(TestCase):
tmp.seek(0)
return tmp
def setUp(self):
self.photo = Photo()
img = self.generate_image()
def generate_lenna(self):
"""
See also:
http://en.wikipedia.org/wiki/Lenna
http://sipi.usc.edu/database/database.php?volume=misc&image=12
"""
tmp = tempfile.TemporaryFile()
lennapath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', 'lenna-800x600-white-border.jpg')
with open(lennapath, "r+b") as lennafile:
Image.open(lennafile).save(tmp, 'JPEG')
tmp.seek(0)
return tmp
def create_photo(self, name):
photo = Photo()
img = self.generate_lenna()
file = ContentFile(img.read())
self.photo.original_image = file
self.photo.original_image.save('test.jpeg', file)
self.photo.save()
photo.original_image = file
photo.original_image.save(name, file)
photo.save()
img.close()
return photo
def setUp(self):
self.photo = self.create_photo('test.jpg')
def test_nodelete(self):
"""Don't delete the spec file when the source image hasn't changed.
"""
filename = self.photo.thumbnail.file.name
thumbnail_timestamp = os.path.getmtime(filename)
self.photo.save()
self.assertTrue(self.photo.thumbnail.storage.exists(filename))
def test_save_image(self):
photo = Photo.objects.get(id=self.photo.id)
@ -47,8 +83,11 @@ class IKTest(TestCase):
self.assertTrue(os.path.isfile(photo.thumbnail.file.name))
def test_thumbnail_size(self):
""" Explicit and smart-cropped thumbnail size """
self.assertEqual(self.photo.thumbnail.width, 50)
self.assertEqual(self.photo.thumbnail.height, 50)
self.assertEqual(self.photo.smartcropped_thumbnail.width, 50)
self.assertEqual(self.photo.smartcropped_thumbnail.height, 50)
def test_thumbnail_source_file(self):
self.assertEqual(