mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-16 21:30:23 +00:00
Merge branch 'release/1.0.0' into develop
* 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:
commit
11c1259ba3
26 changed files with 1154 additions and 832 deletions
49
AUTHORS
49
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
|
||||
|
|
|
|||
225
README.rst
225
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.
|
||||
|
||||
::
|
||||
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -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
26
docs/apireference.rst
Normal 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:
|
||||
44
docs/conf.py
44
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 <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'
|
||||
|
|
|
|||
|
|
@ -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
190
docs/make.bat
Normal 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
|
||||
|
|
@ -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
37
imagekit/admin.py
Normal 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,
|
||||
})
|
||||
|
|
@ -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]
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -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
461
imagekit/models.py
Normal file → Executable 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$'])
|
||||
|
|
|
|||
|
|
@ -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 [])
|
||||
|
|
@ -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
|
||||
172
imagekit/processors/__init__.py
Normal file
172
imagekit/processors/__init__.py
Normal 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
|
||||
123
imagekit/processors/resize.py
Normal file
123
imagekit/processors/resize.py
Normal 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
|
||||
|
|
@ -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)
|
||||
3
imagekit/templates/imagekit/admin/thumbnail.html
Normal file
3
imagekit/templates/imagekit/admin/thumbnail.html
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<a href="{{ model.get_absolute_url|default:original_image.url }}">
|
||||
<img src="{{ thumbnail.url }}">
|
||||
</a>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
25
setup.py
25
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',
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue