Permalink
Browse files

Initial alpha version

  • Loading branch information...
0 parents commit 992dd85c7809c4668e2d61a79b92c44f9601e9ab @SmileyChris committed Oct 8, 2009
25 LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2009, 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 easy-thumbnails 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.
77 README
@@ -0,0 +1,77 @@
+===============
+Easy Thumbnails
+===============
+
+The powerful, yet easy to implement thumbnailing application for Django.
+
+To install this application into your project, just add it to your
+``INSTALLED_APPS`` setting::
+
+ INSTALLED_APPS = (
+ ...
+ 'easy_thumbnails',
+ )
+
+
+Template usage
+==============
+
+To generate thumbnails in your template, use the ``{% thumbnail %}`` tag. To
+make this tag available for use in your template, use::
+
+ {% load thumbnails %}
+
+Basic tag Syntax::
+
+ {% thumbnail [source] [size] [options] %}
+
+*source* must be a ``File`` object, usually an Image/FileField of a model
+instance.
+
+*size* can either be:
+
+* the size in the format ``[width]x[height]`` (for example,
+ ``{% thumbnail person.photo 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 person.photo size_var %}``.
+
+*options* are a space separated list of options which are used when processing
+the image to a thumbnail such as ``sharpen``, ``crop`` and ``quality=90``.
+
+
+Model usage
+===========
+
+You can use the ``ThumbnailerField`` or ``ThumbnailerImageField`` fields (based
+on ``FileField`` and ``ImageField``, respectively) for easier access to
+retrieve (or generate) thumbnail images.
+
+Lower level usage
+=================
+
+Thumbnails are generated with a ``Thumbnailer`` instance. For example::
+
+ from easy_thumbnails import Thumbnailer
+
+ def square_thumbnail(source):
+ thumbnail_options = dict(size=(100, 100), crop=True, bw=True)
+ return Thumbnailer(source).get_thumbnail(thumbnail_options)
+
+By default, ``get_thumbnail`` saves the file (using file storage). The source
+file used to instanciate the ``Thumbnailer`` must have a ``name`` instance
+relative to the storage root.
+
+The ``ThumbnailFile`` object provided makes this easy::
+
+ from easy_thumbnails import ThumbnailFile
+
+ # For an existing file in storage:
+ source = ThumbnailFile('animals/aarvark.jpg')
+ square_thumbnail(source)
+
+ # For a new file:
+ picture = open('/home/zookeeper/pictures/my_anteater.jpg')
+ source = ThumbnailFile('animals/anteater.jpg', file=picture)
+ square_thumbnail(source)
1 easy_thumbnails/__init__.py
@@ -0,0 +1 @@
+from files import Thumbnailer, ThumbnailFile
19 easy_thumbnails/defaults.py
@@ -0,0 +1,19 @@
+DEBUG = False
+
+DEFAULT_STORAGE = 'easy_thumbnails.storage.ThumbnailFileSystemStorage'
+MEDIA_ROOT = ''
+MEDIA_URL = ''
+
+BASEDIR = ''
+SUBDIR = ''
+PREFIX = ''
+
+QUALITY = 85
+EXTENSION = 'jpg'
+PROCESSORS = (
+ 'easy_thumbnails.processors.colorspace',
+ 'easy_thumbnails.processors.autocrop',
+ 'easy_thumbnails.processors.scale_and_crop',
+ 'easy_thumbnails.processors.filters',
+)
+IMAGEMAGICK_FILE_TYPES = ('eps', 'pdf', 'psd')
83 easy_thumbnails/engine.py
@@ -0,0 +1,83 @@
+from easy_thumbnails import defaults, utils
+from django.core.files.base import ContentFile
+import os
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+DEFAULT_PROCESSORS = [utils.dynamic_import(p)
+ for p in utils.get_setting('PROCESSORS')]
+
+
+def process_image(source, processor_options, processors=None):
+ """
+ Process a source PIL image through a series of image processors, returning
+ the (potentially) altered image.
+
+ """
+ if processors is None:
+ processors = DEFAULT_PROCESSORS
+ image = source
+ for processor in processors:
+ image = processor(image, **processor_options)
+ return image
+
+
+def save_image(image, destination=None, format='JPEG', quality=85):
+ """
+ Save a PIL image.
+
+ """
+ if destination is None:
+ destination = StringIO()
+ try:
+ image.save(destination, format=format, quality=quality, optimize=1)
+ except IOError:
+ # Try again, without optimization (PIL can't optimize an image
+ # larger than ImageFile.MAXBLOCK, which is 64k by default)
+ image.save(destination, format=format, quality=quality)
+ if hasattr(destination, 'seek'):
+ destination.seek(0)
+ return destination
+
+
+def get_filetype(filename, is_path=False):
+ """
+ Return the standardized extension based on the ``filename``.
+
+ If ``is_path`` is True, try using imagemagick to determine the file type
+ rather than just relying on the filename extension.
+
+ """
+ if is_path:
+ filetype = get_filetype_magic(filename)
+ if not is_path or not filetype:
+ filetype = os.path.splitext(filename)[1].lower()[:1]
+ if filetype == 'jpeg':
+ filetype = 'jpg'
+ return filetype
+
+
+def get_filetype_magic(path):
+ """
+ Return a standardized extention by using imagemagick (or ``None`` if
+ imagemagick can not be imported).
+
+ """
+ try:
+ import magic
+ except ImportError:
+ return None
+
+ m = magic.open(magic.MAGIC_NONE)
+ m.load()
+ filetype = m.file(path)
+ if filetype.find('Microsoft Office Document') != -1:
+ return 'doc'
+ elif filetype.find('PDF document') != -1:
+ return 'pdf'
+ elif filetype.find('JPEG') != -1:
+ return 'jpg'
+ return filetype
66 easy_thumbnails/fields.py
@@ -0,0 +1,66 @@
+from django.db.models.fields.files import FileField, ImageField
+from easy_thumbnails import files
+
+
+class ThumbnailerField(FileField):
+ """
+ A file field which provides easier access for retrieving (and generating)
+ thumbnails.
+
+ To use a different file storage for thumbnails, provide the
+ ``thumbnail_storage`` keyword argument.
+
+ """
+ attr_class = files.ThumbnailerFieldFile
+
+ def __init__(self, *args, **kwargs):
+ # Arguments not explicitly defined so that the normal ImageField
+ # positional arguments can be used.
+ self.thumbnail_storage = kwargs.pop('thumbnail_storage', None)
+
+ super(ThumbnailerField, self).__init__(*args, **kwargs)
+
+ def south_field_triple(self):
+ """
+ Return a suitable description of this field for South.
+
+ """
+ from south.modelsinspector import introspector
+ field_class = 'django.db.models.fields.files.FileField'
+ args, kwargs = introspector(FileField)
+ return (field_class, args, kwargs)
+
+
+class ThumbnailerImageField(ThumbnailerField, ImageField):
+ """
+ An image field which provides easier access for retrieving (and generating)
+ thumbnails.
+
+ To use a different file storage for thumbnails, provide the
+ ``thumbnail_storage`` keyword argument.
+
+ To thumbnail the original source image before saving, provide the
+ ``resize_source`` keyword argument, passing it a usual thumbnail option
+ dictionary. For example::
+
+ ThumbnailField(..., resize_source=dict(size=(100, 100), sharpen=True))
+
+ """
+ attr_class = files.ThumbnailerImageFieldFile
+
+ def __init__(self, *args, **kwargs):
+ # Arguments not explicitly defined so that the normal ImageField
+ # positional arguments can be used.
+ self.resize_source = kwargs.pop('resize_source', None)
+
+ super(ThumbnailerImageField, self).__init__(*args, **kwargs)
+
+ def south_field_triple(self):
+ """
+ Return a suitable description of this field for South.
+
+ """
+ from south.modelsinspector import introspector
+ field_class = 'django.db.models.fields.files.ImageField'
+ args, kwargs = introspector(ImageField)
+ return (field_class, args, kwargs)
376 easy_thumbnails/files.py
@@ -0,0 +1,376 @@
+from PIL import Image
+from django.core.files.base import File, ContentFile
+from django.core.files.storage import get_storage_class, default_storage
+from django.db.models.fields.files import ImageFieldFile, FieldFile
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from easy_thumbnails import engine, utils
+import os
+
+
+DEFAULT_THUMBNAIL_STORAGE = get_storage_class(
+ utils.get_setting('DEFAULT_STORAGE'))()
+print DEFAULT_THUMBNAIL_STORAGE.base_url
+
+
+def get_thumbnailer(source, relative_name=None):
+ """
+ Get a thumbnailer for a source file.
+
+ """
+ if isinstance(source, Thumbnailer):
+ return source
+ elif isinstance(source, FieldFile):
+ if not relative_name:
+ relative_name = source.name
+ return ThumbnailerFieldFile(source.instance, source.field,
+ relative_name)
+ elif isinstance(source, File):
+ return Thumbnailer(source.file, relative_name)
+ raise TypeError('The source object must either be a Thumbnailer, a '
+ 'FieldFile or a File with the relative_name argument '
+ 'provided.')
+
+
+def save_thumbnail(thumbnail_file, storage):
+ """
+ Save a thumbnailed file.
+
+ """
+ filename = thumbnail_file.name
+ if storage.exists(filename):
+ try:
+ storage.delete(filename)
+ except:
+ pass
+ return storage.save(filename, thumbnail_file)
+
+
+class FakeField(object):
+ name = 'fake'
+
+ def __init__(self, storage=None):
+ self.storage = storage or default_storage
+
+ def generate_filename(self, instance, name, *args, **kwargs):
+ return name
+
+
+class FakeInstance(object):
+ def save(self, *args, **kwargs):
+ pass
+
+
+class ThumbnailFile(ImageFieldFile):
+ """
+ A thumbnailed file.
+
+ """
+ def __init__(self, name, file=None, storage=None, *args, **kwargs):
+ fake_field = FakeField(storage=storage)
+ super(ThumbnailFile, self).__init__(FakeInstance(), fake_field, name,
+ *args, **kwargs)
+ if file:
+ self.file = file
+
+ def _get_image(self):
+ """
+ Get a PIL image instance of this file.
+
+ The image is cached to avoid the file needing to be read again if the
+ function is called again.
+
+ """
+ if not hasattr(self, '_image_cache'):
+ self.image = Image.open(self)
+ return self._image_cache
+
+ def _set_image(self, image):
+ """
+ Set the image for this file.
+
+ This also caches the dimensions of the image.
+
+ """
+ if image:
+ self._image_cache = image
+ self._dimensions_cache = image.size
+ else:
+ if hasattr(self, '_image_cache'):
+ del self._cached_image
+ if hasattr(self, '_dimensions_cache'):
+ del self._dimensions_cache
+
+ image = property(_get_image, _set_image)
+
+ def tag(self, alt='', use_size=True, **attrs):
+ """
+ Return a standard XHTML ``<img ... />`` tag for this field.
+
+ """
+ attrs['alt'] = escape(alt)
+ attrs['src'] = escape(self.url)
+ if use_size:
+ attrs.update(dict(width=self.width, height=self.height))
+ attrs = ' '.join(['%s="%s"' % (key, escape(value))
+ for key, value in attrs.items()])
+ return mark_safe('<img %s />' % attrs)
+
+ tag = property(tag)
+
+ def _get_file(self):
+ self._require_file()
+ if not hasattr(self, '_file') or self._file is None:
+ self._file = self.storage.open(self.name, 'rb')
+ return self._file
+
+ def _set_file(self, file):
+ self._file = file
+
+ def _del_file(self):
+ del self._file
+
+ file = property(_get_file, _set_file, _del_file)
+
+
+class Thumbnailer(File):
+ """
+ A file-like object which provides some methods to generate thumbnail
+ images.
+
+ """
+ thumbnail_basedir = utils.get_setting('BASEDIR')
+ thumbnail_subdir = utils.get_setting('SUBDIR')
+ thumbnail_prefix = utils.get_setting('PREFIX')
+ thumbnail_quality = utils.get_setting('QUALITY')
+ thumbnail_extension = utils.get_setting('EXTENSION')
+
+ def __init__(self, file, name=None, source_storage=None,
+ thumbnail_storage=None, *args, **kwargs):
+ super(Thumbnailer, self).__init__(file, name, *args, **kwargs)
+ self.source_storage = source_storage or default_storage
+ self.thumbnail_storage = (thumbnail_storage or
+ DEFAULT_THUMBNAIL_STORAGE)
+
+ def generate_thumbnail(self, thumbnail_options):
+ """
+ Return a ``ThumbnailFile`` containing a thumbnail image.
+
+ The thumbnail image is generated using the ``thumbnail_options``
+ dictionary.
+
+ """
+ thumbnail_image = engine.process_image(self.image, thumbnail_options)
+ quality = thumbnail_options.get('quality', self.thumbnail_quality)
+ data = engine.save_image(thumbnail_image, quality=quality).read()
+
+ filename = self.get_thumbnail_name(thumbnail_options)
+ thumbnail = ThumbnailFile(filename, ContentFile(data))
+ thumbnail.image = thumbnail_image
+ thumbnail._committed = False
+
+ return thumbnail
+
+ def get_thumbnail_name(self, thumbnail_options):
+ """
+ Return a thumbnail filename for the given ``thumbnail_options``
+ dictionary and ``source_name`` (which defaults to the File's ``name``
+ if not provided).
+
+ """
+ path, source_filename = os.path.split(self.name)
+ source_extension = os.path.splitext(source_filename)[1][1:]
+ filename = '%s%s' % (self.thumbnail_prefix, source_filename)
+ extension = (self.thumbnail_extension or source_extension.lower()
+ or 'jpg')
+
+ thumbnail_options = thumbnail_options.copy()
+ size = tuple(thumbnail_options.pop('size'))
+ quality = thumbnail_options.pop('quality', self.thumbnail_quality)
+ initial_opts = ['%sx%s' % size, 'q%s' % quality]
+
+ opts = thumbnail_options.items()
+ opts.sort() # Sort the options so the file name is consistent.
+ opts = ['%s' % (v is not True and '%s-%s' % (k, v) or k)
+ for k, v in opts if v]
+
+ all_opts = '_'.join(initial_opts + opts)
+
+ data = {'opts': all_opts}
+ basedir = self.thumbnail_basedir % data
+ subdir = self.thumbnail_subdir % data
+
+ filename_parts = [filename]
+ if ('%(opts)s' in self.thumbnail_basedir or
+ '%(opts)s' in self.thumbnail_subdir):
+ if extension != source_extension:
+ filename_parts.append(extension)
+ else:
+ filename_parts += [all_opts, extension]
+ filename = '.'.join(filename_parts)
+
+ return os.path.join(basedir, path, subdir, filename)
+
+ def get_thumbnail(self, thumbnail_options, save=True):
+ """
+ Return a ``ThumbnailFile`` containing a thumbnail.
+
+ It the file already exists, it will simply be returned.
+
+ Otherwise a new thumbnail image is generated using the
+ ``thumbnail_options`` dictionary. If the ``save`` argument is ``True``
+ (default), the generated thumbnail will be saved too.
+
+ """
+ name = self.get_thumbnail_name(thumbnail_options)
+
+ if self.thumbnail_exists(thumbnail_options):
+ thumbnail = ThumbnailFile(name=name,
+ storage=self.thumbnail_storage)
+ return thumbnail
+
+ thumbnail = self.generate_thumbnail(thumbnail_options)
+
+ if save and self.name:
+ save_thumbnail(thumbnail, self.thumbnail_storage)
+ if self.source_storage != self.thumbnail_storage:
+ # If the source storage is local and the thumbnail storage is
+ # remote, save a copy of the thumbnail there too. This helps to
+ # keep the testing of thumbnail existence as a local activity.
+ try:
+ self.thumbnail_storage.path(name)
+ except NotImplementedError:
+ try:
+ self.field_storage.path(name)
+ except NotImplementedError:
+ pass
+ else:
+ self.save_thumbnail(thumbnail, self.field_storage)
+ return thumbnail
+
+ def thumbnail_exists(self, thumbnail_options):
+ """
+ Calculate whether the thumbnail already exists and that the source is
+ not newer than the thumbnail.
+
+ If neither the source nor the thumbnail are using local storages, only
+ the existance of the thumbnail will be checked.
+
+ """
+ filename = self.get_thumbnail_name(thumbnail_options)
+
+ try:
+ source_path = self.source_storage.path(self.name)
+ except NotImplementedError:
+ source_path = None
+ try:
+ thumbnail_path = self.thumbnail_storage.path(filename)
+ except NotImplementedError:
+ thumbnail_path = None
+
+ if not source_path and not thumbnail_path:
+ # This is the worst-case scenario - neither storage was local so
+ # this will cause a remote existence check.
+ return self.thumbnail_storage.exists(filename)
+
+ # If either storage wasn't local, use the other for the path.
+ if not source_path:
+ source_path = self.thumbnail_storage.path(self.name)
+ if not thumbnail_path:
+ thumbnail_path = self.source_storage.path(filename)
+
+ if os.path.isfile(thumbnail_path):
+ if not os.path.isfile(source_path):
+ return True
+ else:
+ return False
+ return (os.path.getmtime(source_path) <=
+ os.path.getmtime(thumbnail_path))
+
+ def _image(self):
+ if not hasattr(self, '_cached_image'):
+ # TODO: Use different methods of generating the file, rather than
+ # just relying on PIL.
+ self._cached_image = Image.open(self)
+ # Image.open() is a lazy operation, so force the load so we
+ # can close this file again if appropriate.
+ self._cached_image.load()
+ return self._cached_image
+
+ image = property(_image)
+
+
+class ThumbnailerFieldFile(FieldFile, Thumbnailer):
+ """
+ A field file which provides some methods for generating (and returning)
+ thumbnail images.
+
+ """
+ def __init__(self, *args, **kwargs):
+ super(ThumbnailerFieldFile, self).__init__(*args, **kwargs)
+ self.source_storage = self.field.storage
+ thumbnail_storage = getattr(self.field, 'thumbnail_storage', None)
+ if thumbnail_storage:
+ self.thumbnail_storage = thumbnail_storage
+
+ def save(self, name, content, *args, **kwargs):
+ """
+ Save the file.
+
+ If the thumbnail storage is local and differs from the field storage,
+ save a place-holder of the source file there too. This helps to keep
+ the testing of thumbnail existence as a local activity.
+
+ """
+ super(ThumbnailerFieldFile, self).save(name, content, *args, **kwargs)
+ # If the thumbnail storage differs and is local, save a place-holder of
+ # the source file there too.
+ if self.thumbnail_storage != self.field.storage:
+ try:
+ path = self.thumbnail_storage.path(self.name)
+ except NotImplementedError:
+ pass
+ else:
+ if not os.path.exists(path):
+ try:
+ os.makedirs(os.path.dirname(path))
+ except OSError:
+ pass
+ open(path, 'w').close()
+
+# TODO: deletion should use the storage for listing and deleting.
+# def delete(self, *args, **kwargs):
+# """
+# Delete the image, along with any thumbnails which match the filename
+# pattern for this source image.
+#
+# """
+# super(ThumbnailFieldFile, self).delete(*args, **kwargs)
+
+
+class ThumbnailerImageFieldFile(ImageFieldFile, ThumbnailerFieldFile):
+ """
+ A field file which provides some methods for generating (and returning)
+ thumbnail images.
+
+ """
+ def save(self, name, content, *args, **kwargs):
+ """
+ Save the image.
+
+ If the thumbnail storage is local and differs from the field storage,
+ save a place-holder of the source image there too. This helps to keep
+ the testing of thumbnail existence as a local activity.
+
+ The image will be resized down using a ``ThumbnailField`` if
+ ``resize_source`` (a dictionary of thumbnail options) is provided by
+ the field.
+
+ """
+ options = getattr(self.field, 'resize_source', None)
+ if options:
+ if not 'quality' in options:
+ options['quality'] = self.thumbnail_quality
+ content = Thumbnailer(content).generate_thumbnail(options)
+ super(ThumbnailerImageFieldFile, self).save(name, content, *args,
+ **kwargs)
1 easy_thumbnails/models.py
@@ -0,0 +1 @@
+# Needs a models.py file so that tests are picked up.
101 easy_thumbnails/processors.py
@@ -0,0 +1,101 @@
+from PIL import Image, ImageFilter, ImageChops
+from easy_thumbnails import utils
+import re
+
+
+def colorspace(im, bw=False, **kwargs):
+ if bw and im.mode != 'L':
+ im = im.convert('L')
+ elif im.mode not in ('L', 'RGB', 'RGBA'):
+ im = im.convert('RGB')
+ return im
+
+
+def autocrop(im, autocrop=False, **kwargs):
+ if autocrop:
+ bw = im.convert('1')
+ bw = bw.filter(ImageFilter.MedianFilter)
+ # White background.
+ bg = Image.new('1', im.size, 255)
+ diff = ImageChops.difference(bw, bg)
+ bbox = diff.getbbox()
+ if bbox:
+ im = im.crop(bbox)
+ return im
+
+
+def scale_and_crop(im, size, crop=False, upscale=False, **kwargs):
+ x, y = [float(v) for v in im.size]
+ xr, yr = [float(v) for v in size]
+
+ if crop:
+ r = max(xr / x, yr / y)
+ else:
+ r = min(xr / x, yr / y)
+
+ if r < 1.0 or (r > 1.0 and upscale):
+ im = im.resize((int(x * r), int(y * r)), resample=Image.ANTIALIAS)
+
+ 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(v) for v in box])
+ return im
+
+
+def filters(im, detail=False, sharpen=False, **kwargs):
+ if detail:
+ im = im.filter(ImageFilter.DETAIL)
+ if sharpen:
+ im = im.filter(ImageFilter.SHARPEN)
+ return im
18 easy_thumbnails/storage.py
@@ -0,0 +1,18 @@
+from django.core.files.storage import FileSystemStorage
+from easy_thumbnails import utils
+
+
+class ThumbnailFileSystemStorage(FileSystemStorage):
+ """
+ Standard file system storage.
+
+ The default ``location`` and ``base_url`` are set to
+ ``THUMBNAIL_MEDIA_ROOT`` and ``THUMBNAIL_MEDIA_URL``, falling back to the
+ standard ``MEDIA_ROOT`` and ``MEDIA_URL`` if the custom settings are blank.
+
+ """
+ def __init__(self, location=None, base_url=None, *args, **kwargs):
+ location = utils.get_setting('MEDIA_ROOT', override=location) or None
+ base_url = utils.get_setting('MEDIA_URL', override=base_url) or None
+ super(ThumbnailFileSystemStorage, self).__init__(location, base_url,
+ *args, **kwargs)
0 easy_thumbnails/templatetags/__init__.py
No changes.
159 easy_thumbnails/templatetags/thumbnails.py
@@ -0,0 +1,159 @@
+from django.template import Library, Node, VariableDoesNotExist, \
+ TemplateSyntaxError
+from easy_thumbnails import utils
+from easy_thumbnails.files import get_thumbnailer
+from django.utils.html import escape
+import re
+
+register = Library()
+
+RE_SIZE = re.compile(r'(\d+)x(\d+)$')
+
+VALID_OPTIONS = utils.valid_processor_options()
+VALID_OPTIONS.remove('size')
+
+
+def split_args(args):
+ """
+ Split a list of argument strings into a dictionary where each key is an
+ argument name.
+
+ An argument looks like ``crop``, ``crop="some option"`` or ``crop=my_var``.
+ Arguments which provide no value get a value of ``True``.
+
+ """
+ args_dict = {}
+ for arg in args:
+ split_arg = arg.split('=', 1)
+ if len(split_arg) > 1:
+ value = split_arg[1]
+ else:
+ value = True
+ args_dict[split_arg[0]] = value
+ return args_dict
+
+
+class ThumbnailNode(Node):
+ def __init__(self, source_var, opts, context_name=None):
+ self.source_var = source_var
+ self.opts = opts
+ self.context_name = context_name
+
+ def render(self, context):
+ # Note that this isn't a global constant because we need to change the
+ # value for tests.
+ raise_errors = utils.get_setting('DEBUG')
+ # Get the source file.
+ try:
+ source = self.source_var.resolve(context)
+ except VariableDoesNotExist:
+ if raise_errors:
+ raise VariableDoesNotExist("Variable '%s' does not exist." %
+ self.source_var)
+ return self.bail_out(context)
+ # Resolve the thumbnail option values.
+ try:
+ opts = {}
+ for key, value in self.opts.iteritems():
+ if hasattr(value, 'resolve'):
+ value = value.resolve(context)
+ opts[str(key)] = value
+ except:
+ if raise_errors:
+ raise
+ return self.bail_out(context)
+ # Size variable can be either a tuple/list of two integers or a
+ # valid string, only the string is checked.
+ size = opts['size']
+ if isinstance(size, basestring):
+ m = RE_SIZE.match(size)
+ if m:
+ opts['size'] = (int(m.group(1)), int(m.group(2)))
+ else:
+ if raise_errors:
+ raise TemplateSyntaxError("Variable '%s' was resolved "
+ "but '%s' is not a valid size." %
+ (self.size_var, size))
+ return self.bail_out(context)
+
+ try:
+ thumbnail = get_thumbnailer(source).get_thumbnail(opts)
+ except:
+ if raise_errors:
+ raise
+ return self.bail_out(context)
+ # Return the thumbnail file url, or put the file on the context.
+ if self.context_name is None:
+ return escape(thumbnail.url)
+ else:
+ context[self.context_name] = thumbnail
+ return ''
+
+ def bail_out(self, context):
+ if self.context_name:
+ context[self.context_name] = ''
+ return ''
+
+
+def thumbnail(parser, token):
+ """
+ Creates a thumbnail of 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 sharpen %}
+
+ To put the ThumbnailedField instance on the context rather than simply
+ rendering the 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))
+
+ opts = {}
+
+ # The first argument is the source file.
+ source_var = parser.compile_filter(args[1])
+
+ # The second argument is the requested size. If it's the static "10x10"
+ # format, wrap it in quotes so that it is compiled correctly.
+ size = args[2]
+ match = RE_SIZE.match(size)
+ if match:
+ size = '"%s"' % size
+ opts['size'] = parser.compile_filter(size)
+
+ # All further arguments are options.
+ args_list = split_args(args[3:]).items()
+ for arg, value in args_list:
+ if arg in VALID_OPTIONS:
+ if value and value is not True:
+ value = parser.compile_filter(value)
+ opts[arg] = value
+ else:
+ raise TemplateSyntaxError("'%s' tag received a bad argument: "
+ "'%s'" % (tag, arg))
+ return ThumbnailNode(source_var, opts=opts, context_name=context_name)
+
+
+register.tag(thumbnail)
73 easy_thumbnails/utils.py
@@ -0,0 +1,73 @@
+from django.conf import settings
+from easy_thumbnails import defaults
+import inspect
+import math
+
+
+def image_entropy(im):
+ """
+ Calculate the entropy of an image. Used for "smart cropping".
+
+ """
+ hist = im.histogram()
+ hist_size = float(sum(hist))
+ hist = [h / hist_size for h in hist]
+ return -sum([p * math.log(p, 2) for p in hist if p != 0])
+
+
+def dynamic_import(import_string):
+ """
+ Dynamically import a module or object.
+
+ """
+ # Use rfind rather than rsplit for Python 2.3 compatibility.
+ lastdot = import_string.rfind('.')
+ if lastdot == -1:
+ return __import__(import_string, {}, {}, [])
+ module_name, attr = import_string[:lastdot], import_string[lastdot + 1:]
+ parent_module = __import__(module_name, {}, {}, [attr])
+ return getattr(parent_module, attr)
+
+
+def valid_processor_options(processors=None):
+ """
+ Return a list of unique valid options for a list of image processors.
+
+ """
+ if processors is None:
+ processors = [dynamic_import(p) for p in
+ get_setting('PROCESSORS')]
+ valid_options = set(['size', 'quality'])
+ for processor in processors:
+ args = inspect.getargspec(processor)[0]
+ # Add all arguments apart from the first (the source image).
+ valid_options.update(args[1:])
+ return list(valid_options)
+
+
+def get_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 is_storage_local(storage):
+ """
+ Check to see if a file storage is local.
+
+ """
+ try:
+ storage.path('test')
+ except NotImplementedError:
+ return False
+ return True
39 setup.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+from distutils.core import setup
+
+
+VERSION = '1.0a'
+
+README_FILE = open('README')
+try:
+ long_description = README_FILE.read()
+finally:
+ README_FILE.close()
+
+
+setup(
+ name='easy-thumbnails',
+ version=VERSION,
+ #url='',
+ #download_url='' % VERSION,
+ description='Easy thumbnails for Django',
+ long_description=long_description,
+ author='Chris Beaven',
+ email='smileychris@gmail.com',
+ platforms=['any'],
+ packages=[
+ 'easy_thumbnails',
+ 'easy_thumbnails.templatetags',
+ ],
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ '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',
+ ],
+)

0 comments on commit 992dd85

Please sign in to comment.