Merge branch 'release/1.0.0'

* release/1.0.0: (68 commits)
  Tests now run again.
  PEP8-ing and whitespacing.
  Requiring versiontools and patching up our setup.py.
  adding introspection rule for users with south
  PEP8 and import tweaks.
  fixing typo
  ImageSpecFile is a proper File
  Typo fix
  A list of ImageSpec names are now stored on the model.
  AdminThumbnailView is now AdminThumbnail
  Docs typo fix
  Adds explicit import of resize module to processors
  fixing bad import in docs
  adding name to AUTHORS
  adding test for new api
  Moved Crop and Fit to resize module.
  More docs edits.
  Typo fix.
  Installation instructions.
  Embracing duck typing.
  ...

Conflicts:
	README.rst
This commit is contained in:
Bryan Veloso 2011-10-31 23:30:09 +09:00
commit 7e55f5e087
26 changed files with 1154 additions and 832 deletions

49
AUTHORS
View file

@ -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

View file

@ -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.
::
<div class="original">
<img src="{{ photo.original_image.url }}" alt="{{ photo.name }}">
</div>
<div class="display">
<img src="{{ photo.display.url }}" alt="{{ photo.name }}">
</div>
<div class="thumbs">
{% for p in photos %}
<img src="{{ p.thumbnail_image.url }}" alt="{{ p.name }}">
{% endfor %}
</div>
Step 6
******
Play with the API.
::
>>> from myapp.models import Photo
>>> p = Photo.objects.all()[0]
<Photo: MyPhoto>
>>> p.display.url
u'/static/photos/myphoto_display.jpg'
>>> p.display.width
600
>>> p.display.height
420
>>> p.display.image
<JpegImagePlugin.JpegImageFile instance at 0xf18990>
>>> p.display.file
<File: /path/to/media/photos/myphoto_display.jpg>
>>> p.display.spec
<class 'myapp.specs.Display'>
Step 7
******
Enjoy a nice beverage.
::
from refrigerator import beer
beer.enjoy()
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

View file

@ -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:

26
docs/apireference.rst Normal file
View file

@ -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:

View file

@ -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 <link> 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'

View file

@ -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 <django:meta-options>` 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`

190
docs/make.bat Normal file
View file

@ -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 ^<target^>` where ^<target^> 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

View file

@ -1,13 +1,5 @@
"""
Django ImageKit
:author: Justin Driscoll <justin.driscoll@gmail.com>
:maintainer: Bryan Veloso <bryan@revyver.com>
: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'

37
imagekit/admin.py Normal file
View file

@ -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,
})

View file

@ -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]

View file

@ -1 +0,0 @@

View file

@ -1 +0,0 @@

View file

@ -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'

461
imagekit/models.py Normal file → Executable file
View file

@ -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'<a href="%s"><img src="%s"></a>' % \
(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'<a href="%s"><img src="%s"></a>' % \
(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$'])

View file

@ -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 [])

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,3 @@
<a href="{{ model.get_absolute_url|default:original_image.url }}">
<img src="{{ thumbnail.url }}">
</a>

View file

@ -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

View file

@ -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',
],
)

View file

@ -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'

View file

@ -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]

View file

@ -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)

View file

@ -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',