Merge branch 'release/2.0.2'

* release/2.0.2: (25 commits)
  Changelog for 2.0.2.
  Bumping the version number.
  Change assertRaises for Python 2.6 compatibility
  Test correct versions of Django
  Fix docs typo; closes #147
  fix API documentation
  Change how signals are used
  Add __init__ to Reflection processor; closes #141
  Add Google Group to README
  Use django-appconf
  Allow callables for AdminThumbnail image_field arg
  Whoops! Messed that up.
  Code blocks.
  Fix pickling of ImageSpecFieldFile
  Create failing test to illustrate #97
  Remove unused stuff
  Derp, forgot to change the tox command.
  add irc channel to docs and README
  Adding a travis-ci configuration file.
  Adding basepython to the tox directives.
  ...

Conflicts:
	docs/changelog.rst
	docs/conf.py
	imagekit/__init__.py
This commit is contained in:
Bryan Veloso 2012-09-14 11:24:21 -07:00
commit 996c75f7d5
24 changed files with 230 additions and 110 deletions

7
.travis.yml Normal file
View file

@ -0,0 +1,7 @@
language: python
python:
- 2.7
install: pip install tox --use-mirrors
script: tox -e py27-django13,py27-django12,py26-django13,py27-django12
notifications:
irc: "irc.freenode.org#imagekit"

View file

@ -1,4 +1,3 @@
ImageKit is a Django app that helps you to add variations of uploaded images
to your models. These variations are called "specs" and can include things
like different sizes (e.g. thumbnails) and black and white versions.
@ -32,7 +31,9 @@ Adding Specs to a Model
-----------------------
Much like ``django.db.models.ImageField``, Specs are defined as properties
of a model class::
of a model class:
.. code-block:: python
from django.db import models
from imagekit.models import ImageSpecField
@ -44,7 +45,9 @@ of a model class::
Accessing the spec through a model instance will create the image and return
an ImageFile-like object (just like with a normal
``django.db.models.ImageField``)::
``django.db.models.ImageField``):
.. code-block:: python
photo = Photo.objects.all()[0]
photo.original_image.url # > '/media/photos/birthday.tiff'
@ -53,13 +56,15 @@ an ImageFile-like object (just like with a normal
Check out ``imagekit.models.ImageSpecField`` for more information.
If you only want to save the processed image (without maintaining the original),
you can use a ``ProcessedImageField``::
you can use a ``ProcessedImageField``:
.. code-block:: python
from django.db import models
from imagekit.models.fields import ProcessedImageField
class Photo(models.Model):
processed_image = ImageSpecField(format='JPEG', options={'quality': 90})
processed_image = ProcessedImageField(format='JPEG', options={'quality': 90})
See the class documentation for details.
@ -69,7 +74,9 @@ Processors
The real power of ImageKit comes from processors. Processors take an image, do
something to it, and return the result. By providing a list of processors to
your spec, you can expose different versions of the original image::
your spec, you can expose different versions of the original image:
.. code-block:: python
from django.db import models
from imagekit.models import ImageSpecField
@ -81,7 +88,9 @@ your spec, you can expose different versions of the original image::
ResizeToFill(50, 50)], image_field='original_image',
format='JPEG', options={'quality': 90})
The ``thumbnail`` property will now return a cropped image::
The ``thumbnail`` property will now return a cropped image:
.. code-block:: python
photo = Photo.objects.all()[0]
photo.thumbnail.url # > '/media/cache/photos/birthday_thumbnail.jpeg'
@ -96,7 +105,9 @@ pass processors to a ``ProcessedImageField`` instead of an ``ImageSpecField``.)
The ``imagekit.processors`` module contains processors for many common
image manipulations, like resizing, rotating, and color adjustments. However,
if they aren't up to the task, you can create your own. All you have to do is
implement a ``process()`` method::
implement a ``process()`` method:
.. code-block:: python
class Watermark(object):
def process(self, image):
@ -115,7 +126,9 @@ Admin
ImageKit also contains a class named ``imagekit.admin.AdminThumbnail``
for displaying specs (or even regular ImageFields) in the
`Django admin change list`_. AdminThumbnail is used as a property on
Django admin classes::
Django admin classes:
.. code-block:: python
from django.contrib import admin
from imagekit.admin import AdminThumbnail
@ -154,17 +167,30 @@ no longer needed in any form, i.e. the model is deleted). Each of these methods
must accept a file object, but the internals are up to you. For example, you
could store the state (valid, invalid) of the cache in a database to avoid
filesystem access. You can then specify your image cache backend on a per-field
basis::
basis:
.. code-block:: python
class Photo(models.Model):
...
thumbnail = ImageSpecField(..., image_cache_backend=MyImageCacheBackend())
Or in your ``settings.py`` file if you want to use it as the default::
Or in your ``settings.py`` file if you want to use it as the default:
.. code-block:: python
IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'path.to.MyImageCacheBackend'
Community
---------
Please use `the GitHub issue tracker <https://github.com/jdriscoll/django-imagekit/issues>`_
to report bugs with django-imagekit. `A mailing list <https://groups.google.com/forum/#!forum/django-imagekit>`_
also exists to discuss the project and ask questions, as well as the official
`#imagekit <irc://irc.freenode.net/imagekit>`_ channel on Freenode.
Contributing
------------

