Commits

Peter Baumgartner committed 989cd00

Initial checkin of sorl-thumbnail legacy project

Comments (0)

Files changed (28)

+Copyright (c) 2007, Mikko Hellsing, Chris Beaven
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name sorl nor the names of its contributors may be used to
+      endorse or promote products derived from this software without specific
+      prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include LICENSE
+include README
+include docs/*
+sorl-thumbnail is a simple to use thumbnailing application for Django.
+
+All documentation is in the "docs" directory and online at
+http://thumbnail.sorl.net/docs/
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+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 "  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 "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of 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)"
+
+clean:
+	-rm -rf _build/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
+	@echo
+	@echo "Build finished. The HTML pages are in _build/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in _build/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in _build/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in _build/qthelp, like this:"
+	@echo "# qcollectiongenerator _build/qthelp/sorl-thumbnail.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile _build/qthelp/sorl-thumbnail.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in _build/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
+	@echo
+	@echo "The overview file is in _build/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in _build/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in _build/doctest/output.txt."
+# -*- coding: utf-8 -*-
+#
+# sorl-thumbnail documentation build configuration file, created by
+# sphinx-quickstart on Sun Aug  9 17:04:20 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+#import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'sorl-thumbnail'
+copyright = u'2009, Mikko Hellsing, Chris Beaven'
+
+# 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 = '3.2'
+# The full version, including alpha/beta/rc tags.
+release = '3.2'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# 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
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'sorl-thumbnaildoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'sorl-thumbnail.tex', u'sorl-thumbnail Documentation',
+   u'Mikko Hellsing, Chris Beaven', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+==============
+sorl-thumbnail
+==============
+
+The sorl-thumbnail package provides an easy way to generate image
+thumbnails.
+
+Requirements
+============
+
+* Python 2.4+ and the Python Imaging Library (PIL_).
+* It does not require Django_, but most features are Django specific.
+  sorl-thumbnail should be compatible with all versions of Django
+  (let us know if not).
+* To enable PDF thumbnails you need ImageMagick_
+* For Word document thumbnail handling you need ImageMagick_ and wvWare_.
+
+.. _PIL: http://www.pythonware.com/products/pil/
+.. _ImageMagick: http://www.imagemagick.org/
+.. _wvWare: http://wvware.sourceforge.net/
+.. _Django: http://www.djangoproject.com/
+
+Installation
+============
+
+#. Download the source.
+#. Put the ``sorl`` directory in your python path (I keep it in site-packages
+   directory).
+#. Include the thumbnail app in your ``settings.py``.
+
+Like this::
+    
+    INSTALLED_APPS = (
+        ...
+        'sorl.thumbnail',
+    )
+
+
+.. _template-tag:
+
+The {% thumbnail %} template tag
+================================
+
+The thumbnail tag creates a thumbnail if it doesn't exist or if the source
+was modified more recently than the existing thumbnail. The resulting
+thumbnail object contains the generated thumbnail image along with some other
+potentially useful data. To use the sorl-thumbnail template tags you need to
+load them in your template::
+    
+    {% load thumbnail %}
+
+Basic tag Syntax::
+
+    {% thumbnail [source] [size] [options] %}
+
+source must be an object where ``force_unicode(object)`` returns a path to a
+file relative to ``MEDIA_ROOT``. This means an Image/FileField or
+string/unicode object containing the relative path to a file.
+
+
+*size* can either be:
+
+* the size in the format ``[width]x[height]`` (for example,
+  ``{% thumbnail source 100x50 %}``) or
+
+* a variable containing a valid size (i.e. either a string in the
+  ``[width]x[height]`` format or a tuple containing two integers):
+  ``{% thumbnail source size_string %}``.
+
+Options
+-------
+
+Options are optional and should be a space separated.
+
+*Note to sorl-thumbnail
+vetarans: The older format of comma separated options is still supported
+(with the limitation that *quality* is the only option to which you can pass
+an argument to).*
+
+Unless you change the :ref:`thumbnail-processors`, valid options are:
+
+crop
+    Crop the source image height or width to exactly match the requested
+    thumbnail size (the default is to proportionally resize the source image
+    to fit within the requested thumbnail size).
+    
+    By default, the image is centered before being cropped. To crop from the
+    edges, pass a comma separated string containing the ``x`` and ``y``
+    percentage offsets (negative values go from the right/bottom). Some
+    examples follow:
+    
+    * ``crop="0,0"`` will crop from the left and top edges.
+
+    * ``crop="-10,-0"`` will crop from the right edge (with a 10% offset) and
+      the bottom edge.
+    
+    * ``crop=",0"`` will keep the default behavior for the x axis (horizontally
+      centering the image) and crop from the top edge.
+    
+    The image can also be "smart cropped" by using ``crop="smart"``. The image
+    is incrementally cropped down to the requested size by removing slices
+    from edges with the least entropy. 
+
+max
+    Will resize the image to the same size as the *crop* option but it
+    does not crop.
+
+autocrop
+    Remove any unnecessary whitespace from the edges of the source image.
+    This occurs before the crop or propotional resize.
+
+bw
+    Make the thumbnail grayscale (not really just black & white).
+
+upscale
+    Allow upscaling of the source image during scaling.
+
+sharpen
+    Sharpen the thumbnail image (using the PIL sharpen filter)
+
+detail
+    Add detail to the image, like a mild *sharpen* (using the PIL detail
+    filter)
+
+quality=[1-100]
+    Alter the quality of the JPEG thumbnail (the default is 85).
+
+An example of basic usage::
+
+    <img src="{% thumbnail person.photo 80x80 crop upscale %}" />
+
+
+DjangoThumbnail class
+---------------------
+The thumbnail tag can also place a ``DjangoThumbnail`` object in the context,
+providing access to the properties of the thumbnail such as the height and
+width::
+
+    {% thumbnail [source] [size] [options] as [variable] %}
+
+When *"as [variable]"* is used, the tag does not return the absolute url of the
+thumbnail. The variable (containing the ``DjangoThumbnail`` object) has the
+following useful methods and properties:
+
+absolute_url
+    The absolute url of the thumbnail (the *__unicode__* method of this
+    object also returns the absolute url, so you can also just do
+    ``{{ thumbnail_variable }}`` in your template).
+
+relative_url
+    The relative url (to ``MEDIA_URL``) of the thumbnail.
+
+width and height
+    The width/height of the thumbnail image.
+
+filesize
+    The file size (in bytes) of the thumbnail.
+    To output user-friendly file sizes, use the included :ref:`filesize-filter`
+    (or Django's built-in more simplistic *filesizeformat* filter).
+
+source_width and source_height
+    The width/height of the source image.
+
+source_filesize
+    The file size of the source. Has same methods as *filesize*.
+
+
+An example of advanced usage::
+
+    {% thumbnail person.photo 250x250 bw autocrop as thumb %}
+    <img src="{{ thumb }}" width="{{ thumb.width }}" height="{{ thumb.height }}" />
+
+Debugging the thumbnail tag
+---------------------------
+
+By default, if there is an error creating the thumbnail or resolving the image
+variable (1st argument) then the thumbnail tag will just return an empty
+string. And if there was a context variable to be set it will also be set to an
+empty string. For example, you will not see an error if the thumbnail could not
+be written to directory because of permissions error. To display those errors
+rather than failing silently, add a ``THUMBNAIL_DEBUG`` property to your
+settings module and set it to ``True``::
+
+	THUMBNAIL_DEBUG = True
+
+
+.. _thumbnail-filenames:
+
+Thumbnail filenames
+===================
+
+The thumbnail filename is generated from the source filename, the target size,
+any options provided and the quality. For example,
+``{% thumbnail "1.jpg" 80x80 crop bw %}`` will save the thumbnail image as::
+
+    MEDIA_ROOT + '1_jpg_80x80_bw_crop_q85.jpg'
+
+By default, thumbnails are saved in the same directory as the source image.
+You can override this behaviour by adding one or more of the following
+properties to your settings module::
+
+    THUMBNAIL_BASEDIR
+    THUMBNAIL_SUBDIR
+    THUMBNAIL_PREFIX
+
+Eaxmples using the tag as follows: ``{% thumbnail "photos/1.jpg" 150x150 %}``::
+
+    # Save thumbnail images to a directory directly off MEDIA_ROOT, still
+    # keeping the relative directory structure of the source image.
+    # Result: MEDIA_ROOT + 'thumbs/photos/1_jpg_150x150_q85.jpg'
+    THUMBNAIL_BASEDIR = 'thumbs'
+    
+    # Save thumbnail images to a sub-directory relative to the source image.
+    # Result: MEDIA_ROOT + 'photos/_thumbs/1_jpg_150x150_q85.jpg'
+    THUMBNAIL_SUBDIR = '_thumbs'
+    
+    # Prepend thumnail filenames with the specified prefix.
+    # Result: MEDIA_ROOT + 'photos/__1_jpg_150x150_q85.jpg'
+    THUMBNAIL_PREFIX = '__'
+
+
+Changing the default quality and image format
+=============================================
+
+If you would rather your thumbnail images have a different default JPEG
+quality than 85, add a ``THUMBNAIL_QUALITY`` property to your settings module.
+For example::
+
+    THUMBNAIL_QUALITY = 95
+
+This will only affect images which have not be explicitly given a quality
+option.  By default, generated thumbnails are saved as JPEG files
+(with the extension '.jpg').
+
+PIL chooses which type of image to save as based on the extension so you can
+change the default image file type by adding a ``THUMBNAIL_EXTENSION`` property
+to your settings module. Note that If you change the extension, the
+``THUMBNAIL_QUALITY`` will have no effect.
+
+Example::
+
+    THUMBNAIL_EXTENSION = 'png'
+
+
+Thumbnails for other document types
+===================================
+
+PDF, EPS and PSD conversion are done with ImageMagick's ``convert`` program.
+The default location where ``sorl.thumbnail`` will look for this program is
+``/usr/bin/convert``.
+
+Word documents are converted to a PostScript file with wvWare's ``wvps``
+program. The default location where ``sorl.thumbnail`` will look for this
+program is ``/usr/bin/wvPS``. This file is then converted to an image with
+ImageMagick's ``convert`` program.
+
+To specify an alternate location for either of these programs, add the relevant
+property to your settings module::
+
+	THUMBNAIL_CONVERT = '/path/to/imagemagick/convert'
+	THUMBNAIL_WVPS = '/path/to/wvPS'
+
+To specify which document types should be converted with ImageMagick, use the
+``THUMBNAIL_IMAGEMAGICK_FILE_TYPES`` setting. The default setting is::
+
+	THUMBNAIL_IMAGEMAGICK_FILE_TYPES = ('eps', 'pdf', 'psd')
+
+
+.. _thumbnail-processors:
+
+Thumbnail Processors
+====================
+
+By specifying a list of ``THUMBNAIL_PROCESSORS`` in your settings module, you
+can change (or add to) the processors which are run when you create a
+thumbnail. Note that the order of the processors is the order in which they
+are called to process the image. Each processor is passed the requested size
+and a dictionary containing all options which the thumbnail was called with
+(except for *quality*, because that's just used internally for saving).
+
+For example, to add your own processor to the list of possible, you would
+create a processor like this::
+
+    def your_processor(image, requested_size, opts):
+        if 'your_option' in opts:
+            process_image(image)
+    your_processor.valid_options = ['your_option']
+
+And add the following to your settings module::
+
+    THUMBNAIL_PROCESSORS = (
+        # Default processors
+        'sorl.thumbnail.processors.colorspace',
+        'sorl.thumbnail.processors.autocrop',
+        'sorl.thumbnail.processors.scale_and_crop',
+        'sorl.thumbnail.processors.filters',
+        # Custom processors
+        'your_project.thumbnail_processors.your_processor',
+    )
+
+Default processors
+------------------
+
+colorspace
+    This processor is best kept at the top of the list since it will convert
+    the image to RGB color space needed by most of following processors. It is
+    also responsible for converting an image to grayscale if *bw* option is
+    specified.
+
+autocrop
+    This will crop the image of white edges and is still pretty experimental.
+
+scale_and_crop
+    This will correctly scale and crop your image as indicated.
+
+filters
+    This provides the *sharpen* and *detail* options described in the
+    options section
+
+Writing a custom processor
+--------------------------
+
+A custom processor takes exactly three arguments: The image as a PIL Image
+Instance, the requested size as a tuple (width, hight), options as strings
+in a list. Your custom processor should return the processed PIL Image instance.
+To make sure we provide our tag with valid options and to make those available
+to your custom processors you have to attach a list of valid options. This is
+simply done by attaching a list called valid_options to your processor as
+described in the above example.
+
+
+Clean-up management command
+===========================
+
+The ``thumbnail_cleanup`` management command is used to delete thumbnails that
+no longer have an original file. Running it is simple::
+
+    ./manage.py thumbnail_cleanup
+
+How it works
+------------
+1. It will look through all your models and find ImageFields, then from the
+   upload_to argument to that it will find all thumbnails.
+2. If then in turn the thumbnail exists but not the original file, it will
+   delete the thumbnail.
+
+Limitations
+-----------
+* It will not even try to delete thumbnails in date formatted directories.
+* There can occur name collisions if a file name matches that of a potential
+  thumbnail (see ``thumb_re``).
+
+
+.. _thumbnail-fields:
+
+Thumbnail Fields
+================
+
+Two field classes (based on Django's ``ImageField``) are provided for use in
+your Django models. They can be imported from ``sorl.thumbnail.fields``.
+
+* ``ThumbnailField`` resizes the source image before saving.
+    
+* ``ImageWithThumbnailsField`` keeps the original source image but
+  provides an easy interface for accessing a predefined thumbnail.
+
+Both fields also allow for :ref:`multiple-thumbnails`, and when the source
+image is deleted, any related thumbnails are also automatically deleted.
+
+ThumbnailField
+--------------
+
+size (required)
+    A 2-length tuple used to size down the width and height of the source image.
+
+options
+    A list of options to use when thumbnailing the source image.
+
+quality
+    Alter the quality of the JPEG thumbnail.
+
+basedir, subdir and prefix
+    Used to override the default :ref:`thumbnail-filenames` settings.
+
+Here is an example model with a ``ThumbnailField``::
+
+    MyModel(models.Model):
+        name = models.TextField(max_length=50)
+        photo = ThumbnailField(upload_to='profiles', size=(200, 200))
+
+ImageWithThumbnailsField
+------------------------
+
+A *thumbnail* argument is required for this field. Pass in a dictionary
+with the following values (all optional except for *size*):
+
+size
+    A 2-length tuple of the thumbnail width and height.
+
+options
+    A list of options for this thumbnail.
+
+quality, basedir, subdir and prefix
+    Used to override the default :ref:`thumbnail-filenames` settings.
+
+Your model instance's field will have a new property, *thumbnail*, which
+returns a ``DjangoThumbnail`` instance for your pleasure (if you use this in a
+template, it'll return the full URL to the thumbnail).
+
+Let's look at an example. Here is a model with an ``ImageWithThumbnailsField``::
+
+    MyModel(models.Model):
+        name = models.TextField(max_length=50)
+        photo = ImageWithThumbnailsField(upload_to='profiles',
+                                         thumbnail={'size': (50, 50)})
+
+A template (passed an instance of *MyModel*) would simply use something like:
+``<img src="{{ my_model.photo.thumbnail }}" alt="{{ my_model.name }}" />`` or
+it could use the :ref:`simple-html-tag`.
+
+.. _simple-html-tag:
+
+Simple HTML tag
+---------------
+
+Your model instance's field (for both thumbnail field types) has a new
+*thumbnail_tag* property which can be used to return HTML like
+``<img src="..." width="..." height="..." alt="" />``.
+
+Now, even simpler for just a basic *img* tag:
+``{{ my_model.photo.thumbnail_tag }}``.
+
+Note that when the source image is deleted, any related thumbnails are also
+automatically deleted.
+
+
+.. _multiple-thumbnails:
+
+Multiple Thumbnails
+-------------------
+
+If you want to use multiple thumbnails for a single field, you can use the
+*extra_thumbnails* argument, passing it a dictionary like so::
+
+    photo = ImageWithThumbnailsField(
+        upload_to='profiles',
+        thumbnail={'size': (50, 50)},
+        extra_thumbnails={
+            'icon': {'size': (16, 16), 'options': ['crop', 'upscale']},
+            'large': {'size': (200, 400)},
+        },
+    )
+
+This would allow you to access the extra thumbnails like this:
+``my_model.photo.extra_thumbnails['icon']`` (or in a template,
+``{{ my_model.photo.extra_thumbnails.icon }}``).
+
+This is available to both thumbnail field types.
+
+Similar to how the :ref:`simple-html-tag` works, you can using the
+*extra_thumbnails_tag* property:
+``my_model.photo.extra_thumbnails_tag['large']`` (or in a template,
+``{{ my_model.photo.extra_thumbnails_tag.large }}``).
+
+When thumbnails are generated
+-----------------------------
+
+The normal behaviour is that thumbnails are only generated when they are
+first accessed. To have them generated as soon as the source image is saved,
+you can set the field's *generate_on_save* attribute to ``True``.
+
+Changing the thumbnail tag HTML
+-------------------------------
+
+If you don't like the default HTML output by the thumbnail tag shortcuts
+provided by this field, you can use the *thumbnail_tag* argument. For
+example, to use HTML4.0 compliant tags, you would do the following::
+
+    photo = ImageWithThumbnailsField(
+        upload_to='profiles',
+        thumbnail={'size': (50, 50)},
+        template_tag='<img src="%(src)s" width="%(width)s" height="%(height)s">'
+    )
+
+Generate a different image type than JPEG
+-----------------------------------------
+
+PIL chooses which type of image to save as based on the extension so you can
+use the *extension* argument to save as a different image type that the
+default JPEG format. For example, to make the generated thumbnail a PNG file::
+
+    photo = ImageWithThumbnailsField(
+        upload_to='profiles',
+        thumbnail={'size': (50, 50), 'extension': 'png'}
+    )
+    avatar = ThumbnailField(
+        upload_to='profiles',
+        size=(50, 50),
+        extension='png'
+    )
+
+
+This just doesn't cover my cravings!
+====================================
+
+1. Use the ``DjangoThumbnail`` class in ``sorl.thumbnail.main`` if you want
+   behaviour similar to :ref:`template-tag`. If you want to use a
+   different file naming method, just subclass and override the
+   *_get_relative_thumbnail* method.
+
+2. Go for the ``Thumbnail`` class in ``sorl.thumbnail.base`` for more
+   low-level creation of thumbnails. This class doesn't have any
+   Django-specific ties.
+
+
+.. _filesize-filter:
+
+Filesize filter
+===============
+
+This filter returns the number of bytes in either the nearest unit or a
+specific unit (depending on the chosen format method). Use this filter to
+output user-friendly file sizes. For example::
+
+	{% thumbnail source 200x200 as thumb %}
+	Thumbnail file size: {{ thumb.filesize|filesize }}
+
+If the generated thumbnail size came to 2000 bytes, this would output
+"Thumbnail file size: 1.9 KiB" (the filter's default format is *auto1024*).
+You can specify a different format like so::
+
+	{{ thumb.filesize|filesize:"auto1000long" }}
+
+Which would output "2 kilobytes".
+
+Acceptable formats are:
+
+auto1024, auto1000
+    convert to the nearest unit, appending the abbreviated unit name to the
+    string (e.g. '2 KiB' or '2 kB'). *auto1024* is the default format.
+
+auto1024long, auto1000long
+    convert to the nearest multiple of 1024 or 1000, appending the correctly
+    pluralized unit name to the string (e.g. '2 kibibytes' or '2 kilobytes').
+
+kB, MB, GB, TB, PB, EB, ZB, YB
+    convert to the exact unit (using multiples of 1000).
+
+KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB
+    convert to the exact unit (using multiples of 1024).
+
+The *auto1024* and *auto1000* formats return a string, appending the
+correct unit to the value. All other formats return the floating point value.
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+set SPHINXBUILD=sphinx-build
+set ALLSPHINXOPTS=-d _build/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+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.  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.  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	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 (_build\*) do rmdir /q /s %%i
+	del /q /s _build\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% _build/html
+	echo.
+	echo.Build finished. The HTML pages are in _build/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% _build/dirhtml
+	echo.
+	echo.Build finished. The HTML pages are in _build/dirhtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% _build/pickle
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% _build/json
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% _build/htmlhelp
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in _build/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% _build/qthelp
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in _build/qthelp, like this:
+	echo.^> qcollectiongenerator _build\qthelp\sorl-thumbnail.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile _build\qthelp\sorl-thumbnail.ghc
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% _build/latex
+	echo.
+	echo.Build finished; the LaTeX files are in _build/latex.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% _build/changes
+	echo.
+	echo.The overview file is in _build/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% _build/linkcheck
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in _build/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% _build/doctest
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in _build/doctest/output.txt.
+	goto end
+)
+
+:end
+#!/usr/bin/env python
+from distutils.core import setup
+
+
+VERSION = '3.2.5' 
+
+README_FILE = open('README')
+try:
+    long_description = README_FILE.read()
+finally:
+    README_FILE.close()
+
+ 
+setup(
+    name='sorl-thumbnail',
+    version=VERSION,
+    url='http://code.google.com/p/sorl-thumbnail/',
+    download_url='http://sorl-thumbnail.googlecode.com/files/sorl-thumbnail-'
+        '%s.tar.gz'  % VERSION,
+    description='Thumbnails for Django',
+    long_description=long_description,
+    author='Mikko Hellsing, Chris Beaven',
+    platforms=['any'],
+    packages=[
+        'sorl',
+        'sorl.thumbnail',
+        'sorl.thumbnail.templatetags',
+        'sorl.thumbnail.tests',
+        'sorl.thumbnail.management',
+        'sorl.thumbnail.management.commands',
+    ],
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Application Frameworks',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+)

sorl/__init__.py

Empty file added.

sorl/thumbnail/__init__.py

Empty file added.

sorl/thumbnail/base.py

+import os
+from os.path import isfile, isdir, getmtime, dirname, splitext, getsize
+from tempfile import mkstemp
+from shutil import copyfile
+
+from PIL import Image
+
+from sorl.thumbnail import defaults
+from sorl.thumbnail.processors import get_valid_options, dynamic_import
+
+
+class ThumbnailException(Exception):
+    # Stop Django templates from choking if something goes wrong.
+    silent_variable_failure = True
+
+
+class Thumbnail(object):
+    imagemagick_file_types = defaults.IMAGEMAGICK_FILE_TYPES
+
+    def __init__(self, source, requested_size, opts=None, quality=85,
+                 dest=None, convert_path=defaults.CONVERT,
+                 wvps_path=defaults.WVPS, processors=None):
+        # Paths to external commands
+        self.convert_path = convert_path
+        self.wvps_path = wvps_path
+        # Absolute paths to files
+        self.source = source
+        self.dest = dest
+
+        # Thumbnail settings
+        try:
+            x, y = [int(v) for v in requested_size]
+        except (TypeError, ValueError):
+            raise TypeError('Thumbnail received invalid value for size '
+                            'argument: %s' % repr(requested_size))
+        else:
+            self.requested_size = (x, y)
+        try:
+            self.quality = int(quality)
+            if not 0 < quality <= 100:
+                raise ValueError
+        except (TypeError, ValueError):
+            raise TypeError('Thumbnail received invalid value for quality '
+                            'argument: %r' % quality)
+
+        # Processors
+        if processors is None:
+            processors = dynamic_import(defaults.PROCESSORS)
+        self.processors = processors
+
+        # Handle old list format for opts.
+        opts = opts or {}
+        if isinstance(opts, (list, tuple)):
+            opts = dict([(opt, None) for opt in opts])
+
+        # Set Thumbnail opt(ion)s
+        VALID_OPTIONS = get_valid_options(processors)
+        for opt in opts:
+            if not opt in VALID_OPTIONS:
+                raise TypeError('Thumbnail received an invalid option: %s'
+                                % opt)
+        self.opts = opts
+
+        if self.dest is not None:
+            self.generate()
+
+    def generate(self):
+        """
+        Generates the thumbnail if it doesn't exist or if the file date of the
+        source file is newer than that of the thumbnail.
+        """
+        # Ensure dest(ination) attribute is set
+        if not self.dest:
+            raise ThumbnailException("No destination filename set.")
+
+        if not isinstance(self.dest, basestring):
+            # We'll assume dest is a file-like instance if it exists but isn't
+            # a string.
+            self._do_generate()
+        elif not isfile(self.dest) or (self.source_exists and
+            getmtime(self.source) > getmtime(self.dest)):
+
+            # Ensure the directory exists
+            directory = dirname(self.dest)
+            if directory and not isdir(directory):
+                os.makedirs(directory)
+
+            self._do_generate()
+
+    def _check_source_exists(self):
+        """
+        Ensure the source file exists. If source is not a string then it is
+        assumed to be a file-like instance which "exists".
+        """
+        if not hasattr(self, '_source_exists'):
+            self._source_exists = (self.source and
+                                   (not isinstance(self.source, basestring) or
+                                    isfile(self.source)))
+        return self._source_exists
+    source_exists = property(_check_source_exists)
+
+    def _get_source_filetype(self):
+        """
+        Set the source filetype. First it tries to use magic and
+        if import error it will just use the extension
+        """
+        if not hasattr(self, '_source_filetype'):
+            if not isinstance(self.source, basestring):
+                # Assuming a file-like object - we won't know it's type.
+                return None
+            try:
+                import magic
+            except ImportError:
+                self._source_filetype = splitext(self.source)[1].lower().\
+                   replace('.', '').replace('jpeg', 'jpg')
+            else:
+                if hasattr(magic, 'from_file'):
+                    # Adam Hupp's ctypes-based magic library
+                    ftype = magic.from_file(self.source)
+                else:
+                    # Brett Funderburg's older python magic bindings
+                    m = magic.open(magic.MAGIC_NONE)
+                    m.load()
+                    ftype = m.file(self.source)
+                if ftype.find('Microsoft Office Document') != -1:
+                    self._source_filetype = 'doc'
+                elif ftype.find('PDF document') != -1:
+                    self._source_filetype = 'pdf'
+                elif ftype.find('JPEG') != -1:
+                    self._source_filetype = 'jpg'
+                else:
+                    self._source_filetype = ftype
+        return self._source_filetype
+    source_filetype = property(_get_source_filetype)
+
+    # data property is the image data of the (generated) thumbnail
+    def _get_data(self):
+        if not hasattr(self, '_data'):
+            try:
+                self._data = Image.open(self.dest)
+            except IOError, detail:
+                raise ThumbnailException(detail)
+        return self._data
+
+    def _set_data(self, im):
+        self._data = im
+    data = property(_get_data, _set_data)
+
+    # source_data property is the image data from the source file
+    def _get_source_data(self):
+        if not hasattr(self, '_source_data'):
+            if not self.source_exists:
+                raise ThumbnailException("Source file: '%s' does not exist." %
+                                         self.source)
+            if self.source_filetype == 'doc':
+                self._convert_wvps(self.source)
+            elif self.source_filetype in self.imagemagick_file_types:
+                self._convert_imagemagick(self.source)
+            else:
+                self.source_data = self.source
+        return self._source_data
+
+    def _set_source_data(self, image):
+        if isinstance(image, Image.Image):
+            self._source_data = image
+        else:
+            try:
+                self._source_data = Image.open(image)
+            except IOError, detail:
+                raise ThumbnailException("%s: %s" % (detail, image))
+            except MemoryError:
+                raise ThumbnailException("Memory Error: %s" % image)
+    source_data = property(_get_source_data, _set_source_data)
+
+    def _convert_wvps(self, filename):
+        try:
+            import subprocess
+        except ImportError:
+            raise ThumbnailException('wvps requires the Python 2.4 subprocess '
+                                     'package.')
+        tmp = mkstemp('.ps')[1]
+        try:
+            p = subprocess.Popen((self.wvps_path, filename, tmp),
+                                 stdout=subprocess.PIPE)
+            p.wait()
+        except OSError, detail:
+            os.remove(tmp)
+            raise ThumbnailException('wvPS error: %s' % detail)
+        self._convert_imagemagick(tmp)
+        os.remove(tmp)
+
+    def _convert_imagemagick(self, filename):
+        try:
+            import subprocess
+        except ImportError:
+            raise ThumbnailException('imagemagick requires the Python 2.4 '
+                                     'subprocess package.')
+        tmp = mkstemp('.png')[1]
+        if 'crop' in self.opts or 'autocrop' in self.opts:
+            x, y = [d * 3 for d in self.requested_size]
+        else:
+            x, y = self.requested_size
+        try:
+            p = subprocess.Popen((self.convert_path, '-size', '%sx%s' % (x, y),
+                '-antialias', '-colorspace', 'rgb', '-format', 'PNG24',
+                '%s[0]' % filename, tmp), stdout=subprocess.PIPE)
+            p.wait()
+        except OSError, detail:
+            os.remove(tmp)
+            raise ThumbnailException('ImageMagick error: %s' % detail)
+        self.source_data = tmp
+        os.remove(tmp)
+
+    def _do_generate(self):
+        """
+        Generates the thumbnail image.
+
+        This a semi-private method so it isn't directly available to template
+        authors if this object is passed to the template context.
+        """
+        im = self.source_data
+
+        for processor in self.processors:
+            im = processor(im, self.requested_size, self.opts)
+
+        self.data = im
+
+        filelike = not isinstance(self.dest, basestring)
+        if not filelike:
+            dest_extension = os.path.splitext(self.dest)[1][1:]
+            format = None
+        else:
+            dest_extension = None
+            format = 'JPEG'
+        if (self.source_filetype and self.source_filetype == dest_extension and
+                self.source_data == self.data):
+            copyfile(self.source, self.dest)
+        else:
+            try:
+                im.save(self.dest, format=format, quality=self.quality,
+                        optimize=1)
+            except IOError:
+                # Try again, without optimization (PIL can't optimize an image
+                # larger than ImageFile.MAXBLOCK, which is 64k by default)
+                try:
+                    im.save(self.dest, format=format, quality=self.quality)
+                except IOError, detail:
+                    raise ThumbnailException(detail)
+
+        if filelike:
+            self.dest.seek(0)
+
+    # Some helpful methods
+
+    def _dimension(self, axis):
+        if self.dest is None:
+            return None
+        return self.data.size[axis]
+
+    def width(self):
+        return self._dimension(0)
+
+    def height(self):
+        return self._dimension(1)
+
+    def _get_filesize(self):
+        if self.dest is None:
+            return None
+        if not hasattr(self, '_filesize'):
+            self._filesize = getsize(self.dest)
+        return self._filesize
+    filesize = property(_get_filesize)
+
+    def _source_dimension(self, axis):
+        if self.source_filetype in ['pdf', 'doc']:
+            return None
+        else:
+            return self.source_data.size[axis]
+
+    def source_width(self):
+        return self._source_dimension(0)
+
+    def source_height(self):
+        return self._source_dimension(1)
+
+    def _get_source_filesize(self):
+        if not hasattr(self, '_source_filesize'):
+            self._source_filesize = getsize(self.source)
+        return self._source_filesize
+    source_filesize = property(_get_source_filesize)

sorl/thumbnail/defaults.py

+DEBUG = False
+BASEDIR = ''
+SUBDIR = ''
+PREFIX = ''
+QUALITY = 85
+CONVERT = '/usr/bin/convert'
+WVPS = '/usr/bin/wvPS'
+EXTENSION = 'jpg'
+PROCESSORS = (
+    'sorl.thumbnail.processors.colorspace',
+    'sorl.thumbnail.processors.autocrop',
+    'sorl.thumbnail.processors.scale_and_crop',
+    'sorl.thumbnail.processors.filters',
+)
+IMAGEMAGICK_FILE_TYPES = ('eps', 'pdf', 'psd')

sorl/thumbnail/fields.py

+from UserDict import DictMixin
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+from django.db.models.fields.files import ImageField, ImageFieldFile
+from django.core.files.base import ContentFile
+from django.utils.safestring import mark_safe
+from django.utils.html import escape
+
+from sorl.thumbnail.base import Thumbnail
+from sorl.thumbnail.main import DjangoThumbnail, build_thumbnail_name
+from sorl.thumbnail.utils import delete_thumbnails
+
+
+REQUIRED_ARGS = ('size',)
+ALL_ARGS = {
+    'size': 'requested_size',
+    'options': 'opts',
+    'quality': 'quality',
+    'basedir': 'basedir',
+    'subdir': 'subdir',
+    'prefix': 'prefix',
+    'extension': 'extension',
+}
+BASE_ARGS = {
+    'size': 'requested_size',
+    'options': 'opts',
+    'quality': 'quality',
+}
+TAG_HTML = '<img src="%(src)s" width="%(width)s" height="%(height)s" alt="" />'
+
+
+class ThumbsDict(object, DictMixin):
+    def __init__(self, descriptor):
+        super(ThumbsDict, self).__init__()
+        self.descriptor = descriptor
+
+    def keys(self):
+        return self.descriptor.field.extra_thumbnails.keys()
+
+
+class LazyThumbs(ThumbsDict):
+    def __init__(self, *args, **kwargs):
+        super(LazyThumbs, self).__init__(*args, **kwargs)
+        self.cached = {}
+
+    def __getitem__(self, key):
+        thumb = self.cached.get(key)
+        if not thumb:
+            args = self.descriptor.field.extra_thumbnails[key]
+            thumb = self.descriptor._build_thumbnail(args)
+            self.cached[key] = thumb
+        return thumb
+
+    def keys(self):
+        return self.descriptor.field.extra_thumbnails.keys()
+
+
+class ThumbTags(ThumbsDict):
+    def __getitem__(self, key):
+        thumb = self.descriptor.extra_thumbnails[key]
+        return self.descriptor._build_thumbnail_tag(thumb)
+
+
+class BaseThumbnailFieldFile(ImageFieldFile):
+    def _build_thumbnail(self, args):
+        # Build the DjangoThumbnail kwargs.
+        kwargs = {}
+        for k, v in args.items():
+            kwargs[ALL_ARGS[k]] = v
+        # Build the destination filename and return the thumbnail.
+        name_kwargs = {}
+        for key in ['size', 'options', 'quality', 'basedir', 'subdir',
+                    'prefix', 'extension']:
+            name_kwargs[key] = args.get(key)
+        source = getattr(self.instance, self.field.name)
+        dest = build_thumbnail_name(source.name, **name_kwargs)
+        return DjangoThumbnail(source, relative_dest=dest, **kwargs)
+
+    def _build_thumbnail_tag(self, thumb):
+        opts = dict(src=escape(thumb), width=thumb.width(),
+                    height=thumb.height())
+        return mark_safe(self.field.thumbnail_tag % opts)
+
+    def _get_extra_thumbnails(self):
+        if self.field.extra_thumbnails is None:
+            return None
+        if not hasattr(self, '_extra_thumbnails'):
+            self._extra_thumbnails = LazyThumbs(self)
+        return self._extra_thumbnails
+    extra_thumbnails = property(_get_extra_thumbnails)
+
+    def _get_extra_thumbnails_tag(self):
+        if self.field.extra_thumbnails is None:
+            return None
+        return ThumbTags(self)
+    extra_thumbnails_tag = property(_get_extra_thumbnails_tag)
+
+    def save(self, *args, **kwargs):
+        # Optionally generate the thumbnails after the image is saved.
+        super(BaseThumbnailFieldFile, self).save(*args, **kwargs)
+        if self.field.generate_on_save:
+            self.generate_thumbnails()
+
+    def delete(self, *args, **kwargs):
+        # Delete any thumbnails too (and not just ones defined here in case
+        # the {% thumbnail %} tag was used or the thumbnail sizes changed).
+        relative_source_path = getattr(self.instance, self.field.name).name
+        delete_thumbnails(relative_source_path)
+        super(BaseThumbnailFieldFile, self).delete(*args, **kwargs)
+
+    def generate_thumbnails(self):
+        # Getting the thumbs generates them.
+        if self.extra_thumbnails:
+            self.extra_thumbnails.values()
+
+
+class ImageWithThumbnailsFieldFile(BaseThumbnailFieldFile):
+    def _get_thumbnail(self):
+        return self._build_thumbnail(self.field.thumbnail)
+    thumbnail = property(_get_thumbnail)
+
+    def _get_thumbnail_tag(self):
+        return self._build_thumbnail_tag(self.thumbnail)
+    thumbnail_tag = property(_get_thumbnail_tag)
+
+    def generate_thumbnails(self, *args, **kwargs):
+        self.thumbnail.generate()
+        Super = super(ImageWithThumbnailsFieldFile, self)
+        return Super.generate_thumbnails(*args, **kwargs)
+
+
+class ThumbnailFieldFile(BaseThumbnailFieldFile):
+    def save(self, name, content, *args, **kwargs):
+        new_content = StringIO()
+        # Build the Thumbnail kwargs.
+        thumbnail_kwargs = {}
+        for k, argk in BASE_ARGS.items():
+            if not k in self.field.thumbnail:
+                continue
+            thumbnail_kwargs[argk] = self.field.thumbnail[k]
+        Thumbnail(source=content, dest=new_content, **thumbnail_kwargs)
+        new_content = ContentFile(new_content.read())
+        super(ThumbnailFieldFile, self).save(name, new_content, *args,
+                                             **kwargs)
+
+    def _get_thumbnail_tag(self):
+        opts = dict(src=escape(self.url), width=self.width,
+                    height=self.height)
+        return mark_safe(self.field.thumbnail_tag % opts)
+    thumbnail_tag = property(_get_thumbnail_tag)
+
+
+class BaseThumbnailField(ImageField):
+    def __init__(self, *args, **kwargs):
+        # The new arguments for this field aren't explicitly defined so that
+        # users can still use normal ImageField positional arguments.
+        self.extra_thumbnails = kwargs.pop('extra_thumbnails', None)
+        self.thumbnail_tag = kwargs.pop('thumbnail_tag', TAG_HTML)
+        self.generate_on_save = kwargs.pop('generate_on_save', False)
+
+        super(BaseThumbnailField, self).__init__(*args, **kwargs)
+        _verify_thumbnail_attrs(self.thumbnail)
+        if self.extra_thumbnails:
+            for extra, attrs in self.extra_thumbnails.items():
+                name = "%r of 'extra_thumbnails'"
+                _verify_thumbnail_attrs(attrs, name)
+
+    def south_field_triple(self):
+        """
+        Return a suitable description of this field for South.
+        """
+        # We'll just introspect ourselves, since we inherit.
+        from south.modelsinspector import introspector
+        field_class = "django.db.models.fields.files.ImageField"
+        args, kwargs = introspector(self)
+        # That's our definition!
+        return (field_class, args, kwargs)
+
+
+class ImageWithThumbnailsField(BaseThumbnailField):
+    """
+    photo = ImageWithThumbnailsField(
+        upload_to='uploads',
+        thumbnail={'size': (80, 80), 'options': ('crop', 'upscale'),
+                   'extension': 'png'},
+        extra_thumbnails={
+            'admin': {'size': (70, 50), 'options': ('sharpen',)},
+        }
+    )
+    """
+    attr_class = ImageWithThumbnailsFieldFile
+
+    def __init__(self, *args, **kwargs):
+        self.thumbnail = kwargs.pop('thumbnail', None)
+        super(ImageWithThumbnailsField, self).__init__(*args, **kwargs)
+
+
+class ThumbnailField(BaseThumbnailField):
+    """
+    avatar = ThumbnailField(
+        upload_to='uploads',
+        size=(200, 200),
+        options=('crop',),
+        extra_thumbnails={
+            'admin': {'size': (70, 50), 'options': (crop, 'sharpen')},
+        }
+    )
+    """
+    attr_class = ThumbnailFieldFile
+
+    def __init__(self, *args, **kwargs):
+        self.thumbnail = {}
+        for attr in ALL_ARGS:
+            if attr in kwargs:
+                self.thumbnail[attr] = kwargs.pop(attr)
+        super(ThumbnailField, self).__init__(*args, **kwargs)
+
+
+def _verify_thumbnail_attrs(attrs, name="'thumbnail'"):
+    for arg in REQUIRED_ARGS:
+        if arg not in attrs:
+            raise TypeError('Required attr %r missing in %s arg' % (arg, name))
+    for attr in attrs:
+        if attr not in ALL_ARGS:
+            raise TypeError('Invalid attr %r found in %s arg' % (arg, name))

sorl/thumbnail/main.py

+import os
+
+from django.conf import settings
+from django.utils.encoding import iri_to_uri, force_unicode
+
+from sorl.thumbnail.base import Thumbnail
+from sorl.thumbnail.processors import dynamic_import
+from sorl.thumbnail import defaults
+
+
+def get_thumbnail_setting(setting, override=None):
+    """
+    Get a thumbnail setting from Django settings module, falling back to the
+    default.
+
+    If override is not None, it will be used instead of the setting.
+    """
+    if override is not None:
+        return override
+    if hasattr(settings, 'THUMBNAIL_%s' % setting):
+        return getattr(settings, 'THUMBNAIL_%s' % setting)
+    else:
+        return getattr(defaults, setting)
+
+
+def build_thumbnail_name(source_name, size, options=None,
+                         quality=None, basedir=None, subdir=None, prefix=None,
+                         extension=None):
+    quality = get_thumbnail_setting('QUALITY', quality)
+    basedir = get_thumbnail_setting('BASEDIR', basedir)
+    subdir = get_thumbnail_setting('SUBDIR', subdir)
+    prefix = get_thumbnail_setting('PREFIX', prefix)
+    extension = get_thumbnail_setting('EXTENSION', extension)
+    path, filename = os.path.split(source_name)
+    basename, ext = os.path.splitext(filename)
+    name = '%s%s' % (basename, ext.replace(os.extsep, '_'))
+    size = '%sx%s' % tuple(size)
+
+    # Handle old list format for opts.
+    options = options or {}
+    if isinstance(options, (list, tuple)):
+        options = dict([(opt, None) for opt in options])
+
+    opts = options.items()
+    opts.sort()   # options are sorted so the filename is consistent
+    opts = ['%s_' % (v is not None and '%s-%s' % (k, v) or k)
+            for k, v in opts]
+    opts = ''.join(opts)
+    extension = extension and '.%s' % extension
+    thumbnail_filename = '%s%s_%s_%sq%s%s' % (prefix, name, size, opts,
+                                              quality, extension)
+    return os.path.join(basedir, path, subdir, thumbnail_filename)
+
+
+class DjangoThumbnail(Thumbnail):
+    imagemagick_file_types = get_thumbnail_setting('IMAGEMAGICK_FILE_TYPES')
+
+    def __init__(self, relative_source, requested_size, opts=None,
+                 quality=None, basedir=None, subdir=None, prefix=None,
+                 relative_dest=None, processors=None, extension=None):
+        relative_source = force_unicode(relative_source)
+        # Set the absolute filename for the source file
+        source = self._absolute_path(relative_source)
+
+        quality = get_thumbnail_setting('QUALITY', quality)
+        convert_path = get_thumbnail_setting('CONVERT')
+        wvps_path = get_thumbnail_setting('WVPS')
+        if processors is None:
+            processors = dynamic_import(get_thumbnail_setting('PROCESSORS'))
+
+        # Call super().__init__ now to set the opts attribute. generate() won't
+        # get called because we are not setting the dest attribute yet.
+        super(DjangoThumbnail, self).__init__(source, requested_size,
+            opts=opts, quality=quality, convert_path=convert_path,
+            wvps_path=wvps_path, processors=processors)
+
+        # Get the relative filename for the thumbnail image, then set the
+        # destination filename
+        if relative_dest is None:
+            relative_dest = \
+               self._get_relative_thumbnail(relative_source, basedir=basedir,
+                                            subdir=subdir, prefix=prefix,
+                                            extension=extension)
+        filelike = not isinstance(relative_dest, basestring)
+        if filelike:
+            self.dest = relative_dest
+        else:
+            self.dest = self._absolute_path(relative_dest)
+
+        # Call generate now that the dest attribute has been set
+        self.generate()
+
+        # Set the relative & absolute url to the thumbnail
+        if not filelike:
+            self.relative_url = \
+                iri_to_uri('/'.join(relative_dest.split(os.sep)))
+            self.absolute_url = '%s%s' % (settings.MEDIA_URL,
+                                          self.relative_url)
+
+    def _get_relative_thumbnail(self, relative_source,
+                                basedir=None, subdir=None, prefix=None,
+                                extension=None):
+        """
+        Returns the thumbnail filename including relative path.
+        """
+        return build_thumbnail_name(relative_source, self.requested_size,
+                                    self.opts, self.quality, basedir, subdir,
+                                    prefix, extension)
+
+    def _absolute_path(self, filename):
+        absolute_filename = os.path.join(settings.MEDIA_ROOT, filename)
+        return absolute_filename.encode(settings.FILE_CHARSET)
+
+    def __unicode__(self):
+        return self.absolute_url

sorl/thumbnail/management/__init__.py

Empty file added.

sorl/thumbnail/management/commands/__init__.py

Empty file added.

sorl/thumbnail/management/commands/thumbnail_cleanup.py

+import os
+import re
+from django.db import models
+from django.conf import settings
+from django.core.management.base import NoArgsCommand
+from sorl.thumbnail.main import get_thumbnail_setting
+
+
+try:
+    set
+except NameError:
+    from sets import Set as set     # For Python 2.3
+
+thumb_re = re.compile(r'^%s(.*)_\d{1,}x\d{1,}_[-\w]*q([1-9]\d?|100)\.jpg' %
+                      get_thumbnail_setting('PREFIX'))
+
+
+def get_thumbnail_path(path):
+    basedir = get_thumbnail_setting('BASEDIR')
+    subdir = get_thumbnail_setting('SUBDIR')
+    return os.path.join(basedir, path, subdir)
+
+
+def clean_up():
+    paths = set()
+    for app in models.get_apps():
+        model_list = models.get_models(app)
+        for model in model_list:
+            for field in model._meta.fields:
+                if isinstance(field, models.ImageField):
+                    #TODO: take care of date formatted and callable upload_to.
+                    if (not callable(field.upload_to) and
+                            field.upload_to.find("%") == -1):
+                        paths = paths.union((field.upload_to,))
+    paths = list(paths)
+    for path in paths:
+        thumbnail_path = get_thumbnail_path(path)
+        try:
+            file_list = os.listdir(os.path.join(settings.MEDIA_ROOT,
+                                                thumbnail_path))
+        except OSError:
+            continue # Dir doesn't exists, no thumbnails here.
+        for fn in file_list:
+            m = thumb_re.match(fn)
+            if m:
+                # Due to that the naming of thumbnails replaces the dot before
+                # extension with an underscore we have 2 possibilities for the
+                # original filename. If either present we do not delete
+                # suspected thumbnail.
+                # org_fn is the expected original filename w/o extension
+                # org_fn_alt is the expected original filename with extension
+                org_fn = m.group(1)
+                org_fn_exists = os.path.isfile(
+                            os.path.join(settings.MEDIA_ROOT, path, org_fn))
+
+                usc_pos = org_fn.rfind("_")
+                if usc_pos != -1:
+                    org_fn_alt = "%s.%s" % (org_fn[0:usc_pos],
+                                            org_fn[usc_pos+1:])
+                    org_fn_alt_exists = os.path.isfile(
+                        os.path.join(settings.MEDIA_ROOT, path, org_fn_alt))
+                else:
+                    org_fn_alt_exists = False
+                if not org_fn_exists and not org_fn_alt_exists:
+                    del_me = os.path.join(settings.MEDIA_ROOT,
+                                          thumbnail_path, fn)
+                    os.remove(del_me)
+
+
+class Command(NoArgsCommand):
+    help = "Deletes thumbnails that no longer have an original file."
+    requires_model_validation = False
+
+    def handle_noargs(self, **options):
+        clean_up()

sorl/thumbnail/models.py

+# Needs a models.py file so that tests are picked up.

sorl/thumbnail/processors.py

+from PIL import Image, ImageFilter, ImageChops
+from sorl.thumbnail import utils
+import re
+
+
+def dynamic_import(names):
+    imported = []
+    for name in names:
+        # Use rfind rather than rsplit for Python 2.3 compatibility.
+        lastdot = name.rfind('.')
+        modname, attrname = name[:lastdot], name[lastdot + 1:]
+        mod = __import__(modname, {}, {}, [''])
+        imported.append(getattr(mod, attrname))
+    return imported
+
+
+def get_valid_options(processors):
+    """
+    Returns a list containing unique valid options from a list of processors
+    in correct order.
+    """
+    valid_options = []
+    for processor in processors:
+        if hasattr(processor, 'valid_options'):
+            valid_options.extend([opt for opt in processor.valid_options
+                                  if opt not in valid_options])
+    return valid_options
+
+
+def colorspace(im, requested_size, opts):
+    if 'bw' in opts and im.mode != "L":
+        im = im.convert("L")
+    elif im.mode not in ("L", "RGB", "RGBA"):
+        im = im.convert("RGB")
+    return im
+colorspace.valid_options = ('bw',)
+
+
+def autocrop(im, requested_size, opts):
+    if 'autocrop' in opts:
+        bw = im.convert("1")
+        bw = bw.filter(ImageFilter.MedianFilter)
+        # white bg
+        bg = Image.new("1", im.size, 255)
+        diff = ImageChops.difference(bw, bg)
+        bbox = diff.getbbox()
+        if bbox:
+            im = im.crop(bbox)
+    return im
+autocrop.valid_options = ('autocrop',)
+
+
+def scale_and_crop(im, requested_size, opts):
+    x, y = [float(v) for v in im.size]
+    xr, yr = [float(v) for v in requested_size]
+
+    if 'crop' in opts or 'max' in opts:
+        r = max(xr / x, yr / y)
+    else:
+        r = min(xr / x, yr / y)
+
+    if r < 1.0 or (r > 1.0 and 'upscale' in opts):
+        im = im.resize((int(round(x * r)), int(round(y * r))), resample=Image.ANTIALIAS)
+
+    crop = opts.get('crop') or 'crop' in opts
+    if crop:
+        # Difference (for x and y) between new image size and requested size.
+        x, y = [float(v) for v in im.size]
+        dx, dy = (x - min(x, xr)), (y - min(y, yr))
+        if dx or dy:
+            # Center cropping (default).
+            ex, ey = dx / 2, dy / 2
+            box = [ex, ey, x - ex, y - ey]
+            # See if an edge cropping argument was provided.
+            edge_crop = (isinstance(crop, basestring) and
+                           re.match(r'(?:(-?)(\d+))?,(?:(-?)(\d+))?$', crop))
+            if edge_crop and filter(None, edge_crop.groups()):
+                x_right, x_crop, y_bottom, y_crop = edge_crop.groups()
+                if x_crop:
+                    offset = min(x * int(x_crop) / 100, dx)
+                    if x_right:
+                        box[0] = dx - offset
+                        box[2] = x - offset
+                    else:
+                        box[0] = offset
+                        box[2] = x - (dx - offset)
+                if y_crop:
+                    offset = min(y * int(y_crop) / 100, dy)
+                    if y_bottom:
+                        box[1] = dy - offset
+                        box[3] = y - offset
+                    else:
+                        box[1] = offset
+                        box[3] = y - (dy - offset)
+            # See if the image should be "smart cropped".
+            elif crop == 'smart':
+                left = top = 0
+                right, bottom = x, y
+                while dx:
+                    slice = min(dx, 10)
+                    l_sl = im.crop((0, 0, slice, y))
+                    r_sl = im.crop((x - slice, 0, x, y))
+                    if utils.image_entropy(l_sl) >= utils.image_entropy(r_sl):
+                        right -= slice
+                    else:
+                        left += slice
+                    dx -= slice
+                while dy:
+                    slice = min(dy, 10)
+                    t_sl = im.crop((0, 0, x, slice))
+                    b_sl = im.crop((0, y - slice, x, y))
+                    if utils.image_entropy(t_sl) >= utils.image_entropy(b_sl):
+                        bottom -= slice
+                    else:
+                        top += slice
+                    dy -= slice
+                box = (left, top, right, bottom)
+            # Finally, crop the image!
+            im = im.crop([int(round(v)) for v in box])
+    return im
+scale_and_crop.valid_options = ('crop', 'upscale', 'max')
+
+
+def filters(im, requested_size, opts):
+    if 'detail' in opts:
+        im = im.filter(ImageFilter.DETAIL)
+    if 'sharpen' in opts:
+        im = im.filter(ImageFilter.SHARPEN)
+    return im
+filters.valid_options = ('detail', 'sharpen')

sorl/thumbnail/templatetags/__init__.py

Empty file added.

sorl/thumbnail/templatetags/thumbnail.py

+import re
+import math
+from django.template import Library, Node, VariableDoesNotExist, \
+    TemplateSyntaxError
+from sorl.thumbnail.main import DjangoThumbnail, get_thumbnail_setting
+from sorl.thumbnail.processors import dynamic_import, get_valid_options
+from sorl.thumbnail.utils import split_args
+
+register = Library()
+
+size_pat = re.compile(r'(\d+)x(\d+)$')
+
+filesize_formats = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
+filesize_long_formats = {
+    'k': 'kilo', 'M': 'mega', 'G': 'giga', 'T': 'tera', 'P': 'peta',
+    'E': 'exa', 'Z': 'zetta', 'Y': 'yotta',
+}
+
+try:
+    PROCESSORS = dynamic_import(get_thumbnail_setting('PROCESSORS'))
+    VALID_OPTIONS = get_valid_options(PROCESSORS)
+except:
+    if get_thumbnail_setting('DEBUG'):
+        raise
+    else:
+        PROCESSORS = []
+        VALID_OPTIONS = []
+TAG_SETTINGS = ['quality']
+
+
+class ThumbnailNode(Node):
+    def __init__(self, source_var, size_var, opts=None,
+                 context_name=None, **kwargs):
+        self.source_var = source_var
+        self.size_var = size_var
+        self.opts = opts
+        self.context_name = context_name
+        self.kwargs = kwargs
+
+    def render(self, context):
+        # Note that this isn't a global constant because we need to change the
+        # value for tests.
+        DEBUG = get_thumbnail_setting('DEBUG')
+        try:
+            # A file object will be allowed in DjangoThumbnail class
+            relative_source = self.source_var.resolve(context)
+        except VariableDoesNotExist:
+            if DEBUG:
+                raise VariableDoesNotExist("Variable '%s' does not exist." %
+                        self.source_var)
+            else:
+                relative_source = None
+        try:
+            requested_size = self.size_var.resolve(context)
+        except VariableDoesNotExist:
+            if DEBUG:
+                raise TemplateSyntaxError("Size argument '%s' is not a"
+                        " valid size nor a valid variable." % self.size_var)
+            else:
+                requested_size = None
+        # Size variable can be either a tuple/list of two integers or a valid
+        # string, only the string is checked.
+        else:
+            if isinstance(requested_size, basestring):
+                m = size_pat.match(requested_size)
+                if m:
+                    requested_size = (int(m.group(1)), int(m.group(2)))
+                elif DEBUG:
+                    raise TemplateSyntaxError("Variable '%s' was resolved but "
+                            "'%s' is not a valid size." %
+                            (self.size_var, requested_size))
+                else:
+                    requested_size = None
+        if relative_source is None or requested_size is None:
+            thumbnail = ''
+        else:
+            try:
+                kwargs = {}
+                for key, value in self.kwargs.items():
+                    kwargs[key] = value.resolve(context)
+                opts = dict([(k, v and v.resolve(context))
+                             for k, v in self.opts.items()])
+                thumbnail = DjangoThumbnail(relative_source, requested_size,
+                                opts=opts, processors=PROCESSORS, **kwargs)
+            except:
+                if DEBUG:
+                    raise
+                else:
+                    thumbnail = ''
+        # Return the thumbnail class, or put it on the context
+        if self.context_name is None:
+            return thumbnail
+        # We need to get here so we don't have old values in the context
+        # variable.
+        context[self.context_name] = thumbnail
+        return ''
+
+
+def thumbnail(parser, token):
+    """
+    Creates a thumbnail of for an ImageField.
+
+    To just output the absolute url to the thumbnail::
+
+        {% thumbnail image 80x80 %}
+
+    After the image path and dimensions, you can put any options::
+
+        {% thumbnail image 80x80 quality=95 crop %}
+
+    To put the DjangoThumbnail class on the context instead of just rendering
+    the absolute url, finish the tag with ``as [context_var_name]``::
+
+        {% thumbnail image 80x80 as thumb %}
+        {{ thumb.width }} x {{ thumb.height }}
+    """
+    args = token.split_contents()
+    tag = args[0]
+    # Check to see if we're setting to a context variable.
+    if len(args) > 4 and args[-2] == 'as':
+        context_name = args[-1]
+        args = args[:-2]
+    else:
+        context_name = None
+
+    if len(args) < 3:
+        raise TemplateSyntaxError("Invalid syntax. Expected "
+            "'{%% %s source size [option1 option2 ...] %%}' or "
+            "'{%% %s source size [option1 option2 ...] as variable %%}'" %
+            (tag, tag))
+
+    # Get the source image path and requested size.
+    source_var = parser.compile_filter(args[1])
+    # If the size argument was a correct static format, wrap it in quotes so
+    # that it is compiled correctly.
+    m = size_pat.match(args[2])
+    if m:
+        args[2] = '"%s"' % args[2]
+    size_var = parser.compile_filter(args[2])
+
+    # Get the options.
+    args_list = split_args(args[3:]).items()
+
+    # Check the options.
+    opts = {}
+    kwargs = {} # key,values here override settings and defaults
+
+    for arg, value in args_list:
+        value = value and parser.compile_filter(value)
+        if arg in TAG_SETTINGS and value is not None:
+            kwargs[str(arg)] = value
+            continue
+        if arg in VALID_OPTIONS:
+            opts[arg] = value
+        else:
+            raise TemplateSyntaxError("'%s' tag received a bad argument: "
+                                      "'%s'" % (tag, arg))
+    return ThumbnailNode(source_var, size_var, opts=opts,
+                         context_name=context_name, **kwargs)
+
+
+def filesize(bytes, format='auto1024'):
+    """
+    Returns the number of bytes in either the nearest unit or a specific unit
+    (depending on the chosen format method).
+
+    Acceptable formats are:
+
+    auto1024, auto1000
+      convert to the nearest unit, appending the abbreviated unit name to the
+      string (e.g. '2 KiB' or '2 kB').
+      auto1024 is the default format.
+    auto1024long, auto1000long
+      convert to the nearest multiple of 1024 or 1000, appending the correctly
+      pluralized unit name to the string (e.g. '2 kibibytes' or '2 kilobytes').
+    kB, MB, GB, TB, PB, EB, ZB or YB