mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-23 16:30:25 +00:00
Merge branch 'develop' into templatetags
Conflicts: imagekit/utils.py
This commit is contained in:
commit
c50e6cea3b
32 changed files with 1158 additions and 657 deletions
7
.travis.yml
Normal file
7
.travis.yml
Normal 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"
|
||||
7
AUTHORS
7
AUTHORS
|
|
@ -22,7 +22,9 @@ Contributors
|
|||
* `Alexander Bohn`_
|
||||
* `Timothée Peignier`_
|
||||
* `Madis Väin`_
|
||||
|
||||
* `Jan Sagemüller`_
|
||||
* `Clay McClure`_
|
||||
* `Jannis Leidel`_
|
||||
|
||||
.. _Justin Driscoll: http://github.com/jdriscoll
|
||||
.. _HZDG: http://hzdg.com
|
||||
|
|
@ -39,3 +41,6 @@ Contributors
|
|||
.. _Alexander Bohn: http://github.com/fish2000
|
||||
.. _Timothée Peignier: http://github.com/cyberdelia
|
||||
.. _Madis Väin: http://github.com/madisvain
|
||||
.. _Jan Sagemüller: https://github.com/version2
|
||||
.. _Clay McClure: https://github.com/claymation
|
||||
.. _Jannis Leidel: https://github.com/jezdez
|
||||
|
|
|
|||
114
README.rst
114
README.rst
|
|
@ -1,11 +1,12 @@
|
|||
|
||||
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.
|
||||
|
||||
For the full documentation, see `ImageKit on RTD`_.
|
||||
**For the complete documentation on the latest stable version of ImageKit, see**
|
||||
`ImageKit on RTD`_. Our `changelog is also available`_.
|
||||
|
||||
.. _`ImageKit on RTD`: http://django-imagekit.readthedocs.org
|
||||
.. _`changelog is also available`: http://django-imagekit.readthedocs.org/en/latest/changelog.html
|
||||
|
||||
|
||||
Installation
|
||||
|
|
@ -30,10 +31,12 @@ 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.fields import ImageSpecField
|
||||
from imagekit.models import ImageSpecField
|
||||
|
||||
class Photo(models.Model):
|
||||
original_image = models.ImageField(upload_to='photos')
|
||||
|
|
@ -42,13 +45,28 @@ 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'
|
||||
photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg'
|
||||
|
||||
Check out ``imagekit.models.fields.ImageSpecField`` for more information.
|
||||
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``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.db import models
|
||||
from imagekit.models.fields import ProcessedImageField
|
||||
|
||||
class Photo(models.Model):
|
||||
processed_image = ProcessedImageField(format='JPEG', options={'quality': 90})
|
||||
|
||||
See the class documentation for details.
|
||||
|
||||
|
||||
Processors
|
||||
|
|
@ -56,19 +74,23 @@ 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.fields import ImageSpecField
|
||||
from imagekit.processors import resize, Adjust
|
||||
from imagekit.models import ImageSpecField
|
||||
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::
|
||||
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'
|
||||
|
|
@ -76,13 +98,16 @@ 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
|
||||
original.
|
||||
result of running the ``imagekit.processors.ResizeToFill`` processor on the
|
||||
original. (If you only need to save the processed image, and not the original,
|
||||
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):
|
||||
|
|
@ -101,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
|
||||
|
|
@ -119,3 +146,60 @@ AdminThumbnail can even use a custom template. For more information, see
|
|||
``imagekit.admin.AdminThumbnail``.
|
||||
|
||||
.. _`Django admin change list`: https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list
|
||||
|
||||
|
||||
Image Cache Backends
|
||||
--------------------
|
||||
|
||||
Whenever you access properties like ``url``, ``width`` and ``height`` of an
|
||||
``ImageSpecField``, its cached image is validated; whenever you save a new image
|
||||
to the ``ImageField`` your spec uses as a source, the spec image is invalidated.
|
||||
The default way to validate a cache image is to check to see if the file exists
|
||||
and, if not, generate a new one; the default way to invalidate the cache is to
|
||||
delete the image. This is a very simple and straightforward way to handle cache
|
||||
validation, but it has its drawbacks—for example, checking to see if the image
|
||||
exists means frequently hitting the storage backend.
|
||||
|
||||
Because of this, ImageKit allows you to define custom image cache backends. To
|
||||
be a valid image cache backend, a class must implement three methods:
|
||||
``validate``, ``invalidate``, and ``clear`` (which is called when the image is
|
||||
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:
|
||||
|
||||
.. 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:
|
||||
|
||||
.. 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
|
||||
------------
|
||||
|
||||
We love contributions! And you don't have to be an expert with the library—or
|
||||
even Django—to contribute either: ImageKit's processors are standalone classes
|
||||
that are completely separate from the more intimidating internals of Django's
|
||||
ORM. If you've written a processor that you think might be useful to other
|
||||
people, open a pull request so we can take a look!
|
||||
|
||||
ImageKit's image cache backends are also fairly isolated from the ImageKit guts.
|
||||
If you've fine-tuned one to work perfectly for a popular file storage backend,
|
||||
let us take a look! Maybe other people could use it.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ API Reference
|
|||
:mod:`models` Module
|
||||
--------------------
|
||||
|
||||
.. automodule:: imagekit.models
|
||||
.. automodule:: imagekit.models.fields
|
||||
:members:
|
||||
|
||||
|
||||
|
|
@ -13,13 +13,13 @@ API Reference
|
|||
------------------------
|
||||
|
||||
.. automodule:: imagekit.processors
|
||||
:members:
|
||||
:members:
|
||||
|
||||
.. automodule:: imagekit.processors.resize
|
||||
:members:
|
||||
:members:
|
||||
|
||||
.. automodule:: imagekit.processors.crop
|
||||
:members:
|
||||
:members:
|
||||
|
||||
|
||||
:mod:`admin` Module
|
||||
|
|
|
|||
|
|
@ -1,6 +1,76 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
v2.0.1
|
||||
------
|
||||
|
||||
- Fixed a file descriptor leak in the `utils.quiet()` context manager.
|
||||
|
||||
|
||||
v2.0.0
|
||||
------
|
||||
|
||||
- Added the concept of image cache backends. Image cache backends assume
|
||||
control of validating and invalidating the cached images from `ImageSpec` in
|
||||
versions past. The default backend maintins the current behavior: invalidating
|
||||
an image deletes it, while validating checks whether the file exists and
|
||||
creates the file if it doesn't. One can create custom image cache backends to
|
||||
control how their images are cached (e.g., Celery, etc.).
|
||||
|
||||
ImageKit ships with three built-in backends:
|
||||
|
||||
- ``imagekit.imagecache.PessimisticImageCacheBackend`` - A very safe image
|
||||
cache backend. Guarantees that files will always be available, but at the
|
||||
cost of hitting the storage backend.
|
||||
- ``imagekit.imagecache.NonValidatingImageCacheBackend`` - A backend that is
|
||||
super optimistic about the existence of spec files. It will hit your file
|
||||
storage much less frequently than the pessimistic backend, but it is
|
||||
technically possible for a cache file to be missing after validation.
|
||||
- ``imagekit.imagecache.celery.CeleryImageCacheBackend`` - A pessimistic cache
|
||||
state backend that uses celery to generate its spec images. Like
|
||||
``PessimisticCacheStateBackend``, this one checks to see if the file
|
||||
exists on validation, so the storage is hit fairly frequently, but an
|
||||
image is guaranteed to exist. However, while validation guarantees the
|
||||
existence of *an* image, it does not necessarily guarantee that you will
|
||||
get the correct image, as the spec may be pending regeneration. In other
|
||||
words, while there are ``generate`` tasks in the queue, it is possible to
|
||||
get a stale spec image. The tradeoff is that calling ``invalidate()``
|
||||
won't block to interact with file storage.
|
||||
|
||||
- Some of the processors have been renamed and several new ones have been added:
|
||||
|
||||
- ``imagekit.processors.ResizeToFill`` - (previously
|
||||
``imagekit.processors.resize.Crop``) Scales the image to fill the provided
|
||||
dimensions and then trims away the excess.
|
||||
- ``imagekit.processors.ResizeToFit`` - (previously
|
||||
``imagekit.processors.resize.Fit``) Scale to fit the provided dimensions.
|
||||
- ``imagekit.processors.SmartResize`` - Like ``ResizeToFill``, but crops using
|
||||
entroy (``SmartCrop``) instead of an anchor argument.
|
||||
- ``imagekit.processors.BasicCrop`` - Crop using provided box.
|
||||
- ``imagekit.processors.SmartCrop`` - (previously
|
||||
``imagekit.processors.resize.SmartCrop``) Crop to provided size, trimming
|
||||
based on entropy.
|
||||
- ``imagekit.processors.TrimBorderColor`` - Trim the specified color from the
|
||||
specified sides.
|
||||
- ``imagekit.processors.AddBorder`` - Add a border of specific color and
|
||||
thickness to an image.
|
||||
- ``imagekit.processors.Resize`` - Scale to the provided dimensions (can distort).
|
||||
- ``imagekit.processors.ResizeToCover`` - Scale to the smallest size that will
|
||||
cover the specified dimensions. Used internally by ``Fill`` and
|
||||
``SmartFill``.
|
||||
- ``imagekit.processors.ResizeCanvas`` - Takes an image an resizes the canvas,
|
||||
using a specific background color if the new size is larger than the current
|
||||
image.
|
||||
|
||||
- ``mat_color`` has been added as an arguemnt to ``ResizeToFit``. If set, the
|
||||
the target image size will be enforced and the specified color will be
|
||||
used as background color to pad the image.
|
||||
|
||||
- We now use `Tox`_ to automate testing.
|
||||
|
||||
.. _`Tox`: http://pypi.python.org/pypi/tox
|
||||
|
||||
|
||||
v1.1.0
|
||||
------
|
||||
|
||||
|
|
@ -15,6 +85,7 @@ v1.1.0
|
|||
|
||||
- The private ``_Resize`` class has been removed.
|
||||
|
||||
|
||||
v1.0.3
|
||||
------
|
||||
|
||||
|
|
@ -30,6 +101,7 @@ v1.0.3
|
|||
|
||||
- Fixed PIL zeroing out files when write mode is enabled.
|
||||
|
||||
|
||||
v1.0.2
|
||||
------
|
||||
|
||||
|
|
@ -42,6 +114,7 @@ v1.0.2
|
|||
- Fixed a regression from the 0.4.x series in which ImageKit was unable to
|
||||
convert a PNG file in ``P`` or "palette" mode to JPEG.
|
||||
|
||||
|
||||
v1.0.1
|
||||
------
|
||||
|
||||
|
|
@ -51,6 +124,7 @@ v1.0.1
|
|||
- Fixed the included admin template not being found when ImageKit was and
|
||||
the packaging of the included admin templates.
|
||||
|
||||
|
||||
v1.0
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ 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'
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
|
|
@ -49,9 +50,9 @@ copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett &
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.1.0'
|
||||
version = '2.0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.1.0'
|
||||
release = '2.0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
@ -121,7 +122,7 @@ html_theme_path = ['_themes']
|
|||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ Getting Started
|
|||
Commands
|
||||
--------
|
||||
|
||||
.. automodule:: imagekit.management.commands.ikflush
|
||||
.. automodule:: imagekit.management.commands.ikcacheinvalidate
|
||||
|
||||
.. automodule:: imagekit.management.commands.ikcachevalidate
|
||||
|
||||
|
||||
Authors
|
||||
|
|
@ -16,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
|
||||
--------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
__title__ = 'django-imagekit'
|
||||
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge'
|
||||
__version__ = (1, 1, 0, 'final', 0)
|
||||
__version__ = (2, 0, 1, 'final', 0)
|
||||
__license__ = 'BSD'
|
||||
|
||||
|
||||
def get_version(version=None):
|
||||
"""Derives a PEP386-compliant version number from VERSION."""
|
||||
if version is None:
|
||||
version = __version__
|
||||
assert len(version) == 5
|
||||
assert version[3] in ('alpha', 'beta', 'rc', 'final')
|
||||
|
||||
# Now build the two parts of the version number:
|
||||
# main = X.Y[.Z]
|
||||
# sub = .devN - for pre-alpha releases
|
||||
# | {a|b|c}N - for alpha, beta and rc releases
|
||||
|
||||
parts = 2 if version[2] == 0 else 3
|
||||
main = '.'.join(str(x) for x in version[:parts])
|
||||
|
||||
sub = ''
|
||||
if version[3] == 'alpha' and version[4] == 0:
|
||||
# At the toplevel, this would cause an import loop.
|
||||
from django.utils.version import get_svn_revision
|
||||
svn_revision = get_svn_revision()[4:]
|
||||
if svn_revision != 'unknown':
|
||||
sub = '.dev%s' % svn_revision
|
||||
|
||||
elif version[3] != 'final':
|
||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
|
||||
sub = mapping[version[3]] + str(version[4])
|
||||
|
||||
return main + sub
|
||||
|
|
|
|||
|
|
@ -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
5
imagekit/conf.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from appconf import AppConf
|
||||
|
||||
|
||||
class ImageKitConf(AppConf):
|
||||
DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend'
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
import os
|
||||
from StringIO import StringIO
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from .processors import ProcessorPipeline, AutoConvert
|
||||
from .utils import img_to_fobj, open_image, \
|
||||
format_to_extension, extension_to_format, UnknownFormatError, \
|
||||
UnknownExtensionError
|
||||
from .lib import StringIO
|
||||
from .processors import ProcessorPipeline
|
||||
from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format,
|
||||
UnknownExtensionError)
|
||||
|
||||
|
||||
class SpecFileGenerator(object):
|
||||
def __init__(self, processors=None, format=None, options={},
|
||||
def __init__(self, processors=None, format=None, options=None,
|
||||
autoconvert=True, storage=None):
|
||||
self.processors = processors
|
||||
self.format = format
|
||||
self.options = options
|
||||
self.options = options or {}
|
||||
self.autoconvert = autoconvert
|
||||
self.storage = storage
|
||||
|
||||
|
|
@ -32,7 +28,7 @@ class SpecFileGenerator(object):
|
|||
|
||||
# Determine the format.
|
||||
format = self.format
|
||||
if not format:
|
||||
if filename and not format:
|
||||
# Try to guess the format from the extension.
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
if extension:
|
||||
|
|
@ -42,39 +38,10 @@ class SpecFileGenerator(object):
|
|||
pass
|
||||
format = format or img.format or original_format or 'JPEG'
|
||||
|
||||
# Run the AutoConvert processor
|
||||
if self.autoconvert:
|
||||
autoconvert_processor = AutoConvert(format)
|
||||
img = autoconvert_processor.process(img)
|
||||
options = dict(autoconvert_processor.save_kwargs.items() + \
|
||||
options.items())
|
||||
|
||||
imgfile = img_to_fobj(img, format, **options)
|
||||
content = ContentFile(imgfile.read())
|
||||
content = IKContentFile(filename, imgfile.read(), format=format)
|
||||
return img, content
|
||||
|
||||
def suggest_extension(self, name):
|
||||
original_extension = os.path.splitext(name)[1]
|
||||
try:
|
||||
suggested_extension = format_to_extension(self.format)
|
||||
except UnknownFormatError:
|
||||
extension = original_extension
|
||||
else:
|
||||
if suggested_extension.lower() == original_extension.lower():
|
||||
extension = original_extension
|
||||
else:
|
||||
try:
|
||||
original_format = extension_to_format(original_extension)
|
||||
except UnknownExtensionError:
|
||||
extension = suggested_extension
|
||||
else:
|
||||
# If the formats match, give precedence to the original extension.
|
||||
if self.format.lower() == original_format.lower():
|
||||
extension = original_extension
|
||||
else:
|
||||
extension = suggested_extension
|
||||
return extension
|
||||
|
||||
def generate_file(self, filename, source_file, save=True):
|
||||
"""
|
||||
Generates a new image file by processing the source file and returns
|
||||
|
|
|
|||
34
imagekit/imagecache/__init__.py
Normal file
34
imagekit/imagecache/__init__.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from imagekit.imagecache.base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend
|
||||
|
||||
_default_image_cache_backend = None
|
||||
|
||||
|
||||
def get_default_image_cache_backend():
|
||||
"""
|
||||
Get the default image cache backend. Uses the same method as
|
||||
django.core.file.storage.get_storage_class
|
||||
|
||||
"""
|
||||
global _default_image_cache_backend
|
||||
if not _default_image_cache_backend:
|
||||
from django.conf import settings
|
||||
import_path = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND
|
||||
try:
|
||||
dot = import_path.rindex('.')
|
||||
except ValueError:
|
||||
raise ImproperlyConfigured("%s isn't an image cache backend module." % \
|
||||
import_path)
|
||||
module, classname = import_path[:dot], import_path[dot + 1:]
|
||||
try:
|
||||
mod = import_module(module)
|
||||
except ImportError, e:
|
||||
raise ImproperlyConfigured('Error importing image cache backend module %s: "%s"' % (module, e))
|
||||
try:
|
||||
cls = getattr(mod, classname)
|
||||
_default_image_cache_backend = cls()
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('Image cache backend module "%s" does not define a "%s" class.' % (module, classname))
|
||||
return _default_image_cache_backend
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
|
||||
class InvalidImageCacheBackendError(ImproperlyConfigured):
|
||||
pass
|
||||
|
||||
|
||||
class PessimisticImageCacheBackend(object):
|
||||
|
|
@ -55,33 +58,3 @@ class NonValidatingImageCacheBackend(object):
|
|||
|
||||
def clear(self, file):
|
||||
file.delete(save=False)
|
||||
|
||||
|
||||
_default_image_cache_backend = None
|
||||
|
||||
|
||||
def get_default_image_cache_backend():
|
||||
"""
|
||||
Get the default image cache backend. Uses the same method as
|
||||
django.core.file.storage.get_storage_class
|
||||
|
||||
"""
|
||||
global _default_image_cache_backend
|
||||
if not _default_image_cache_backend:
|
||||
from .settings import DEFAULT_IMAGE_CACHE_BACKEND as import_path
|
||||
try:
|
||||
dot = import_path.rindex('.')
|
||||
except ValueError:
|
||||
raise ImproperlyConfigured("%s isn't an image cache backend module." % \
|
||||
import_path)
|
||||
module, classname = import_path[:dot], import_path[dot+1:]
|
||||
try:
|
||||
mod = import_module(module)
|
||||
except ImportError, e:
|
||||
raise ImproperlyConfigured('Error importing image cache backend module %s: "%s"' % (module, e))
|
||||
try:
|
||||
cls = getattr(mod, classname)
|
||||
_default_image_cache_backend = cls()
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('Image cache backend module "%s" does not define a "%s" class.' % (module, classname))
|
||||
return _default_image_cache_backend
|
||||
43
imagekit/imagecache/celery.py
Normal file
43
imagekit/imagecache/celery.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
from imagekit.imagecache import PessimisticImageCacheBackend, InvalidImageCacheBackendError
|
||||
|
||||
|
||||
def generate(model, pk, attr):
|
||||
try:
|
||||
instance = model._default_manager.get(pk=pk)
|
||||
except model.DoesNotExist:
|
||||
pass # The model was deleted since the task was scheduled. NEVER MIND!
|
||||
else:
|
||||
field_file = getattr(instance, attr)
|
||||
field_file.delete(save=False)
|
||||
field_file.generate(save=True)
|
||||
|
||||
|
||||
class CeleryImageCacheBackend(PessimisticImageCacheBackend):
|
||||
"""
|
||||
A pessimistic cache state backend that uses celery to generate its spec
|
||||
images. Like PessimisticCacheStateBackend, this one checks to see if the
|
||||
file exists on validation, so the storage is hit fairly frequently, but an
|
||||
image is guaranteed to exist. However, while validation guarantees the
|
||||
existence of *an* image, it does not necessarily guarantee that you will get
|
||||
the correct image, as the spec may be pending regeneration. In other words,
|
||||
while there are `generate` tasks in the queue, it is possible to get a
|
||||
stale spec image. The tradeoff is that calling `invalidate()` won't block
|
||||
to interact with file storage.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
try:
|
||||
from celery.task import task
|
||||
except:
|
||||
raise InvalidImageCacheBackendError("Celery image cache backend requires the 'celery' library")
|
||||
if not getattr(CeleryImageCacheBackend, '_task', None):
|
||||
CeleryImageCacheBackend._task = task(generate)
|
||||
|
||||
def invalidate(self, file):
|
||||
self._task.delay(file.instance.__class__, file.instance.pk, file.attname)
|
||||
|
||||
def clear(self, file):
|
||||
file.delete(save=False)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from .. import conf
|
||||
from .fields import ImageSpecField, ProcessedImageField
|
||||
import warnings
|
||||
|
||||
|
|
@ -5,6 +6,6 @@ import warnings
|
|||
class ImageSpec(ImageSpecField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn('ImageSpec has been moved to'
|
||||
' imagekit.models.fields.ImageSpecField. Please use that'
|
||||
' instead.', DeprecationWarning)
|
||||
' imagekit.models.ImageSpecField. Please use that instead.',
|
||||
DeprecationWarning)
|
||||
super(ImageSpec, self).__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
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 .utils import ImageSpecFieldDescriptor, ImageKitMeta, BoundImageKitMeta
|
||||
from ..receivers import configure_receivers
|
||||
from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta
|
||||
from ...utils import suggest_extension
|
||||
|
||||
|
||||
configure_receivers()
|
||||
|
||||
|
||||
class ImageSpecField(object):
|
||||
|
|
@ -13,7 +19,7 @@ class ImageSpecField(object):
|
|||
variants of uploaded images to your models.
|
||||
|
||||
"""
|
||||
def __init__(self, processors=None, format=None, options={},
|
||||
def __init__(self, processors=None, format=None, options=None,
|
||||
image_field=None, pre_cache=None, storage=None, cache_to=None,
|
||||
autoconvert=True, image_cache_backend=None):
|
||||
"""
|
||||
|
|
@ -45,8 +51,8 @@ class ImageSpecField(object):
|
|||
based on that format. if not, the extension of the
|
||||
original file will be passed. You do not have to use
|
||||
this extension, it's only a recommendation.
|
||||
:param autoconvert: Specifies whether the AutoConvert processor
|
||||
should be run before saving.
|
||||
:param autoconvert: Specifies whether automatic conversion using
|
||||
``prepare_image()`` should be performed prior to saving.
|
||||
:param image_cache_backend: An object responsible for managing the state
|
||||
of cached files. Defaults to an instance of
|
||||
IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND
|
||||
|
|
@ -73,59 +79,27 @@ class ImageSpecField(object):
|
|||
get_default_image_cache_backend()
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
setattr(cls, name, ImageSpecFieldDescriptor(self, name))
|
||||
setattr(cls, name, ImageSpecFileDescriptor(self, name))
|
||||
try:
|
||||
ik = getattr(cls, '_ik')
|
||||
except AttributeError:
|
||||
ik = ImageKitMeta()
|
||||
# Make sure we don't modify an inherited ImageKitMeta instance
|
||||
ik = cls.__dict__['ik']
|
||||
except KeyError:
|
||||
try:
|
||||
base = getattr(cls, '_ik')
|
||||
except AttributeError:
|
||||
ik = ImageKitMeta()
|
||||
else:
|
||||
# Inherit all the spec fields.
|
||||
ik = ImageKitMeta(base.spec_fields)
|
||||
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):
|
||||
"""
|
||||
|
|
@ -137,14 +111,14 @@ class ProcessedImageField(models.ImageField):
|
|||
"""
|
||||
attr_class = ProcessedImageFieldFile
|
||||
|
||||
def __init__(self, processors=None, format=None, options={},
|
||||
def __init__(self, processors=None, format=None, options=None,
|
||||
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 ``options`` arguments of
|
||||
:class:`imagekit.models.fields.ImageSpecField`.
|
||||
:class:`imagekit.models.ImageSpecField`.
|
||||
|
||||
"""
|
||||
if 'quality' in kwargs:
|
||||
|
|
@ -160,8 +134,8 @@ class ProcessedImageField(models.ImageField):
|
|||
filename = os.path.normpath(self.storage.get_valid_name(
|
||||
os.path.basename(filename)))
|
||||
name, ext = os.path.splitext(filename)
|
||||
ext = self.generator.suggest_extension(filename)
|
||||
return '%s%s' % (name, ext)
|
||||
ext = suggest_extension(filename, self.generator.format)
|
||||
return u'%s%s' % (name, ext)
|
||||
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import os
|
||||
import datetime
|
||||
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from django.db.models.fields.files import ImageField, ImageFieldFile
|
||||
from django.utils.encoding import force_unicode, smart_str
|
||||
|
||||
from ...utils import suggest_extension
|
||||
|
||||
|
||||
class ImageSpecFieldFile(ImageFieldFile):
|
||||
def __init__(self, instance, field, attname):
|
||||
ImageFieldFile.__init__(self, instance, field, None)
|
||||
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):
|
||||
|
|
@ -19,7 +20,7 @@ class ImageSpecFieldFile(ImageFieldFile):
|
|||
else:
|
||||
image_fields = [getattr(self.instance, f.attname) for f in \
|
||||
self.instance.__class__._meta.fields if \
|
||||
isinstance(f, models.ImageField)]
|
||||
isinstance(f, ImageField)]
|
||||
if len(image_fields) == 0:
|
||||
raise Exception('%s does not define any ImageFields, so your' \
|
||||
' %s ImageSpecField has no image to act on.' % \
|
||||
|
|
@ -36,12 +37,8 @@ class ImageSpecFieldFile(ImageFieldFile):
|
|||
def _require_file(self):
|
||||
if not self.source_file:
|
||||
raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname)
|
||||
|
||||
def _get_file(self):
|
||||
self.validate()
|
||||
return super(ImageFieldFile, self).file
|
||||
|
||||
file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file)
|
||||
else:
|
||||
self.validate()
|
||||
|
||||
def clear(self):
|
||||
return self.field.image_cache_backend.clear(self)
|
||||
|
|
@ -61,11 +58,6 @@ class ImageSpecFieldFile(ImageFieldFile):
|
|||
return self.field.generator.generate_file(self.name, self.source_file,
|
||||
save)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
self.validate()
|
||||
return super(ImageFieldFile, self).url
|
||||
|
||||
def delete(self, save=False):
|
||||
"""
|
||||
Pulled almost verbatim from ``ImageFieldFile.delete()`` and
|
||||
|
|
@ -107,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):
|
||||
|
|
@ -127,9 +119,8 @@ class ImageSpecFieldFile(ImageFieldFile):
|
|||
raise Exception('No cache_to or default_cache_to value'
|
||||
' specified')
|
||||
if callable(cache_to):
|
||||
suggested_extension = \
|
||||
self.field.generator.suggest_extension(
|
||||
self.source_file.name)
|
||||
suggested_extension = suggest_extension(
|
||||
self.source_file.name, self.field.generator.format)
|
||||
new_filename = force_unicode(
|
||||
datetime.datetime.now().strftime(
|
||||
smart_str(cache_to(self.instance,
|
||||
|
|
@ -153,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):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class BoundImageKitMeta(object):
|
|||
|
||||
class ImageKitMeta(object):
|
||||
def __init__(self, spec_fields=None):
|
||||
self.spec_fields = spec_fields or []
|
||||
self.spec_fields = list(spec_fields) if spec_fields else []
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
|
|
@ -24,7 +24,7 @@ class ImageKitMeta(object):
|
|||
return ik
|
||||
|
||||
|
||||
class ImageSpecFieldDescriptor(object):
|
||||
class ImageSpecFileDescriptor(object):
|
||||
def __init__(self, field, attname):
|
||||
self.attname = attname
|
||||
self.field = field
|
||||
|
|
@ -35,5 +35,8 @@ class ImageSpecFieldDescriptor(object):
|
|||
else:
|
||||
img_spec_file = ImageSpecFieldFile(instance, self.field,
|
||||
self.attname)
|
||||
setattr(instance, self.attname, img_spec_file)
|
||||
instance.__dict__[self.attname] = img_spec_file
|
||||
return img_spec_file
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance.__dict__[self.attname] = value
|
||||
|
|
|
|||
48
imagekit/models/receivers.py
Normal file
48
imagekit/models/receivers.py
Normal 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)
|
||||
|
|
@ -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 *
|
||||
|
|
|
|||
209
imagekit/processors/base.py
Normal file
209
imagekit/processors/base.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
from imagekit.lib import Image, ImageColor, ImageEnhance
|
||||
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
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('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("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.
|
||||
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("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]))
|
||||
# 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 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -21,11 +21,11 @@ class BasicResize(object):
|
|||
return img.resize((self.width, self.height), Image.ANTIALIAS)
|
||||
|
||||
|
||||
class Cover(object):
|
||||
class ResizeToCover(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):
|
||||
img = Cover(self.width, self.height).process(img)
|
||||
return crop.Crop(self.width, self.height,
|
||||
from .crop import Crop
|
||||
img = ResizeToCover(self.width, self.height).process(img)
|
||||
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):
|
||||
img = Cover(self.width, self.height).process(img)
|
||||
return crop.SmartCrop(self.width, self.height).process(img)
|
||||
from .crop import SmartCrop
|
||||
img = ResizeToCover(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)
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
DEFAULT_IMAGE_CACHE_BACKEND = getattr(settings,
|
||||
'IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND',
|
||||
'imagekit.imagecache.PessimisticImageCacheBackend')
|
||||
|
|
@ -1,29 +1,49 @@
|
|||
import tempfile
|
||||
import os
|
||||
import mimetypes
|
||||
import sys
|
||||
import types
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models.loading import cache
|
||||
from django.utils.functional import wraps
|
||||
from django.utils.encoding import smart_str, smart_unicode
|
||||
|
||||
from .imagecache import get_default_image_cache_backend
|
||||
from .lib import Image, ImageFile
|
||||
from .lib import Image, ImageFile, StringIO
|
||||
|
||||
|
||||
def img_to_fobj(img, format, **kwargs):
|
||||
tmp = tempfile.TemporaryFile()
|
||||
try:
|
||||
img.save(tmp, format, **kwargs)
|
||||
except IOError:
|
||||
# PIL can have problems saving large JPEGs if MAXBLOCK isn't big enough,
|
||||
# So if we have a problem saving, we temporarily increase it. See
|
||||
# http://github.com/jdriscoll/django-imagekit/issues/50
|
||||
old_maxblock = ImageFile.MAXBLOCK
|
||||
ImageFile.MAXBLOCK = img.size[0] * img.size[1]
|
||||
try:
|
||||
img.save(tmp, format, **kwargs)
|
||||
finally:
|
||||
ImageFile.MAXBLOCK = old_maxblock
|
||||
tmp.seek(0)
|
||||
return tmp
|
||||
RGBA_TRANSPARENCY_FORMATS = ['PNG']
|
||||
PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF']
|
||||
|
||||
|
||||
class IKContentFile(ContentFile):
|
||||
"""
|
||||
Wraps a ContentFile in a file-like object with a filename and a
|
||||
content_type. A PIL image format can be optionally be provided as a content
|
||||
type hint.
|
||||
|
||||
"""
|
||||
def __init__(self, filename, content, format=None):
|
||||
self.file = ContentFile(content)
|
||||
self.file.name = filename
|
||||
mimetype = getattr(self.file, 'content_type', None)
|
||||
if format and not mimetype:
|
||||
mimetype = format_to_mimetype(format)
|
||||
if not mimetype:
|
||||
ext = os.path.splitext(filename or '')[1]
|
||||
mimetype = extension_to_mimetype(ext)
|
||||
self.file.content_type = mimetype
|
||||
|
||||
def __str__(self):
|
||||
return smart_str(self.file.name or '')
|
||||
|
||||
def __unicode__(self):
|
||||
return smart_unicode(self.file.name or u'')
|
||||
|
||||
|
||||
def img_to_fobj(img, format, autoconvert=True, **options):
|
||||
return save_image(img, StringIO(), format, options, autoconvert)
|
||||
|
||||
|
||||
def get_spec_files(instance):
|
||||
|
|
@ -108,6 +128,19 @@ def _format_to_extension(format):
|
|||
return None
|
||||
|
||||
|
||||
def extension_to_mimetype(ext):
|
||||
try:
|
||||
filename = 'a%s' % (ext or '') # guess_type requires a full filename, not just an extension
|
||||
mimetype = mimetypes.guess_type(filename)[0]
|
||||
except IndexError:
|
||||
mimetype = None
|
||||
return mimetype
|
||||
|
||||
|
||||
def format_to_mimetype(format):
|
||||
return extension_to_mimetype(format_to_extension(format))
|
||||
|
||||
|
||||
def extension_to_format(extension):
|
||||
"""Returns the format that corresponds to the provided extension.
|
||||
|
||||
|
|
@ -168,6 +201,188 @@ def validate_app_cache(apps, force_revalidation=False):
|
|||
f.validate()
|
||||
|
||||
|
||||
def suggest_extension(name, format):
|
||||
original_extension = os.path.splitext(name)[1]
|
||||
try:
|
||||
suggested_extension = format_to_extension(format)
|
||||
except UnknownFormatError:
|
||||
extension = original_extension
|
||||
else:
|
||||
if suggested_extension.lower() == original_extension.lower():
|
||||
extension = original_extension
|
||||
else:
|
||||
try:
|
||||
original_format = extension_to_format(original_extension)
|
||||
except UnknownExtensionError:
|
||||
extension = suggested_extension
|
||||
else:
|
||||
# If the formats match, give precedence to the original extension.
|
||||
if format.lower() == original_format.lower():
|
||||
extension = original_extension
|
||||
else:
|
||||
extension = suggested_extension
|
||||
return extension
|
||||
|
||||
|
||||
def save_image(img, outfile, format, options=None, autoconvert=True):
|
||||
"""
|
||||
Wraps PIL's ``Image.save()`` method. There are two main benefits of using
|
||||
this function over PIL's:
|
||||
|
||||
1. It gracefully handles the infamous "Suspension not allowed here" errors.
|
||||
2. It prepares the image for saving using ``prepare_image()``, which will do
|
||||
some common-sense processing given the target format.
|
||||
|
||||
"""
|
||||
options = options or {}
|
||||
|
||||
if autoconvert:
|
||||
img, save_kwargs = prepare_image(img, format)
|
||||
options = dict(save_kwargs.items() + options.items())
|
||||
|
||||
# Attempt to reset the file pointer.
|
||||
try:
|
||||
outfile.seek(0)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
with quiet():
|
||||
img.save(outfile, format, **options)
|
||||
except IOError:
|
||||
# PIL can have problems saving large JPEGs if MAXBLOCK isn't big enough,
|
||||
# So if we have a problem saving, we temporarily increase it. See
|
||||
# http://github.com/jdriscoll/django-imagekit/issues/50
|
||||
old_maxblock = ImageFile.MAXBLOCK
|
||||
ImageFile.MAXBLOCK = img.size[0] * img.size[1]
|
||||
try:
|
||||
img.save(outfile, format, **options)
|
||||
finally:
|
||||
ImageFile.MAXBLOCK = old_maxblock
|
||||
|
||||
try:
|
||||
outfile.seek(0)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return outfile
|
||||
|
||||
|
||||
class quiet(object):
|
||||
"""
|
||||
A context manager for suppressing the stderr activity of PIL's C libraries.
|
||||
Based on http://stackoverflow.com/a/978264/155370
|
||||
|
||||
"""
|
||||
def __enter__(self):
|
||||
self.stderr_fd = sys.__stderr__.fileno()
|
||||
self.null_fd = os.open(os.devnull, os.O_RDWR)
|
||||
self.old = os.dup(self.stderr_fd)
|
||||
os.dup2(self.null_fd, self.stderr_fd)
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
os.dup2(self.old, self.stderr_fd)
|
||||
os.close(self.null_fd)
|
||||
os.close(self.old)
|
||||
|
||||
|
||||
def prepare_image(img, format):
|
||||
"""
|
||||
Prepares the image for saving to the provided format by doing some
|
||||
common-sense conversions. This includes things like preserving transparency
|
||||
and quantizing. This function is used automatically by ``save_image()``
|
||||
(and classes like ``ImageSpecField`` and ``ProcessedImageField``)
|
||||
immediately before saving unless you specify ``autoconvert=False``. It is
|
||||
provided as a utility for those doing their own processing.
|
||||
|
||||
:param img: The image to prepare for saving.
|
||||
:param format: The format that the image will be saved to.
|
||||
|
||||
"""
|
||||
matte = False
|
||||
save_kwargs = {}
|
||||
|
||||
if img.mode == 'RGBA':
|
||||
if format in RGBA_TRANSPARENCY_FORMATS:
|
||||
pass
|
||||
elif 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.
|
||||
|
||||
# Work around a bug in PIL: split() doesn't check to see if
|
||||
# img is loaded.
|
||||
img.load()
|
||||
|
||||
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)
|
||||
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 format in PALETTE_TRANSPARENCY_FORMATS:
|
||||
try:
|
||||
save_kwargs['transparency'] = img.info['transparency']
|
||||
except KeyError:
|
||||
pass
|
||||
elif 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 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 format == 'JPEG':
|
||||
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
|
||||
|
||||
|
||||
def autodiscover():
|
||||
"""
|
||||
Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
Django >= 1.3.1
|
||||
Django>=1.3.1
|
||||
django-appconf>=0.5
|
||||
PIL>=1.1.7
|
||||
|
|
|
|||
13
setup.py
13
setup.py
|
|
@ -11,9 +11,12 @@ if 'publish' in sys.argv:
|
|||
|
||||
read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
|
||||
|
||||
# Dynamically calculate the version based on imagekit.VERSION.
|
||||
version = __import__('imagekit').get_version()
|
||||
|
||||
setup(
|
||||
name='django-imagekit',
|
||||
version=':versiontools:imagekit:',
|
||||
version=version,
|
||||
description='Automated image processing for Django models.',
|
||||
long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')),
|
||||
author='Justin Driscoll',
|
||||
|
|
@ -25,8 +28,11 @@ setup(
|
|||
packages=find_packages(),
|
||||
zip_safe=False,
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'django-appconf>=0.5',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Intended Audience :: Developers',
|
||||
|
|
@ -37,7 +43,4 @@ setup(
|
|||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Utilities'
|
||||
],
|
||||
setup_requires=[
|
||||
'versiontools >= 1.8',
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
from django.db import models
|
||||
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import Adjust
|
||||
from imagekit.processors import ResizeToFill
|
||||
from imagekit.processors import SmartCrop
|
||||
|
||||
|
||||
class Photo(models.Model):
|
||||
original_image = models.ImageField(upload_to='photos')
|
||||
|
||||
thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1),
|
||||
ResizeToFill(50, 50)], image_field='original_image', format='JPEG',
|
||||
options={'quality': 90})
|
||||
|
||||
smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2,
|
||||
sharpness=1.1), SmartCrop(50, 50)], image_field='original_image',
|
||||
format='JPEG', options={'quality': 90})
|
||||
|
||||
|
||||
class AbstractImageModel(models.Model):
|
||||
original_image = models.ImageField(upload_to='photos')
|
||||
abstract_class_spec = ImageSpecField()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ConcreteImageModel1(AbstractImageModel):
|
||||
first_spec = ImageSpecField()
|
||||
|
||||
|
||||
class ConcreteImageModel2(AbstractImageModel):
|
||||
second_spec = ImageSpecField()
|
||||
|
|
@ -1,73 +1,25 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
|
||||
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
|
||||
|
||||
|
||||
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',
|
||||
options={'quality': 90})
|
||||
|
||||
smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2,
|
||||
sharpness=1.1), SmartCrop(50, 50)], image_field='original_image',
|
||||
format='JPEG', options={'quality': 90})
|
||||
from .models import (Photo, AbstractImageModel, ConcreteImageModel1,
|
||||
ConcreteImageModel2)
|
||||
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 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())
|
||||
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')
|
||||
self.photo = 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))
|
||||
|
||||
|
|
@ -109,3 +61,27 @@ class IKUtilsTest(TestCase):
|
|||
|
||||
with self.assertRaises(utils.UnknownFormatError):
|
||||
utils.format_to_extension('TXT')
|
||||
|
||||
|
||||
class PickleTest(TestCase):
|
||||
def test_model(self):
|
||||
ph = pickleback(create_photo('pickletest.jpg'))
|
||||
|
||||
# This isn't supposed to error.
|
||||
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):
|
||||
def test_abstract_base(self):
|
||||
self.assertEqual(set(AbstractImageModel._ik.spec_fields),
|
||||
set(['abstract_class_spec']))
|
||||
self.assertEqual(set(ConcreteImageModel1._ik.spec_fields),
|
||||
set(['abstract_class_spec', 'first_spec']))
|
||||
self.assertEqual(set(ConcreteImageModel2._ik.spec_fields),
|
||||
set(['abstract_class_spec', 'second_spec']))
|
||||
|
|
|
|||
46
tests/core/testutils.py
Normal file
46
tests/core/testutils.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from imagekit.lib import Image, StringIO
|
||||
from .models import Photo
|
||||
import pickle
|
||||
|
||||
|
||||
def generate_lenna():
|
||||
"""
|
||||
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_instance(model_class, image_name):
|
||||
instance = model_class()
|
||||
img = generate_lenna()
|
||||
file = ContentFile(img.read())
|
||||
instance.original_image = file
|
||||
instance.original_image.save(image_name, file)
|
||||
instance.save()
|
||||
img.close()
|
||||
return instance
|
||||
|
||||
|
||||
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)
|
||||
22
tox.ini
22
tox.ini
|
|
@ -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-django14]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.4
|
||||
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
|
||||
Pillow
|
||||
|
||||
[testenv:py26-django14]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.4
|
||||
Pillow
|
||||
|
||||
[testenv:py26-django13]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.3,<=1.4
|
||||
Pillow
|
||||
|
||||
[testenv:py26-django12]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.2,<=1.3
|
||||
Pillow
|
||||
|
|
|
|||
Loading…
Reference in a new issue