View file

@ -5,7 +5,7 @@ API Reference
:mod:`models` Module
--------------------
.. automodule:: imagekit.models
.. automodule:: imagekit.models.fields
:members:

View file

@ -1,6 +1,17 @@
Changelog
=========
v2.0.2
------
- Fixed the pickling of ImageSpecFieldFile.
- Signals are now connected without specifying the class and non-IK models
are filitered out in the receivers. This is necessary beacuse of a bug
with how Django handles abstract models.
- Fixed a `ZeroDivisionError` in the Reflection processor.
- `cStringIO` is now used if it's available.
- Reflections on images now use RGBA instead of RGB.
v2.0.1
------

View file

@ -16,7 +16,7 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
sys.path.append(os.path.abspath('_themes'))
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
@ -50,9 +50,9 @@ copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett &
# built documents.
#
# The short X.Y version.
version = '2.0.1'
version = '2.0.2'
# The full version, including alpha/beta/rc tags.
release = '2.0.1'
release = '2.0.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -18,6 +18,14 @@ Authors
.. include:: ../AUTHORS
Community
---------
The official Freenode channel for ImageKit is `#imagekit <irc://irc.freenode.net/imagekit>`_.
You should always find some fine people to answer your questions
about ImageKit there.
Digging Deeper
--------------

View file

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

View file

@ -21,11 +21,14 @@ class AdminThumbnail(object):
self.template = template
def __call__(self, obj):
try:
thumbnail = getattr(obj, self.image_field)
except AttributeError:
raise Exception('The property %s is not defined on %s.' % \
(self.image_field, obj.__class__.__name__))
if callable(self.image_field):
thumbnail = self.image_field(obj)
else:
try:
thumbnail = getattr(obj, self.image_field)
except AttributeError:
raise Exception('The property %s is not defined on %s.' % \
(self.image_field, obj.__class__.__name__))
original_image = getattr(thumbnail, 'source_file', None) or thumbnail
template = self.template or 'imagekit/admin/thumbnail.html'

5
imagekit/conf.py Normal file
View file

@ -0,0 +1,5 @@
from appconf import AppConf
class ImageKitConf(AppConf):
DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend'

View file

@ -1,6 +1,5 @@
import os
from StringIO import StringIO
from .lib import StringIO
from .processors import ProcessorPipeline
from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format,
UnknownExtensionError)

View file

