--- /dev/null
+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:
+ 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)
--- /dev/null
+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')
--- /dev/null
+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))
--- /dev/null
+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
--- /dev/null
+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()
--- /dev/null
+# Needs a models.py file so that tests are picked up.
--- /dev/null
+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(x * r), int(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(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')
--- /dev/null
+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
+ convert to the exact unit (using multiples of 1000).
+ KiB, MiB, GiB, TiB, PiB, EiB, ZiB or 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.
+
+ If an invalid format is specified, the bytes are returned unchanged.
+ """
+ format_len = len(format)
+ # Check for valid format
+ if format_len in (2, 3):
+ if format_len == 3 and format[0] == 'K':
+ format = 'k%s' % format[1:]
+ if not format[-1] == 'B' or format[0] not in filesize_formats:
+ return bytes
+ if format_len == 3 and format[1] != 'i':
+ return bytes
+ elif format not in ('auto1024', 'auto1000',
+ 'auto1024long', 'auto1000long'):
+ return bytes
+ # Check for valid bytes
+ try:
+ bytes = long(bytes)
+ except (ValueError, TypeError):
+ return bytes
+
+ # Auto multiple of 1000 or 1024
+ if format.startswith('auto'):
+ if format[4:8] == '1000':
+ base = 1000
+ else:
+ base = 1024
+ logarithm = bytes and math.log(bytes, base) or 0
+ index = min(int(logarithm) - 1, len(filesize_formats) - 1)
+ if index >= 0:
+ if base == 1000:
+ bytes = bytes and bytes / math.pow(1000, index + 1)
+ else:
+ bytes = bytes >> (10 * (index))
+ bytes = bytes and bytes / 1024.0
+ unit = filesize_formats[index]
+ else:
+ # Change the base to 1000 so the unit will just output 'B' not 'iB'
+ base = 1000
+ unit = ''
+ if bytes >= 10 or ('%.1f' % bytes).endswith('.0'):
+ bytes = '%.0f' % bytes
+ else:
+ bytes = '%.1f' % bytes
+ if format.endswith('long'):
+ unit = filesize_long_formats.get(unit, '')
+ if base == 1024 and unit:
+ unit = '%sbi' % unit[:2]
+ unit = '%sbyte%s' % (unit, bytes != '1' and 's' or '')
+ else:
+ unit = '%s%s' % (base == 1024 and unit.upper() or unit,
+ base == 1024 and 'iB' or 'B')
+
+ return '%s %s' % (bytes, unit)
+
+ if bytes == 0:
+ return bytes
+ base = filesize_formats.index(format[0]) + 1
+ # Exact multiple of 1000
+ if format_len == 2:
+ return bytes / (1000.0 ** base)
+ # Exact multiple of 1024
+ elif format_len == 3:
+ bytes = bytes >> (10 * (base - 1))
+ return bytes / 1024.0
+
+
+register.tag(thumbnail)
+register.filter(filesize)
--- /dev/null
+# For these tests to run successfully, two conditions must be met:
+# 1. MEDIA_URL and MEDIA_ROOT must be set in settings
+# 2. The user running the tests must have read/write access to MEDIA_ROOT
+
+# Unit tests:
+from sorl.thumbnail.tests.classes import ThumbnailTest, DjangoThumbnailTest
+from sorl.thumbnail.tests.templatetags import ThumbnailTagTest
+from sorl.thumbnail.tests.fields import FieldTest, \
+ ImageWithThumbnailsFieldTest, ThumbnailFieldTest
+# Doc tests:
+from sorl.thumbnail.tests.utils import utils_tests
+from sorl.thumbnail.tests.templatetags import filesize_tests
+__test__ = {
+ 'utils_tests': utils_tests,
+ 'filesize_tests': filesize_tests,
+}
--- /dev/null
+import unittest
+import os
+from PIL import Image
+from django.conf import settings
+from sorl.thumbnail.base import Thumbnail
+
+try:
+ set
+except NameError:
+ from sets import Set as set # For Python 2.3
+
+
+def get_default_settings():
+ from sorl.thumbnail import defaults
+ def_settings = {}
+ for key in dir(defaults):
+ if key == key.upper() and key not in ['WVPS', 'CONVERT']:
+ def_settings[key] = getattr(defaults, key)
+ return def_settings
+
+
+DEFAULT_THUMBNAIL_SETTINGS = get_default_settings()
+RELATIVE_PIC_NAME = "sorl-thumbnail-test_source.jpg"
+PIC_NAME = os.path.join(settings.MEDIA_ROOT, RELATIVE_PIC_NAME)
+THUMB_NAME = os.path.join(settings.MEDIA_ROOT, "sorl-thumbnail-test_%02d.jpg")
+PIC_SIZE = (800, 600)
+
+
+class ChangeSettings:
+ def __init__(self):
+ self.default_settings = DEFAULT_THUMBNAIL_SETTINGS.copy()
+
+ def change(self, override=None):
+ if override is not None:
+ self.default_settings.update(override)
+ for setting, default in self.default_settings.items():
+ settings_s = 'THUMBNAIL_%s' % setting
+ self_s = 'original_%s' % setting
+ if hasattr(settings, settings_s) and not hasattr(self, self_s):
+ setattr(self, self_s, getattr(settings, settings_s))
+ if hasattr(settings, settings_s) or \
+ default != DEFAULT_THUMBNAIL_SETTINGS[setting]:
+ setattr(settings, settings_s, default)
+
+ def revert(self):
+ for setting in self.default_settings:
+ settings_s = 'THUMBNAIL_%s' % setting
+ self_s = 'original_%s' % setting
+ if hasattr(self, self_s):
+ setattr(settings, settings_s, getattr(self, self_s))
+ delattr(self, self_s)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ self.images_to_delete = set()
+ # Create the test image
+ Image.new('RGB', PIC_SIZE).save(PIC_NAME, 'JPEG')
+ self.images_to_delete.add(PIC_NAME)
+ # Change settings so we know they will be constant
+ self.change_settings = ChangeSettings()
+ self.change_settings.change()
+
+ def verify_thumbnail(self, expected_size, thumbnail=None,
+ expected_filename=None, expected_mode=None):
+ assert thumbnail is not None or expected_filename is not None, \
+ 'verify_thumbnail should be passed at least a thumbnail or an' \
+ 'expected filename.'
+
+ if thumbnail is not None:
+ # Verify that the templatetag method returned a Thumbnail instance
+ self.assertTrue(isinstance(thumbnail, Thumbnail))
+ thumb_name = thumbnail.dest
+ else:
+ thumb_name = expected_filename
+
+ if isinstance(thumb_name, basestring):
+ # Verify that the thumbnail file exists
+ self.assert_(os.path.isfile(thumb_name),
+ 'Thumbnail file not found')
+
+ # Remember to delete the file
+ self.images_to_delete.add(thumb_name)
+
+ # If we got an expected_filename, check that it is right
+ if expected_filename is not None and thumbnail is not None:
+ self.assertEqual(thumbnail.dest, expected_filename)
+
+ image = Image.open(thumb_name)
+
+ # Verify the thumbnail has the expected dimensions
+ self.assertEqual(image.size, expected_size)
+
+ if expected_mode is not None:
+ self.assertEqual(image.mode, expected_mode)
+
+ def tearDown(self):
+ # Remove all the files that have been created
+ for image in self.images_to_delete:
+ try:
+ os.remove(image)
+ except:
+ pass
+ # Change settings back to original
+ self.change_settings.revert()
--- /dev/null
+# -*- coding: utf-8 -*-
+import os
+import time
+from StringIO import StringIO
+
+from PIL import Image
+from django.conf import settings
+
+from sorl.thumbnail.base import Thumbnail
+from sorl.thumbnail.main import DjangoThumbnail, get_thumbnail_setting
+from sorl.thumbnail.processors import dynamic_import, get_valid_options
+from sorl.thumbnail.tests.base import BaseTest, RELATIVE_PIC_NAME, PIC_NAME,\
+ THUMB_NAME, PIC_SIZE
+
+
+class ThumbnailTest(BaseTest):
+ def testThumbnails(self):
+ # Thumbnail
+ thumb = Thumbnail(source=PIC_NAME, dest=THUMB_NAME % 1,
+ requested_size=(240, 240))
+ self.verify_thumbnail((240, 180), thumb)
+
+ # Cropped thumbnail
+ thumb = Thumbnail(source=PIC_NAME, dest=THUMB_NAME % 2,
+ requested_size=(240, 240), opts=['crop'])
+ self.verify_thumbnail((240, 240), thumb)
+
+ # Thumbnail with altered JPEG quality
+ thumb = Thumbnail(source=PIC_NAME, dest=THUMB_NAME % 3,
+ requested_size=(240, 240), quality=95)
+ self.verify_thumbnail((240, 180), thumb)
+
+ def testRegeneration(self):
+ # Create thumbnail
+ thumb_name = THUMB_NAME % 4
+ thumb_size = (240, 240)
+ Thumbnail(source=PIC_NAME, dest=thumb_name, requested_size=thumb_size)
+ self.images_to_delete.add(thumb_name)
+ thumb_mtime = os.path.getmtime(thumb_name)
+ time.sleep(1)
+
+ # Create another instance, shouldn't generate a new thumb
+ Thumbnail(source=PIC_NAME, dest=thumb_name, requested_size=thumb_size)
+ self.assertEqual(os.path.getmtime(thumb_name), thumb_mtime)
+
+ # Recreate the source image, then see if a new thumb is generated
+ Image.new('RGB', PIC_SIZE).save(PIC_NAME, 'JPEG')
+ Thumbnail(source=PIC_NAME, dest=thumb_name, requested_size=thumb_size)
+ self.assertNotEqual(os.path.getmtime(thumb_name), thumb_mtime)
+
+ def testFilelikeDest(self):
+ # Thumbnail
+ filelike_dest = StringIO()
+ thumb = Thumbnail(source=PIC_NAME, dest=filelike_dest,
+ requested_size=(240, 240))
+ self.verify_thumbnail((240, 180), thumb)
+
+ def testRGBA(self):
+ # RGBA image
+ rgba_pic_name = os.path.join(settings.MEDIA_ROOT,
+ 'sorl-thumbnail-test_rgba_source.png')
+ Image.new('RGBA', PIC_SIZE).save(rgba_pic_name)
+ self.images_to_delete.add(rgba_pic_name)
+ # Create thumb and verify it's still RGBA
+ rgba_thumb_name = os.path.join(settings.MEDIA_ROOT,
+ 'sorl-thumbnail-test_rgba_dest.png')
+ thumb = Thumbnail(source=rgba_pic_name, dest=rgba_thumb_name,
+ requested_size=(240, 240))
+ self.verify_thumbnail((240, 180), thumb, expected_mode='RGBA')
+
+
+class DjangoThumbnailTest(BaseTest):
+ def setUp(self):
+ super(DjangoThumbnailTest, self).setUp()
+ # Add another source image in a sub-directory for testing subdir and
+ # basedir.
+ self.sub_dir = os.path.join(settings.MEDIA_ROOT, 'test_thumbnail')
+ try:
+ os.mkdir(self.sub_dir)
+ except OSError:
+ pass
+ self.pic_subdir = os.path.join(self.sub_dir, RELATIVE_PIC_NAME)
+ Image.new('RGB', PIC_SIZE).save(self.pic_subdir, 'JPEG')
+ self.images_to_delete.add(self.pic_subdir)
+
+ def testFilenameGeneration(self):
+ basename = RELATIVE_PIC_NAME.replace('.', '_')
+ # Basic filename
+ thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+ requested_size=(240, 120))
+ expected = os.path.join(settings.MEDIA_ROOT, basename)
+ expected += '_240x120_q85.jpg'
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+
+ # Changed quality and cropped
+ thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+ requested_size=(240, 120), opts=['crop'],
+ quality=95)
+ expected = os.path.join(settings.MEDIA_ROOT, basename)
+ expected += '_240x120_crop_q95.jpg'
+ self.verify_thumbnail((240, 120), thumb, expected_filename=expected)
+
+ # All options on
+ processors = dynamic_import(get_thumbnail_setting('PROCESSORS'))
+ valid_options = get_valid_options(processors)
+
+ thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+ requested_size=(240, 120), opts=valid_options)
+ expected = (os.path.join(settings.MEDIA_ROOT, basename) + '_240x120_'
+ 'autocrop_bw_crop_detail_max_sharpen_upscale_q85.jpg')
+ self.verify_thumbnail((240, 120), thumb, expected_filename=expected)
+
+ # Different basedir
+ basedir = 'sorl-thumbnail-test-basedir'
+ self.change_settings.change({'BASEDIR': basedir})
+ thumb = DjangoThumbnail(relative_source=self.pic_subdir,
+ requested_size=(240, 120))
+ expected = os.path.join(basedir, self.sub_dir, basename)
+ expected += '_240x120_q85.jpg'
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+ # Different subdir
+ self.change_settings.change({'BASEDIR': '', 'SUBDIR': 'subdir'})
+ thumb = DjangoThumbnail(relative_source=self.pic_subdir,
+ requested_size=(240, 120))
+ expected = os.path.join(settings.MEDIA_ROOT,
+ os.path.basename(self.sub_dir), 'subdir',
+ basename)
+ expected += '_240x120_q85.jpg'
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+ # Different prefix
+ self.change_settings.change({'SUBDIR': '', 'PREFIX': 'prefix-'})
+ thumb = DjangoThumbnail(relative_source=self.pic_subdir,
+ requested_size=(240, 120))
+ expected = os.path.join(self.sub_dir, 'prefix-' + basename)
+ expected += '_240x120_q85.jpg'
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+
+ def testAlternateExtension(self):
+ basename = RELATIVE_PIC_NAME.replace('.', '_')
+ # Control JPG
+ thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+ requested_size=(240, 120))
+ expected = os.path.join(settings.MEDIA_ROOT, basename)
+ expected += '_240x120_q85.jpg'
+ expected_jpg = expected
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+ # Test PNG
+ thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+ requested_size=(240, 120), extension='png')
+ expected = os.path.join(settings.MEDIA_ROOT, basename)
+ expected += '_240x120_q85.png'
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+ # Compare the file size to make sure it's not just saving as a JPG with
+ # a different extension.
+ self.assertNotEqual(os.path.getsize(expected_jpg),
+ os.path.getsize(expected))
+
+ def testUnicodeName(self):
+ unicode_name = 'sorl-thumbnail-ążśź_source.jpg'
+ unicode_path = os.path.join(settings.MEDIA_ROOT, unicode_name)
+ Image.new('RGB', PIC_SIZE).save(unicode_path)
+ self.images_to_delete.add(unicode_path)
+ thumb = DjangoThumbnail(relative_source=unicode_name,
+ requested_size=(240, 120))
+ base_name = unicode_name.replace('.', '_')
+ expected = os.path.join(settings.MEDIA_ROOT,
+ base_name + '_240x120_q85.jpg')
+ self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+
+ def tearDown(self):
+ super(DjangoThumbnailTest, self).tearDown()
+ subdir = os.path.join(self.sub_dir, 'subdir')
+ if os.path.exists(subdir):
+ os.rmdir(subdir)
+ os.rmdir(self.sub_dir)
--- /dev/null
+import os.path
+
+from django.db import models
+from django.conf import settings
+from django.core.files.base import ContentFile
+
+from sorl.thumbnail.fields import ImageWithThumbnailsField, ThumbnailField
+from sorl.thumbnail.tests.base import BaseTest, RELATIVE_PIC_NAME, PIC_NAME
+
+thumbnail = {
+ 'size': (50, 50),
+}
+extra_thumbnails = {
+ 'admin': {
+ 'size': (30, 30),
+ 'options': ('crop',),
+ }
+}
+extension_thumbnail = thumbnail.copy()
+extension_thumbnail['extension'] = 'png'
+
+
+# Temporary models for field_tests
+class TestThumbnailFieldModel(models.Model):
+ avatar = ThumbnailField(upload_to='test', size=(300, 300))
+ photo = ImageWithThumbnailsField(upload_to='test', thumbnail=thumbnail,
+ extra_thumbnails=extra_thumbnails)
+
+
+class TestThumbnailFieldExtensionModel(models.Model):
+ photo = ImageWithThumbnailsField(upload_to='test',
+ thumbnail=extension_thumbnail,
+ extra_thumbnails=extra_thumbnails)
+
+
+class TestThumbnailFieldGenerateModel(models.Model):
+ photo = ImageWithThumbnailsField(upload_to='test', thumbnail=thumbnail,
+ extra_thumbnails=extra_thumbnails,
+ generate_on_save=True)
+
+
+class FieldTest(BaseTest):
+ """
+ Test the base field functionality. These use an ImageWithThumbnailsField
+ but all the functionality tested is from BaseThumbnailField.
+ """
+ def test_extra_thumbnails(self):
+ model = TestThumbnailFieldModel(photo=RELATIVE_PIC_NAME)
+ self.assertTrue('admin' in model.photo.extra_thumbnails)
+ thumb = model.photo.extra_thumbnails['admin']
+ tag = model.photo.extra_thumbnails_tag['admin']
+ expected_filename = os.path.join(settings.MEDIA_ROOT,
+ 'sorl-thumbnail-test_source_jpg_30x30_crop_q85.jpg')
+ self.verify_thumbnail((30, 30), thumb, expected_filename)
+ expected_tag = '<img src="%s" width="30" height="30" alt="" />' % \
+ '/'.join((settings.MEDIA_URL.rstrip('/'),
+ 'sorl-thumbnail-test_source_jpg_30x30_crop_q85.jpg'))
+ self.assertEqual(tag, expected_tag)
+
+ def test_extension(self):
+ model = TestThumbnailFieldExtensionModel(photo=RELATIVE_PIC_NAME)
+ thumb = model.photo.thumbnail
+ tag = model.photo.thumbnail_tag
+ expected_filename = os.path.join(settings.MEDIA_ROOT,
+ 'sorl-thumbnail-test_source_jpg_50x50_q85.png')
+ self.verify_thumbnail((50, 37), thumb, expected_filename)
+ expected_tag = '<img src="%s" width="50" height="37" alt="" />' % \
+ '/'.join((settings.MEDIA_URL.rstrip('/'),
+ 'sorl-thumbnail-test_source_jpg_50x50_q85.png'))
+ self.assertEqual(tag, expected_tag)
+
+ def test_delete_thumbnails(self):
+ model = TestThumbnailFieldModel(photo=RELATIVE_PIC_NAME)
+ thumb_file = model.photo.thumbnail.dest
+ open(thumb_file, 'wb').close()
+ self.assert_(os.path.exists(thumb_file))
+ model.photo.delete(save=False)
+ self.assertFalse(os.path.exists(thumb_file))
+
+ def test_generate_on_save(self):
+ main_thumb = os.path.join(settings.MEDIA_ROOT, 'test',
+ 'sorl-thumbnail-test_source_jpg_50x50_q85.jpg')
+ admin_thumb = os.path.join(settings.MEDIA_ROOT, 'test',
+ 'sorl-thumbnail-test_source_jpg_30x30_crop_q85.jpg')
+ self.images_to_delete.add(main_thumb)
+ self.images_to_delete.add(admin_thumb)
+ # Default setting is to only generate when the thumbnail is used.
+ model = TestThumbnailFieldModel()
+ source = ContentFile(open(PIC_NAME).read())
+ model.photo.save(RELATIVE_PIC_NAME, source, save=False)
+ self.images_to_delete.add(model.photo.path)
+ self.assertFalse(os.path.exists(main_thumb))
+ self.assertFalse(os.path.exists(admin_thumb))
+ os.remove(model.photo.path)
+ # But it's easy to set it up the other way...
+ model = TestThumbnailFieldGenerateModel()
+ source = ContentFile(open(PIC_NAME).read())
+ model.photo.save(RELATIVE_PIC_NAME, source, save=False)
+ self.assert_(os.path.exists(main_thumb))
+ self.assert_(os.path.exists(admin_thumb))
+
+
+class ImageWithThumbnailsFieldTest(BaseTest):
+ def test_thumbnail(self):
+ model = TestThumbnailFieldModel(photo=RELATIVE_PIC_NAME)
+ thumb = model.photo.thumbnail
+ tag = model.photo.thumbnail_tag
+ base_name = RELATIVE_PIC_NAME.replace('.', '_')
+ expected_filename = os.path.join(settings.MEDIA_ROOT,
+ '%s_50x50_q85.jpg' % base_name)
+ self.verify_thumbnail((50, 37), thumb, expected_filename)
+ expected_tag = ('<img src="%s" width="50" height="37" alt="" />' %
+ '/'.join([settings.MEDIA_URL.rstrip('/'),
+ '%s_50x50_q85.jpg' % base_name]))
+ self.assertEqual(tag, expected_tag)
+
+
+class ThumbnailFieldTest(BaseTest):
+ def test_thumbnail(self):
+ model = TestThumbnailFieldModel()
+ source = ContentFile(open(PIC_NAME).read())
+ dest_name = 'sorl-thumbnail-test_dest.jpg'
+ model.avatar.save(dest_name, source, save=False)
+ expected_filename = os.path.join(model.avatar.path)
+ self.verify_thumbnail((300, 225), expected_filename=expected_filename)
+
+ tag = model.avatar.thumbnail_tag
+ expected_tag = ('<img src="%s" width="300" height="225" alt="" />' %
+ '/'.join([settings.MEDIA_URL.rstrip('/'), 'test',
+ dest_name]))
+ self.assertEqual(tag, expected_tag)
--- /dev/null
+import os
+from django.conf import settings
+from django.template import Template, Context, TemplateSyntaxError
+from sorl.thumbnail.tests.classes import BaseTest, RELATIVE_PIC_NAME
+
+
+class ThumbnailTagTest(BaseTest):
+ def render_template(self, source):
+ context = Context({
+ 'source': RELATIVE_PIC_NAME,
+ 'invalid_source': 'not%s' % RELATIVE_PIC_NAME,
+ 'size': (90, 100),
+ 'invalid_size': (90, 'fish'),
+ 'strsize': '80x90',
+ 'invalid_strsize': ('1notasize2'),
+ 'invalid_q': 'notanumber'})
+ source = '{% load thumbnail %}' + source
+ return Template(source).render(context)
+
+ def testTagInvalid(self):
+ # No args, or wrong number of args
+ src = '{% thumbnail %}'
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+ src = '{% thumbnail source %}'
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+ src = '{% thumbnail source 80x80 as variable crop %}'
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+
+ # Invalid option
+ src = '{% thumbnail source 240x200 invalid %}'
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+
+ # Old comma separated options format can only have an = for quality
+ src = '{% thumbnail source 80x80 crop=1,quality=1 %}'
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+
+ # Invalid quality
+ src_invalid = '{% thumbnail source 240x200 quality=invalid_q %}'
+ src_missing = '{% thumbnail source 240x200 quality=missing_q %}'
+ # ...with THUMBNAIL_DEBUG = False
+ self.assertEqual(self.render_template(src_invalid), '')
+ self.assertEqual(self.render_template(src_missing), '')
+ # ...and with THUMBNAIL_DEBUG = True
+ self.change_settings.change({'DEBUG': True})
+ self.assertRaises(TemplateSyntaxError, self.render_template,
+ src_invalid)
+ self.assertRaises(TemplateSyntaxError, self.render_template,
+ src_missing)
+
+ # Invalid source
+ src = '{% thumbnail invalid_source 80x80 %}'
+ src_on_context = '{% thumbnail invalid_source 80x80 as thumb %}'
+ # ...with THUMBNAIL_DEBUG = False
+ self.change_settings.change({'DEBUG': False})
+ self.assertEqual(self.render_template(src), '')
+ # ...and with THUMBNAIL_DEBUG = True
+ self.change_settings.change({'DEBUG': True})
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+ self.assertRaises(TemplateSyntaxError, self.render_template,
+ src_on_context)
+
+ # Non-existant source
+ src = '{% thumbnail non_existant_source 80x80 %}'
+ src_on_context = '{% thumbnail non_existant_source 80x80 as thumb %}'
+ # ...with THUMBNAIL_DEBUG = False
+ self.change_settings.change({'DEBUG': False})
+ self.assertEqual(self.render_template(src), '')
+ # ...and with THUMBNAIL_DEBUG = True
+ self.change_settings.change({'DEBUG': True})
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+
+ # Invalid size as a tuple:
+ src = '{% thumbnail source invalid_size %}'
+ # ...with THUMBNAIL_DEBUG = False
+ self.change_settings.change({'DEBUG': False})
+ self.assertEqual(self.render_template(src), '')
+ # ...and THUMBNAIL_DEBUG = True
+ self.change_settings.change({'DEBUG': True})
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+ # Invalid size as a string:
+ src = '{% thumbnail source invalid_strsize %}'
+ # ...with THUMBNAIL_DEBUG = False
+ self.change_settings.change({'DEBUG': False})
+ self.assertEqual(self.render_template(src), '')
+ # ...and THUMBNAIL_DEBUG = True
+ self.change_settings.change({'DEBUG': True})
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+
+ # Non-existant size
+ src = '{% thumbnail source non_existant_size %}'
+ # ...with THUMBNAIL_DEBUG = False
+ self.change_settings.change({'DEBUG': False})
+ self.assertEqual(self.render_template(src), '')
+ # ...and THUMBNAIL_DEBUG = True
+ self.change_settings.change({'DEBUG': True})
+ self.assertRaises(TemplateSyntaxError, self.render_template, src)
+
+ def testTag(self):
+ expected_base = RELATIVE_PIC_NAME.replace('.', '_')
+ # Set DEBUG = True to make it easier to trace any failures
+ self.change_settings.change({'DEBUG': True})
+
+ # Basic
+ output = self.render_template('src="'
+ '{% thumbnail source 240x240 %}"')
+ expected = '%s_240x240_q85.jpg' % expected_base
+ expected_fn = os.path.join(settings.MEDIA_ROOT, expected)
+ self.verify_thumbnail((240, 180), expected_filename=expected_fn)
+ expected_url = ''.join((settings.MEDIA_URL, expected))
+ self.assertEqual(output, 'src="%s"' % expected_url)
+
+ # Size from context variable
+ # as a tuple:
+ output = self.render_template('src="'
+ '{% thumbnail source size %}"')
+ expected = '%s_90x100_q85.jpg' % expected_base
+ expected_fn = os.path.join(settings.MEDIA_ROOT, expected)
+ self.verify_thumbnail((90, 67), expected_filename=expected_fn)
+ expected_url = ''.join((settings.MEDIA_URL, expected))
+ self.assertEqual(output, 'src="%s"' % expected_url)
+ # as a string:
+ output = self.render_template('src="'
+ '{% thumbnail source strsize %}"')
+ expected = '%s_80x90_q85.jpg' % expected_base
+ expected_fn = os.path.join(settings.MEDIA_ROOT, expected)
+ self.verify_thumbnail((80, 60), expected_filename=expected_fn)
+ expected_url = ''.join((settings.MEDIA_URL, expected))
+ self.assertEqual(output, 'src="%s"' % expected_url)
+
+ # On context
+ output = self.render_template('height:'
+ '{% thumbnail source 240x240 as thumb %}{{ thumb.height }}')
+ self.assertEqual(output, 'height:180')
+
+ # With options and quality
+ output = self.render_template('src="'
+ '{% thumbnail source 240x240 sharpen crop quality=95 %}"')
+ # Note that the opts are sorted to ensure a consistent filename.
+ expected = '%s_240x240_crop_sharpen_q95.jpg' % expected_base
+ expected_fn = os.path.join(settings.MEDIA_ROOT, expected)
+ self.verify_thumbnail((240, 240), expected_filename=expected_fn)
+ expected_url = ''.join((settings.MEDIA_URL, expected))
+ self.assertEqual(output, 'src="%s"' % expected_url)
+
+ # With option and quality on context (also using its unicode method to
+ # display the url)
+ output = self.render_template(
+ '{% thumbnail source 240x240 sharpen crop quality=95 as thumb %}'
+ 'width:{{ thumb.width }}, url:{{ thumb }}')
+ self.assertEqual(output, 'width:240, url:%s' % expected_url)
+
+ # Old comma separated format for options is still supported.
+ output = self.render_template(
+ '{% thumbnail source 240x240 sharpen,crop,quality=95 as thumb %}'
+ 'width:{{ thumb.width }}, url:{{ thumb }}')
+ self.assertEqual(output, 'width:240, url:%s' % expected_url)
+
+filesize_tests = r"""
+>>> from sorl.thumbnail.templatetags.thumbnail import filesize
+
+>>> filesize('abc')
+'abc'
+>>> filesize(100, 'invalid')
+100
+
+>>> bytes = 20
+>>> filesize(bytes)
+'20 B'
+>>> filesize(bytes, 'auto1000')
+'20 B'
+
+>>> bytes = 1001
+>>> filesize(bytes)
+'1001 B'
+>>> filesize(bytes, 'auto1000')
+'1 kB'
+
+>>> bytes = 10100
+>>> filesize(bytes)
+'9.9 KiB'
+
+# Note that the decimal place is only used if < 10
+>>> filesize(bytes, 'auto1000')
+'10 kB'
+
+>>> bytes = 190000000
+>>> filesize(bytes)
+'181 MiB'
+>>> filesize(bytes, 'auto1000')
+'190 MB'
+
+# 'auto*long' methods use pluralisation:
+>>> filesize(1, 'auto1024long')
+'1 byte'
+>>> filesize(1, 'auto1000long')
+'1 byte'
+>>> filesize(2, 'auto1024long')
+'2 bytes'
+>>> filesize(0, 'auto1000long')
+'0 bytes'
+
+# Test all 'auto*long' output:
+>>> for i in range(1,10):
+... print '%s, %s' % (filesize(1024**i, 'auto1024long'),
+... filesize(1000**i, 'auto1000long'))
+1 kibibyte, 1 kilobyte
+1 mebibyte, 1 megabyte
+1 gibibyte, 1 gigabyte
+1 tebibyte, 1 terabyte
+1 pebibyte, 1 petabyte
+1 exbibyte, 1 exabyte
+1 zebibyte, 1 zettabyte
+1 yobibyte, 1 yottabyte
+1024 yobibytes, 1000 yottabytes
+
+# Test all fixed outputs (eg 'kB' or 'MiB')
+>>> from sorl.thumbnail.templatetags.thumbnail import filesize_formats,\
+... filesize_long_formats
+>>> for f in filesize_formats:
+... print '%s (%siB, %sB):' % (filesize_long_formats[f], f.upper(), f)
+... for i in range(0, 10):
+... print ' %s, %s' % (filesize(1024**i, '%siB' % f.upper()),
+... filesize(1000**i, '%sB' % f))
+kilo (KiB, kB):
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+ 1073741824.0, 1000000000.0
+ 1.09951162778e+12, 1e+12
+ 1.12589990684e+15, 1e+15
+ 1.15292150461e+18, 1e+18
+ 1.18059162072e+21, 1e+21
+ 1.20892581961e+24, 1e+24
+mega (MiB, MB):
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+ 1073741824.0, 1000000000.0
+ 1.09951162778e+12, 1e+12
+ 1.12589990684e+15, 1e+15
+ 1.15292150461e+18, 1e+18
+ 1.18059162072e+21, 1e+21
+giga (GiB, GB):
+ 0.0, 1e-09
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+ 1073741824.0, 1000000000.0
+ 1.09951162778e+12, 1e+12
+ 1.12589990684e+15, 1e+15
+ 1.15292150461e+18, 1e+18
+tera (TiB, TB):
+ 0.0, 1e-12
+ 0.0, 1e-09
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+ 1073741824.0, 1000000000.0
+ 1.09951162778e+12, 1e+12
+ 1.12589990684e+15, 1e+15
+peta (PiB, PB):
+ 0.0, 1e-15
+ 0.0, 1e-12
+ 0.0, 1e-09
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+ 1073741824.0, 1000000000.0
+ 1.09951162778e+12, 1e+12
+exa (EiB, EB):
+ 0.0, 1e-18
+ 0.0, 1e-15
+ 0.0, 1e-12
+ 0.0, 1e-09
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+ 1073741824.0, 1000000000.0
+zetta (ZiB, ZB):
+ 0.0, 1e-21
+ 0.0, 1e-18
+ 0.0, 1e-15
+ 0.0, 1e-12
+ 0.0, 1e-09
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+ 1048576.0, 1000000.0
+yotta (YiB, YB):
+ 0.0, 1e-24
+ 0.0, 1e-21
+ 0.0, 1e-18
+ 0.0, 1e-15
+ 0.0, 1e-12
+ 0.0, 1e-09
+ 0.0, 1e-06
+ 0.0009765625, 0.001
+ 1.0, 1.0
+ 1024.0, 1000.0
+"""
--- /dev/null
+from django.conf import settings
+from sorl.thumbnail.utils import *
+
+try:
+ set
+except NameError:
+ from sets import Set as set # For Python 2.3
+
+MEDIA_ROOT_LENGTH = len(os.path.normpath(settings.MEDIA_ROOT))
+
+utils_tests = r"""
+>>> from sorl.thumbnail.tests.utils import *
+>>> from sorl.thumbnail.tests.base import ChangeSettings
+>>> from django.conf import settings
+
+>>> change_settings = ChangeSettings()
+>>> change_settings.change()
+
+>>> media_root = settings.MEDIA_ROOT.rstrip('/')
+
+#==============================================================================
+# Set up test images
+#==============================================================================
+
+>>> make_image('test-thumbnail-utils/subdir/test_jpg_110x110_q85.jpg')
+>>> make_image('test-thumbnail-utils/test_jpg_80x80_q85.jpg')
+>>> make_image('test-thumbnail-utils/test_jpg_80x80_q95.jpg')
+>>> make_image('test-thumbnail-utils/another_test_jpg_80x80_q85.jpg')
+>>> make_image('test-thumbnail-utils/test_with_opts_jpg_80x80_crop_bw_q85.jpg')
+>>> make_image('test-thumbnail-basedir/test-thumbnail-utils/test_jpg_100x100_'
+... 'q85.jpg')
+>>> make_image('test-thumbnail-utils/prefix-test_jpg_120x120_q85.jpg')
+
+#==============================================================================
+# all_thumbnails()
+#==============================================================================
+
+# Find all thumbs
+>>> thumb_dir = os.path.join(settings.MEDIA_ROOT, 'test-thumbnail-utils')
+>>> thumbs = all_thumbnails(thumb_dir)
+>>> k = thumbs.keys()
+>>> k.sort()
+>>> [consistent_slash(path) for path in k]
+['another_test.jpg', 'prefix-test.jpg', 'subdir/test.jpg', 'test.jpg',
+ 'test_with_opts.jpg']
+
+# Find all thumbs, no recurse
+>>> thumbs = all_thumbnails(thumb_dir, recursive=False)
+>>> k = thumbs.keys()
+>>> k.sort()
+>>> k
+['another_test.jpg', 'prefix-test.jpg', 'test.jpg', 'test_with_opts.jpg']
+
+#==============================================================================
+# thumbnails_for_file()
+#==============================================================================
+
+>>> output = []
+>>> for thumb in thumbs['test.jpg']:
+... thumb['rel_fn'] = strip_media_root(thumb['filename'])
+... output.append('%(x)sx%(y)s %(quality)s %(rel_fn)s' % thumb)
+>>> output.sort()
+>>> output
+['80x80 85 test-thumbnail-utils/test_jpg_80x80_q85.jpg',
+ '80x80 95 test-thumbnail-utils/test_jpg_80x80_q95.jpg']
+
+# Thumbnails for file
+>>> output = []
+>>> for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
+... output.append(strip_media_root(thumb['filename']))
+>>> output.sort()
+>>> output
+['test-thumbnail-utils/test_jpg_80x80_q85.jpg',
+ 'test-thumbnail-utils/test_jpg_80x80_q95.jpg']
+
+# Thumbnails for file - shouldn't choke on non-existant file
+>>> thumbnails_for_file('test-thumbnail-utils/non-existant.jpg')
+[]
+
+# Thumbnails for file, with basedir setting
+>>> change_settings.change({'BASEDIR': 'test-thumbnail-basedir'})
+>>> for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
+... print strip_media_root(thumb['filename'])
+test-thumbnail-basedir/test-thumbnail-utils/test_jpg_100x100_q85.jpg
+
+# Thumbnails for file, with subdir setting
+>>> change_settings.change({'SUBDIR': 'subdir', 'BASEDIR': ''})
+>>> for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
+... print strip_media_root(thumb['filename'])
+test-thumbnail-utils/subdir/test_jpg_110x110_q85.jpg
+
+# Thumbnails for file, with prefix setting
+>>> change_settings.change({'PREFIX': 'prefix-', 'SUBDIR': ''})
+>>> for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
+... print strip_media_root(thumb['filename'])
+test-thumbnail-utils/prefix-test_jpg_120x120_q85.jpg
+
+#==============================================================================
+# Clean up images / directories
+#==============================================================================
+
+>>> clean_up()
+"""
+
+images_to_delete = set()
+dirs_to_delete = []
+
+
+def make_image(relative_image):
+ absolute_image = os.path.join(settings.MEDIA_ROOT, relative_image)
+ make_dirs(os.path.dirname(relative_image))
+ open(absolute_image, 'w').close()
+ images_to_delete.add(absolute_image)
+
+
+def make_dirs(relative_path):
+ if not relative_path:
+ return
+ absolute_path = os.path.join(settings.MEDIA_ROOT, relative_path)
+ if os.path.isdir(absolute_path):
+ return
+ if absolute_path not in dirs_to_delete:
+ dirs_to_delete.append(absolute_path)
+ make_dirs(os.path.dirname(relative_path))
+ os.mkdir(absolute_path)
+
+
+def clean_up():
+ for image in images_to_delete:
+ os.remove(image)
+ for path in dirs_to_delete:
+ os.rmdir(path)
+
+
+def strip_media_root(path):
+ path = os.path.normpath(path)
+ # chop off the MEDIA_ROOT and strip any leading os.sep
+ path = path[MEDIA_ROOT_LENGTH:].lstrip(os.sep)
+ return consistent_slash(path)
+
+
+def consistent_slash(path):
+ """
+ Ensure we're always testing against the '/' os separator (otherwise tests
+ fail against Windows).
+ """
+ if os.sep != '/':
+ path = path.replace(os.sep, '/')
+ return path
--- /dev/null
+import math
+import os
+import re
+
+
+re_thumbnail_file = re.compile(r'(?P<source_filename>.+)_(?P<x>\d+)x(?P<y>\d+)'
+ r'(?:_(?P<options>\w+))?_q(?P<quality>\d+)'
+ r'(?:.[^.]+)?$')
+re_new_args = re.compile('(?<!quality)=')
+
+
+def all_thumbnails(path, recursive=True, prefix=None, subdir=None):
+ """
+ Return a dictionary referencing all files which match the thumbnail format.
+
+ Each key is a source image filename, relative to path.
+ Each value is a list of dictionaries as explained in `thumbnails_for_file`.
+ """
+ # Fall back to using thumbnail settings. These are local imports so that
+ # there is no requirement of Django to use the utils module.
+ if prefix is None:
+ from sorl.thumbnail.main import get_thumbnail_setting
+ prefix = get_thumbnail_setting('PREFIX')
+ if subdir is None:
+ from sorl.thumbnail.main import get_thumbnail_setting
+ subdir = get_thumbnail_setting('SUBDIR')
+ thumbnail_files = {}
+ if not path.endswith('/'):
+ path = '%s/' % path
+ len_path = len(path)
+ if recursive:
+ all = os.walk(path)
+ else:
+ files = []
+ for file in os.listdir(path):
+ if os.path.isfile(os.path.join(path, file)):
+ files.append(file)
+ all = [(path, [], files)]
+ for dir_, subdirs, files in all:
+ rel_dir = dir_[len_path:]
+ for file in files:
+ thumb = re_thumbnail_file.match(file)
+ if not thumb:
+ continue
+ d = thumb.groupdict()
+ source_filename = d.pop('source_filename')
+ if prefix:
+ source_path, source_filename = os.path.split(source_filename)
+ if not source_filename.startswith(prefix):
+ continue
+ source_filename = os.path.join(source_path,
+ source_filename[len(prefix):])
+ d['options'] = d['options'] and d['options'].split('_') or []
+ if subdir and rel_dir.endswith(subdir):
+ rel_dir = rel_dir[:-len(subdir)]
+ # Corner-case bug: if the filename didn't have an extension but did
+ # have an underscore, the last underscore will get converted to a
+ # '.'.
+ m = re.match(r'(.*)_(.*)', source_filename)
+ if m:
+ source_filename = '%s.%s' % m.groups()
+ filename = os.path.join(rel_dir, source_filename)
+ thumbnail_file = thumbnail_files.setdefault(filename, [])
+ d['filename'] = os.path.join(dir_, file)
+ thumbnail_file.append(d)
+ return thumbnail_files
+
+
+def thumbnails_for_file(relative_source_path, root=None, basedir=None,
+ subdir=None, prefix=None):
+ """
+ Return a list of dictionaries, one for each thumbnail belonging to the
+ source image.
+
+ The following list explains each key of the dictionary:
+
+ `filename` -- absolute thumbnail path
+ `x` and `y` -- the size of the thumbnail
+ `options` -- list of options for this thumbnail
+ `quality` -- quality setting for this thumbnail
+ """
+ # Fall back to using thumbnail settings. These are local imports so that
+ # there is no requirement of Django to use the utils module.
+ if root is None:
+ from django.conf import settings
+ root = settings.MEDIA_ROOT
+ if prefix is None:
+ from sorl.thumbnail.main import get_thumbnail_setting
+ prefix = get_thumbnail_setting('PREFIX')
+ if subdir is None:
+ from sorl.thumbnail.main import get_thumbnail_setting
+ subdir = get_thumbnail_setting('SUBDIR')
+ if basedir is None:
+ from sorl.thumbnail.main import get_thumbnail_setting
+ basedir = get_thumbnail_setting('BASEDIR')
+ source_dir, filename = os.path.split(relative_source_path)
+ thumbs_path = os.path.join(root, basedir, source_dir, subdir)
+ if not os.path.isdir(thumbs_path):
+ return []
+ files = all_thumbnails(thumbs_path, recursive=False, prefix=prefix,
+ subdir='')
+ return files.get(filename, [])
+
+
+def delete_thumbnails(relative_source_path, root=None, basedir=None,
+ subdir=None, prefix=None):
+ """
+ Delete all thumbnails for a source image.
+ """
+ thumbs = thumbnails_for_file(relative_source_path, root, basedir, subdir,
+ prefix)
+ return _delete_using_thumbs_list(thumbs)
+
+
+def _delete_using_thumbs_list(thumbs):
+ deleted = 0
+ for thumb_dict in thumbs:
+ filename = thumb_dict['filename']
+ try:
+ os.remove(filename)
+ except:
+ pass
+ else:
+ deleted += 1
+ return deleted
+
+
+def delete_all_thumbnails(path, recursive=True):
+ """
+ Delete all files within a path which match the thumbnails pattern.
+
+ By default, matching files from all sub-directories are also removed. To
+ only remove from the path directory, set recursive=False.
+ """
+ total = 0
+ for thumbs in all_thumbnails(path, recursive=recursive).values():
+ total += _delete_using_thumbs_list(thumbs)
+ return total
+
+
+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 ``None``.
+ """
+ if not args:
+ return {}
+ # Handle the old comma separated argument format.
+ if len(args) == 1 and not re_new_args.search(args[0]):
+ args = args[0].split(',')
+ # Separate out the key and value for each argument.
+ args_dict = {}
+ for arg in args:
+ split_arg = arg.split('=', 1)
+ value = len(split_arg) > 1 and split_arg[1] or None
+ args_dict[split_arg[0]] = value
+ return args_dict
+
+
+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])
from django.utils.translation import ugettext_lazy as _
from django.template.loader import render_to_string
+from sorl.thumbnail.fields import ImageWithThumbnailsField
from sponsors.fields import JSONField
class Sponsor(models.Model):
name = models.CharField(_('name'), max_length=120)
_description = models.CharField(_('description'), blank=True, max_length=255)
- logo = models.ImageField(_('logo'), upload_to='sponsors/sponsor/logo')
+ logo = ImageWithThumbnailsField(
+ _('logo'),
+ upload_to='sponsors/sponsor/logo',
+ thumbnail={
+ 'size': (150, 75),
+ 'extension': 'png',
+ 'options': ['upscale', 'pad', 'detail'],
+ })
url = models.URLField(_('url'), blank=True, verify_exists=False)
def __unicode__(self):
--- /dev/null
+from PIL import Image, ImageFilter, ImageChops
+
+
+def add_padding(image, requested_size, opts):
+ if 'pad' in opts:
+ padded_image = Image.new('RGBA', requested_size, '#fff')
+ width, height = image.size
+ requested_width, requested_height = requested_size
+ padded_image.paste(image, (0, requested_height - height / 2))
+ return padded_image
+ return image
+
+add_padding.valid_options = ('pad',)
.sponsors .sponsors-sponsor-group {
float: left;
- width: 200px;
+ width: 180px;
border: 1px solid #CCC;
margin: 2px 2px 0 0;
}
margin: -2px -2px -2px -4px;
padding: 0;
height: 15px;
- width: 180px;
+ width: 160px;
}
.sponsors .sponsors-remove-sponsor-group {
}
.sponsors-sponsor {
- margin: 0 0 2px 0;
- padding: 2px;
+ margin: 0 30px 2px 0;
+ width: 150px;
+ height: 75px;
border: 1px solid #CCC;
background-color: #EEE;
cursor: default;
for (var i = 0; i < sponsors.length; i++) {
- $('<li class="sponsors-sponsor">' + sponsors[i].name + '</li>')
+ $('<li class="sponsors-sponsor"><img src="' + sponsors[i].image + '" alt="' + sponsors[i].name + '"/></li>')
.data('obj_id', sponsors[i].id)
.appendTo(groupList);
}
});
for (i = 0; i < settings.sponsors.length; i++) {
- $('<li class="sponsors-sponsor">' + settings.sponsors[i].name + '</li>')
+ $('<li class="sponsors-sponsor"><img src="' + settings.sponsors[i].image + '" alt="' + settings.sponsors[i].name + '"/></li>')
.data('obj_id', settings.sponsors[i].id)
.appendTo(unusedList);
}
<div class="sponsors-sponsor-page">
{% for column in sponsors %}
- <div class="sponsors-sponsor-column" style="width: {{ column_width }}px">
- <p class="sponsors-sponsor-column-name">{{ column.name }}</p>
+ <div class="sponsors-sponsor-column" style="width: 150px">
+ <p class="sponsors-sponsor-column-name">{{ column.name|default:" " }}</p>
{% for sponsor in column.sponsors %}
- <div class="sponsors-sponsor">{% if sponsor.url %}<a style="sponsors-sponsor-link" href="{{ sponsor.url }}" >{% endif %}<img class="sponsors-sponsor-logo" src="{{ sponsor.logo.url }}" alt="{{ sponsor.description }}"/>{% if sponsor.url %}</a>{% endif %}</div>
+ <div class="sponsors-sponsor">{% if sponsor.url %}<a style="sponsors-sponsor-link" href="{{ sponsor.url }}" >{% endif %}<img class="sponsors-sponsor-logo" src="{{ sponsor.logo.thumbnail }}" alt="{{ sponsor.description }}"/>{% if sponsor.url %}</a>{% endif %}</div>
{% endfor %}
</div>
{% endfor %}
def render(self, name, value, attrs=None):
output = [super(SponsorPageWidget, self).render(name, value, attrs)]
- sponsors = [(unicode(obj), obj.pk) for obj in models.Sponsor.objects.all()]
- sponsors_js = ', '.join('{name: "%s", id: %d}' % sponsor for sponsor in sponsors)
+ sponsors = [(unicode(obj), obj.pk, obj.logo.thumbnail) for obj in models.Sponsor.objects.all()]
+ sponsors_js = ', '.join('{name: "%s", id: %d, image: "%s"}' % sponsor for sponsor in sponsors)
output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
# external
'south',
+ 'sorl.thumbnail',
'sponsors',
'newtagging',
'pagination',
COMPRESS_CSS_FILTERS = None
+THUMBNAIL_QUALITY = 95
+THUMBNAIL_EXTENSION = 'png'
+
+THUMBNAIL_PROCESSORS = (
+ # Default processors
+ 'sorl.thumbnail.processors.colorspace',
+ 'sorl.thumbnail.processors.autocrop',
+ 'sorl.thumbnail.processors.scale_and_crop',
+ 'sorl.thumbnail.processors.filters',
+ # Custom processors
+ 'sponsors.processors.add_padding',
+)
# Load localsettings, if they exist
try:
.sponsors .sponsors-sponsor-group {
float: left;
- width: 200px;
+ width: 180px;
border: 1px solid #CCC;
margin: 2px 2px 0 0;
}
margin: -2px -2px -2px -4px;
padding: 0;
height: 15px;
- width: 180px;
+ width: 160px;
}
.sponsors .sponsors-remove-sponsor-group {
}
.sponsors-sponsor {
- margin: 0 0 2px 0;
- padding: 2px;
+ margin: 0 30px 2px 0;
+ width: 150px;
+ height: 75px;
border: 1px solid #CCC;
background-color: #EEE;
cursor: default;
for (var i = 0; i < sponsors.length; i++) {
- $('<li class="sponsors-sponsor">' + sponsors[i].name + '</li>')
+ $('<li class="sponsors-sponsor"><img src="' + sponsors[i].image + '" alt="' + sponsors[i].name + '"/></li>')
.data('obj_id', sponsors[i].id)
.appendTo(groupList);
}
});
for (i = 0; i < settings.sponsors.length; i++) {
- $('<li class="sponsors-sponsor">' + settings.sponsors[i].name + '</li>')
+ $('<li class="sponsors-sponsor"><img src="' + settings.sponsors[i].image + '" alt="' + settings.sponsors[i].name + '"/></li>')
.data('obj_id', settings.sponsors[i].id)
.appendTo(unusedList);
}