diff --git a/AUTHORS b/AUTHORS index 4545012..ed2df28 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,4 +16,5 @@ Contributors: * Jonathan Slenders (jonathanslenders) * Matthew Tretter (matthewwithanm) * Markus Kaiserswerth (mkai) +* Ryan Bagwell (ryanbagwell) diff --git a/README.rst b/README.rst index 28b543e..e82b37e 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,8 @@ django-imagekit =============== +NOTE: This, the **class-based** version of ImageKit, has been discontinued. + ImageKit In 7 Steps =================== @@ -61,7 +63,7 @@ Create your specifications. width = 600 # now let's create an adjustment processor to enhance the image at small sizes - class EnchanceThumb(processors.Adjustment): + class EnhanceThumb(processors.Adjustment): contrast = 1.2 sharpness = 1.1 @@ -70,7 +72,7 @@ Create your specifications. quality = 90 # defaults to 70 access_as = 'thumbnail_image' pre_cache = True - processors = [ResizeThumb, EnchanceThumb] + processors = [ResizeThumb, EnhanceThumb] # and our display spec class Display(ImageSpec): diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..1dc0357 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @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 $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @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.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/ImageKit" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ImageKit" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..5b21fe1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# +# ImageKit documentation build configuration file, created by +# sphinx-quickstart on Fri Apr 1 12:20:11 2011. +# +# 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.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# 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'] + +# 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-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'ImageKit' +copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman & Chris Drackett' + +# 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' +# The full version, including alpha/beta/rc tags. +release = '0.3.6' + +# 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 patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_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. See the documentation for +# a list of builtin themes. +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 +# " v 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ImageKitdoc' + + +# -- 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', 'ImageKit.tex', u'ImageKit Documentation', + u'Justin Driscoll, Bryan Veloso, Greg Newman \\& Chris Drackett', '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 + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# 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 = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (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) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (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', + 'ImageKit', 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# 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/' + ) +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e66d8c6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +ImageKit +======== + +ImageKit brings automated image processing to your Django models. With an API +similar to that of Django's :ref:`models.Meta ` options +class and the creation of a specification file, you'll be able to create an +infinite number of renditions for any image using any number of preprocessors. + +Contents: + +.. toctree:: + :maxdepth: 2 + + tutorial + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..c3a6092 --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,141 @@ +.. _ref-tutorial: + +ImageKit in 7 Steps +=================== + +Step 1 +****** + +:: + + $ pip install django-imagekit + +(or clone the source and put the imagekit module on your path) + +Step 2 +****** + +Add ImageKit to your models. + +:: + + # myapp/models.py + + from django.db import models + from imagekit.models import ImageModel + + class Photo(ImageModel): + name = models.CharField(max_length=100) + original_image = models.ImageField(upload_to='photos') + num_views = models.PositiveIntegerField(editable=False, default=0) + + 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' + +Step 3 +****** + +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 EnchanceThumb(processors.Adjustment): + contrast = 1.2 + sharpness = 1.1 + + # now we can define our thumbnail spec + class Thumbnail(ImageSpec): + access_as = 'thumbnail_image' + pre_cache = True + processors = [ResizeThumb, EnchanceThumb] + + # and our display spec + class Display(ImageSpec): + 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. + +:: + +
+ {{ photo.name }} +
+ +
+ {{ photo.name }} +
+ +
+ {% for p in photos %} + {{ p.name }} + {% endfor %} +
+ +Step 6 +****** + +Play with the API. + +:: + + >>> from myapp.models import Photo + >>> p = Photo.objects.all()[0] + + >>> p.display.url + u'/static/photos/myphoto_display.jpg' + >>> p.display.width + 600 + >>> p.display.height + 420 + >>> p.display.image + + >>> p.display.file + + >>> p.display.spec + + +Step 7 +****** + +Enjoy a nice beverage. + +:: + + from refrigerator import beer + + beer.enjoy() + + diff --git a/imagekit/__init__.py b/imagekit/__init__.py index baf8eca..8993c70 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,11 +1,13 @@ """ Django ImageKit -Author: Justin Driscoll -Version: 0.4.0 +:author: Justin Driscoll +:maintainer: Bryan Veloso +:license: BSD """ +__title__ = 'django-imagekit' +__version__ = '0.4.1' +__build__ = 0x000400 __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett' -__version__ = (0, 4, 0) - - +__license__ = 'BSD' diff --git a/imagekit/defaults.py b/imagekit/defaults.py index 5e6c249..c3475f6 100644 --- a/imagekit/defaults.py +++ b/imagekit/defaults.py @@ -3,23 +3,28 @@ from imagekit.specs import ImageSpec from imagekit import processors + class ResizeThumbnail(processors.Resize): width = 100 height = 50 crop = True + class EnhanceSmall(processors.Adjustment): contrast = 1.2 sharpness = 1.1 + class SampleReflection(processors.Reflection): size = 0.5 background_color = "#000000" + class PNGFormat(processors.Format): format = 'PNG' extension = 'png' + class DjangoAdminThumbnail(ImageSpec): access_as = 'admin_thumbnail' processors = [ResizeThumbnail, EnhanceSmall, SampleReflection, PNGFormat] diff --git a/imagekit/lib.py b/imagekit/lib.py index 06c0e4d..092cb5c 100644 --- a/imagekit/lib.py +++ b/imagekit/lib.py @@ -1,17 +1,13 @@ # Required PIL classes may or may not be available from the root namespace # depending on the installation method used. try: - import Image - import ImageFile - import ImageFilter - import ImageEnhance - import ImageColor + from PIL import Image, ImageColor, ImageEnhance, ImageFile, ImageFilter except ImportError: try: - from PIL import Image - from PIL import ImageFile - from PIL import ImageFilter - from PIL import ImageEnhance - from PIL import ImageColor + import Image + import ImageColor + import ImageEnhance + import ImageFile + import ImageFilter except ImportError: raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.') diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index ffda42b..792112f 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -14,6 +14,7 @@ class Command(BaseCommand): def handle(self, *args, **options): return flush_cache(args, options) + def flush_cache(apps, options): """ Clears the image cache @@ -25,7 +26,7 @@ def flush_cache(apps, options): models = [m for m in cache.get_models(app) if issubclass(m, ImageModel)] for model in models: print 'Flushing cache for "%s.%s"' % (app_label, model.__name__) - for obj in model.objects.order_by('-id'): + 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: diff --git a/imagekit/models.py b/imagekit/models.py index 3241d09..48a3ce6 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -1,5 +1,3 @@ -import os -from datetime import datetime from django.conf import settings from django.core.files.base import ContentFile from django.db import models @@ -9,7 +7,7 @@ from django.utils.html import conditional_escape as escape from django.utils.translation import ugettext_lazy as _ from imagekit import specs -from imagekit.lib import * +from imagekit.lib import Image, ImageFile from imagekit.options import Options from imagekit.utils import img_to_fobj @@ -156,5 +154,3 @@ class ImageModel(models.Model): post_delete.connect(ImageModel.clear_cache, sender=ImageModel) - - diff --git a/imagekit/options.py b/imagekit/options.py index 7170799..f3d3820 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -14,7 +14,7 @@ class Options(object): cache_dir = 'cache' save_count_as = None cache_filename_fields = ['pk', ] - cache_filename_format = "%(filename)s_%(specname)s.%(extension)s" + cache_filename_format = "%(filename)s_%(specname)s_%(original_extension)s.%(extension)s" admin_thumbnail_spec = 'admin_thumbnail' spec_module = 'imagekit.defaults' specs = None diff --git a/imagekit/processors.py b/imagekit/processors.py index 4988f60..226fcfc 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -1,12 +1,13 @@ """ 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. +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 * +from imagekit.lib import Image, ImageEnhance, ImageColor + class ImageProcessor(object): """ Base image processor class """ @@ -60,8 +61,8 @@ class Reflection(ImageProcessor): # 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 + 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): @@ -99,7 +100,7 @@ class Resize(ImageProcessor): 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) + 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)) @@ -117,15 +118,15 @@ class Resize(ImageProcessor): 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) + ratio = min(float(cls.width) / cur_width, + float(cls.height) / cur_height) else: if cls.width is None: - ratio = float(cls.height)/cur_height + 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))) + 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: diff --git a/imagekit/specs.py b/imagekit/specs.py index 0332187..0a11ffe 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -6,12 +6,14 @@ spec found. """ import os -from StringIO import StringIO -from imagekit import processors -from imagekit.lib import * -from imagekit.utils import img_to_fobj +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 @@ -60,7 +62,7 @@ class Accessor(object): except IOError: return fp.seek(0) - fp = StringIO(fp.read()) + 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()) @@ -82,11 +84,13 @@ class Accessor(object): 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( diff --git a/imagekit/utils.py b/imagekit/utils.py index af8d40f..90cafa2 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -2,8 +2,16 @@ import tempfile + def img_to_fobj(img, format, **kwargs): tmp = tempfile.TemporaryFile() - img.convert('RGB').save(tmp, format, **kwargs) + + # Preserve transparency if the image is in Pallette (P) mode. + if img.mode == 'P': + kwargs['transparency'] = len(img.split()[-1].getcolors()) + else: + img.convert('RGB') + + img.save(tmp, format, **kwargs) tmp.seek(0) return tmp diff --git a/setup.py b/setup.py index 379c771..2e3939f 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,20 @@ #/usr/bin/env python -from distutils.core import setup +import os +import sys +import imagekit + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +if 'publish' in sys.argv: + os.system('python setup.py sdist upload') + sys.exit() setup( name='django-imagekit', - version='0.4.0', + version=imagekit.__version__, description='Automated image processing for Django models.', author='Justin Driscoll', author_email='justin@driscolldev.com', @@ -29,5 +40,3 @@ setup( 'Topic :: Utilities' ] ) - - diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/models.py b/tests/core/models.py new file mode 100644 index 0000000..2f153bd --- /dev/null +++ b/tests/core/models.py @@ -0,0 +1,14 @@ +from django.db import models + +from imagekit.models import ImageModel + + +class TestPhoto(ImageModel): + """ + Minimal ImageModel class for testing. + + """ + image = models.ImageField(upload_to='images') + + class IKOptions: + spec_module = 'core.specs' diff --git a/tests/core/specs.py b/tests/core/specs.py new file mode 100644 index 0000000..5b38cc8 --- /dev/null +++ b/tests/core/specs.py @@ -0,0 +1,28 @@ +from imagekit import processors +from imagekit.specs import ImageSpec + + +class ResizeToWidth(processors.Resize): + width = 100 + +class ResizeToHeight(processors.Resize): + height = 100 + +class ResizeToFit(processors.Resize): + width = 100 + height = 100 + +class ResizeCropped(ResizeToFit): + crop = ('center', 'center') + +class TestResizeToWidth(ImageSpec): + access_as = 'to_width' + processors = [ResizeToWidth] + +class TestResizeToHeight(ImageSpec): + access_as = 'to_height' + processors = [ResizeToHeight] + +class TestResizeCropped(ImageSpec): + access_as = 'cropped' + processors = [ResizeCropped] diff --git a/imagekit/tests.py b/tests/core/tests.py similarity index 65% rename from imagekit/tests.py rename to tests/core/tests.py index eb2ec6c..e96af80 100644 --- a/imagekit/tests.py +++ b/tests/core/tests.py @@ -1,52 +1,18 @@ 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 import processors -from imagekit.models import ImageModel -from imagekit.specs import ImageSpec from imagekit.lib import Image - -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] - -class TestPhoto(ImageModel): - """ Minimal ImageModel class for testing """ - image = models.ImageField(upload_to='images') - - class IKOptions: - spec_module = 'imagekit.tests' +from core.models import TestPhoto class IKTest(TestCase): - """ Base TestCase class """ + """ Base TestCase class. """ def generate_image(self): tmp = tempfile.TemporaryFile() Image.new('RGB', (800, 600)).save(tmp, 'JPEG') @@ -93,7 +59,20 @@ class IKTest(TestCase): self.assertEqual(self.p.to_width.url, "%s%s/%s" % tup) def tearDown(self): - # make sure image file is deleted + # 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)) diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..6d3f37b --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,6 @@ +#!/bin/bash +PYTHONPATH=$PWD:$PWD/..${PYTHONPATH:+:$PYTHONPATH} +export PYTHONPATH + +echo "Running django-imagekit tests..." +django-admin.py test core --settings=settings diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..082adb9 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,24 @@ +import os + +ADMINS = ( + ('test@example.com', 'TEST-R'), +) + +BASE_PATH = os.path.abspath(os.path.dirname(__file__)) + +MEDIA_ROOT = os.path.normpath(os.path.join(BASE_PATH, 'media')) + +DATABASE_ENGINE = 'sqlite3' +DATABASE_NAME = 'imagekit.db' +TEST_DATABASE_NAME = 'imagekit-test.db' + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'imagekit', + 'core', +] + +DEBUG = True +TEMPLATE_DEBUG = DEBUG +CACHE_BACKEND = 'locmem://'