@ -14,7 +14,8 @@ def get_default_image_cache_backend():
"""
global _default_image_cache_backend
if not _default_image_cache_backend:
from ..settings import DEFAULT_IMAGE_CACHE_BACKEND as import_path
from django.conf import settings
import_path = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND
try:
dot = import_path.rindex('.')
except ValueError:

View file

@ -15,3 +15,8 @@ except ImportError:
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.')
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO

View file

@ -1,3 +1,4 @@
from .. import conf
from .fields import ImageSpecField, ProcessedImageField
import warnings

View file

@ -1,15 +1,18 @@
import os
from django.db import models
from django.db.models.signals import post_init, post_save, post_delete
from ...imagecache import get_default_image_cache_backend
from ...generators import SpecFileGenerator
from .files import ImageSpecFieldFile, ProcessedImageFieldFile
from ..receivers import configure_receivers
from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta
from ...utils import suggest_extension
configure_receivers()
class ImageSpecField(object):
"""
The heart and soul of the ImageKit library, ImageSpecField allows you to add
@ -91,51 +94,12 @@ class ImageSpecField(object):
setattr(cls, '_ik', ik)
ik.spec_fields.append(name)
# Connect to the signals only once for this class.
uid = '%s.%s' % (cls.__module__, cls.__name__)
post_init.connect(ImageSpecField._post_init_receiver, sender=cls,
dispatch_uid=uid)
post_save.connect(ImageSpecField._post_save_receiver, sender=cls,
dispatch_uid=uid)
post_delete.connect(ImageSpecField._post_delete_receiver, sender=cls,
dispatch_uid=uid)
# Register the field with the image_cache_backend
try:
self.image_cache_backend.register_field(cls, self, name)
except AttributeError:
pass
@staticmethod
def _post_save_receiver(sender, instance=None, created=False, raw=False, **kwargs):
if not raw:
old_hashes = instance._ik._source_hashes.copy()
new_hashes = ImageSpecField._update_source_hashes(instance)
for attname in instance._ik.spec_fields:
if old_hashes[attname] != new_hashes[attname]:
getattr(instance, attname).invalidate()
@staticmethod
def _update_source_hashes(instance):
"""
Stores hashes of the source image files so that they can be compared
later to see whether the source image has changed (and therefore whether
the spec file needs to be regenerated).
"""
instance._ik._source_hashes = dict((f.attname, hash(f.source_file)) \
for f in instance._ik.spec_files)
return instance._ik._source_hashes
@staticmethod
def _post_delete_receiver(sender, instance=None, **kwargs):
for spec_file in instance._ik.spec_files:
spec_file.clear()
@staticmethod
def _post_init_receiver(sender, instance, **kwargs):
ImageSpecField._update_source_hashes(instance)
class ProcessedImageField(models.ImageField):
"""

View file

@ -11,7 +11,6 @@ class ImageSpecFieldFile(ImageFieldFile):
def __init__(self, instance, field, attname):
super(ImageSpecFieldFile, self).__init__(instance, field, None)
self.attname = attname
self.storage = self.field.storage or self.source_file.storage
@property
def source_file(self):
@ -100,7 +99,7 @@ class ImageSpecFieldFile(ImageFieldFile):
filepath, basename = os.path.split(path)
filename = os.path.splitext(basename)[0]
new_name = '%s_%s%s' % (filename, specname, extension)
return os.path.join(os.path.join('cache', filepath), new_name)
return os.path.join('cache', filepath, new_name)
@property
def name(self):
@ -145,6 +144,25 @@ class ImageSpecFieldFile(ImageFieldFile):
# it at least that one time.
pass
@property
def storage(self):
return getattr(self, '_storage', None) or self.field.storage or self.source_file.storage
@storage.setter
def storage(self, storage):
self._storage = storage
def __getstate__(self):
return dict(
attname=self.attname,
instance=self.instance,
)
def __setstate__(self, state):
self.attname = state['attname']
self.instance = state['instance']
self.field = getattr(self.instance.__class__, self.attname)
class ProcessedImageFieldFile(ImageFieldFile):
def save(self, name, content, save=True):

View file

@ -0,0 +1,48 @@
from django.db.models.signals import post_init, post_save, post_delete
from ..utils import ik_model_receiver
def update_source_hashes(instance):
"""
Stores hashes of the source image files so that they can be compared
later to see whether the source image has changed (and therefore whether
the spec file needs to be regenerated).
"""
instance._ik._source_hashes = dict((f.attname, hash(f.source_file)) \
for f in instance._ik.spec_files)
return instance._ik._source_hashes
@ik_model_receiver
def post_save_receiver(sender, instance=None, created=False, raw=False, **kwargs):
if not raw:
old_hashes = instance._ik._source_hashes.copy()
new_hashes = update_source_hashes(instance)
for attname in instance._ik.spec_fields:
if old_hashes[attname] != new_hashes[attname]:
getattr(instance, attname).invalidate()
@ik_model_receiver
def post_delete_receiver(sender, instance=None, **kwargs):
for spec_file in instance._ik.spec_files:
spec_file.clear()
@ik_model_receiver
def post_init_receiver(sender, instance, **kwargs):
update_source_hashes(instance)
def configure_receivers():
# Connect the signals. We have to listen to every model (not just those
# with IK fields) and filter in our receivers because of a Django issue with
# abstract base models.
# Related:
# https://github.com/jdriscoll/django-imagekit/issues/126
# https://code.djangoproject.com/ticket/9318
uid = 'ik_spec_field_receivers'
post_init.connect(post_init_receiver, dispatch_uid=uid)
post_save.connect(post_save_receiver, dispatch_uid=uid)
post_delete.connect(post_delete_receiver, dispatch_uid=uid)

View file

@ -69,19 +69,20 @@ class Reflection(object):
Creates an image with a reflection.
"""
background_color = '#FFFFFF'
size = 0.0
opacity = 0.6
def __init__(self, background_color='#FFFFFF', size=0.0, opacity=0.6):
self.background_color = background_color
self.size = size
self.opacity = opacity
def process(self, img):
# Convert bgcolor string to RGB value.
background_color = ImageColor.getrgb(self.background_color)
# Handle palleted images.
img = img.convert('RGB')
img = img.convert('RGBA')
# 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)
background = Image.new("RGBA", 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.
@ -101,7 +102,7 @@ class Reflection(object):
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)
composite = Image.new("RGBA", (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]))

