mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-05-24 20:23:44 +00:00
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:
commit
c0ac951be9
18 changed files with 324 additions and 241 deletions
4
AUTHORS
4
AUTHORS
|
|
@ -6,6 +6,7 @@ Maintainers
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
* `Bryan Veloso`_
|
* `Bryan Veloso`_
|
||||||
|
* `Matthew Tretter`_
|
||||||
* `Chris Drackett`_
|
* `Chris Drackett`_
|
||||||
* `Greg Newman`_
|
* `Greg Newman`_
|
||||||
|
|
||||||
|
|
@ -14,11 +15,11 @@ Contributors
|
||||||
|
|
||||||
* `Josh Ourisman`_
|
* `Josh Ourisman`_
|
||||||
* `Jonathan Slenders`_
|
* `Jonathan Slenders`_
|
||||||
* `Matthew Tretter`_
|
|
||||||
* `Eric Eldredge`_
|
* `Eric Eldredge`_
|
||||||
* `Chris McKenzie`_
|
* `Chris McKenzie`_
|
||||||
* `Markus Kaiserswerth`_
|
* `Markus Kaiserswerth`_
|
||||||
* `Ryan Bagwell`_
|
* `Ryan Bagwell`_
|
||||||
|
* `Alexander Bohn`_
|
||||||
|
|
||||||
|
|
||||||
.. _Justin Driscoll: http://github.com/jdriscoll
|
.. _Justin Driscoll: http://github.com/jdriscoll
|
||||||
|
|
@ -33,3 +34,4 @@ Contributors
|
||||||
.. _Chris McKenzie: http://github.com/kenzic
|
.. _Chris McKenzie: http://github.com/kenzic
|
||||||
.. _Ryan Bagwell: http://github.com/ryanbagwell
|
.. _Ryan Bagwell: http://github.com/ryanbagwell
|
||||||
.. _Markus Kaiserswerth: http://github.com/mkai
|
.. _Markus Kaiserswerth: http://github.com/mkai
|
||||||
|
.. _Alexander Bohn: http://github.com/fish2000
|
||||||
|
|
|
||||||
20
README.rst
20
README.rst
|
|
@ -6,9 +6,19 @@ like different sizes (e.g. thumbnails) and black and white versions.
|
||||||
Installation
|
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)
|
(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
|
Adding Specs to a Model
|
||||||
|
|
@ -23,7 +33,7 @@ of a model class::
|
||||||
class Photo(models.Model):
|
class Photo(models.Model):
|
||||||
original_image = models.ImageField(upload_to='photos')
|
original_image = models.ImageField(upload_to='photos')
|
||||||
formatted_image = ImageSpec(image_field='original_image', format='JPEG',
|
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
|
Accessing the spec through a model instance will create the image and return
|
||||||
an ImageFile-like object (just like with a normal
|
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')
|
original_image = models.ImageField(upload_to='photos')
|
||||||
thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1),
|
thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1),
|
||||||
resize.Crop(50, 50)], image_field='original_image',
|
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::
|
The ``thumbnail`` property will now return a cropped image::
|
||||||
|
|
||||||
|
|
@ -77,7 +87,7 @@ implement a ``process()`` method::
|
||||||
class Photo(models.Model):
|
class Photo(models.Model):
|
||||||
original_image = models.ImageField(upload_to='photos')
|
original_image = models.ImageField(upload_to='photos')
|
||||||
watermarked_image = ImageSpec([Watermark()], image_field='original_image',
|
watermarked_image = ImageSpec([Watermark()], image_field='original_image',
|
||||||
format='JPEG', quality=90)
|
format='JPEG', options={'quality': 90})
|
||||||
|
|
||||||
|
|
||||||
Admin
|
Admin
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ API Reference
|
||||||
.. automodule:: imagekit.processors.resize
|
.. automodule:: imagekit.processors.resize
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. automodule:: imagekit.processors.crop
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
:mod:`admin` Module
|
:mod:`admin` Module
|
||||||
--------------------
|
--------------------
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
Changelog
|
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
|
v1.0.3
|
||||||
------
|
------
|
||||||
|
|
||||||
- ``ImageSpec._create()`` was renamed ``ImageSpec.generate()`` and is now
|
- ``ImageSpec._create()`` was renamed ``ImageSpec.generate()`` and is now
|
||||||
available in the public API.
|
available in the public API.
|
||||||
|
|
||||||
- Added an ``AutoConvert`` processor to encapsulate the trasnparency
|
- Added an ``AutoConvert`` processor to encapsulate the transparency
|
||||||
handling logic.
|
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
|
the situations in which one would convert to or from formats that support
|
||||||
transparency.
|
transparency.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,9 @@ copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett &
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '1.0.3'
|
version = '1.1.0'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# 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
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
__title__ = 'django-imagekit'
|
__title__ = 'django-imagekit'
|
||||||
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge'
|
__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'
|
__license__ = 'BSD'
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
# Required PIL classes may or may not be available from the root namespace
|
# Required PIL classes may or may not be available from the root namespace
|
||||||
# depending on the installation method used.
|
# depending on the installation method used.
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageColor, ImageEnhance, ImageFile, ImageFilter
|
from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageFile, \
|
||||||
|
ImageFilter, ImageDraw, ImageStat
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
import Image
|
import Image
|
||||||
import ImageColor
|
import ImageColor
|
||||||
|
import ImageChops
|
||||||
import ImageEnhance
|
import ImageEnhance
|
||||||
import ImageFile
|
import ImageFile
|
||||||
import ImageFilter
|
import ImageFilter
|
||||||
|
import ImageDraw
|
||||||
|
import ImageStat
|
||||||
except ImportError:
|
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.')
|
raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ def flush_cache(apps, options):
|
||||||
print 'Flushing cache for "%s.%s"' % (app_label, model.__name__)
|
print 'Flushing cache for "%s.%s"' % (app_label, model.__name__)
|
||||||
for obj in model.objects.order_by('-pk'):
|
for obj in model.objects.order_by('-pk'):
|
||||||
for spec_file in get_spec_files(obj):
|
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:
|
if spec_file.field.pre_cache:
|
||||||
spec_file.generate(False)
|
spec_file.generate(False)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,15 @@ from imagekit.processors import ProcessorPipeline, AutoConvert
|
||||||
|
|
||||||
|
|
||||||
class _ImageSpecMixin(object):
|
class _ImageSpecMixin(object):
|
||||||
def __init__(self, processors=None, quality=70, format=None,
|
def __init__(self, processors=None, format=None, options={},
|
||||||
autoconvert=True):
|
autoconvert=True):
|
||||||
self.processors = processors
|
self.processors = processors
|
||||||
self.quality = quality
|
|
||||||
self.format = format
|
self.format = format
|
||||||
|
self.options = options
|
||||||
self.autoconvert = autoconvert
|
self.autoconvert = autoconvert
|
||||||
|
|
||||||
def process(self, image, file):
|
def process(self, image, file):
|
||||||
processors = ProcessorPipeline(self.processors)
|
processors = ProcessorPipeline(self.processors or [])
|
||||||
return processors.process(image.copy())
|
return processors.process(image.copy())
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,16 +35,19 @@ class ImageSpec(_ImageSpecMixin):
|
||||||
"""
|
"""
|
||||||
_upload_to_attr = 'cache_to'
|
_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,
|
image_field=None, pre_cache=False, storage=None, cache_to=None,
|
||||||
autoconvert=True):
|
autoconvert=True):
|
||||||
"""
|
"""
|
||||||
:param processors: A list of processors to run on the original image.
|
: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,
|
:param format: The format of the output file. If not provided,
|
||||||
ImageSpec will try to guess the appropriate format based on the
|
ImageSpec will try to guess the appropriate format based on the
|
||||||
extension of the filename and the format of the input image.
|
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
|
:param image_field: The name of the model property that contains the
|
||||||
original image.
|
original image.
|
||||||
:param pre_cache: A boolean that specifies whether the image should
|
:param pre_cache: A boolean that specifies whether the image should
|
||||||
|
|
@ -71,8 +74,8 @@ class ImageSpec(_ImageSpecMixin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_ImageSpecMixin.__init__(self, processors, quality=quality,
|
_ImageSpecMixin.__init__(self, processors, format=format,
|
||||||
format=format, autoconvert=autoconvert)
|
options=options, autoconvert=autoconvert)
|
||||||
self.image_field = image_field
|
self.image_field = image_field
|
||||||
self.pre_cache = pre_cache
|
self.pre_cache = pre_cache
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
|
|
@ -89,12 +92,10 @@ class ImageSpec(_ImageSpecMixin):
|
||||||
|
|
||||||
# Connect to the signals only once for this class.
|
# Connect to the signals only once for this class.
|
||||||
uid = '%s.%s' % (cls.__module__, cls.__name__)
|
uid = '%s.%s' % (cls.__module__, cls.__name__)
|
||||||
post_save.connect(_post_save_handler,
|
post_save.connect(_post_save_handler, sender=cls,
|
||||||
sender=cls,
|
dispatch_uid='%s_save' % uid)
|
||||||
dispatch_uid='%s_save' % uid)
|
post_delete.connect(_post_delete_handler, sender=cls,
|
||||||
post_delete.connect(_post_delete_handler,
|
dispatch_uid='%s.delete' % uid)
|
||||||
sender=cls,
|
|
||||||
dispatch_uid='%s.delete' % uid)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_suggested_extension(name, format):
|
def _get_suggested_extension(name, format):
|
||||||
|
|
@ -125,6 +126,7 @@ class _ImageSpecFileMixin(object):
|
||||||
img = open_image(content)
|
img = open_image(content)
|
||||||
original_format = img.format
|
original_format = img.format
|
||||||
img = self.field.process(img, self)
|
img = self.field.process(img, self)
|
||||||
|
options = dict(self.field.options or {})
|
||||||
|
|
||||||
# Determine the format.
|
# Determine the format.
|
||||||
format = self.field.format
|
format = self.field.format
|
||||||
|
|
@ -139,29 +141,45 @@ class _ImageSpecFileMixin(object):
|
||||||
pass
|
pass
|
||||||
format = format or img.format or original_format or 'JPEG'
|
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
|
# Run the AutoConvert processor
|
||||||
if getattr(self.field, 'autoconvert', True):
|
if getattr(self.field, 'autoconvert', True):
|
||||||
autoconvert_processor = AutoConvert(format)
|
autoconvert_processor = AutoConvert(format)
|
||||||
img = autoconvert_processor.process(img)
|
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())
|
content = ContentFile(imgfile.read())
|
||||||
return img, content
|
return img, content
|
||||||
|
|
||||||
|
|
||||||
class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile):
|
class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile):
|
||||||
def __init__(self, instance, field, attname, source_file):
|
def __init__(self, instance, field, attname):
|
||||||
ImageFieldFile.__init__(self, instance, field, None)
|
ImageFieldFile.__init__(self, instance, field, None)
|
||||||
self.storage = field.storage or source_file.storage
|
|
||||||
self.attname = attname
|
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):
|
def _require_file(self):
|
||||||
if not self.source_file:
|
if not self.source_file:
|
||||||
|
|
@ -220,10 +238,11 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile):
|
||||||
self.close()
|
self.close()
|
||||||
del self.file
|
del self.file
|
||||||
|
|
||||||
try:
|
if self.name and self.storage.exists(self.name):
|
||||||
self.storage.delete(self.name)
|
try:
|
||||||
except (NotImplementedError, IOError):
|
self.storage.delete(self.name)
|
||||||
pass
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Delete the filesize cache.
|
# Delete the filesize cache.
|
||||||
if hasattr(self, '_size'):
|
if hasattr(self, '_size'):
|
||||||
|
|
@ -291,33 +310,11 @@ class _ImageSpecDescriptor(object):
|
||||||
self.attname = attname
|
self.attname = attname
|
||||||
self.field = field
|
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):
|
def __get__(self, instance, owner):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self.field
|
return self.field
|
||||||
else:
|
else:
|
||||||
img_spec_file = ImageSpecFile(instance, self.field,
|
img_spec_file = ImageSpecFile(instance, self.field, self.attname)
|
||||||
self.attname, self._get_image_field_file(instance))
|
|
||||||
setattr(instance, self.attname, img_spec_file)
|
setattr(instance, self.attname, img_spec_file)
|
||||||
return img_spec_file
|
return img_spec_file
|
||||||
|
|
||||||
|
|
@ -358,18 +355,22 @@ class ProcessedImageField(models.ImageField, _ImageSpecMixin):
|
||||||
_upload_to_attr = 'upload_to'
|
_upload_to_attr = 'upload_to'
|
||||||
attr_class = ProcessedImageFieldFile
|
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,
|
verbose_name=None, name=None, width_field=None, height_field=None,
|
||||||
autoconvert=True, **kwargs):
|
autoconvert=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
The ProcessedImageField constructor accepts all of the arguments that
|
The ProcessedImageField constructor accepts all of the arguments that
|
||||||
the :class:`django.db.models.ImageField` constructor accepts, as well
|
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`.
|
:class:`imagekit.models.ImageSpec`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
_ImageSpecMixin.__init__(self, processors, quality=quality,
|
if 'quality' in kwargs:
|
||||||
format=format, autoconvert=autoconvert)
|
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,
|
models.ImageField.__init__(self, verbose_name, name, width_field,
|
||||||
height_field, **kwargs)
|
height_field, **kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from both the filesystem and the ORM.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from imagekit.lib import Image, ImageColor, ImageEnhance
|
from imagekit.lib import Image, ImageColor, ImageEnhance
|
||||||
from imagekit.processors import resize
|
from imagekit.processors import resize, crop
|
||||||
|
|
||||||
|
|
||||||
RGBA_TRANSPARENCY_FORMATS = ['PNG']
|
RGBA_TRANSPARENCY_FORMATS = ['PNG']
|
||||||
|
|
@ -167,7 +167,7 @@ class Transpose(object):
|
||||||
try:
|
try:
|
||||||
orientation = img._getexif()[0x0112]
|
orientation = img._getexif()[0x0112]
|
||||||
ops = self._EXIF_ORIENTATION_STEPS[orientation]
|
ops = self._EXIF_ORIENTATION_STEPS[orientation]
|
||||||
except (TypeError, AttributeError):
|
except (KeyError, TypeError, AttributeError):
|
||||||
ops = []
|
ops = []
|
||||||
else:
|
else:
|
||||||
ops = self.methods
|
ops = self.methods
|
||||||
|
|
@ -187,10 +187,10 @@ class AutoConvert(object):
|
||||||
|
|
||||||
def __init__(self, format):
|
def __init__(self, format):
|
||||||
self.format = format
|
self.format = format
|
||||||
self.save_kwargs = {}
|
|
||||||
|
|
||||||
def process(self, img):
|
def process(self, img):
|
||||||
matte = False
|
matte = False
|
||||||
|
self.save_kwargs = {}
|
||||||
if img.mode == 'RGBA':
|
if img.mode == 'RGBA':
|
||||||
if self.format in RGBA_TRANSPARENCY_FORMATS:
|
if self.format in RGBA_TRANSPARENCY_FORMATS:
|
||||||
pass
|
pass
|
||||||
|
|
@ -227,7 +227,10 @@ class AutoConvert(object):
|
||||||
matte = True
|
matte = True
|
||||||
elif img.mode == 'P':
|
elif img.mode == 'P':
|
||||||
if self.format in PALETTE_TRANSPARENCY_FORMATS:
|
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:
|
elif self.format in RGBA_TRANSPARENCY_FORMATS:
|
||||||
# Currently PIL doesn't support any RGBA-mode formats that
|
# Currently PIL doesn't support any RGBA-mode formats that
|
||||||
# aren't also P-mode formats, so this will never happen.
|
# aren't also P-mode formats, so this will never happen.
|
||||||
|
|
@ -250,4 +253,7 @@ class AutoConvert(object):
|
||||||
bg.paste(img, img)
|
bg.paste(img, img)
|
||||||
img = bg.convert('RGB')
|
img = bg.convert('RGB')
|
||||||
|
|
||||||
|
if self.format == 'JPEG':
|
||||||
|
self.save_kwargs['optimize'] = True
|
||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
|
||||||
71
imagekit/processors/crop.py
Normal file
71
imagekit/processors/crop.py
Normal 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
|
||||||
|
|
@ -1,21 +1,9 @@
|
||||||
|
|
||||||
|
import math
|
||||||
from imagekit.lib import Image
|
from imagekit.lib import Image
|
||||||
|
|
||||||
|
|
||||||
class _Resize(object):
|
class Crop(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):
|
|
||||||
"""
|
"""
|
||||||
Resizes an image , cropping it to the specified width and height.
|
Resizes an image , cropping it to the specified width and height.
|
||||||
|
|
||||||
|
|
@ -60,7 +48,8 @@ class Crop(_Resize):
|
||||||
- Crop.BOTTOM_RIGHT
|
- Crop.BOTTOM_RIGHT
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super(Crop, self).__init__(width, height)
|
self.width = width
|
||||||
|
self.height = height
|
||||||
self.anchor = anchor
|
self.anchor = anchor
|
||||||
|
|
||||||
def process(self, img):
|
def process(self, img):
|
||||||
|
|
@ -86,7 +75,7 @@ class Crop(_Resize):
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
class Fit(_Resize):
|
class Fit(object):
|
||||||
"""
|
"""
|
||||||
Resizes an image to fit within the specified dimensions.
|
Resizes an image to fit within the specified dimensions.
|
||||||
|
|
||||||
|
|
@ -100,7 +89,8 @@ class Fit(_Resize):
|
||||||
dimensions.
|
dimensions.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super(Fit, self).__init__(width, height)
|
self.width = width
|
||||||
|
self.height = height
|
||||||
self.upscale = upscale
|
self.upscale = upscale
|
||||||
|
|
||||||
def process(self, img):
|
def process(self, img):
|
||||||
|
|
@ -121,3 +111,87 @@ class Fit(_Resize):
|
||||||
return img
|
return img
|
||||||
img = img.resize(new_dimensions, Image.ANTIALIAS)
|
img = img.resize(new_dimensions, Image.ANTIALIAS)
|
||||||
return img
|
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
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Django >= 1.3.1
|
||||||
BIN
tests/core/assets/Lenna.png
Normal file
BIN
tests/core/assets/Lenna.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
BIN
tests/core/assets/lenna-800x600-white-border.jpg
Normal file
BIN
tests/core/assets/lenna-800x600-white-border.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
BIN
tests/core/assets/lenna-800x600.jpg
Normal file
BIN
tests/core/assets/lenna-800x600.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
@ -9,13 +11,19 @@ from imagekit import utils
|
||||||
from imagekit.lib import Image
|
from imagekit.lib import Image
|
||||||
from imagekit.models import ImageSpec
|
from imagekit.models import ImageSpec
|
||||||
from imagekit.processors import Adjust
|
from imagekit.processors import Adjust
|
||||||
from imagekit.processors.resize import Crop
|
from imagekit.processors.resize import Crop, SmartCrop
|
||||||
|
|
||||||
|
|
||||||
class Photo(models.Model):
|
class Photo(models.Model):
|
||||||
original_image = models.ImageField(upload_to='photos')
|
original_image = models.ImageField(upload_to='photos')
|
||||||
|
|
||||||
thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)],
|
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):
|
class IKTest(TestCase):
|
||||||
|
|
@ -25,14 +33,42 @@ class IKTest(TestCase):
|
||||||
tmp.seek(0)
|
tmp.seek(0)
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
def setUp(self):
|
def generate_lenna(self):
|
||||||
self.photo = Photo()
|
"""
|
||||||
img = self.generate_image()
|
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())
|
file = ContentFile(img.read())
|
||||||
self.photo.original_image = file
|
photo.original_image = file
|
||||||
self.photo.original_image.save('test.jpeg', file)
|
photo.original_image.save(name, file)
|
||||||
self.photo.save()
|
photo.save()
|
||||||
img.close()
|
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):
|
def test_save_image(self):
|
||||||
photo = Photo.objects.get(id=self.photo.id)
|
photo = Photo.objects.get(id=self.photo.id)
|
||||||
|
|
@ -47,8 +83,11 @@ class IKTest(TestCase):
|
||||||
self.assertTrue(os.path.isfile(photo.thumbnail.file.name))
|
self.assertTrue(os.path.isfile(photo.thumbnail.file.name))
|
||||||
|
|
||||||
def test_thumbnail_size(self):
|
def test_thumbnail_size(self):
|
||||||
|
""" Explicit and smart-cropped thumbnail size """
|
||||||
self.assertEqual(self.photo.thumbnail.width, 50)
|
self.assertEqual(self.photo.thumbnail.width, 50)
|
||||||
self.assertEqual(self.photo.thumbnail.height, 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):
|
def test_thumbnail_source_file(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue