diff --git a/AUTHORS b/AUTHORS
index ed2df28..0ae6234 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,20 +1,35 @@
-Original Author:
+ImageKit was originally written by `Justin Driscoll`_.
-* Justin Driscoll (jdriscoll)
+The field-based API was written by the bright minds at HZDG_.
+
+Maintainers
+~~~~~~~~~~~
+
+* `Bryan Veloso`_
+* `Chris Drackett`_
+* `Greg Newman`_
+
+Contributors
+~~~~~~~~~~~~
+
+* `Josh Ourisman`_
+* `Jonathan Slenders`_
+* `Matthew Tretter`_
+* `Eric Eldredge`_
+* `Chris McKenzie`_
+* `Markus Kaiserswerth`_
+* `Ryan Bagwell`_
-Maintainers:
-
-* Bryan Veloso (bryanveloso)
-* Chris Drackett (chrisdrackett)
-* Greg Newman (gregnewman)
-
-
-Contributors:
-
-* Josh Ourisman (joshourisman)
-* Jonathan Slenders (jonathanslenders)
-* Matthew Tretter (matthewwithanm)
-* Markus Kaiserswerth (mkai)
-* Ryan Bagwell (ryanbagwell)
-
+.. _Justin Driscoll: http://github.com/jdriscoll
+.. _HZDG: http://hzdg.com
+.. _Bryan Veloso: http://github.com/bryanveloso
+.. _Chris Drackett: http://github.com/chrisdrackett
+.. _Greg Newman: http://github.com/gregnewman
+.. _Josh Ourisman: http://github.com/joshourisman
+.. _Jonathan Slenders: http://github.com/jonathanslenders
+.. _Matthew Tretter: http://github.com/matthewwithanm
+.. _Eric Eldredge: http://github.com/lettertwo
+.. _Chris McKenzie: http://github.com/kenzic
+.. _Ryan Bagwell: http://github.com/ryanbagwell
+.. _Markus Kaiserswerth: http://github.com/mkai
diff --git a/README.rst b/README.rst
index e82b37e..c423623 100644
--- a/README.rst
+++ b/README.rst
@@ -1,147 +1,108 @@
-===============
-django-imagekit
-===============
+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.
-NOTE: This, the **class-based** version of ImageKit, has been discontinued.
-ImageKit In 7 Steps
-===================
+Installation
+------------
-Step 1
-******
+1. ``pip install django-imagekit``
+ (or clone the source and put the imagekit module on your path)
+2. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py
-::
- $ pip install django-imagekit
+Adding Specs to a Model
+-----------------------
-(or clone the source and put the imagekit module on your path)
-
-Step 2
-******
-
-Add ImageKit to your models.
-
-::
-
- # myapp/models.py
+Much like ``django.db.models.ImageField``, Specs are defined as properties
+of a model class::
from django.db import models
- from imagekit.models import ImageModel
+ from imagekit.models import ImageSpec
- class Photo(ImageModel):
- name = models.CharField(max_length=100)
+ class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
- num_views = models.PositiveIntegerField(editable=False, default=0)
+ formatted_image = ImageSpec(image_field='original_image', format='JPEG',
+ quality=90)
- class IKOptions:
- # This inner class is where we define the ImageKit options for the model
- spec_module = 'myapp.specs'
- cache_dir = 'photos'
- image_field = 'original_image'
- save_count_as = 'num_views'
+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``)::
-Step 3
-******
+ photo = Photo.objects.all()[0]
+ photo.original_image.url # > '/media/photos/birthday.tiff'
+ photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg'
-Create your specifications.
-
-::
-
- # myapp/specs.py
-
- from imagekit.specs import ImageSpec
- from imagekit import processors
-
- # first we define our thumbnail resize processor
- class ResizeThumb(processors.Resize):
- width = 100
- height = 75
- crop = True
-
- # now we define a display size resize processor
- class ResizeDisplay(processors.Resize):
- width = 600
-
- # now let's create an adjustment processor to enhance the image at small sizes
- class EnhanceThumb(processors.Adjustment):
- contrast = 1.2
- sharpness = 1.1
-
- # now we can define our thumbnail spec
- class Thumbnail(ImageSpec):
- quality = 90 # defaults to 70
- access_as = 'thumbnail_image'
- pre_cache = True
- processors = [ResizeThumb, EnhanceThumb]
-
- # and our display spec
- class Display(ImageSpec):
- quality = 90 # defaults to 70
- increment_count = True
- processors = [ResizeDisplay]
-
-Step 4
-******
-
-Flush the cache and pre-generate thumbnails (ImageKit has to be added to ``INSTALLED_APPS`` for management command to work).
-
-::
-
- $ python manage.py ikflush myapp
-
-Step 5
-******
-
-Use your new model in templates.
-
-::
-
-
-
-
-
-
-
-
-
-
- {% for p in photos %}
-
- {% endfor %}
-
-
-Step 6
-******
-
-Play with the API.
-
-::
-
- >>> from myapp.models import Photo
- >>> p = Photo.objects.all()[0]
-
- >>> p.display.url
- u'/static/photos/myphoto_display.jpg'
- >>> p.display.width
- 600
- >>> p.display.height
- 420
- >>> p.display.image
-
- >>> p.display.file
-
- >>> p.display.spec
-
-
-Step 7
-******
-
-Enjoy a nice beverage.
-
-::
-
- from refrigerator import beer
-
- beer.enjoy()
+Check out ``imagekit.models.ImageSpec`` for more information.
+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::
+
+ from django.db import models
+ from imagekit.models import ImageSpec
+ from imagekit.processors import resize, Adjust
+
+ class Photo(models.Model):
+ original_image = models.ImageField(upload_to='photos')
+ thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1),
+ resize.Crop(50, 50)], image_field='original_image',
+ format='JPEG', quality=90)
+
+The ``thumbnail`` property will now return a cropped image::
+
+ photo = Photo.objects.all()[0]
+ photo.thumbnail.url # > '/media/cache/photos/birthday_thumbnail.jpeg'
+ photo.thumbnail.width # > 50
+ 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.Crop`` processor on the
+original.
+
+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::
+
+ class Watermark(object):
+ def process(self, image):
+ # Code for adding the watermark goes here.
+ return image
+
+ class Photo(models.Model):
+ original_image = models.ImageField(upload_to='photos')
+ watermarked_image = ImageSpec([Watermark()], image_field='original_image',
+ format='JPEG', quality=90)
+
+
+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::
+
+ from django.contrib import admin
+ from imagekit.admin import AdminThumbnail
+ from .models import Photo
+
+
+ class PhotoAdmin(admin.ModelAdmin):
+ list_display = ('__str__', 'admin_thumbnail')
+ admin_thumbnail = AdminThumbnail(image_field='thumbnail')
+
+
+ admin.site.register(Photo, PhotoAdmin)
+
+AdminThumbnail can even use a custom template. For more information, see
+``imagekit.admin.AdminThumbnail``.
+
+
+__ https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list
+__ https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display
diff --git a/docs/Makefile b/docs/Makefile
index 1dc0357..5e9e72d 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -3,9 +3,9 @@
# You can set these variables from the command line.
SPHINXOPTS =
-SPHINXBUILD = sphinx-build
+SPHINXBUILD = python $(shell which sphinx-build)
PAPER =
-BUILDDIR = _build
+BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
@@ -105,7 +105,7 @@ latex:
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
- make -C $(BUILDDIR)/latex all-pdf
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
diff --git a/docs/apireference.rst b/docs/apireference.rst
new file mode 100644
index 0000000..c997026
--- /dev/null
+++ b/docs/apireference.rst
@@ -0,0 +1,26 @@
+API Reference
+=============
+
+
+:mod:`models` Module
+--------------------
+
+.. automodule:: imagekit.models
+ :members:
+
+
+:mod:`processors` Module
+------------------------
+
+.. automodule:: imagekit.processors
+ :members:
+
+.. automodule:: imagekit.processors.resize
+ :members:
+
+
+:mod:`admin` Module
+--------------------
+
+.. automodule:: imagekit.admin
+ :members:
diff --git a/docs/conf.py b/docs/conf.py
index 5b21fe1..a751d2c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# ImageKit documentation build configuration file, created by
-# sphinx-quickstart on Fri Apr 1 12:20:11 2011.
+# sphinx-quickstart on Sun Sep 25 17:05:55 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
@@ -25,7 +25,7 @@ import sys, os
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
+extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -41,16 +41,16 @@ master_doc = 'index'
# General information about the project.
project = u'ImageKit'
-copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman & Chris Drackett'
+copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
-version = '0.3.6'
+version = '1.0alpha'
# The full version, including alpha/beta/rc tags.
-release = '0.3.6'
+release = '1.0alpha'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -64,7 +64,7 @@ release = '0.3.6'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
@@ -153,7 +153,7 @@ html_static_path = ['_static']
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+html_show_copyright = False
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
@@ -169,17 +169,22 @@ htmlhelp_basename = 'ImageKitdoc'
# -- Options for LaTeX output --------------------------------------------------
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'ImageKit.tex', u'ImageKit Documentation',
- u'Justin Driscoll, Bryan Veloso, Greg Newman \\& Chris Drackett', 'manual'),
+ u'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett \\& Matthew Tretter', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -196,9 +201,6 @@ latex_documents = [
# If true, show URL addresses after external links.
#latex_show_urls = False
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
# Documents to append as an appendix to all manuals.
#latex_appendices = []
@@ -212,7 +214,7 @@ latex_documents = [
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'imagekit', u'ImageKit Documentation',
- [u'Justin Driscoll, Bryan Veloso, Greg Newman & Chris Drackett'], 1)
+ [u'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter'], 1)
]
# If true, show URL addresses after external links.
@@ -225,7 +227,7 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- ('index', 'ImageKit', u'ImageKit Documentation', u'Justin Driscoll, Bryan Veloso, Greg Newman & Chris Drackett',
+ ('index', 'ImageKit', u'ImageKit Documentation', u'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & Matthew Tretter',
'ImageKit', 'One line description of project.', 'Miscellaneous'),
]
@@ -238,12 +240,4 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
-
-# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {
- 'py': ('http://docs.python.org/', None),
- 'django': (
- 'http://docs.djangoproject.com/en/1.3',
- 'http://docs.djangoproject.com/en/1.3/_objects/'
- )
-}
+autoclass_content = 'both'
diff --git a/docs/index.rst b/docs/index.rst
index e66d8c6..01916fd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,17 +1,28 @@
-ImageKit
-========
+Getting Started
+===============
-ImageKit brings automated image processing to your Django models. With an API
-similar to that of Django's :ref:`models.Meta ` options
-class and the creation of a specification file, you'll be able to create an
-infinite number of renditions for any image using any number of preprocessors.
+.. include:: ../README.rst
-Contents:
+
+Commands
+--------
+
+.. automodule:: imagekit.management.commands.ikflush
+
+
+Authors
+-------
+
+.. include:: ../AUTHORS
+
+
+Digging Deeper
+--------------
.. toctree::
- :maxdepth: 2
- tutorial
+ apireference
+
Indices and tables
==================
@@ -19,4 +30,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
-
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..aad0d30
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,190 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^` where ^ is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ImageKit.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ImageKit.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
diff --git a/imagekit/__init__.py b/imagekit/__init__.py
index 8993c70..c12553b 100644
--- a/imagekit/__init__.py
+++ b/imagekit/__init__.py
@@ -1,13 +1,5 @@
-"""
-Django ImageKit
-
-:author: Justin Driscoll
-:maintainer: Bryan Veloso
-:license: BSD
-
-"""
__title__ = 'django-imagekit'
-__version__ = '0.4.1'
-__build__ = 0x000400
-__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett'
+__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge'
+__version__ = (1, 0, 0, 'final', 0)
+__build__ = 0x001000
__license__ = 'BSD'
diff --git a/imagekit/admin.py b/imagekit/admin.py
new file mode 100644
index 0000000..cc24d29
--- /dev/null
+++ b/imagekit/admin.py
@@ -0,0 +1,37 @@
+from django.utils.translation import ugettext_lazy as _
+from django.template.loader import render_to_string
+
+
+class AdminThumbnail(object):
+ """
+ A convenience utility for adding thumbnails to Django's admin change list.
+
+ """
+ short_description = _('Thumbnail')
+ allow_tags = True
+
+ def __init__(self, image_field, template=None):
+ """
+ :param image_field: The name of the ImageField or ImageSpec on the
+ model to use for the thumbnail.
+ :param template: The template with which to render the thumbnail
+
+ """
+ self.image_field = image_field
+ self.template = template
+
+ def __call__(self, obj):
+ thumbnail = getattr(obj, self.image_field, None)
+
+ if not thumbnail:
+ raise Exception('The property {0} is not defined on {1}.'.format(
+ obj, self.image_field))
+
+ original_image = getattr(thumbnail, 'source_file', None) or thumbnail
+ template = self.template or 'imagekit/admin/thumbnail.html'
+
+ return render_to_string(template, {
+ 'model': obj,
+ 'thumbnail': thumbnail,
+ 'original_image': original_image,
+ })
diff --git a/imagekit/defaults.py b/imagekit/defaults.py
deleted file mode 100644
index c3475f6..0000000
--- a/imagekit/defaults.py
+++ /dev/null
@@ -1,30 +0,0 @@
-""" Default ImageKit configuration """
-
-from imagekit.specs import ImageSpec
-from imagekit import processors
-
-
-class ResizeThumbnail(processors.Resize):
- width = 100
- height = 50
- crop = True
-
-
-class EnhanceSmall(processors.Adjustment):
- contrast = 1.2
- sharpness = 1.1
-
-
-class SampleReflection(processors.Reflection):
- size = 0.5
- background_color = "#000000"
-
-
-class PNGFormat(processors.Format):
- format = 'PNG'
- extension = 'png'
-
-
-class DjangoAdminThumbnail(ImageSpec):
- access_as = 'admin_thumbnail'
- processors = [ResizeThumbnail, EnhanceSmall, SampleReflection, PNGFormat]
diff --git a/imagekit/management/__init__.py b/imagekit/management/__init__.py
index 8b13789..e69de29 100644
--- a/imagekit/management/__init__.py
+++ b/imagekit/management/__init__.py
@@ -1 +0,0 @@
-
diff --git a/imagekit/management/commands/__init__.py b/imagekit/management/commands/__init__.py
index 8b13789..e69de29 100644
--- a/imagekit/management/commands/__init__.py
+++ b/imagekit/management/commands/__init__.py
@@ -1 +0,0 @@
-
diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py
index 792112f..d718992 100644
--- a/imagekit/management/commands/ikflush.py
+++ b/imagekit/management/commands/ikflush.py
@@ -1,8 +1,11 @@
+"""
+Flushes and re-caches all images under ImageKit.
+
+"""
from django.db.models.loading import cache
-from django.core.management.base import BaseCommand, CommandError
-from optparse import make_option
-from imagekit.models import ImageModel
-from imagekit.specs import ImageSpec
+from django.core.management.base import BaseCommand
+
+from imagekit.utils import get_spec_files
class Command(BaseCommand):
@@ -16,22 +19,17 @@ class Command(BaseCommand):
def flush_cache(apps, options):
- """ Clears the image cache
-
- """
apps = [a.strip(',') for a in apps]
if apps:
for app_label in apps:
app = cache.get_app(app_label)
- models = [m for m in cache.get_models(app) if issubclass(m, ImageModel)]
- for model in models:
+ for model in [m for m in cache.get_models(app)]:
print 'Flushing cache for "%s.%s"' % (app_label, model.__name__)
for obj in model.objects.order_by('-pk'):
- for spec in model._ik.specs:
- prop = getattr(obj, spec.name(), None)
- if prop is not None:
- prop._delete()
- if spec.pre_cache:
- prop._create()
+ for spec_file in get_spec_files(obj):
+ if spec_file is not None:
+ spec_file.delete(save=False)
+ if spec_file.field.pre_cache:
+ spec_file._create()
else:
- print 'Please specify on or more app names'
+ print 'Please specify one or more app names'
diff --git a/imagekit/models.py b/imagekit/models.py
old mode 100644
new mode 100755
index 48a3ce6..815a972
--- a/imagekit/models.py
+++ b/imagekit/models.py
@@ -1,156 +1,365 @@
+import os
+import datetime
+from StringIO import StringIO
+
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models
-from django.db.models.base import ModelBase
-from django.db.models.signals import post_delete
-from django.utils.html import conditional_escape as escape
-from django.utils.translation import ugettext_lazy as _
+from django.db.models.fields.files import ImageFieldFile
+from django.db.models.signals import post_save, post_delete
+from django.utils.encoding import force_unicode, smart_str
-from imagekit import specs
from imagekit.lib import Image, ImageFile
-from imagekit.options import Options
-from imagekit.utils import img_to_fobj
+from imagekit.utils import img_to_fobj, get_spec_files, open_image
+from imagekit.processors import ProcessorPipeline
# Modify image file buffer size.
ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10)
-# Choice tuples for specifying the crop origin.
-# These are provided for convenience.
-CROP_HORZ_CHOICES = (
- (0, _('left')),
- (1, _('center')),
- (2, _('right')),
-)
-CROP_VERT_CHOICES = (
- (0, _('top')),
- (1, _('center')),
- (2, _('bottom')),
-)
+class _ImageSpecMixin(object):
+ def __init__(self, processors=None, quality=70, format=None):
+ self.processors = processors
+ self.quality = quality
+ self.format = format
+
+ def process(self, image, file):
+ processors = ProcessorPipeline(self.processors)
+ return processors.process(image.copy())
-class ImageModelBase(ModelBase):
- """ ImageModel metaclass
-
- This metaclass parses IKOptions and loads the specified specification
- module.
+class ImageSpec(_ImageSpecMixin):
+ """
+ The heart and soul of the ImageKit library, ImageSpec allows you to add
+ variants of uploaded images to your models.
"""
- def __init__(cls, name, bases, attrs):
- parents = [b for b in bases if isinstance(b, ImageModelBase)]
- if not parents:
+ _upload_to_attr = 'cache_to'
+
+ def __init__(self, processors=None, quality=70, format=None,
+ image_field=None, pre_cache=False, storage=None, cache_to=None):
+ """
+ :param processors: A list of processors to run on the original image.
+ :param quality: The quality of the output image. This option is only
+ used for the JPEG format.
+ :param format: The format of the output file. If not provided,
+ ImageSpec will try to guess the appropriate format based on the
+ extension of the filename and the format of the input image.
+ :param image_field: The name of the model property that contains the
+ original image.
+ :param pre_cache: A boolean that specifies whether the image should
+ be generated immediately (True) or on demand (False).
+ :param storage: A Django storage system to use to save the generated
+ image.
+ :param cache_to: Specifies the filename to use when saving the image
+ cache file. This is modeled after ImageField's ``upload_to`` and
+ can be either a string (that specifies a directory) or a
+ callable (that returns a filepath). Callable values should
+ accept the following arguments:
+
+ - instance -- The model instance this spec belongs to
+ - path -- The path of the original image
+ - specname -- the property name that the spec is bound to on
+ the model instance
+ - extension -- A recommended extension. If the format of the
+ spec is set explicitly, this suggestion will be
+ 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.
+
+ """
+
+ _ImageSpecMixin.__init__(self, processors, quality=quality,
+ format=format)
+ self.image_field = image_field
+ self.pre_cache = pre_cache
+ self.storage = storage
+ self.cache_to = cache_to
+
+ def contribute_to_class(self, cls, name):
+ setattr(cls, name, _ImageSpecDescriptor(self, name))
+ try:
+ ik = getattr(cls, '_ik')
+ except AttributeError:
+ ik = type('ImageKitMeta', (object,), {'spec_file_names': []})
+ setattr(cls, '_ik', ik)
+ ik.spec_file_names.append(name)
+
+ # Connect to the signals only once for this class.
+ uid = '%s.%s' % (cls.__module__, cls.__name__)
+ post_save.connect(_post_save_handler,
+ sender=cls,
+ dispatch_uid='%s_save' % uid)
+ post_delete.connect(_post_delete_handler,
+ sender=cls,
+ dispatch_uid='%s.delete' % uid)
+
+
+def _get_suggested_extension(name, format):
+ if format:
+ # Try to look up an extension by the format.
+ extensions = [k for k, v in Image.EXTENSION.iteritems() \
+ if v == format.upper()]
+ else:
+ extensions = []
+ original_extension = os.path.splitext(name)[1]
+ if not extensions or original_extension.lower() in extensions:
+ # If the original extension matches the format, use it.
+ extension = original_extension
+ else:
+ extension = extensions[0]
+ return extension
+
+
+class _ImageSpecFileMixin(object):
+ def _process_content(self, filename, content):
+ img = open_image(content)
+ original_format = img.format
+ img = self.field.process(img, self)
+
+ # Determine the format.
+ format = self.field.format
+ if not format:
+ if callable(getattr(self.field, self.field._upload_to_attr)):
+ # The extension is explicit, so assume they want the matching format.
+ extension = os.path.splitext(filename)[1].lower()
+ # Try to guess the format from the extension.
+ format = Image.EXTENSION.get(extension)
+ format = format or img.format or original_format or 'JPEG'
+
+ if format != 'JPEG':
+ imgfile = img_to_fobj(img, format)
+ else:
+ imgfile = img_to_fobj(img, format,
+ quality=int(self.field.quality),
+ optimize=True)
+ content = ContentFile(imgfile.read())
+ return img, content
+
+
+class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile):
+ def __init__(self, instance, field, attname, source_file):
+ ImageFieldFile.__init__(self, instance, field, None)
+ self.storage = field.storage or source_file.storage
+ self.attname = attname
+ self.source_file = source_file
+
+ 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._create(True)
+ return super(ImageFieldFile, self).file
+
+ file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file)
+
+ @property
+ def url(self):
+ self._create(True)
+ return super(ImageFieldFile, self).url
+
+ def _create(self, lazy=False):
+ """
+ Creates a new image by running the processors on the source file.
+
+ Keyword Arguments:
+ lazy -- True if an already-existing image should be returned;
+ False if a new image should be created and the existing
+ one overwritten.
+
+ """
+ if lazy and (getattr(self, '_file', None) or self.storage.exists(self.name)):
return
- user_opts = getattr(cls, 'IKOptions', None)
- opts = Options(user_opts)
- if not opts.specs:
+
+ if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist?
+ # Process the original image file.
try:
- module = __import__(opts.spec_module, {}, {}, [''])
- except ImportError:
- raise ImportError('Unable to load imagekit config module: %s' \
- % opts.spec_module)
- opts.specs.extend([spec for spec in module.__dict__.values() \
- if isinstance(spec, type) \
- and issubclass(spec, specs.ImageSpec) \
- and spec != specs.ImageSpec])
- for spec in opts.specs:
- setattr(cls, spec.name(), specs.Descriptor(spec))
- setattr(cls, '_ik', opts)
+ fp = self.source_file.storage.open(self.source_file.name)
+ except IOError:
+ return
+ fp.seek(0)
+ fp = StringIO(fp.read())
+ img, content = self._process_content(self.name, fp)
+ self.storage.save(self.name, content)
-class ImageModel(models.Model):
- """ Abstract base class implementing all core ImageKit functionality
+ def delete(self, save=False):
+ """
+ Pulled almost verbatim from ``ImageFieldFile.delete()`` and
+ ``FieldFile.delete()`` but with the attempts to reset the instance
+ property removed.
- Subclasses of ImageModel are augmented with accessors for each defined
- image specification and can override the inner IKOptions class to customize
- storage locations and other options.
+ """
+ # Clear the image dimensions cache
+ if hasattr(self, '_dimensions_cache'):
+ del self._dimensions_cache
- """
- __metaclass__ = ImageModelBase
+ # Only close the file if it's already open, which we know by the
+ # presence of self._file.
+ if hasattr(self, '_file'):
+ self.close()
+ del self.file
- class Meta:
- abstract = True
+ try:
+ self.storage.delete(self.name)
+ except (NotImplementedError, IOError):
+ pass
- class IKOptions:
+ # Delete the filesize cache.
+ if hasattr(self, '_size'):
+ del self._size
+ self._committed = False
+
+ if save:
+ self.instance.save()
+
+ @property
+ def _suggested_extension(self):
+ return _get_suggested_extension(self.source_file.name, self.field.format)
+
+ def _default_cache_to(self, instance, path, specname, extension):
+ """
+ Determines the filename to use for the transformed image. Can be
+ overridden on a per-spec basis by setting the cache_to property on
+ the spec.
+
+ """
+ filepath, basename = os.path.split(path)
+ filename = os.path.splitext(basename)[0]
+ new_name = '{0}_{1}{2}'.format(filename, specname, extension)
+ return os.path.join(os.path.join('cache', filepath), new_name)
+
+ @property
+ def name(self):
+ """
+ Specifies the filename that the cached image will use. The user can
+ control this by providing a `cache_to` method to the ImageSpec.
+
+ """
+ filename = self.source_file.name
+ if filename:
+ cache_to = self.field.cache_to or self._default_cache_to
+
+ if not cache_to:
+ raise Exception('No cache_to or default_cache_to value specified')
+ if callable(cache_to):
+ new_filename = force_unicode(datetime.datetime.now().strftime( \
+ smart_str(cache_to(self.instance, self.source_file.name,
+ self.attname, self._suggested_extension))))
+ else:
+ dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to))))
+ filename = os.path.normpath(os.path.basename(filename))
+ new_filename = os.path.join(dir_name, filename)
+
+ return new_filename
+
+ @name.setter
+ def name(self, value):
+ # TODO: Figure out a better way to handle this. We really don't want
+ # to allow anybody to set the name, but ``File.__init__`` (which is
+ # called by ``ImageSpecFile.__init__``) does, so we have to allow it
+ # at least that one time.
pass
- def admin_thumbnail_view(self):
- if not self._imgfield:
- return None
- prop = getattr(self, self._ik.admin_thumbnail_spec, None)
- if prop is None:
- return 'An "%s" image spec has not been defined.' % \
- self._ik.admin_thumbnail_spec
+
+class _ImageSpecDescriptor(object):
+ def __init__(self, field, attname):
+ self.attname = attname
+ self.field = field
+
+ def _get_image_field_file(self, instance):
+ field_name = getattr(self.field, 'image_field', None)
+ if field_name:
+ field = getattr(instance, field_name)
else:
- if hasattr(self, 'get_absolute_url'):
- return u'' % \
- (escape(self.get_absolute_url()), escape(prop.url))
+ image_fields = [getattr(instance, f.attname) for f in \
+ instance.__class__._meta.fields if \
+ isinstance(f, models.ImageField)]
+ if len(image_fields) == 0:
+ raise Exception('{0} does not define any ImageFields, so your '
+ '{1} ImageSpec has no image to act on.'.format(
+ instance.__class__.__name__, self.attname))
+ elif len(image_fields) > 1:
+ raise Exception('{0} defines multiple ImageFields, but you have '
+ 'not specified an image_field for your {1} '
+ 'ImageSpec.'.format(instance.__class__.__name__,
+ self.attname))
else:
- return u'' % \
- (escape(self._imgfield.url), escape(prop.url))
- admin_thumbnail_view.short_description = _('Thumbnail')
- admin_thumbnail_view.allow_tags = True
+ field = image_fields[0]
+ return field
- @property
- def _imgfield(self):
- return getattr(self, self._ik.image_field)
-
- @property
- def _storage(self):
- return getattr(self._ik, 'storage', self._imgfield.storage)
-
- def _clear_cache(self):
- for spec in self._ik.specs:
- prop = getattr(self, spec.name())
- prop._delete()
-
- def _pre_cache(self):
- for spec in self._ik.specs:
- if spec.pre_cache:
- prop = getattr(self, spec.name())
- prop._create()
-
- def save_image(self, name, image, save=True, replace=True):
- if self._imgfield and replace:
- self._imgfield.delete(save=False)
- if hasattr(image, 'read'):
- data = image.read()
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self.field
else:
- data = image
- content = ContentFile(data)
- self._imgfield.save(name, content, save)
-
- def save(self, clear_cache=True, *args, **kwargs):
- super(ImageModel, self).save(*args, **kwargs)
-
- is_new_object = self._get_pk_val() is None
- if is_new_object:
- clear_cache = False
-
- if self._imgfield:
- spec = self._ik.preprocessor_spec
- if spec is not None:
- newfile = self._imgfield.storage.open(str(self._imgfield))
- img = Image.open(newfile)
- img, format = spec.process(img, self)
- if format != 'JPEG':
- imgfile = img_to_fobj(img, format)
- else:
- imgfile = img_to_fobj(img, format,
- quality=int(spec.quality),
- optimize=True)
- content = ContentFile(imgfile.read())
- newfile.close()
- name = str(self._imgfield)
- self._imgfield.storage.delete(name)
- self._imgfield.storage.save(name, content)
- if self._imgfield:
- if clear_cache:
- self._clear_cache()
- self._pre_cache()
-
- def clear_cache(self, **kwargs):
- assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
- self._clear_cache()
+ img_spec_file = ImageSpecFile(instance, self.field,
+ self.attname, self._get_image_field_file(instance))
+ setattr(instance, self.attname, img_spec_file)
+ return img_spec_file
-post_delete.connect(ImageModel.clear_cache, sender=ImageModel)
+def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs):
+ if raw:
+ return
+ spec_files = get_spec_files(instance)
+ for spec_file in spec_files:
+ if not created:
+ spec_file.delete(save=False)
+ if spec_file.field.pre_cache:
+ spec_file._create()
+
+
+def _post_delete_handler(sender, instance=None, **kwargs):
+ assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname)
+ spec_files = get_spec_files(instance)
+ for spec_file in spec_files:
+ spec_file.delete(save=False)
+
+
+class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFileMixin):
+ def save(self, name, content, save=True):
+ new_filename = self.field.generate_filename(self.instance, name)
+ img, content = self._process_content(new_filename, content)
+ return super(ProcessedImageFieldFile, self).save(name, content, save)
+
+
+class ProcessedImageField(models.ImageField, _ImageSpecMixin):
+ """
+ ProcessedImageField is an ImageField that runs processors on the uploaded
+ image *before* saving it to storage. This is in contrast to specs, which
+ maintain the original. Useful for coercing fileformats or keeping images
+ within a reasonable size.
+
+ """
+ _upload_to_attr = 'upload_to'
+ attr_class = ProcessedImageFieldFile
+
+ def __init__(self, processors=None, quality=70, format=None,
+ verbose_name=None, name=None, width_field=None, height_field=None,
+ **kwargs):
+ """
+ The ProcessedImageField constructor accepts all of the arguments that
+ the :class:`django.db.models.ImageField` constructor accepts, as well
+ as the ``processors``, ``format``, and ``quality`` arguments of
+ :class:`imagekit.models.ImageSpec`.
+
+ """
+ _ImageSpecMixin.__init__(self, processors, quality=quality,
+ format=format)
+ models.ImageField.__init__(self, verbose_name, name, width_field,
+ height_field, **kwargs)
+
+ def get_filename(self, filename):
+ filename = os.path.normpath(self.storage.get_valid_name(os.path.basename(filename)))
+ name, ext = os.path.splitext(filename)
+ ext = _get_suggested_extension(filename, self.format)
+ return '{0}{1}'.format(name, ext)
+
+
+try:
+ from south.modelsinspector import add_introspection_rules
+except ImportError:
+ pass
+else:
+ add_introspection_rules([], [r'^imagekit\.models\.ProcessedImageField$'])
diff --git a/imagekit/options.py b/imagekit/options.py
deleted file mode 100644
index f3d3820..0000000
--- a/imagekit/options.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Imagekit options
-from imagekit import processors
-from imagekit.specs import ImageSpec
-
-
-class Options(object):
- """ Class handling per-model imagekit options
-
- """
- image_field = 'image'
- crop_horz_field = 'crop_horz'
- crop_vert_field = 'crop_vert'
- preprocessor_spec = None
- cache_dir = 'cache'
- save_count_as = None
- cache_filename_fields = ['pk', ]
- cache_filename_format = "%(filename)s_%(specname)s_%(original_extension)s.%(extension)s"
- admin_thumbnail_spec = 'admin_thumbnail'
- spec_module = 'imagekit.defaults'
- specs = None
- #storage = defaults to image_field.storage
-
- def __init__(self, opts):
- for key, value in opts.__dict__.iteritems():
- setattr(self, key, value)
- self.specs = list(self.specs or [])
diff --git a/imagekit/processors.py b/imagekit/processors.py
deleted file mode 100644
index 226fcfc..0000000
--- a/imagekit/processors.py
+++ /dev/null
@@ -1,178 +0,0 @@
-""" Imagekit Image "ImageProcessors"
-
-A processor defines a set of class variables (optional) and a class method
-named "process" which processes the supplied image using the class properties
-as settings. The process method can be overridden as well allowing user to
-define their own effects/processes entirely.
-
-"""
-from imagekit.lib import Image, ImageEnhance, ImageColor
-
-
-class ImageProcessor(object):
- """ Base image processor class """
-
- @classmethod
- def process(cls, img, fmt, obj):
- return img, fmt
-
-
-class Adjustment(ImageProcessor):
- color = 1.0
- brightness = 1.0
- contrast = 1.0
- sharpness = 1.0
-
- @classmethod
- def process(cls, img, fmt, obj):
- img = img.convert('RGB')
- for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']:
- factor = getattr(cls, name.lower())
- if factor != 1.0:
- try:
- img = getattr(ImageEnhance, name)(img).enhance(factor)
- except ValueError:
- pass
- return img, fmt
-
-
-class Format(ImageProcessor):
- format = 'JPEG'
- extension = 'jpg'
-
- @classmethod
- def process(cls, img, fmt, obj):
- return img, cls.format
-
-
-class Reflection(ImageProcessor):
- background_color = '#FFFFFF'
- size = 0.0
- opacity = 0.6
-
- @classmethod
- def process(cls, img, fmt, obj):
- # convert bgcolor string to rgb value
- background_color = ImageColor.getrgb(cls.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 * cls.opacity)) # The start of our gradient
- steps = int(255 * cls.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] * cls.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]))
- # Save the file as a JPEG
- fmt = 'JPEG'
- # return the image complete with reflection effect
- return composite, fmt
-
-
-class Resize(ImageProcessor):
- width = None
- height = None
- crop = False
- upscale = False
-
- @classmethod
- def process(cls, img, fmt, obj):
- cur_width, cur_height = img.size
- if cls.crop:
- crop_horz = getattr(obj, obj._ik.crop_horz_field, 1)
- crop_vert = getattr(obj, obj._ik.crop_vert_field, 1)
- ratio = max(float(cls.width) / cur_width, float(cls.height) / cur_height)
- resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio))
- crop_x, crop_y = (abs(cls.width - resize_x), abs(cls.height - resize_y))
- x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2))
- box_left, box_right = {
- 0: (0, cls.width),
- 1: (int(x_diff), int(x_diff + cls.width)),
- 2: (int(crop_x), int(resize_x)),
- }[crop_horz]
- box_upper, box_lower = {
- 0: (0, cls.height),
- 1: (int(y_diff), int(y_diff + cls.height)),
- 2: (int(crop_y), int(resize_y)),
- }[crop_vert]
- box = (box_left, box_upper, box_right, box_lower)
- img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box)
- else:
- if not cls.width is None and not cls.height is None:
- ratio = min(float(cls.width) / cur_width,
- float(cls.height) / cur_height)
- else:
- if cls.width is None:
- ratio = float(cls.height) / cur_height
- else:
- ratio = float(cls.width) / cur_width
- new_dimensions = (int(round(cur_width * ratio)),
- int(round(cur_height * ratio)))
- if new_dimensions[0] > cur_width or \
- new_dimensions[1] > cur_height:
- if not cls.upscale:
- return img, fmt
- img = img.resize(new_dimensions, Image.ANTIALIAS)
- return img, fmt
-
-
-class Transpose(ImageProcessor):
- """ Rotates or flips the image
-
- Method should be one of the following strings:
- - FLIP_LEFT RIGHT
- - FLIP_TOP_BOTTOM
- - ROTATE_90
- - ROTATE_270
- - ROTATE_180
- - auto
-
- If method is set to 'auto' the processor will attempt to rotate the image
- according to the EXIF Orientation data.
-
- """
- EXIF_ORIENTATION_STEPS = {
- 1: [],
- 2: ['FLIP_LEFT_RIGHT'],
- 3: ['ROTATE_180'],
- 4: ['FLIP_TOP_BOTTOM'],
- 5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'],
- 6: ['ROTATE_270'],
- 7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'],
- 8: ['ROTATE_90'],
- }
-
- method = 'auto'
-
- @classmethod
- def process(cls, img, fmt, obj):
- if cls.method == 'auto':
- try:
- orientation = Image.open(obj._imgfield.file)._getexif()[0x0112]
- ops = cls.EXIF_ORIENTATION_STEPS[orientation]
- except:
- ops = []
- else:
- ops = [cls.method]
- for method in ops:
- img = img.transpose(getattr(Image, method))
- return img, fmt
diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py
new file mode 100644
index 0000000..72e7f8d
--- /dev/null
+++ b/imagekit/processors/__init__.py
@@ -0,0 +1,172 @@
+"""
+Imagekit image processors.
+
+A processor accepts an image, does some stuff, and returns the result.
+Processors can do anything with the image you want, but their responsibilities
+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
+
+
+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):
+ img = img.convert('RGB')
+ 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
+ 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 AttributeError:
+ ops = []
+ else:
+ ops = self.methods
+ for method in ops:
+ img = img.transpose(method)
+ return img
diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py
new file mode 100644
index 0000000..e2788d5
--- /dev/null
+++ b/imagekit/processors/resize.py
@@ -0,0 +1,123 @@
+from imagekit.lib import Image
+
+
+class _Resize(object):
+ width = None
+ height = None
+
+ def __init__(self, width=None, height=None):
+ if width is not None:
+ self.width = width
+ if height is not None:
+ self.height = height
+
+ def process(self, img):
+ raise NotImplementedError('process must be overridden by subclasses.')
+
+
+class Crop(_Resize):
+ """
+ Resizes an image , cropping it to the specified width and height.
+
+ """
+ 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
+
+ """
+ super(Crop, self).__init__(width, height)
+ self.anchor = anchor
+
+ def process(self, img):
+ cur_width, cur_height = img.size
+ horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \
+ Crop.CENTER]
+ ratio = max(float(self.width) / cur_width, float(self.height) / cur_height)
+ resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio))
+ crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y))
+ x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2))
+ box_left, box_right = {
+ 0: (0, self.width),
+ 0.5: (int(x_diff), int(x_diff + self.width)),
+ 1: (int(crop_x), int(resize_x)),
+ }[horizontal_anchor]
+ box_upper, box_lower = {
+ 0: (0, self.height),
+ 0.5: (int(y_diff), int(y_diff + self.height)),
+ 1: (int(crop_y), int(resize_y)),
+ }[vertical_anchor]
+ box = (box_left, box_upper, box_right, box_lower)
+ img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box)
+ return img
+
+
+class Fit(_Resize):
+ """
+ Resizes an image to fit within the specified dimensions.
+
+ """
+ def __init__(self, width=None, height=None, upscale=None):
+ """
+ :param width: The maximum width of the desired image.
+ :param height: The maximum height of the desired image.
+ :param upscale: A boolean value specifying whether the image should
+ be enlarged if its dimensions are smaller than the target
+ dimensions.
+
+ """
+ super(Fit, self).__init__(width, height)
+ self.upscale = upscale
+
+ def process(self, img):
+ cur_width, cur_height = img.size
+ if not self.width is None and not self.height is None:
+ ratio = min(float(self.width) / cur_width,
+ float(self.height) / cur_height)
+ else:
+ if self.width is None:
+ ratio = float(self.height) / cur_height
+ else:
+ ratio = float(self.width) / cur_width
+ new_dimensions = (int(round(cur_width * ratio)),
+ int(round(cur_height * ratio)))
+ if new_dimensions[0] > cur_width or \
+ new_dimensions[1] > cur_height:
+ if not self.upscale:
+ return img
+ img = img.resize(new_dimensions, Image.ANTIALIAS)
+ return img
diff --git a/imagekit/specs.py b/imagekit/specs.py
deleted file mode 100644
index 0a11ffe..0000000
--- a/imagekit/specs.py
+++ /dev/null
@@ -1,149 +0,0 @@
-""" ImageKit image specifications
-
-All imagekit specifications must inherit from the ImageSpec class. Models
-inheriting from ImageModel will be modified with a descriptor/accessor for each
-spec found.
-
-"""
-import os
-import cStringIO as StringIO
-
-from django.core.files.base import ContentFile
-
-from imagekit import processors
-from imagekit.lib import Image
-from imagekit.utils import img_to_fobj
-
-
-class ImageSpec(object):
- pre_cache = False
- quality = 70
- increment_count = False
- processors = []
-
- @classmethod
- def name(cls):
- return getattr(cls, 'access_as', cls.__name__.lower())
-
- @classmethod
- def process(cls, image, obj):
- fmt = image.format
- img = image.copy()
- for proc in cls.processors:
- img, fmt = proc.process(img, fmt, obj)
- img.format = fmt
- return img, fmt
-
-
-class Accessor(object):
- def __init__(self, obj, spec):
- self._img = None
- self._fmt = None
- self._obj = obj
- self.spec = spec
-
- def _get_imgfile(self):
- format = self._img.format or 'JPEG'
- if format != 'JPEG':
- imgfile = img_to_fobj(self._img, format)
- else:
- imgfile = img_to_fobj(self._img, format,
- quality=int(self.spec.quality),
- optimize=True)
- return imgfile
-
- def _create(self):
- if self._obj._imgfield:
- if self._exists():
- return
- # process the original image file
- try:
- fp = self._obj._imgfield.storage.open(self._obj._imgfield.name)
- except IOError:
- return
- fp.seek(0)
- fp = StringIO.StringIO(fp.read())
- self._img, self._fmt = self.spec.process(Image.open(fp), self._obj)
- # save the new image to the cache
- content = ContentFile(self._get_imgfile().read())
- self._obj._storage.save(self.name, content)
-
- def _delete(self):
- if self._obj._imgfield:
- try:
- self._obj._storage.delete(self.name)
- except (NotImplementedError, IOError):
- return
-
- def _exists(self):
- if self._obj._imgfield:
- return self._obj._storage.exists(self.name)
-
- @property
- def name(self):
- if self._obj._imgfield.name:
- filepath, basename = os.path.split(self._obj._imgfield.name)
- filename, extension = os.path.splitext(basename)
- original_extension = extension
- for processor in self.spec.processors:
- if issubclass(processor, processors.Format):
- extension = processor.extension
- filename_format_dict = {'filename': filename,
- 'specname': self.spec.name(),
- 'original_extension': original_extension,
- 'extension': extension.lstrip('.')}
- cache_filename_fields = self._obj._ik.cache_filename_fields
- filename_format_dict.update(dict(zip(
- cache_filename_fields,
- [getattr(self._obj, field) for
- field in cache_filename_fields])))
- cache_filename = self._obj._ik.cache_filename_format % \
- filename_format_dict
-
- if callable(self._obj._ik.cache_dir):
- return self._obj._ik.cache_dir(self._obj, filepath,
- cache_filename)
- else:
- return os.path.join(self._obj._ik.cache_dir, filepath,
- cache_filename)
-
- @property
- def url(self):
- if not self.spec.pre_cache:
- self._create()
- if self.spec.increment_count:
- fieldname = self._obj._ik.save_count_as
- if fieldname is not None:
- current_count = getattr(self._obj, fieldname)
- setattr(self._obj, fieldname, current_count + 1)
- self._obj.save(clear_cache=False)
- return self._obj._storage.url(self.name)
-
- @property
- def file(self):
- self._create()
- return self._obj._storage.open(self.name)
-
- @property
- def image(self):
- if not self._img:
- self._create()
- if not self._img:
- self._img = Image.open(self.file)
- return self._img
-
- @property
- def width(self):
- return self.image.size[0]
-
- @property
- def height(self):
- return self.image.size[1]
-
-
-class Descriptor(object):
- def __init__(self, spec):
- self._spec = spec
-
- def __get__(self, obj, type=None):
- return Accessor(obj, self._spec)
diff --git a/imagekit/templates/imagekit/admin/thumbnail.html b/imagekit/templates/imagekit/admin/thumbnail.html
new file mode 100644
index 0000000..6531391
--- /dev/null
+++ b/imagekit/templates/imagekit/admin/thumbnail.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/imagekit/utils.py b/imagekit/utils.py
index 90cafa2..637433e 100644
--- a/imagekit/utils.py
+++ b/imagekit/utils.py
@@ -1,6 +1,9 @@
-""" ImageKit utility functions """
-
import tempfile
+import types
+
+from django.utils.functional import wraps
+
+from imagekit.lib import Image
def img_to_fobj(img, format, **kwargs):
@@ -15,3 +18,34 @@ def img_to_fobj(img, format, **kwargs):
img.save(tmp, format, **kwargs)
tmp.seek(0)
return tmp
+
+
+def get_spec_files(instance):
+ try:
+ ik = getattr(instance, '_ik')
+ except AttributeError:
+ return []
+ else:
+ return [getattr(instance, n) for n in ik.spec_file_names]
+
+
+def open_image(target):
+ img = Image.open(target)
+ img.copy = types.MethodType(_wrap_copy(img.copy), img, img.__class__)
+ return img
+
+
+def _wrap_copy(f):
+ @wraps(f)
+ def copy(self):
+ img = f()
+ try:
+ img.app = self.app
+ except AttributeError:
+ pass
+ try:
+ img._getexif = self._getexif
+ except AttributeError:
+ pass
+ return img
+ return copy
diff --git a/setup.py b/setup.py
index 2e3939f..579d7ee 100644
--- a/setup.py
+++ b/setup.py
@@ -1,34 +1,30 @@
#/usr/bin/env python
+import codecs
import os
import sys
-import imagekit
-try:
- from setuptools import setup
-except ImportError:
- from distutils.core import setup
+from setuptools import setup, find_packages
if 'publish' in sys.argv:
os.system('python setup.py sdist upload')
sys.exit()
+read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
+
setup(
name='django-imagekit',
- version=imagekit.__version__,
+ version=':versiontools:imagekit:',
description='Automated image processing for Django models.',
+ long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')),
author='Justin Driscoll',
author_email='justin@driscolldev.com',
maintainer='Bryan Veloso',
maintainer_email='bryan@revyver.com',
license='BSD',
url='http://github.com/jdriscoll/django-imagekit/',
- packages=[
- 'imagekit',
- 'imagekit.management',
- 'imagekit.management.commands'
- ],
+ packages=find_packages(),
classifiers=[
- 'Development Status :: 3 - Alpha',
+ 'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
@@ -38,5 +34,8 @@ setup(
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Utilities'
- ]
+ ],
+ setup_requires=[
+ 'versiontools >= 1.8',
+ ],
)
diff --git a/tests/core/models.py b/tests/core/models.py
index 2f153bd..e69de29 100644
--- a/tests/core/models.py
+++ b/tests/core/models.py
@@ -1,14 +0,0 @@
-from django.db import models
-
-from imagekit.models import ImageModel
-
-
-class TestPhoto(ImageModel):
- """
- Minimal ImageModel class for testing.
-
- """
- image = models.ImageField(upload_to='images')
-
- class IKOptions:
- spec_module = 'core.specs'
diff --git a/tests/core/specs.py b/tests/core/specs.py
deleted file mode 100644
index 5b38cc8..0000000
--- a/tests/core/specs.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from imagekit import processors
-from imagekit.specs import ImageSpec
-
-
-class ResizeToWidth(processors.Resize):
- width = 100
-
-class ResizeToHeight(processors.Resize):
- height = 100
-
-class ResizeToFit(processors.Resize):
- width = 100
- height = 100
-
-class ResizeCropped(ResizeToFit):
- crop = ('center', 'center')
-
-class TestResizeToWidth(ImageSpec):
- access_as = 'to_width'
- processors = [ResizeToWidth]
-
-class TestResizeToHeight(ImageSpec):
- access_as = 'to_height'
- processors = [ResizeToHeight]
-
-class TestResizeCropped(ImageSpec):
- access_as = 'cropped'
- processors = [ResizeCropped]
diff --git a/tests/core/tests.py b/tests/core/tests.py
index e96af80..dd83fce 100644
--- a/tests/core/tests.py
+++ b/tests/core/tests.py
@@ -1,14 +1,20 @@
import os
import tempfile
-import unittest
-from django.conf import settings
from django.core.files.base import ContentFile
+from django.db import models
from django.test import TestCase
from imagekit.lib import Image
+from imagekit.models import ImageSpec
+from imagekit.processors import Adjust
+from imagekit.processors.resize import Crop
-from core.models import TestPhoto
+
+class Photo(models.Model):
+ original_image = models.ImageField(upload_to='photos')
+ thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)],
+ image_field='original_image', format='JPEG', quality=90)
class IKTest(TestCase):
@@ -20,59 +26,30 @@ class IKTest(TestCase):
return tmp
def setUp(self):
- self.p = TestPhoto()
+ self.photo = Photo()
img = self.generate_image()
- self.p.save_image('test.jpeg', ContentFile(img.read()))
- self.p.save()
+ file = ContentFile(img.read())
+ self.photo.original_image = file
+ self.photo.original_image.save('test.jpeg', file)
+ self.photo.save()
img.close()
def test_save_image(self):
- img = self.generate_image()
- path = self.p.image.path
- self.p.save_image('test2.jpeg', ContentFile(img.read()))
- self.failIf(os.path.isfile(path))
- path = self.p.image.path
- img.seek(0)
- self.p.save_image('test.jpeg', ContentFile(img.read()))
- self.failIf(os.path.isfile(path))
- img.close()
+ photo = Photo.objects.get(id=self.photo.id)
+ self.assertTrue(os.path.isfile(photo.original_image.path))
def test_setup(self):
- self.assertEqual(self.p.image.width, 800)
- self.assertEqual(self.p.image.height, 600)
+ self.assertEqual(self.photo.original_image.width, 800)
+ self.assertEqual(self.photo.original_image.height, 600)
- def test_to_width(self):
- self.assertEqual(self.p.to_width.width, 100)
- self.assertEqual(self.p.to_width.height, 75)
+ def test_thumbnail_creation(self):
+ photo = Photo.objects.get(id=self.photo.id)
+ self.assertTrue(os.path.isfile(photo.thumbnail.file.name))
- def test_to_height(self):
- self.assertEqual(self.p.to_height.width, 133)
- self.assertEqual(self.p.to_height.height, 100)
+ def test_thumbnail_size(self):
+ self.assertEqual(self.photo.thumbnail.width, 50)
+ self.assertEqual(self.photo.thumbnail.height, 50)
- def test_crop(self):
- self.assertEqual(self.p.cropped.width, 100)
- self.assertEqual(self.p.cropped.height, 100)
-
- def test_url(self):
- tup = (settings.MEDIA_URL, self.p._ik.cache_dir,
- 'images/test_to_width.jpeg')
- self.assertEqual(self.p.to_width.url, "%s%s/%s" % tup)
-
- def tearDown(self):
- # ImageKit doesn't delete processed files unless you clear the cache.
- # We also attempt to remove the cache directory as to not clutter up
- # your filesystem.
- self.p._clear_cache()
- try:
- os.removedirs(os.path.join(settings.MEDIA_ROOT, self.p._ik.cache_dir, 'images'))
- except OSError:
- pass
-
- # As of Django 1.3, FileFields no longer delete the underlying image
- # when you delete the model. For the sanity of these tests, we have
- # to do this ourselves.
- path = self.p.image.path
- os.remove(os.path.join(settings.MEDIA_ROOT, path))
- os.removedirs(os.path.join(settings.MEDIA_ROOT, 'images'))
- self.p.delete()
- self.failIf(os.path.isfile(path))
+ def test_thumbnail_source_file(self):
+ self.assertEqual(
+ self.photo.thumbnail.source_file, self.photo.original_image)
diff --git a/tests/settings.py b/tests/settings.py
index 082adb9..f034e9c 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -8,10 +8,19 @@ BASE_PATH = os.path.abspath(os.path.dirname(__file__))
MEDIA_ROOT = os.path.normpath(os.path.join(BASE_PATH, 'media'))
+# Django <= 1.2
DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = 'imagekit.db'
TEST_DATABASE_NAME = 'imagekit-test.db'
+# Django >= 1.3
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'imagekit.db',
+ },
+}
+
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',