View file

@ -1,5 +0,0 @@
from django.conf import settings
DEFAULT_IMAGE_CACHE_BACKEND = getattr(settings,
'IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND',
'imagekit.imagecache.PessimisticImageCacheBackend')

View file

@ -1,6 +1,5 @@
import os
import mimetypes
from StringIO import StringIO
import sys
import types
@ -9,7 +8,7 @@ from django.db.models.loading import cache
from django.utils.functional import wraps
from django.utils.encoding import smart_str, smart_unicode
from .lib import Image, ImageFile
from .lib import Image, ImageFile, StringIO
RGBA_TRANSPARENCY_FORMATS = ['PNG']
@ -372,3 +371,11 @@ def prepare_image(img, format):
save_kwargs['optimize'] = True
return img, save_kwargs
def ik_model_receiver(fn):
@wraps(fn)
def receiver(sender, **kwargs):
if getattr(sender, '_ik', None):
fn(sender, **kwargs)
return receiver

View file

@ -1 +1,3 @@
Django >= 1.3.1
Django>=1.3.1
django-appconf>=0.5
PIL>=1.1.7

View file

@ -28,6 +28,9 @@ setup(
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=[
'django-appconf>=0.5',
],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',

View file

@ -1,23 +1,16 @@
from __future__ import with_statement
import os
import pickle
from StringIO import StringIO
from django.test import TestCase
from imagekit import utils
from .models import (Photo, AbstractImageModel, ConcreteImageModel1,
ConcreteImageModel2)
from .testutils import generate_lenna, create_photo
from .testutils import create_photo, pickleback
class IKTest(TestCase):
def generate_image(self):
tmp = tempfile.TemporaryFile()
Image.new('RGB', (800, 600)).save(tmp, 'JPEG')
tmp.seek(0)
return tmp
def setUp(self):
self.photo = create_photo('test.jpg')
@ -27,7 +20,6 @@ class IKTest(TestCase):
"""
filename = self.photo.thumbnail.file.name
thumbnail_timestamp = os.path.getmtime(filename)
self.photo.save()
self.assertTrue(self.photo.thumbnail.storage.exists(filename))
@ -60,27 +52,29 @@ class IKUtilsTest(TestCase):
self.assertEqual(utils.extension_to_format('.jpeg'), 'JPEG')
self.assertEqual(utils.extension_to_format('.rgba'), 'SGI')
with self.assertRaises(utils.UnknownExtensionError):
utils.extension_to_format('.txt')
self.assertRaises(utils.UnknownExtensionError,
lambda: utils.extension_to_format('.txt'))
def test_format_to_extension_no_init(self):
self.assertEqual(utils.format_to_extension('PNG'), '.png')
self.assertEqual(utils.format_to_extension('ICO'), '.ico')
with self.assertRaises(utils.UnknownFormatError):
utils.format_to_extension('TXT')
self.assertRaises(utils.UnknownFormatError,
lambda: utils.format_to_extension('TXT'))
class PickleTest(TestCase):
def test_source_file(self):
ph = create_photo('pickletest.jpg')
pickled_model = StringIO()
pickle.dump(ph, pickled_model)
pickled_model.seek(0)
unpickled_model = pickle.load(pickled_model)
def test_model(self):
ph = pickleback(create_photo('pickletest.jpg'))
# This isn't supposed to error.
unpickled_model.thumbnail.source_file
ph.thumbnail.source_file
def test_field(self):
thumbnail = pickleback(create_photo('pickletest2.jpg').thumbnail)
# This isn't supposed to error.
thumbnail.source_file
class InheritanceTest(TestCase):

View file

@ -3,8 +3,9 @@ import tempfile
from django.core.files.base import ContentFile
from imagekit.lib import Image
from imagekit.lib import Image, StringIO
from .models import Photo
import pickle
def generate_lenna():
@ -36,3 +37,10 @@ def create_instance(model_class, image_name):
def create_photo(name):
return create_instance(Photo, name)
def pickleback(obj):
pickled = StringIO()
pickle.dump(obj, pickled)
pickled.seek(0)
return pickle.load(pickled)

32
tox.ini
View file

@ -1,31 +1,45 @@
[tox]
envlist =
py27-django13,
py27-django12,
py26-django13,
py26-django12
py27-django14, py27-django13, py27-django12,
py26-django14, py26-django13, py26-django12
[testenv]
changedir = tests
setenv = PYTHONPATH = {toxinidir}/tests
commands = django-admin.py test core --settings=settings
[testenv:py27-django13]
[testenv:py27-django14]
basepython = python2.7
deps =
Django>=1.3,<=1.4
Django>=1.4,<1.5
Pillow
[testenv:py27-django13]
basepython = python2.7
deps =
Django>=1.3,<1.4
Pillow
[testenv:py27-django12]
basepython = python2.7
deps =
Django>=1.2,<=1.3
Django>=1.2,<1.3
Pillow
[testenv:py26-django14]
basepython = python2.6
deps =
Django>=1.4,<1.5
Pillow
[testenv:py26-django13]
basepython = python2.6
deps =
Django>=1.3,<=1.4
Django>=1.3,<1.4
Pillow
[testenv:py26-django12]
basepython = python2.6
deps =
Django>=1.2,<=1.3
Django>=1.2,<1.3
Pillow