sorl-thumbnail-11.12/0000755000175000017500000000000011677075433013501 5ustar mikkomikkosorl-thumbnail-11.12/README.rst0000644000175000017500000000114411666137404015163 0ustar mikkomikkosorl-thumbnail ============== Thumbnails for Django. Totally rewritten. Features at a glance -------------------- - Storage support - Pluggable Engine support (ImageMagick, PIL, pgmagick included) - Pluggable Key Value Store support (cached db, redis) - Pluggable Backend support - Admin integration with possibility to delete - Dummy generation (placeholders) - Flexible, simple syntax, generates no html - ImageField for model that deletes thumbnails - CSS style cropping options - Margin calculation for vertical positioning Read more in `the documentation (latest version) `_ sorl-thumbnail-11.12/setup.py0000644000175000017500000000205411666137404015207 0ustar mikkomikkoimport sorl from setuptools import setup, find_packages from setuptools.command.test import test class TestCommand(test): def run(self): from tests.runtests import runtests runtests() setup( name='sorl-thumbnail', version=sorl.__version__, description='Thumbnails for Django', long_description=open('README.rst').read(), author='Mikko Hellsing', author_email='mikko@aino.se', license='BSD', url='https://github.com/sorl/sorl-thumbnail', packages=find_packages(exclude=['tests', 'tests.*']), platforms='any', zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Multimedia :: Graphics', 'Framework :: Django', ], cmdclass={"test": TestCommand}, ) sorl-thumbnail-11.12/tests/0000755000175000017500000000000011677075433014643 5ustar mikkomikkosorl-thumbnail-11.12/tests/__init__.py0000644000175000017500000000000011677073446016744 0ustar mikkomikkosorl-thumbnail-11.12/tests/settings/0000755000175000017500000000000011677075433016503 5ustar mikkomikkosorl-thumbnail-11.12/tests/settings/__init__.py0000644000175000017500000000000011677073446020604 0ustar mikkomikkosorl-thumbnail-11.12/tests/settings/imagemagick.py0000644000175000017500000000017111677073446021314 0ustar mikkomikkofrom .default import * THUMBNAIL_ENGINE = 'sorl.thumbnail.engines.convert_engine.Engine' THUMBNAIL_CONVERT = 'convert' sorl-thumbnail-11.12/tests/settings/pgmagick.py0000644000175000017500000000013411677073446020637 0ustar mikkomikkofrom .default import * THUMBNAIL_ENGINE = 'sorl.thumbnail.engines.pgmagick_engine.Engine' sorl-thumbnail-11.12/tests/settings/pil.py0000644000175000017500000000012711677073446017643 0ustar mikkomikkofrom .default import * THUMBNAIL_ENGINE = 'sorl.thumbnail.engines.pil_engine.Engine' sorl-thumbnail-11.12/tests/settings/graphicsmagick.py0000644000175000017500000000023711677073446022035 0ustar mikkomikkofrom .default import * THUMBNAIL_ENGINE = 'sorl.thumbnail.engines.convert_engine.Engine' THUMBNAIL_CONVERT = 'gm convert' THUMBNAIL_IDENTIFY = 'gm identify' sorl-thumbnail-11.12/tests/settings/default.py0000644000175000017500000000160711677073446020507 0ustar mikkomikkofrom os.path import join as pjoin, abspath, dirname, pardir PROJ_ROOT = abspath(pjoin(dirname(__file__), pardir)) DATA_ROOT = pjoin(PROJ_ROOT, 'data') THUMBNAIL_PREFIX = 'test/cache/' THUMBNAIL_DEBUG = True THUMBNAIL_LOG_HANDLER = { 'class': 'sorl.thumbnail.log.ThumbnailLogHandler', 'level': 'ERROR', } THUMBNAIL_KVSTORE = 'thumbnail_tests.kvstore.TestKVStore' THUMBNAIL_STORAGE = 'thumbnail_tests.storage.TestStorage' DEFAULT_FILE_STORAGE = 'thumbnail_tests.storage.TestStorage' ADMINS = ( ('Sorl', 'thumbnail@sorl.net'), ) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'thumbnail_tests', } } MEDIA_ROOT = pjoin(PROJ_ROOT, 'media') MEDIA_URL = '/media/' ROOT_URLCONF = 'thumbnail_tests.urls' INSTALLED_APPS = ( 'thumbnail', 'thumbnail_tests', ) TEMPLATE_CONTEXT_PROCESSORS = ( "django.core.context_processors.request", ) sorl-thumbnail-11.12/tests/settings/redis.py0000644000175000017500000000013511677073446020164 0ustar mikkomikkofrom .default import * THUMBNAIL_KVSTORE = 'sorl.thumbnail.kvstores.redis_kvstore.KVStore' sorl-thumbnail-11.12/tests/runtests.py0000755000175000017500000000326311677073446017115 0ustar mikkomikko#!/usr/bin/env python import os import sys from os.path import abspath, dirname, join as pjoin from django.conf import settings def runtests(verbosity=1, interactive=True, failfast=True, settings_module='settings.default'): here = abspath(dirname(__file__)) root = pjoin(here, os.pardir) sys.path[0:0] = [ here, root, pjoin(root, 'sorl') ] os.environ['DJANGO_SETTINGS_MODULE'] = settings_module from django.test.utils import get_runner print "Running tests for '%s'" % settings_module TestRunner = get_runner(settings) test_runner = TestRunner( verbosity=verbosity, interactive=interactive, failfast=failfast ) return test_runner.run_tests(settings.INSTALLED_APPS) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser( description='Runs the test suite for sorl-thumbnail.' ) parser.add_argument( '--settings', dest='settings_module', action='store', default='settings.default', help='Specify settings module.', ) parser.add_argument( '--noinput', dest='interactive', action='store_false', default=True, help='Do not prompt the user for input of any kind.', ) parser.add_argument( '--failfast', dest='failfast', action='store_true', default=False, help='Stop running the test suite after first failed test.', ) args = parser.parse_args() failures = runtests( verbosity=1, interactive=args.interactive, failfast=args.failfast, settings_module=args.settings_module, ) if failures: sys.exit(bool(failures)) sorl-thumbnail-11.12/tests/thumbnail_tests/0000755000175000017500000000000011677075433020050 5ustar mikkomikkosorl-thumbnail-11.12/tests/thumbnail_tests/__init__.py0000644000175000017500000000000011677073446022151 0ustar mikkomikkosorl-thumbnail-11.12/tests/thumbnail_tests/storage.py0000644000175000017500000000514311677073446022073 0ustar mikkomikkofrom django.core.files.storage import FileSystemStorage class SlogHandler(object): _log = [] _active = False def start_log(self): self._active = True def stop_log(self): self._active = False log = self._log[:] self._log = [] return log def log(self, s): if self._active: self._log.append(s) slog = SlogHandler() class TestStorageMixin(object): def open(self, name, *args, **kwargs): slog.log('open: %s' % name) return super(TestStorageMixin, self).open(name, *args, **kwargs) def save(self, name, *args, **kwargs): slog.log('save: %s' % name) return super(TestStorageMixin, self).save(name, *args, **kwargs) def get_valid_name(self, name, *args, **kwargs): slog.log('get_valid_name: %s' % name) return super(TestStorageMixin, self).get_valid_name(name, *args, **kwargs) def get_available_name(self, name, *args, **kwargs): slog.log('get_available_name: %s' % name) return super(TestStorageMixin, self).get_available_name(name, *args, **kwargs) def path(self, name, *args, **kwargs): #slog.log('path: %s' % name) return super(TestStorageMixin, self).path(name, *args, **kwargs) def delete(self, name, *args, **kwargs): slog.log('delete: %s' % name) return super(TestStorageMixin, self).delete(name, *args, **kwargs) def exists(self, name, *args, **kwargs): slog.log('exists: %s' % name) return super(TestStorageMixin, self).exists(name, *args, **kwargs) def listdir(self, name, *args, **kwargs): slog.log('listdir: %s' % name) return super(TestStorageMixin, self).listdir(name, *args, **kwargs) def size(self, name, *args, **kwargs): slog.log('size: %s' % name) return super(TestStorageMixin, self).size(name, *args, **kwargs) def url(self, name, *args, **kwargs): #slog.log('url: %s' % name) return super(TestStorageMixin, self).url(name, *args, **kwargs) def accessed_time(self, name, *args, **kwargs): slog.log('accessed_time: %s' % name) return super(TestStorageMixin, self).accessed_time(name, *args, **kwargs) def created_time(self, name, *args, **kwargs): slog.log('created_time: %s' % name) return super(TestStorageMixin, self).created_time(name, *args, **kwargs) def modified_time(self, name, *args, **kwargs): slog.log('modified_time: %s' % name) return super(TestStorageMixin, self).modified_time(name, *args, **kwargs) class TestStorage(TestStorageMixin, FileSystemStorage): pass sorl-thumbnail-11.12/tests/thumbnail_tests/views.py0000644000175000017500000000000011677073446021547 0ustar mikkomikkosorl-thumbnail-11.12/tests/thumbnail_tests/urls.py0000644000175000017500000000050211677073446021406 0ustar mikkomikkofrom django.conf.urls.defaults import * from django.conf import settings urlpatterns = patterns('', (r'^media/(?P.*)$', 'django.views.static.serve', { 'document_root': settings.MEDIA_ROOT, 'show_indexes': True} ), (r'^(.*\.html)$', 'django.views.generic.simple.direct_to_template'), ) sorl-thumbnail-11.12/tests/thumbnail_tests/models.py0000644000175000017500000000020711677073446021706 0ustar mikkomikkofrom django.db import models from sorl.thumbnail import ImageField class Item(models.Model): image = ImageField(upload_to=True) sorl-thumbnail-11.12/tests/thumbnail_tests/templates/0000755000175000017500000000000011677075433022046 5ustar mikkomikkosorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail1.html0000644000175000017500000000036511677073446025006 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image "200x100" crop="50% 50%" as im %} {% empty %} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnaild1.html0000644000175000017500000000031511677073446025145 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail anything "200x100" as im %} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail6.html0000644000175000017500000000044311677073446025010 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image "100x100" as th %} {% thumbnail item.image "400x400" as im %} {% endthumbnail %} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail8a.html0000644000175000017500000000025011677073446025147 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image "100x100" crop="center" upscale=True quality=77 as th %} {{ th.url }} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail3.html0000644000175000017500000000036511677073446025010 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail "http://www.aino.se/media/i/logo.png" "20x20" crop="center" as im %} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail7.html0000644000175000017500000000025211677073446025007 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image "100x100" crop="center" upscale="True" quality=70 as th %} {{ th.url }} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail5.html0000644000175000017500000000037111677073446025007 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail sorl "30x30" crop="50% 50%" as im %} {% empty %}

empty{{ im }}

{% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail7a.html0000644000175000017500000000025211677073446025150 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image "100x100" quality=70 crop="center" upscale="True" as th %} {{ th.url }} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnaild3.html0000644000175000017500000000025211677073446025147 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail "" "x400" as im %} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail8.html0000644000175000017500000000022211677073446025005 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image "100x100" options=options as th %} {{ th.url }} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail4.html0000644000175000017500000000060111677073446025002 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% if source|is_portrait %} {% thumbnail source '1x1' as im %} {% endthumbnail %} {% else %} {% thumbnail source dims as im %} {% endthumbnail %} {% endif %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail2.html0000644000175000017500000000035411677073446025005 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail item.image.name "200x100" format="PNG" quality=99 as im %} {% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail9.html0000644000175000017500000000037611677073446025020 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail "invalid" "30x30" crop="50% 50%" as im %} {% empty %}

empty{{ im }}

{% endthumbnail %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnail20.html0000644000175000017500000000022311677073446025060 0ustar mikkomikko{% load thumbnail %} {% thumbnail image "32x32" crop="center" as im %} {% empty %}

fail

{% endthumbnail %} sorl-thumbnail-11.12/tests/thumbnail_tests/templates/thumbnaild2.html0000644000175000017500000000034011677073446025144 0ustar mikkomikko{% load thumbnail %}{% spaceless %} {% thumbnail anything "300" as im %} {% endthumbnail %} {% if not ""|is_portrait %}

NOT

{% endif %} {% endspaceless %} sorl-thumbnail-11.12/tests/thumbnail_tests/kvstore.py0000644000175000017500000000157411677073446022130 0ustar mikkomikkofrom sorl.thumbnail.kvstores.cached_db_kvstore import KVStore class KVlogHandler(object): _log = [] _active = False def start_log(self): self._active = True def stop_log(self): self._active = False log = self._log[:] self._log = [] return log def log(self, s): if self._active: self._log.append(s) kvlog = KVlogHandler() class TestKvStoreMixin(object): def get(self, *args, **kwargs): kvlog.log('get') return super(TestKvStoreMixin, self).get(*args, **kwargs) def set(self, *args, **kwargs): kvlog.log('set') return super(TestKvStoreMixin, self).set(*args, **kwargs) def delete(self, *args, **kwargs): kvlog.log('delete') return super(TestKvStoreMixin, self).delete(*args, **kwargs) class TestKVStore(TestKvStoreMixin, KVStore): pass sorl-thumbnail-11.12/tests/thumbnail_tests/tests.py0000644000175000017500000005363411677073446021601 0ustar mikkomikko#coding=utf-8 import logging import operator import os import re import shutil from PIL import Image from django.core.files.storage import default_storage from django.template.loader import render_to_string from django.test.client import Client from django.utils import unittest from os.path import join as pjoin from sorl.thumbnail import default, get_thumbnail, delete from sorl.thumbnail.conf import settings from sorl.thumbnail.engines.pil_engine import Engine as PILEngine from sorl.thumbnail.helpers import get_module_class, ThumbnailError from sorl.thumbnail.images import ImageFile from sorl.thumbnail.log import ThumbnailLogHandler from sorl.thumbnail.parsers import parse_crop, parse_geometry from sorl.thumbnail.templatetags.thumbnail import margin from subprocess import Popen, PIPE from thumbnail_tests.models import Item from thumbnail_tests.storage import slog handler = ThumbnailLogHandler() handler.setLevel(logging.ERROR) logging.getLogger('sorl.thumbnail').addHandler(handler) class StorageTestCase(unittest.TestCase): def setUp(self): name = 'org.jpg' os.makedirs(settings.MEDIA_ROOT) fn = pjoin(settings.MEDIA_ROOT, name) Image.new('L', (100, 100)).save(fn) self.im = ImageFile(name) def test_a_new(self): slog.start_log() get_thumbnail(self.im, '50x50') log = slog.stop_log() actions = [ 'exists: test/cache/20/c7/20c7ceda51cd4d26f8f4f375cf9dddf3.jpg', # first see if the file exists 'open: org.jpg', # open the original for thumbnailing 'save: test/cache/20/c7/20c7ceda51cd4d26f8f4f375cf9dddf3.jpg', # save the file 'get_available_name: test/cache/20/c7/20c7ceda51cd4d26f8f4f375cf9dddf3.jpg', # cehck for filename 'exists: test/cache/20/c7/20c7ceda51cd4d26f8f4f375cf9dddf3.jpg', # called by get_available_name ] self.assertEqual(log, actions) def test_b_cached(self): slog.start_log() get_thumbnail(self.im, '50x50') log = slog.stop_log() self.assertEqual(log, []) # now this should all be in cache def test_c_safe_methods(self): slog.start_log() im = default.kvstore.get(self.im) im.url, im.x, im.y log = slog.stop_log() self.assertEqual(log, []) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) class ParsersTestCase(unittest.TestCase): def testAliasCrop(self): crop = parse_crop('center', (500, 500), (400, 400)) self.assertEqual(crop, (50, 50)) crop = parse_crop('right', (500, 500), (400, 400)) self.assertEqual(crop, (100, 50)) def testPercentCrop(self): crop = parse_crop('50% 0%', (500, 500), (400, 400)) self.assertEqual(crop, (50, 0)) crop = parse_crop('10% 80%', (500, 500), (400, 400)) self.assertEqual(crop, (10, 80)) def testPxCrop(self): crop = parse_crop('200px 33px', (500, 500), (400, 400)) self.assertEqual(crop, (100, 33)) def testBadCrop(self): self.assertRaises(ThumbnailError, parse_crop, '-200px', (500, 500), (400, 400)) def testGeometry(self): g = parse_geometry('222x30') self.assertEqual(g, (222, 30)) g = parse_geometry('222') self.assertEqual(g, (222, None)) g = parse_geometry('x999') self.assertEqual(g, (None, 999)) class SimpleTestCaseBase(unittest.TestCase): def setUp(self): self.backend = get_module_class(settings.THUMBNAIL_BACKEND)() self.engine = get_module_class(settings.THUMBNAIL_ENGINE)() self.kvstore = get_module_class(settings.THUMBNAIL_KVSTORE)() if not os.path.exists(settings.MEDIA_ROOT): os.makedirs(settings.MEDIA_ROOT) dims = [ (500, 500), (100, 100), ] for dim in dims: name = '%sx%s.jpg' % dim fn = pjoin(settings.MEDIA_ROOT, name) im = Image.new('L', dim) im.save(fn) Item.objects.get_or_create(image=name) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) class SimpleTestCase(SimpleTestCaseBase): def testSimple(self): item = Item.objects.get(image='500x500.jpg') t = self.backend.get_thumbnail(item.image, '400x300', crop='center') self.assertEqual(t.x, 400) self.assertEqual(t.y, 300) t = self.backend.get_thumbnail(item.image, '1200x900', crop='13% 89%') self.assertEqual(t.x, 1200) self.assertEqual(t.y, 900) def testUpscale(self): item = Item.objects.get(image='100x100.jpg') t = self.backend.get_thumbnail(item.image, '400x300', upscale=False) self.assertEqual(t.x, 100) self.assertEqual(t.y, 100) t = self.backend.get_thumbnail(item.image, '400x300', upscale=True) self.assertEqual(t.x, 300) self.assertEqual(t.y, 300) def testKVStore(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) self.kvstore.delete_thumbnails(im) th1 = self.backend.get_thumbnail(im, '50') th2 = self.backend.get_thumbnail(im, 'x50') th3 = self.backend.get_thumbnail(im, '20x20') self.assertEqual( set([th1.key, th2.key, th3.key]), set(self.kvstore._get(im.key, identity='thumbnails')) ) self.kvstore.delete_thumbnails(im) self.assertEqual( None, self.kvstore._get(im.key, identity='thumbnails') ) def testIsPortrait(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) th = self.backend.get_thumbnail(im, '50x200', crop='center') self.assertEqual(th.is_portrait(), True) th = self.backend.get_thumbnail(im, '500x2', crop='center') self.assertEqual(th.is_portrait(), False) def testMargin(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) self.assertEqual(margin(im, '1000x1000'), '250px 250px 250px 250px') self.assertEqual(margin(im, '800x1000'), '250px 150px 250px 150px') self.assertEqual(margin(im, '500x500'), '0px 0px 0px 0px') self.assertEqual(margin(im, '500x501'), '0px 0px 1px 0px') self.assertEqual(margin(im, '503x500'), '0px 2px 0px 1px') self.assertEqual(margin(im, '300x300'), '-100px -100px -100px -100px') def testKVStoreGetSet(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) self.kvstore.delete(im) self.assertEqual(self.kvstore.get(im), None) self.kvstore.set(im) self.assertEqual(im.size, [500, 500]) def test_cleanup1(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) self.kvstore.delete_thumbnails(im) th = self.backend.get_thumbnail(im, '3x3') self.assertEqual(th.exists(), True) th.delete() self.assertEqual(th.exists(), False) self.assertEqual(self.kvstore.get(th).x, 3) self.assertEqual(self.kvstore.get(th).y, 3) self.kvstore.cleanup() self.assertEqual(self.kvstore.get(th), None) self.kvstore.delete(im) def test_cleanup2(self): self.kvstore.clear() im = ImageFile(Item.objects.get(image='500x500.jpg').image) th3 = self.backend.get_thumbnail(im, '27x27') th4 = self.backend.get_thumbnail(im, '81x81') def keys_test(x, y, z): self.assertEqual(x, len(list(self.kvstore._find_keys(identity='image')))) self.assertEqual(y, len(list(self.kvstore._find_keys(identity='thumbnails')))) self.assertEqual(z, len(self.kvstore._get(im.key, identity='thumbnails') or [])) keys_test(3, 1, 2) th3.delete() keys_test(3, 1, 2) self.kvstore.cleanup() keys_test(2, 1, 1) th4.delete() keys_test(2, 1, 1) self.kvstore.cleanup() keys_test(1, 0, 0) self.kvstore.clear() keys_test(0, 0, 0) def test_storage_serialize(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) self.assertEqual(im.serialize_storage(), 'thumbnail_tests.storage.TestStorage') self.assertEqual( ImageFile('http://www.image.jpg').serialize_storage(), 'sorl.thumbnail.images.UrlStorage', ) self.assertEqual( ImageFile('http://www.image.jpg', default.storage).serialize_storage(), 'thumbnail_tests.storage.TestStorage', ) self.assertEqual( ImageFile('getit', default_storage).serialize_storage(), 'thumbnail_tests.storage.TestStorage', ) def test_quality(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) th = self.backend.get_thumbnail(im, '100x100', quality=50) p1 = Popen(['identify', '-verbose', th.storage.path(th.name)], stdout=PIPE) p2 = Popen(['grep', '-c', 'Quality: 50'], stdin=p1.stdout, stdout=PIPE) p1.stdout.close() output = p2.communicate()[0].strip() self.assertEqual(output, '1') def test_image_file_deserialize(self): im = ImageFile(Item.objects.get(image='500x500.jpg').image) default.kvstore.set(im) self.assertEqual( default.kvstore.get(im).serialize_storage(), 'thumbnail_tests.storage.TestStorage', ) im = ImageFile('http://www.aino.se/media/i/logo.png') default.kvstore.set(im) self.assertEqual( default.kvstore.get(im).serialize_storage(), 'sorl.thumbnail.images.UrlStorage', ) def test_abspath(self): item = Item.objects.get(image='500x500.jpg') image = ImageFile(item.image.path) val = render_to_string('thumbnail20.html', { 'image': image, }).strip() im = self.backend.get_thumbnail(image, '32x32', crop='center') self.assertEqual('' % im.url, val) class TemplateTestCaseA(SimpleTestCaseBase): def testModel(self): item = Item.objects.get(image='500x500.jpg') val = render_to_string('thumbnail1.html', { 'item': item, }).strip() self.assertEqual(val, u'') val = render_to_string('thumbnail2.html', { 'item': item, }).strip() self.assertEqual(val, u'') def test_nested(self): item = Item.objects.get(image='500x500.jpg') val = render_to_string('thumbnail6.html', { 'item': item, }).strip() self.assertEqual(val, ('' '' '')) def test_serialization_options(self): item = Item.objects.get(image='500x500.jpg') for j in xrange(0, 20): # we could be lucky... val0 = render_to_string('thumbnail7.html', { 'item': item, }).strip() val1 = render_to_string('thumbnail7a.html', { 'item': item, }).strip() self.assertEqual(val0, val1) def test_options(self): item = Item.objects.get(image='500x500.jpg') options = { 'crop': "center", 'upscale': True, 'quality': 77, } val0 = render_to_string('thumbnail8.html', { 'item': item, 'options': options, }).strip() val1 = render_to_string('thumbnail8a.html', { 'item': item, }).strip() self.assertEqual(val0, val1) def test_progressive(self): im = Item.objects.get(image='500x500.jpg').image th = self.backend.get_thumbnail(im, '100x100', progressive=True) path = pjoin(settings.MEDIA_ROOT, th.name) p = Popen(['identify', '-verbose', path], stdout=PIPE) p.wait() m = re.search('Interlace: JPEG', p.stdout.read()) self.assertEqual(bool(m), True) def test_nonprogressive(self): im = Item.objects.get(image='500x500.jpg').image th = self.backend.get_thumbnail(im, '100x100', progressive=False) path = pjoin(settings.MEDIA_ROOT, th.name) p = Popen(['identify', '-verbose', path], stdout=PIPE) p.wait() m = re.search('Interlace: None', p.stdout.read()) self.assertEqual(bool(m), True) def test_orientation(self): data_dir = pjoin(settings.MEDIA_ROOT, 'data') shutil.copytree(settings.DATA_ROOT, data_dir) ref = Image.open(pjoin(data_dir, '1_topleft.jpg')) top = ref.getpixel((14, 7)) left = ref.getpixel((7, 14)) engine = PILEngine() def epsilon(x, y): if isinstance(x, (tuple, list)): x = sum(x) / len(x) if isinstance(y, (tuple, list)): y = sum(y) / len(y) return abs(x - y) for name in sorted(os.listdir(data_dir)): th = self.backend.get_thumbnail('data/%s' % name, '30x30') im = engine.get_image(th) self.assertLess(epsilon(top, im.getpixel((14, 7))), 10) self.assertLess(epsilon(left, im.getpixel((7, 14))), 10) exif = im._getexif() if exif: self.assertEqual(exif.get(0x0112), 1) class TemplateTestCaseB(unittest.TestCase): def tearDown(self): try: shutil.rmtree(settings.MEDIA_ROOT) except Exception: pass def testUrl(self): val = render_to_string('thumbnail3.html', {}).strip() self.assertEqual(val, '') def testPortrait(self): val = render_to_string('thumbnail4.html', { 'source': 'http://www.aino.se/media/i/logo.png', 'dims': 'x666', }).strip() self.assertEqual(val, '') def testEmpty(self): val = render_to_string('thumbnail5.html', {}).strip() self.assertEqual(val, '

empty

') class TemplateTestCaseClient(unittest.TestCase): def setUp(self): self.org_settings = {} params = { 'THUMBNAIL_DEBUG': False, } for k, v in params.iteritems(): self.org_settings[k] = getattr(settings, k) setattr(settings, k, v) def testEmptyError(self): client = Client() response = client.get('/thumbnail9.html') self.assertEqual(response.content.strip(), '

empty

') from django.core.mail import outbox self.assertEqual(outbox[0].subject, '[sorl-thumbnail] ERROR: /thumbnail9.html') end = outbox[0].body.split('\n\n')[-2][-20:-1] self.assertEqual(end, 'tests/media/invalid') def tearDown(self): for k, v in self.org_settings.iteritems(): setattr(settings, k, v) class CropTestCase(unittest.TestCase): def setUp(self): self.backend = get_module_class(settings.THUMBNAIL_BACKEND)() self.engine = get_module_class(settings.THUMBNAIL_ENGINE)() self.kvstore = get_module_class(settings.THUMBNAIL_KVSTORE)() if not os.path.exists(settings.MEDIA_ROOT): os.makedirs(settings.MEDIA_ROOT) # portrait name = 'portrait.jpg' fn = pjoin(settings.MEDIA_ROOT, name) im = Image.new('L', (100, 200)) im.paste(255, (0, 0, 100, 100)) im.save(fn) self.portrait = ImageFile(Item.objects.get_or_create(image=name)[0].image) self.kvstore.delete(self.portrait) # landscape name = 'landscape.jpg' fn = pjoin(settings.MEDIA_ROOT, name) im = Image.new('L', (200, 100)) im.paste(255, (0, 0, 100, 100)) im.save(fn) self.landscape = ImageFile(Item.objects.get_or_create(image=name)[0].image) self.kvstore.delete(self.landscape) def testPortraitCrop(self): def mean_pixel(x, y): values = im.getpixel((x, y)) if not isinstance(values, (tuple, list)): values = [values] return reduce(operator.add, values) / len(values) for crop in ('center', '88% 50%', '50px'): th = self.backend.get_thumbnail(self.portrait, '100x100', crop=crop) engine = PILEngine() im = engine.get_image(th) self.assertEqual(mean_pixel(50,0), 255) self.assertEqual(mean_pixel(50,45), 255) self.assertEqual(250 < mean_pixel(50,49) <= 255, True) self.assertEqual(mean_pixel(50,55), 0) self.assertEqual(mean_pixel(50,99), 0) for crop in ('top', '0%', '0px'): th = self.backend.get_thumbnail(self.portrait, '100x100', crop=crop) engine = PILEngine() im = engine.get_image(th) for x in xrange(0, 99, 10): for y in xrange(0, 99, 10): self.assertEqual(250 < mean_pixel(x, y) <= 255, True) for crop in ('bottom', '100%', '100px'): th = self.backend.get_thumbnail(self.portrait, '100x100', crop=crop) engine = PILEngine() im = engine.get_image(th) for x in xrange(0, 99, 10): for y in xrange(0, 99, 10): self.assertEqual(0 <= mean_pixel(x, y) < 5, True) def testLandscapeCrop(self): def mean_pixel(x, y): values = im.getpixel((x, y)) if not isinstance(values, (tuple, list)): values = [values] return reduce(operator.add, values) / len(values) for crop in ('center', '50% 200%', '50px 700px'): th = self.backend.get_thumbnail(self.landscape, '100x100', crop=crop) engine = PILEngine() im = engine.get_image(th) self.assertEqual(mean_pixel(0, 50), 255) self.assertEqual(mean_pixel(45, 50), 255) self.assertEqual(250 < mean_pixel(49, 50) <= 255, True) self.assertEqual(mean_pixel(55, 50), 0) self.assertEqual(mean_pixel(99, 50), 0) for crop in ('left', '0%', '0px'): th = self.backend.get_thumbnail(self.landscape, '100x100', crop=crop) engine = PILEngine() im = engine.get_image(th) for x in xrange(0, 99, 10): for y in xrange(0, 99, 10): self.assertEqual(250 < mean_pixel(x, y) <= 255, True) for crop in ('right', '100%', '100px'): th = self.backend.get_thumbnail(self.landscape, '100x100', crop=crop) engine = PILEngine() im = engine.get_image(th) for x in xrange(0, 99, 10): for y in xrange(0, 99, 10): self.assertEqual(0 <= mean_pixel(x, y) < 5, True) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) class DummyTestCase(unittest.TestCase): def setUp(self): self.backend = get_module_class(settings.THUMBNAIL_BACKEND)() self.org_settings = {} params = { 'THUMBNAIL_DUMMY': True, } for k, v in params.iteritems(): self.org_settings[k] = getattr(settings, k) setattr(settings, k, v) def test_dummy_tags(self): val = render_to_string('thumbnaild1.html', { 'anything': 'AINO', }).strip() self.assertEqual(val, '') val = render_to_string('thumbnaild2.html', { 'anything': None, }).strip() self.assertEqual(val, '

NOT

') val = render_to_string('thumbnaild3.html', { }).strip() self.assertEqual(val, '') def tearDown(self): for k, v in self.org_settings.iteritems(): setattr(settings, k, v) class ModelTestCase(SimpleTestCaseBase): def test_field1(self): self.kvstore.clear() item = Item.objects.get(image='100x100.jpg') im = ImageFile(item.image) self.assertEqual(None, self.kvstore.get(im)) self.backend.get_thumbnail(im, '27x27') self.backend.get_thumbnail(im, '81x81') self.assertNotEqual(None, self.kvstore.get(im)) self.assertEqual(3, len(list(self.kvstore._find_keys(identity='image')))) self.assertEqual(1, len(list(self.kvstore._find_keys(identity='thumbnails')))) class BackendTest(SimpleTestCaseBase): def test_delete(self): im1 = Item.objects.get(image='100x100.jpg').image im2 = Item.objects.get(image='500x500.jpg').image default.kvstore.get_or_set(ImageFile(im1)) # exists in kvstore and in storage self.assertTrue(bool(default.kvstore.get(ImageFile(im1)))) self.assertTrue(ImageFile(im1).exists()) # delete delete(im1) self.assertFalse(bool(default.kvstore.get(ImageFile(im1)))) self.assertFalse(ImageFile(im1).exists()) default.kvstore.get_or_set(ImageFile(im2)) # exists in kvstore and in storage self.assertTrue(bool(default.kvstore.get(ImageFile(im2)))) self.assertTrue(ImageFile(im2).exists()) # delete delete(im2, delete_file=False) self.assertFalse(bool(default.kvstore.get(ImageFile(im2)))) self.assertTrue(ImageFile(im2).exists()) class TestInputCase(unittest.TestCase): def setUp(self): if not os.path.exists(settings.MEDIA_ROOT): os.makedirs(settings.MEDIA_ROOT) self.name = u'åäö.jpg' fn = pjoin(settings.MEDIA_ROOT, self.name) im = Image.new('L', (666, 666)) im.save(fn) def test_nonascii(self): # also test the get_thumbnail shortcut th = get_thumbnail(self.name, '200x200') self.assertEqual( th.url, '/media/test/cache/8a/17/8a17eff95c6ecf46f82d0807d93631e9.jpg' ) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) sorl-thumbnail-11.12/tests/runalltests.sh0000755000175000017500000000017211677073446017564 0ustar mikkomikko#/bin/bash for name in pil pgmagick imagemagick graphicsmagick redis do ./runtests.py --settings=settings.$name done sorl-thumbnail-11.12/tests/data/0000755000175000017500000000000011677075433015554 5ustar mikkomikkosorl-thumbnail-11.12/tests/data/8_leftbottom.jpg0000644000175000017500000000102411677073446020663 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac  ?  & #'678$%)59CFGHXdei?_BUa㳛Y7sorl-thumbnail-11.12/tests/data/4_bottomleft.jpg0000644000175000017500000000101611677073446020660 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac   > & 769#$%5CFIWXdegh?r֎מnL'Q‹țiiה32YgeL ?/76ښ]1/d@\LGhZ\d/ӧ\A1L =YeԱRⶼ[Y$D\ qr9Wo4ɚ\F"s9mBb Ϊ*@Wf_m}f 7sorl-thumbnail-11.12/tests/data/2_topright.jpg0000644000175000017500000000102711677073446020341 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac   A   $%&6F#'579IWXefgh? [FڨtjbtL1:ig}PUQ/gF}N:rqY=۠3%!]gʣvyAxx=SyNMsI㲼m ).]$ۧ"q' ʪ .tw]|;?? /2osorl-thumbnail-11.12/tests/data/1_topleft.jpg0000644000175000017500000000102311677073446020151 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac  > % #'7f$459EGIWXgh?A mg^=LBWWhH (Df8+DKwU`,6Z`2o[պacLSm].b]QwyԻݫakohl mRwoڈl|7{Dm"'M R?Yz;̚JrW%"ZqGxeQ24.IrF . ϼ %!x\U6eD ק7Fo>j1Qz}\%'sorl-thumbnail-11.12/tests/data/5_lefttop.jpg0000644000175000017500000000102511677073446020157 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac  > % 8Gf#$&'4569EHXi?;u&lf.JJjVB?eُ8!E y3X: 6۞N 0q+e.v>EJQ׿p1. V'ݳ'y;[b @QՕ^7_ mp}|!Uv_Q?sorl-thumbnail-11.12/tests/data/7_rightbottom.jpg0000644000175000017500000000102211677073446021043 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac   @   %'7G #$&)4589DHXefi?-uf-TvwoB >o yO1wb3dNxEqVj{1SZ3@B,qJRJN ӄf\FrBwRgj1Q|\RMπEX#.ޤ5څϵ!;4+JՊK:F7.Ly \j]L"2tv}dT)O ?#;* it=>*Y-dsorl-thumbnail-11.12/tests/data/6_righttop.jpg0000644000175000017500000000103111677073446020340 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac   @   $%&6F#5789GHXefi?+u& UV_(WJF9gc6NBɤRy}O% dDB@DVaRaf3B,NOēebF2jb>K"cQ'O>vYniƓ4U|)uUIΤq؋raGơЌgvW>^7sk4WNNHT<4ZiWGŗO\^ww9rɿ,c?O ,wjsorl-thumbnail-11.12/tests/data/3_bottomright.jpg0000644000175000017500000000102111677073446021036 0ustar mikkomikkoJFIFHH"ExifII*CCreated with GIMP on a Mac  C  %&'#$(456789DGIWXefgh? ϮݙO‹؛bPŤ_nKd4i:ǜb)}C4bA3@B,qRJ]d35I d\#*I- N%pn#1pZm'^S=̳₪2vwo?IYI`! GwqG]w֫jnh&e/$'daDS )М^Ue>TAsorl-thumbnail-11.12/sorl_thumbnail.egg-info/0000755000175000017500000000000011677075433020215 5ustar mikkomikkosorl-thumbnail-11.12/sorl_thumbnail.egg-info/not-zip-safe0000644000175000017500000000000111553522402022425 0ustar mikkomikko sorl-thumbnail-11.12/sorl_thumbnail.egg-info/SOURCES.txt0000644000175000017500000000572111677075433022106 0ustar mikkomikkoLICENSE MANIFEST.in README.rst setup.py docs/Makefile docs/conf.py docs/examples.rst docs/index.rst docs/installation.rst docs/logging.rst docs/make.bat docs/management.rst docs/operation.rst docs/requirements.rst docs/template.rst docs/_theme/nature/theme.conf docs/_theme/nature/static/nature.css_t docs/_theme/nature/static/pygments.css docs/reference/image.rst docs/reference/index.rst docs/reference/settings.rst sorl/__init__.py sorl/thumbnail/__init__.py sorl/thumbnail/base.py sorl/thumbnail/default.py sorl/thumbnail/fields.py sorl/thumbnail/helpers.py sorl/thumbnail/images.py sorl/thumbnail/log.py sorl/thumbnail/models.py sorl/thumbnail/parsers.py sorl/thumbnail/shortcuts.py sorl/thumbnail/urls.py sorl/thumbnail/admin/__init__.py sorl/thumbnail/admin/compat.py sorl/thumbnail/admin/current.py sorl/thumbnail/conf/__init__.py sorl/thumbnail/conf/defaults.py sorl/thumbnail/engines/__init__.py sorl/thumbnail/engines/base.py sorl/thumbnail/engines/convert_engine.py sorl/thumbnail/engines/pgmagick_engine.py sorl/thumbnail/engines/pil_engine.py sorl/thumbnail/kvstores/__init__.py sorl/thumbnail/kvstores/base.py sorl/thumbnail/kvstores/cached_db_kvstore.py sorl/thumbnail/kvstores/redis_kvstore.py sorl/thumbnail/management/__init__.py sorl/thumbnail/management/commands/__init__.py sorl/thumbnail/management/commands/thumbnail.py sorl/thumbnail/templatetags/__init__.py sorl/thumbnail/templatetags/thumbnail.py sorl_thumbnail.egg-info/PKG-INFO sorl_thumbnail.egg-info/SOURCES.txt sorl_thumbnail.egg-info/dependency_links.txt sorl_thumbnail.egg-info/not-zip-safe sorl_thumbnail.egg-info/top_level.txt tests/__init__.py tests/runalltests.sh tests/runtests.py tests/data/1_topleft.jpg tests/data/2_topright.jpg tests/data/3_bottomright.jpg tests/data/4_bottomleft.jpg tests/data/5_lefttop.jpg tests/data/6_righttop.jpg tests/data/7_rightbottom.jpg tests/data/8_leftbottom.jpg tests/settings/__init__.py tests/settings/default.py tests/settings/graphicsmagick.py tests/settings/imagemagick.py tests/settings/pgmagick.py tests/settings/pil.py tests/settings/redis.py tests/thumbnail_tests/__init__.py tests/thumbnail_tests/kvstore.py tests/thumbnail_tests/models.py tests/thumbnail_tests/storage.py tests/thumbnail_tests/tests.py tests/thumbnail_tests/urls.py tests/thumbnail_tests/views.py tests/thumbnail_tests/templates/thumbnail1.html tests/thumbnail_tests/templates/thumbnail2.html tests/thumbnail_tests/templates/thumbnail20.html tests/thumbnail_tests/templates/thumbnail3.html tests/thumbnail_tests/templates/thumbnail4.html tests/thumbnail_tests/templates/thumbnail5.html tests/thumbnail_tests/templates/thumbnail6.html tests/thumbnail_tests/templates/thumbnail7.html tests/thumbnail_tests/templates/thumbnail7a.html tests/thumbnail_tests/templates/thumbnail8.html tests/thumbnail_tests/templates/thumbnail8a.html tests/thumbnail_tests/templates/thumbnail9.html tests/thumbnail_tests/templates/thumbnaild1.html tests/thumbnail_tests/templates/thumbnaild2.html tests/thumbnail_tests/templates/thumbnaild3.htmlsorl-thumbnail-11.12/sorl_thumbnail.egg-info/top_level.txt0000644000175000017500000000000511677075433022742 0ustar mikkomikkosorl sorl-thumbnail-11.12/sorl_thumbnail.egg-info/dependency_links.txt0000644000175000017500000000000111677075433024263 0ustar mikkomikko sorl-thumbnail-11.12/sorl_thumbnail.egg-info/PKG-INFO0000644000175000017500000000262011677075433021312 0ustar mikkomikkoMetadata-Version: 1.0 Name: sorl-thumbnail Version: 11.12 Summary: Thumbnails for Django Home-page: https://github.com/sorl/sorl-thumbnail Author: Mikko Hellsing Author-email: mikko@aino.se License: BSD Description: sorl-thumbnail ============== Thumbnails for Django. Totally rewritten. Features at a glance -------------------- - Storage support - Pluggable Engine support (ImageMagick, PIL, pgmagick included) - Pluggable Key Value Store support (cached db, redis) - Pluggable Backend support - Admin integration with possibility to delete - Dummy generation (placeholders) - Flexible, simple syntax, generates no html - ImageField for model that deletes thumbnails - CSS style cropping options - Margin calculation for vertical positioning Read more in `the documentation (latest version) `_ Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Multimedia :: Graphics Classifier: Framework :: Django sorl-thumbnail-11.12/LICENSE0000644000175000017500000000275611666137404014513 0ustar mikkomikkoCopyright (c) 2010, Mikko Hellsing 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 of the sorl-thumbnail 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 HOLDER 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. sorl-thumbnail-11.12/docs/0000755000175000017500000000000011677075433014431 5ustar mikkomikkosorl-thumbnail-11.12/docs/examples.rst0000644000175000017500000001140311635604732016772 0ustar mikkomikko******** Examples ******** Template examples ================= .. highlight:: html+django All of the examples assume that you first load the ``thumbnail`` template tag in your template:: {% load thumbnail %} Simple:: {% thumbnail item.image "100x100" crop="center" as im %} {% endthumbnail %} Crop using margin filter, x, y aliases:: {% thumbnail item.image "100x700" as im %} {% endthumbnail %} Using external images and advanced cropping:: {% thumbnail "http://www.aino.se/media/i/logo.png" "40x40" crop="80% top" as im %} {% endthumbnail %} Using the empty feature, the empty section is rendered when the source is resolved to an empty value or an invalid image source, you can think of it as rendering when the thumbnail becomes undefined:: {% thumbnail item.image my_size_string crop="left" as im %} {% empty %}

No image

{% endthumbnail %} Nesting tags and setting size (geometry) for width only:: {% thumbnail item.image "1000" as big %} {% thumbnail item.image "50x50" crop="center" as small %} {% endthumbnail %} {% endthumbnail %} Setting geometry for height only:: {% thumbnail item.image "x300" as im %} {% endthumbnail %} Setting format and using the is_portrait filter:: {% if item.image|is_portrait %}
{% thumbnail item.image "100" crop="10px 10px" format="PNG" as im %} {% endthumbnail %}
{% else %}
{% thumbnail item.image "50" crop="bottom" format="PNG" as im %} {% endthumbnail %}

Undefined behaviour

{% endif %} .. highlight:: python Model examples ============== Using the ImageField that automatically deletes references to itself in the key value store and its thumbnail references and the thumbnail files when deleted:: from django.db import models from sorl.thumbnail import ImageField class Item(models.Model): image = ImageField() .. note:: You do not need to use the ``sorl.thumbnail.ImageField`` to use ``sorl.thumbnail``. The standard ``django.db.models.ImageField`` is fine except that it does not know how to delete itself from the Key Value Store or its thumbnails if you delete it. Also using the ``sorl.thumbnail.ImageField`` lets you plugin the nice admin addition explained in the next section. Another example on how to use ``sorl.thumbnail.ImageField`` in your existing project with only small code changes:: # util/models.py from django.db.models import * from sorl.thumbnail import ImageField # myapp/models.py from util import models class MyModel(models.Model): logo = models.ImageField(upload_to='/dev/null') Admin examples ============== Recommended usage using ``sorl.thumbnail.admin.AdminImageMixin``:: # myapp/admin.py from django.contrib import admin from myapp.models import MyModel from sorl.thumbnail.admin import AdminImageMixin class MyModelAdmin(AdminImageMixin, admin.ModelAdmin): pass And the same thing For inlines:: # myapp/admin.py from django.contrib import admin from myapp.models import MyModel, MyInlineModel from sorl.thumbnail.admin import AdminImageMixin class MyInlineModelAdmin(AdminImageMixin, admin.TabularInline): model = MyInlineModel class MyModelAdmin(admin.ModelAdmin): inlines = [MyInlineModelAdmin] Easy to plugin solution example with little code to change:: # util/admin.py from django.contrib.admin import * from sorl.thumbnail.admin import AdminImageMixin class ModelAdmin(AdminImageMixin, ModelAdmin): pass class TabularInline(AdminImageMixin, TabularInline): pass class StackedInline(AdminImageMixin, StackedInline): pass # myapp/admin.py from util import admin from myapp.models import MyModel class MyModelAdmin(admin.ModelAdmin): pass Low level API examples ====================== How to get make a thumbnail in you python code:: from sorl.thumbnail import get_thumbnail im = get_thumbnail(my_file, '100x100', crop='center', quality=99) How to delete a file, its thumbnails as well as references in the Key Value Store:: from sorl.thumbnail import delete delete(my_file) sorl-thumbnail-11.12/docs/requirements.rst0000644000175000017500000000617411666137404017711 0ustar mikkomikko************ Requirements ************ Base requirements ================= - `Python`_ 2.5+ - `Django`_ - :ref:`kvstore-requirements` - :ref:`image-library` .. _kvstore-requirements: Key Value Store =============== sorl-thumbnail needs a Key Value Store for its operation. You can choose between a **cached database** which requires no special installation to your normal Django setup besides installing a proper cache like memcached **or** you can setup **redis** which requires a little bit more work. Cached DB --------- All you need to use the cached database key value store is a database and `cache `_ setup properly using memcached. This cache needs to be really fast so **using anything else than memcached is not recomended**. Redis ----- Redis is a fast key value store also suited for the job. To use the `redis`_ key value store you first need to install the `redis server `_. After that install the `redis client `_:: pip install redis .. _image-library: Image Library ============= You need to have an image library installed. sorl-thumbnail ships with support for `Python Imaging Library`_, `pgmagick`_, `ImageMagick`_ (or `GraphicsMagick`) command line tools. `pgmagick`_ are python bindings for `GraphicsMagick`_ (Magick++)`, The `ImageMagick`_ based engine ``sorl.thumbnail.engines.convert_engine.Engine`` by default calls ``convert`` and ``identify`` shell commands. You can change the paths to these tools by setting ``THUMBNAIL_CONVERT`` and ``THUMBNAIL_IDENTIFY`` respectively. Note that youneed to change these to use `GraphicsMagick`_ to ``/path/to/gm convert`` and ``/path/to/gm identify``. Python Imaging Library installation ----------------------------------- Prerequisites: - libjpeg - zlib Ubuntu 10.04 package installation:: sudo apt-get install libjpeg62 libjpeg62-dev zlib1g-dev Installing `Python Imaging Library`_ using pip:: pip install PIL Watch the output for messages on what support got compiled in, you at least want to see the following:: --- JPEG support available --- ZLIB (PNG/ZIP) support available pgmagick installation --------------------- Prerequisites: - GraphicsMagick - Boost.Python Ubuntu 10.04 package installation:: sudo apt-get install libgraphicsmagick++-dev sudo apt-get install libboost-python1.40-dev Fedora installation:: yum install GraphicsMagick-c++-devel yum install boost-devel Installing `pgmagick`_ using pip:: pip install pgmagick ImageMagick installation ------------------------ Ubuntu 10.04 package installation:: sudo apt-get install imagemagick Or if you prefer `GraphicsMagick`_:: sudo apt-get install graphicsmagick .. _Python Imaging Library: http://www.pythonware.com/products/pil/ .. _ImageMagick: http://imagemagick.com/ .. _GraphicsMagick: http://www.graphicsmagick.org/ .. _redis: http://code.google.com/p/redis/ .. _redis-py: https://github.com/andymccurdy/redis-py/ .. _Django: http://www.djangoproject.com/ .. _Python: http://www.python.org/ .. _pgmagick: http://bitbucket.org/hhatto/pgmagick/src sorl-thumbnail-11.12/docs/logging.rst0000644000175000017500000000216411635604732016606 0ustar mikkomikko**************** Errors & Logging **************** .. highlight:: python Background ========== When ``THUMBNAIL_DEBUG = False`` errors will be suppressed if they are raised during rendering the ``thumbnail`` tag or raised within the included filters. This is the recommended production setting. However it can still be useful to be notified of those errors. Thus sorl-thumbnail logs errors to a logger and provides a log handler that sends emails to ``settings.ADMINS``. How to setup logging ==================== To enable logging you need to add a handler to the 'sorl.thumbnail' logger. The following example adds the provided handler that sends emails to site admins in case an error is raised with debugging off:: import logging from sorl.thumbnail.log import ThumbnailLogHandler handler = ThumbnailLogHandler() handler.setLevel(logging.ERROR) logging.getLogger('sorl.thumbnail').addHandler(handler) You will need to load this code somewhere in your django project, it could be in urls.py, settings.py or project/app __init__.py file for example. You could of course also provide your own logging handler. sorl-thumbnail-11.12/docs/index.rst0000644000175000017500000000052511476142267016271 0ustar mikkomikko****************************** sorl-thumbnail's documentation ****************************** Contents: .. toctree:: :maxdepth: 2 examples installation requirements template management logging operation reference/index Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` sorl-thumbnail-11.12/docs/make.bat0000644000175000017500000001003011467213637016025 0ustar mikkomikko@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sorlthumbnail.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sorlthumbnail.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end sorl-thumbnail-11.12/docs/Makefile0000644000175000017500000001101211467213637016061 0ustar mikkomikko# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sorlthumbnail.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sorlthumbnail.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/sorlthumbnail" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sorlthumbnail" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." sorl-thumbnail-11.12/docs/reference/0000755000175000017500000000000011677075433016367 5ustar mikkomikkosorl-thumbnail-11.12/docs/reference/index.rst0000644000175000017500000000012311501673041020205 0ustar mikkomikko********* Reference ********* .. toctree:: :maxdepth: 2 image settings sorl-thumbnail-11.12/docs/reference/settings.rst0000644000175000017500000001534011666137404020757 0ustar mikkomikko******** Settings ******** .. highlight:: python ``THUMBNAIL_DEBUG`` =================== - Default: ``False`` When set to ``True`` the ``ThumbnailNode.render`` method can raise errors. Django recommends that tags never raise errors in the ``Node.render`` method but since sorl-thumbnail is such a complex tag we will need to have more debugging available. ``THUMBNAIL_BACKEND`` ===================== - Default: ``'sorl.thumbnail.base.ThumbnailBackend'`` This is the entry point for generating thumbnails, you probably want to keep the default one but just in case you would like to generate thumbnails filenames differently or need some special functionality you can override this and use your own implementation. ``THUMBNAIL_KVSTORE`` ===================== - Default: ``'sorl.thumbnail.kvstores.cached_db_kvstore.KVStore'`` sorl-thumbnail needs a Key Value Store to :doc:`/operation`. sorl-thumbnail ships with support for two Key Value Stores: Cached DB --------- ``sorl.thumbnail.kvstores.cached_db_kvstore.KVStore``. This is the default and preferred Key Value Store. Features ^^^^^^^^ * Fast persistent storage * First query uses database which is slow. Successive queries are cached and if you use memcached this is very fast. * Easy to transfer data between environments since the data is in the default database. * If you get the database and fast cache out of sync there could be problems. Redis ----- ``sorl.thumbnail.kvstores.redis_kvstore.KVStore``. It requires you to install a Redis server as well as a `redis python client `_. Features ^^^^^^^^ * Fast persistent storage * More dependencies * Requires a little extra work to transfer data between environments ``THUMBNAIL_KEY_DBCOLUMN`` ========================== - Default ``'key'`` Since MSSQL reserved the ``key`` name for db columns you can change this to something else using this setting. ``THUMBNAIL_ENGINE`` ==================== - Default: ``'sorl.thumbnail.engines.pil_engine.Engine'`` This is the processing class for sorl-thumbnail. It does all the resizing, cropping or whatever processing you want to perform. sorl-thumbnail ships with three engines: PIL --- ``'sorl.thumbnail.engines.pil_engine.Engine'``. This is the default engine because it is what most people have installed already. Features: * Easy to install * Produces good quality images but not the best * It is fast * Can not handle CMYK sources Pgmagick -------- ``'sorl.thumbnail.engines.pgmagick_engine.Engine'``. Pgmagick uses `Graphics `_. Fatures: * Not easy to install unless on linux, very slow to compile * Produces high quality images * It is a tad slow? * Can handle CMYK sources ImageMagick / GraphicsMagick ---------------------------- ``'sorl.thumbnail.engines.convert_engine.Engine'``. This engine uses the ImageMagick ``convert`` or GraphicsMagic ``gm convert`` command. Features: * Easy to install * Produces high quality images * It is pretty fast * Can handle CMYK sources * It is a command line command, that is less than ideal, ``THUMBNAIL_CONVERT`` ===================== - Default ``'convert'`` Path to convert command, use ``'gm convert'`` for GraphicsMagick. Only applicable for the convert Engine. ``THUMBNAIL_IDENTIFY`` ===================== - Default ``'identify'`` Path to identify command, use ``'gm identify'`` for GraphicsMagick. Only applicable for the convert Engine. ``THUMBNAIL_STORAGE`` ===================== - Default: ``settings.DEFAULT_FILE_STORAGE`` The storage class to use for the generated thumbnails. ``THUMBNAIL_REDIS_DB`` ====================== - Default: ``0`` The Redis database. Only applicable for the Redis Key Value Store ``THUMBNAIL_REDIS_PASSWORD`` ============================ - Default: ``''`` The password for Redis server. Only applicable for the Redis Key Value Store ``THUMBNAIL_REDIS_HOST`` ======================== - Default: ``'localhost'`` The host for Redis server. Only applicable for the Redis Key Value Store ``THUMBNAIL_REDIS_PORT`` ======================== - Default: ``6379`` The port for Redis server. Only applicable for the Redis Key Value Store ``THUMBNAIL_CACHE_TIMEOUT`` =========================== - Default: ``3600 * 24 * 365 * 10`` Cache timeout for Cached DB Key Value Store. You should probably keep this at maximum or ``None`` if your caching backend can handle that as infinite. Only applicable for the Cached DB Key Value Store. ``THUMBNAIL_KEY_PREFIX`` ======================== - Default: ``'sorl-thumbnail'`` Key prefix used by the key value store. ``THUMBNAIL_PREFIX`` ==================== - Default: ``'cache/'`` The generated thumbnails filename prefix. ``THUMBNAIL_FORMAT`` ==================== - Default: ``'JPEG'`` Default image format, supported formats are: ``'JPEG'``, ``'PNG'``. This also implicitly sets the filename extension. This can be overridden by individual options. ``THUMBNAIL_COLORSPACE`` ======================== - Default: ``'RGB'`` Default thumbnail color space, engines are required to implement: ``'RGB'``, ``'GRAY'`` Setting this to None will keep the original color space. This can be overridden by individual options. ``THUMBNAIL_UPSCALE`` ===================== - Default: ``True`` Should we upscale by default? ``True`` means we upscale images by default. ``False`` means we don't. This can be overridden by individual options. ``THUMBNAIL_QUALITY`` ===================== - Default: ``95`` Default thumbnail quality. A value between 0 and 100 is allowed. This can be overridden by individual options. ``THUMBNAIL_PROGRESSIVE`` ========================= - Default: ``True`` Saves jpeg thumbnails as progressive jpegs. This can be overridden by individual options. ``THUMBNAIL_DUMMY`` =================== - Default: ``False`` This is a very powerful option which came from real world frustration. The use case is when you want to do development on a deployed project that has image references in its database. Instead of downloading all the image files from the server hosting the deployed project and all its thumbnails we just set this option to ``True``. This will generate placeholder images for all thumbnails missing input source. ``THUMBNAIL_DUMMY_SOURCE`` ========================== - Default ``http://dummyimage.com/%(width)sx%(height)s`` This is the generated thumbnail whensource of the presented thumbnail. Width and Height is passed to the string for formatting. Other options are for example: - ``http://placehold.it/%(width)sx%(height)s`` - ``http://placekitten.com/%(width)s/%(height)s`` ``THUMBNAIL_DUMMY_RATIO`` ========================= - Default: ``1.5`` This value sets an image ratio to all thumbnails that are not defined by width **and** height since we cannot determine from the file input (since we don't have that). sorl-thumbnail-11.12/docs/reference/image.rst0000644000175000017500000000361711501673271020200 0ustar mikkomikko********* ImageFile ********* .. highlight:: html+django ``ImageFile`` is an image abstraction that contains useful attributes when working with images. The ``thumbnail`` template tag puts the generated thumbnail in context as an ``ImageFile`` instance. In the following example:: {% thumbnail item.image "100x100" as im %} {% endthumbnail %} ``im`` will be an ``ImageFile`` instance. .. highlight:: python ImageFile attributes ==================== ``name`` -------- Name of the image as returned from the underlaying storage. ``storage`` ----------- Returns the storage instance. ``width`` --------- Returns the width of the image in pixels. ``x`` ----- Alias of ``width`` ``height`` ---------- Returns the height of the image in pixels. ``y`` ----- Alias of ``width`` ``ratio`` --------- Returns the image ratio (y/x) as a float ``url`` ------- URL of the image url as returned by the underlaying storage. ``src`` ------- Alias of ``url`` ``size`` -------- Returns the image size in pixels as a (x, y) tuple ``key`` ------- Returns a unique key based on ``name`` and ``storage``. ImageFile methods ================= ``exists`` ---------- Returns whether the file exists as returned by the underlaying storage. ``is_portrait`` --------------- Returns ``True`` if ``y > x``, else ``False`` ``set_size`` ------------ Sets the size of the image, takes an optional size tuple (x, y) as argument. ``read`` -------- Reads the file as done from the underlaying storage. ``write`` --------- Writes content to the file. Takes content as argument. Content is either raw data or an instance of ``django.core.files.base.ContentFile``. ``delete`` ---------- Deletes the file from underlaying storage. ``serialize`` ------------- Returns a serialized version of self. ``serialize_storage`` --------------------- Returns the ``self.storage`` as a serialized dot name path string. sorl-thumbnail-11.12/docs/management.rst0000644000175000017500000000157411476142267017303 0ustar mikkomikko******************* Management commands ******************* .. highlight:: python .. _thumbnail-cleanup: thumbnail cleanup ================= ``python manage.py thumbnail cleanup`` This cleans up the Key Value Store from stale cache. It removes references to images that do not exist and thumbnail references and their actual files for images that do not exist. It removes thumbnails for unknown images. .. _thumbnail-clear: thumbnail clear =============== ``python manage.py thumbnail clear`` This totally empties the Key Value Store from all keys that start with the :ref:`THUMBNAIL_KEY_PREFIX`. It does not delete any files. It is generally safe to run this if you do not reference the generated thumbnails by name somewhere else in your code. The Key Value store will update when you hit the template tags, and if the thumbnails still exist they will be used and not overwritten. sorl-thumbnail-11.12/docs/template.rst0000644000175000017500000001674311666137404017004 0ustar mikkomikko************************* Template tags and filters ************************* .. highlight:: html+django Sorl-thumbnail comes with one template tag `thumbnail`_ and two filters: `is_portrait`_ and `margin`_. To use any of them in you templates you first need to load them:: {% load thumbnail %} .. _thumbnail: thumbnail ========= Syntax:: {% thumbnail source geometry [key1=value1, key2=value2...] as var %} {% endthumbnail %} Alternative syntax using empty:: {% thumbnail source geometry [key1=value1, key2=value2...] as var %} {% empty %} {% endthumbnail %} The ``{% empty %}`` section is rendered if the thumbnail source is resolved to an empty value or an invalid image source, you can think of it as rendering when the thumbnail becomes undefined. .. _source: Source ------ .. highlight:: python Source can be an ImageField, FileField, a file name (assuming default_storage), a url. What we need to know is name and storage, see how ImageFile figures these things out:: class ImageFile(BaseImageFile): _size = None def __init__(self, file_, storage=None): if not file_: raise ThumbnailError('File is empty.') # figure out name if hasattr(file_, 'name'): self.name = file_.name else: self.name = force_unicode(file_) # figure out storage if storage is not None: self.storage = storage elif hasattr(file_, 'storage'): self.storage = file_.storage elif url_pat.match(self.name): self.storage = UrlStorage() else: self.storage = default_storage Geometry -------- .. highlight:: html+django Geometry is specified as ``widthxheight``, ``width`` or ``xheight``. Width and height are in pixels. Geometry can either be a string or resolve into a valid geometry string. Examples:: {% thumbnail item.image "200x100" as im %} {% endthumbnail %} {% thumbnail item.image "200" as im %} {% endthumbnail %} {% thumbnail item.image "x100" as im %} {% endthumbnail %} {% thumbnail item.image geometry as im %} {% endthumbnail %} If width and height are given the image is rescaled to maximum values of height and width given. Aspect ratio preserved. Options ------- Options are passed on to the backend and engine, the backend generates the thumbnail filename from it and the engine can use it for processing. Option keys are not resolved in context but values are. Passing all options to the engine means that you can easily subclass an engine and create new features like rounded corners or what ever processing you like. The options described below are how they are used and interpreted in the shipped engines. ``crop`` ^^^^^^^^ This option is only used if both width and height is given. Crop behaves much like `css background-position`_. The image is first rescaled to minimum values of height and width given, this will be equivalent to the `padding box` in the above text. After it is rescaled it will apply the cropping options. There are some differences to the `css background-position`_: - Only % and px are valid lengths (units) - ``noop`` (No Operation) is a valid option which means there is no cropping after the initial rescaling to minimum of width and height. There are many overlapping options here for example ``center`` is equivalent to ``50%``. There is not a problem with that in it self but it is a bit of a problem if you will for sorl-thumbnail. Sorl-thumbnail will generate a new thumbnail for every unique source, geometry and options. This is a design choice because we want to stay flexible with the options and not interpret them anywhere else but in the engine methods. In clear words, be consistent in your cropping options if you don't want to generate unnecessary thumbnails. In case you are wondering, sorl-thumbnail sorts the options so the order does not matter, same options but in different order will generate only one thumbnail. ``upscale`` ^^^^^^^^^^^ Upscale is a boolean and controls if the image can be upscaled or not. For example if your source is 100x100 and you request a thumbnail of size 200x200 and upscale is False this will return a thumbnail of size 100x100. If upscale was True this would result in a thumbnail size 200x200 (upscaled). The default value is ``True``. ``quality`` ^^^^^^^^^^^ Quality is a value between 0-100 and controls the thumbnail write quality. Default value is ``95``. ``progressive`` ^^^^^^^^^^^^^^^ This controls whether to save jpeg thumbnails as progressive jpegs. Default value is ``True``. ``orientation`` ^^^^^^^^^^^^^^^ This controls whether to orientate the resulting thumbnail with respect to the source EXIF tags for orientation. Default value is ``True``. ``format`` ^^^^^^^^^^ This controls the write format and thumbnail extension. Formats supported by the shipped engines are ``'JPEG'`` and ``'PNG'``. Default value is ``'JPEG'``. ``colorspace`` ^^^^^^^^^^^^^^ This controls the resulting thumbnails color space, valid values are: ``'RGB'`` and ``'GRAY'``. Default value is ``'RGB'``. ``options`` ^^^^^^^^^^^ Yes this option is called ``options``. This needs to be a context variable that resolves to a dictionary. This dictionary can contain multiple options, for example:: options = {'colorspace': 'GRAY', 'quality': 75, 'crop': 'center'} You can use this option together with the other options but beware that the order will matter. As soon as the keyword ``options`` is encountered all the options that have a key in ``options`` are overwritten. Similarly, options in the ``options`` dict will be overwritten by options set after the options keyword argument to the thumbnail tag. is_portrait =========== This filter returns True if the image height is larger than the image width. Examples:: {% thumbnail item.image "100x100" %} {% if item.image|is_portrait %}
{% else %}
{% endif %} {% endthumbnail %} {% if item.image|is_portrait %} {% thumbnail item.image "100x200" crop="center" %} {% endthumbnail %} {% else %} {% thumbnail item.image "100x200" crop="center" %} {% endthumbnail %} {% endif %} margin ====== Margin is a filter for calculating margins against a padding box. For example lets say you have an image ``item.image`` and you want to pad it vertically in a 1000x1000 box, you would simply write::
The above is a rather synthetic example the more common use case is when you want boxes of images of a certain size but you do not want to crop them:: {% for profile in profiles %}
{% thumbnail profile.photo "100x100" as im %} {% empty %} {% endthumbnail %}
{% enfor %} The more problematic is to get the top margin, however the margin filter outputs all values. .. _css background-position: http://www.w3.org/TR/CSS2/colors.html#propdef-background-position sorl-thumbnail-11.12/docs/operation.rst0000644000175000017500000000617311635604732017164 0ustar mikkomikko*************************** How sorl-thumbnail operates *************************** .. highlight:: python When you use the ``thumbnail`` template tag sorl-thumbnail looks up the thumbnail in a :ref:`kvstore-requirements`. The key for a thumbnail is generated from its filename and storage. The thumbnail filename in turn is generated from the source and requested thumbnail size and options. If the key for the thumbnail is found in the |kvstore|, the serialized thumbnail information is fetched from it and returned. If the thumbnail key is not found there sorl-thumbnail continues to generate the thumbnail and stores necessary information in the |kvstore|. It is worth noting that sorl-thumbnail does not check if source or thumbnail exists if the thumbnail key is found in the |kvstore|. .. note:: This means that if you change or delete a source file or delete the thumbnail, sorl-thumbnail will still fetch from the |kvstore|. Therefore it is important that if you delete or change a source or thumbnail file notify the |kvstore|. If you change or delete a source or a thumbnail for some reason, you can use the ``delete`` method of the ``ThumbnailBackend`` class or subclass:: from sorl.thumbnail import delete # Delete the Key Value Store reference but **not** the file. # Use this if you have changed the source delete(my_file, delete_file=False) # Delete the Key Value Store reference and the file # Use this if you want to delete the source file delete(my_file) # delete_file=True is default The ``sorl.thumbnail.delete`` method always deletes the input files thumbnail Key Value Store references as well as thumbnail files. You can use this method on thumbnails as well as source files. Alternatively if you have **deleted** a file you can use the management command :ref:`thumbnail-cleanup`. Deleting an image using the ``sorl.thumbnail.ImageField`` will notify the |kvstore| to delete references to it and delete all of its thumbnail references and files, exactly like the above code example. **Why you ask?** Why go through all the trouble with a |kvstore| and risk stale cache? Why not use a database to cache if you are going to do that? The reason is speed and especially with storages other than local file storage. Checking if a file exists before serving it will cost too much. Speed is also the reason for not choosing to use a standard database for this kind of persistent caching. However sorl-thumbnail does ship with a *cached* database |kvstore|. .. note:: We have to assume the thumbnail exists if the thumbnail key exists in the |kvstore| **There are bonuses**. We can store meta data in the |kvstore| that would be too costly to retrieve even for local file storage. Today this meta data consists only of the image size but this could be expanded to for example EXIF data. The other bonus is that we can keep track of what thumbnails has been generated from a particular source and deleting them too when the source is deleted. `Schematic view of how things are done `_ .. |kvstore| replace:: Key Value Store sorl-thumbnail-11.12/docs/_theme/0000755000175000017500000000000011677075433015672 5ustar mikkomikkosorl-thumbnail-11.12/docs/_theme/nature/0000755000175000017500000000000011677075433017170 5ustar mikkomikkosorl-thumbnail-11.12/docs/_theme/nature/static/0000755000175000017500000000000011677075433020457 5ustar mikkomikkosorl-thumbnail-11.12/docs/_theme/nature/static/nature.css_t0000644000175000017500000000744011467213637023014 0ustar mikkomikko/** * Sphinx stylesheet -- default theme * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: Arial, sans-serif; font-size: 100%; background-color: #111; color: #555; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } hr{ border: 1px solid #B1B4B6; } div.document { background-color: #eee; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; font-size: 0.8em; } div.footer { color: #555; width: 100%; padding: 13px 0; text-align: center; font-size: 75%; } div.footer a { color: #444; text-decoration: underline; } div.related { background-color: #6BA81E; line-height: 32px; color: #fff; text-shadow: 0px 1px 0 #444; font-size: 0.80em; } div.related a { color: #E2F3CC; } div.sphinxsidebar { font-size: 0.75em; line-height: 1.5em; } div.sphinxsidebarwrapper{ padding: 20px 0; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: Arial, sans-serif; color: #222; font-size: 1.2em; font-weight: normal; margin: 0; padding: 5px 10px; background-color: #ddd; text-shadow: 1px 1px 0 white } div.sphinxsidebar h4{ font-size: 1.1em; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p { color: #888; padding: 5px 20px; } div.sphinxsidebar p.topless { } div.sphinxsidebar ul { margin: 10px 20px; padding: 0; color: #000; } div.sphinxsidebar a { color: #444; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: sans-serif; font-size: 1em; } div.sphinxsidebar input[type=text]{ margin-left: 20px; } /* -- body styles ----------------------------------------------------------- */ a { color: #005B81; text-decoration: none; } a:hover { color: #E32E00; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: Arial, sans-serif; background-color: #BED4EB; font-weight: normal; color: #212224; margin: 30px 0px 10px 0px; padding: 5px 0 5px 10px; text-shadow: 0px 1px 0 white } div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } div.body h2 { font-size: 150%; background-color: #C8D5E3; } div.body h3 { font-size: 120%; background-color: #D8DEE3; } div.body h4 { font-size: 110%; background-color: #D8DEE3; } div.body h5 { font-size: 100%; background-color: #D8DEE3; } div.body h6 { font-size: 100%; background-color: #D8DEE3; } a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; } a.headerlink:hover { background-color: #c60f0f; color: white; } div.body p, div.body dd, div.body li { line-height: 1.5em; } div.admonition p.admonition-title + p { display: inline; } div.highlight{ background-color: white; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre { padding: 10px; background-color: White; color: #222; line-height: 1.2em; border: 1px solid #C6C9CB; font-size: 1.2em; margin: 1.5em 0 1.5em 0; -webkit-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 1px 1px 1px #d8d8d8; } tt { background-color: #ecf0f3; color: #222; padding: 1px 2px; font-size: 1.2em; font-family: monospace; } sorl-thumbnail-11.12/docs/_theme/nature/static/pygments.css0000644000175000017500000000523511467213637023041 0ustar mikkomikko.c { color: #999988; font-style: italic } /* Comment */ .k { font-weight: bold } /* Keyword */ .o { font-weight: bold } /* Operator */ .cm { color: #999988; font-style: italic } /* Comment.Multiline */ .cp { color: #999999; font-weight: bold } /* Comment.preproc */ .c1 { color: #999988; font-style: italic } /* Comment.Single */ .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .ge { font-style: italic } /* Generic.Emph */ .gr { color: #aa0000 } /* Generic.Error */ .gh { color: #999999 } /* Generic.Heading */ .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .go { color: #111 } /* Generic.Output */ .gp { color: #555555 } /* Generic.Prompt */ .gs { font-weight: bold } /* Generic.Strong */ .gu { color: #aaaaaa } /* Generic.Subheading */ .gt { color: #aa0000 } /* Generic.Traceback */ .kc { font-weight: bold } /* Keyword.Constant */ .kd { font-weight: bold } /* Keyword.Declaration */ .kp { font-weight: bold } /* Keyword.Pseudo */ .kr { font-weight: bold } /* Keyword.Reserved */ .kt { color: #445588; font-weight: bold } /* Keyword.Type */ .m { color: #009999 } /* Literal.Number */ .s { color: #bb8844 } /* Literal.String */ .na { color: #008080 } /* Name.Attribute */ .nb { color: #999999 } /* Name.Builtin */ .nc { color: #445588; font-weight: bold } /* Name.Class */ .no { color: #ff99ff } /* Name.Constant */ .ni { color: #800080 } /* Name.Entity */ .ne { color: #990000; font-weight: bold } /* Name.Exception */ .nf { color: #990000; font-weight: bold } /* Name.Function */ .nn { color: #555555 } /* Name.Namespace */ .nt { color: #000080 } /* Name.Tag */ .nv { color: purple } /* Name.Variable */ .ow { font-weight: bold } /* Operator.Word */ .mf { color: #009999 } /* Literal.Number.Float */ .mh { color: #009999 } /* Literal.Number.Hex */ .mi { color: #009999 } /* Literal.Number.Integer */ .mo { color: #009999 } /* Literal.Number.Oct */ .sb { color: #bb8844 } /* Literal.String.Backtick */ .sc { color: #bb8844 } /* Literal.String.Char */ .sd { color: #bb8844 } /* Literal.String.Doc */ .s2 { color: #bb8844 } /* Literal.String.Double */ .se { color: #bb8844 } /* Literal.String.Escape */ .sh { color: #bb8844 } /* Literal.String.Heredoc */ .si { color: #bb8844 } /* Literal.String.Interpol */ .sx { color: #bb8844 } /* Literal.String.Other */ .sr { color: #808000 } /* Literal.String.Regex */ .s1 { color: #bb8844 } /* Literal.String.Single */ .ss { color: #bb8844 } /* Literal.String.Symbol */ .bp { color: #999999 } /* Name.Builtin.Pseudo */ .vc { color: #ff99ff } /* Name.Variable.Class */ .vg { color: #ff99ff } /* Name.Variable.Global */ .vi { color: #ff99ff } /* Name.Variable.Instance */ .il { color: #009999 } /* Literal.Number.Integer.Long */sorl-thumbnail-11.12/docs/_theme/nature/theme.conf0000644000175000017500000000010711467213637021134 0ustar mikkomikko[theme] inherit = basic stylesheet = nature.css pygments_style = tango sorl-thumbnail-11.12/docs/conf.py0000644000175000017500000001654111666137404015732 0ustar mikkomikko# -*- coding: utf-8 -*- # # sorl-thumbnail documentation build configuration file, created by # sphinx-quickstart on Fri Nov 12 00:51:21 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sphinx import sys sys.path.insert(0, os.path.pardir) import sorl for j in xrange(0, len(sphinx.__version__)): try: version = float(sphinx.__version__[:-j]) break except ValueError: pass version = 0 # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. if version < 1.0: extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] else: extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'sorl-thumbnail' copyright = u'2010, Mikko Hellsing' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = sorl.__version__ # The full version, including alpha/beta/rc tags. release = sorl.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if version < 1.0: html_theme = 'default' else: html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_theme'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'sorlthumbnaildoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'sorlthumbnail.tex', u'sorl-thumbnail Documentation', u'Mikko Hellsing', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'sorlthumbnail', u'sorl-thumbnail Documentation', [u'Mikko Hellsing'], 1) ] sorl-thumbnail-11.12/docs/installation.rst0000644000175000017500000000104511666137404017657 0ustar mikkomikko******************** Installation & Setup ******************** Installation ============ First you need to make sure to read the :doc:`requirements`. To install sorl-thumbnail is easy:: pip install sorl-thumbnail Or you can go to `the github page `_ Setup ===== .. highlight:: python 1. Add ``sorl.thumbnail`` to your ``settings.INSTALLED_APPS``. 2. Configure your ``settings`` 3. If you are using the cached database key value store you need to sync the database:: python manage.py syncdb sorl-thumbnail-11.12/MANIFEST.in0000644000175000017500000000017211677075264015241 0ustar mikkomikkoinclude LICENSE README.rst recursive-include docs * recursive-include tests * recursive-exclude * *.pyc prune docs/_build sorl-thumbnail-11.12/sorl/0000755000175000017500000000000011677075433014460 5ustar mikkomikkosorl-thumbnail-11.12/sorl/__init__.py0000644000175000017500000000047111677050643016567 0ustar mikkomikkoimport logging __version__ = '11.12' VERSION = tuple(map(int, __version__.split('.'))) class NullHandler(logging.Handler): def emit(self, record): pass # Add a logging handler that does nothing to silence messages with no logger # configured logging.getLogger('sorl').addHandler(NullHandler()) sorl-thumbnail-11.12/sorl/thumbnail/0000755000175000017500000000000011677075433016443 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/__init__.py0000644000175000017500000000021711666137404020547 0ustar mikkomikkofrom sorl.thumbnail.fields import ImageField from sorl.thumbnail.shortcuts import get_thumbnail, delete from sorl import __version__, VERSION sorl-thumbnail-11.12/sorl/thumbnail/fields.py0000644000175000017500000000523211635604732020257 0ustar mikkomikkofrom __future__ import with_statement from django.db import models from django.db.models import Q from django import forms from django.utils.translation import ugettext_lazy as _ from sorl.thumbnail import default __all__ = ('ImageField', 'ImageFormField') class ImageField(models.FileField): def delete_file(self, instance, sender, **kwargs): """ Adds deletion of thumbnails and key kalue store references to the parent class implementation. Only called in Django < 1.2.5 """ file_ = getattr(instance, self.attname) # If no other object of this type references the file, and it's not the # default value for future objects, delete it from the backend. query = Q(**{self.name: file_.name}) & ~Q(pk=instance.pk) qs = sender._default_manager.filter(query) if (file_ and file_.name != self.default and not qs): default.backend.delete(file_) elif file_: # Otherwise, just close the file, so it doesn't tie up resources. file_.close() def formfield(self, **kwargs): defaults = {'form_class': ImageFormField} defaults.update(kwargs) return super(ImageField, self).formfield(**defaults) def save_form_data(self, instance, data): if data is not None: setattr(instance, self.name, data or '') def south_field_triple(self): from south.modelsinspector import introspector cls_name = '%s.%s' % (self.__class__.__module__ , self.__class__.__name__) args, kwargs = introspector(self) return (cls_name, args, kwargs) class ImageFormField(forms.FileField): default_error_messages = { 'invalid_image': _(u"Upload a valid image. The file you uploaded was " u"either not an image or a corrupted image."), } def to_python(self, data): """ Checks that the file-upload field data contains a valid image (GIF, JPG, PNG, possibly others -- whatever the engine supports). """ f = super(ImageFormField, self).to_python(data) if f is None: return None # We need to get a file raw data to validate it. if hasattr(data, 'temporary_file_path'): with open(data.temporary_file_path(), 'rb') as fp: raw_data = fp.read() elif hasattr(data, 'read'): raw_data = data.read() else: raw_data = data['content'] if not default.engine.is_valid_image(raw_data): raise forms.ValidationError(self.error_messages['invalid_image']) if hasattr(f, 'seek') and callable(f.seek): f.seek(0) return f sorl-thumbnail-11.12/sorl/thumbnail/log.py0000644000175000017500000000224411666137404017573 0ustar mikkomikkoimport logging from django.core.mail.message import EmailMessage from sorl.thumbnail.conf import settings class ThumbnailLogHandler(logging.Handler): """ An exception log handler for thumbnail errors. """ def emit(self, record): import traceback if not settings.ADMINS: return try: # Hack to try to get request from context request = record.exc_info[2].tb_frame.f_locals['context']['request'] request_repr = repr(request) request_path = request.path except Exception: request_repr = "Request unavailable" request_path = 'Unknown URL' if record.exc_info: stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) else: stack_trace = 'No stack trace available' message = "%s\n\n%s" % (stack_trace, request_repr) msg = EmailMessage( u'[sorl-thumbnail] %s: %s' % (record.levelname, request_path), message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS], connection=None ) msg.send(fail_silently=True) sorl-thumbnail-11.12/sorl/thumbnail/urls.py0000644000175000017500000000000011666137404017763 0ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/models.py0000644000175000017500000000037611666137404020301 0ustar mikkomikkofrom django.db import models from sorl.thumbnail.conf import settings class KVStore(models.Model): key = models.CharField(max_length=200, primary_key=True, db_column=settings.THUMBNAIL_KEY_DBCOLUMN ) value = models.TextField() sorl-thumbnail-11.12/sorl/thumbnail/engines/0000755000175000017500000000000011677075433020073 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/engines/__init__.py0000644000175000017500000000000011467213637022167 0ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/engines/pil_engine.py0000644000175000017500000000531711666137404022557 0ustar mikkomikkofrom cStringIO import StringIO from sorl.thumbnail.engines.base import EngineBase try: from PIL import Image, ImageFile, ImageDraw except ImportError: import Image, ImageFile, ImageDraw class Engine(EngineBase): def get_image(self, source): buf = StringIO(source.read()) return Image.open(buf) def get_image_size(self, image): return image.size def is_valid_image(self, raw_data): buf = StringIO(raw_data) try: trial_image = Image.open(buf) trial_image.verify() except Exception: return False return True def _orientation(self, image): try: exif = image._getexif() except AttributeError: exif = None if exif: orientation = exif.get(0x0112) if orientation == 2: image = image.transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 3: image = image.rotate(180) elif orientation == 4: image = image.transpose(Image.FLIP_TOP_BOTTOM) elif orientation == 5: image = image.rotate(-90).transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 6: image = image.rotate(-90) elif orientation == 7: image = image.rotate(90).transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 8: image = image.rotate(90) return image def _colorspace(self, image, colorspace): if colorspace == 'RGB': if image.mode == 'RGBA': return image # RGBA is just RGB + Alpha if image.mode == 'P' and 'transparency' in image.info: return image.convert('RGBA') return image.convert('RGB') if colorspace == 'GRAY': return image.convert('L') return image def _scale(self, image, width, height): return image.resize((width, height), resample=Image.ANTIALIAS) def _crop(self, image, width, height, x_offset, y_offset): return image.crop((x_offset, y_offset, width + x_offset, height + y_offset)) def _get_raw_data(self, image, format_, quality, progressive=False): ImageFile.MAXBLOCK = 1024 * 1024 buf = StringIO() params = { 'format': format_, 'quality': quality, 'optimize': 1, } if format_ == 'JPEG' and progressive: params['progressive'] = True try: image.save(buf, **params) except IOError: params.pop('optimize') image.save(buf, **params) raw_data = buf.getvalue() buf.close() return raw_data sorl-thumbnail-11.12/sorl/thumbnail/engines/base.py0000644000175000017500000001022411666137404021351 0ustar mikkomikko#coding=utf-8 from sorl.thumbnail.conf import settings from sorl.thumbnail.helpers import toint from sorl.thumbnail.parsers import parse_crop class EngineBase(object): """ ABC for Thumbnail engines, methods are static """ def create(self, image, geometry, options): """ Processing conductor, returns the thumbnail as an image engine instance """ image = self.orientation(image, geometry, options) image = self.colorspace(image, geometry, options) image = self.scale(image, geometry, options) image = self.crop(image, geometry, options) return image def orientation(self, image, geometry, options): """ Wrapper for ``_orientation`` """ if options.get('orientation', settings.THUMBNAIL_ORIENTATION): return self._orientation(image) return image def colorspace(self, image, geometry, options): """ Wrapper for ``_colorspace`` """ colorspace = options['colorspace'] return self._colorspace(image, colorspace) def scale(self, image, geometry, options): """ Wrapper for ``_scale`` """ crop = options['crop'] upscale = options['upscale'] x_image, y_image = map(float, self.get_image_size(image)) # calculate scaling factor factors = (geometry[0] / x_image, geometry[1] / y_image) factor = max(factors) if crop else min(factors) if factor < 1 or upscale: width = toint(x_image * factor) height = toint(y_image * factor) image = self._scale(image, width, height) return image def crop(self, image, geometry, options): """ Wrapper for ``_crop`` """ crop = options['crop'] if not crop or crop == 'noop': return image x_image, y_image = self.get_image_size(image) x_offset, y_offset = parse_crop(crop, (x_image, y_image), geometry) return self._crop(image, geometry[0], geometry[1], x_offset, y_offset) def write(self, image, options, thumbnail): """ Wrapper for ``_write`` """ format_ = options['format'] quality = options['quality'] # additional non-default-value options: progressive = options.get('progressive', settings.THUMBNAIL_PROGRESSIVE) raw_data = self._get_raw_data(image, format_, quality, progressive=progressive ) thumbnail.write(raw_data) def get_image_ratio(self, image): """ Calculates the image ratio """ x, y = self.get_image_size(image) return float(x) / y # # Methods which engines need to implement # The ``image`` argument refers to a backend image object # def get_image(self, source): """ Returns the backend image objects from an ImageFile instance """ raise NotImplemented() def get_image_size(self, image): """ Returns the image width and height as a tuple """ raise NotImplemented() def is_valid_image(self, raw_data): """ Checks if the supplied raw data is valid image data """ raise NotImplemented() def _orientation(self, image): """ Read orientation exif data and orientate the image accordingly """ return image def _colorspace(self, image, colorspace): """ `Valid colorspaces `_. Backends need to implement the following:: RGB, GRAY """ raise NotImplemented() def _scale(self, image, width, height): """ Does the resizing of the image """ raise NotImplemented() def _crop(self, image, width, height, x_offset, y_offset): """ Crops the image """ raise NotImplemented() def _get_raw_data(self, image, format_, quality, progressive=False): """ Gets raw data given the image, format and quality. This method is called from :meth:`write` """ raise NotImplemented() sorl-thumbnail-11.12/sorl/thumbnail/engines/convert_engine.py0000644000175000017500000001152411666137567023462 0ustar mikkomikkofrom __future__ import with_statement import re import os from django.utils.datastructures import SortedDict from django.utils.encoding import smart_str from sorl.thumbnail.base import EXTENSIONS from sorl.thumbnail.conf import settings from sorl.thumbnail.engines.base import EngineBase from subprocess import Popen, PIPE from tempfile import mkstemp size_re = re.compile(r'^(?:.+) (?:[A-Z]+) (?P\d+)x(?P\d+)') class Engine(EngineBase): """ Image object is a dict with source path, options and size """ def write(self, image, options, thumbnail): """ Writes the thumbnail image """ handle, out = mkstemp(suffix='.%s' % EXTENSIONS[options['format']]) if ( options['format'] == 'JPEG' and options.get('progressive', settings.THUMBNAIL_PROGRESSIVE) ): image['options']['interlace'] = 'line' image['options']['quality'] = options['quality'] args = settings.THUMBNAIL_CONVERT.split(' ') args.append(image['source']) for k, v in image['options'].iteritems(): args.append('-%s' % k) if v is not None: args.append('%s' % v) args.append(out) args = map(smart_str, args) p = Popen(args) p.wait() with open(out, 'r') as fp: thumbnail.write(fp.read()) os.close(handle) os.remove(out) os.remove(image['source']) # we should not need this now def get_image(self, source): """ Returns the backend image objects from a ImageFile instance """ handle, tmp = mkstemp() with open(tmp, 'w') as fp: fp.write(source.read()) os.close(handle) return {'source': tmp, 'options': SortedDict(), 'size': None} def get_image_size(self, image): """ Returns the image width and height as a tuple """ if image['size'] is None: args = settings.THUMBNAIL_IDENTIFY.split(' ') args.append(image['source']) p = Popen(args, stdout=PIPE) p.wait() m = size_re.match(p.stdout.read()) image['size'] = int(m.group('x')), int(m.group('y')) return image['size'] def is_valid_image(self, raw_data): """ This is not very good for imagemagick because it will say anything is valid that it can use as input. """ handle, tmp = mkstemp() with open(tmp, 'w') as fp: fp.write(raw_data) fp.flush() args = settings.THUMBNAIL_IDENTIFY.split(' ') args.append(tmp) p = Popen(args) retcode = p.wait() os.close(handle) os.remove(tmp) return retcode == 0 def _orientation(self, image): if settings.THUMBNAIL_CONVERT.endswith('gm convert'): args = settings.THUMBNAIL_IDENTIFY.split() args.extend([ '-format', '%[exif:orientation]', image['source'] ]) p = Popen(args, stdout=PIPE) p.wait() result = p.stdout.read().strip() if result: result = int(result) options = image['options'] if result == 2: options['flop'] = None elif result == 3: options['rotate'] = '180' elif result == 4: options['flip'] = None elif result == 5: options['rotate'] = '90' options['flop'] = None elif result == 6: options['rotate'] = '90' elif result == 7: options['rotate'] = '-90' options['flop'] = None elif result == 8: options['rotate'] = '-90' else: # ImageMagick also corrects the orientation exif data for # destination image['options']['auto-orient'] = None return image def _colorspace(self, image, colorspace): """ `Valid colorspaces `_. Backends need to implement the following:: RGB, GRAY """ image['options']['colorspace'] = colorspace return image def _crop(self, image, width, height, x_offset, y_offset): """ Crops the image """ image['options']['crop'] = '%sx%s+%s+%s' % ( width, height, x_offset, y_offset ) image['size'] = (width, height) # update image size return image def _scale(self, image, width, height): """ Does the resizing of the image """ image['options']['scale'] = '%sx%s!' % (width, height) image['size'] = (width, height) # update image size return image sorl-thumbnail-11.12/sorl/thumbnail/engines/pgmagick_engine.py0000644000175000017500000000501211677047621023550 0ustar mikkomikkofrom pgmagick import Blob, ColorspaceType, Geometry, Image, ImageType from pgmagick import InterlaceType, OrientationType from sorl.thumbnail.engines.base import EngineBase try: from pgmagick._pgmagick import get_blob_data except ImportError: from base64 import b64decode def get_blob_data(blob): return b64decode(blob.base64()) class Engine(EngineBase): def get_image(self, source): blob = Blob() blob.update(source.read()) return Image(blob) def get_image_size(self, image): geometry = image.size() return geometry.width(), geometry.height() def is_valid_image(self, raw_data): blob = Blob() blob.update(raw_data) im = Image(blob) return im.isValid() def _orientation(self, image): orientation = image.orientation() if orientation == OrientationType.TopRightOrientation: image.flop() elif orientation == OrientationType.BottomRightOrientation: image.rotate(180) elif orientation == OrientationType.BottomLeftOrientation: image.flip() elif orientation == OrientationType.LeftTopOrientation: image.rotate(90) image.flop() elif orientation == OrientationType.RightTopOrientation: image.rotate(90) elif orientation == OrientationType.RightBottomOrientation: image.rotate(-90) image.flop() elif orientation == OrientationType.LeftBottomOrientation: image.rotate(-90) image.orientation(OrientationType.TopLeftOrientation) return image def _colorspace(self, image, colorspace): if colorspace == 'RGB': image.type(ImageType.TrueColorMatteType) elif colorspace == 'GRAY': image.type(ImageType.GrayscaleMatteType) else: return image return image def _scale(self, image, width, height): geometry = Geometry(width, height) image.scale(geometry) return image def _crop(self, image, width, height, x_offset, y_offset): geometry = Geometry(width, height, x_offset, y_offset) image.crop(geometry) return image def _get_raw_data(self, image, format_, quality, progressive=False): image.magick(format_.encode('utf8')) image.quality(quality) if format_ == 'JPEG' and progressive: image.interlaceType(InterlaceType.LineInterlace) blob = Blob() image.write(blob) return get_blob_data(blob) sorl-thumbnail-11.12/sorl/thumbnail/kvstores/0000755000175000017500000000000011677075433020323 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/kvstores/__init__.py0000644000175000017500000000000011467213637022417 0ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/kvstores/redis_kvstore.py0000644000175000017500000000162511666137507023563 0ustar mikkomikkofrom redis import Redis from sorl.thumbnail.kvstores.base import KVStoreBase from sorl.thumbnail.conf import settings class KVStore(KVStoreBase): def __init__(self, *args, **kwargs): super(KVStore, self).__init__(*args, **kwargs) self.connection = Redis( host=settings.THUMBNAIL_REDIS_HOST, port=settings.THUMBNAIL_REDIS_PORT, db=settings.THUMBNAIL_REDIS_DB, password=settings.THUMBNAIL_REDIS_PASSWORD, unix_socket_path=settings.THUMBNAIL_REDIS_UNIX_SOCKET_PATH, ) def _get_raw(self, key): return self.connection.get(key) def _set_raw(self, key, value): return self.connection.set(key, value) def _delete_raw(self, *keys): return self.connection.delete(*keys) def _find_keys_raw(self, prefix): pattern = prefix + '*' return self.connection.keys(pattern=pattern) sorl-thumbnail-11.12/sorl/thumbnail/kvstores/cached_db_kvstore.py0000644000175000017500000000306011666137404024320 0ustar mikkomikkofrom django.core.cache import cache from sorl.thumbnail.kvstores.base import KVStoreBase from sorl.thumbnail.conf import settings from sorl.thumbnail.models import KVStore as KVStoreModel class EMPTY_VALUE(object): pass class KVStore(KVStoreBase): def clear(self): """ We can clear the database more efficiently using the prefix here rather than calling :meth:`_delete_raw`. """ prefix = settings.THUMBNAIL_KEY_PREFIX for key in self._find_keys_raw(prefix): cache.delete(key) KVStoreModel.objects.filter(key__startswith=prefix).delete() def _get_raw(self, key): value = cache.get(key) if value is None: try: value = KVStoreModel.objects.get(key=key).value except KVStoreModel.DoesNotExist: # we set the cache to prevent further db lookups value = EMPTY_VALUE cache.set(key, value, settings.THUMBNAIL_CACHE_TIMEOUT) if value == EMPTY_VALUE: return None return value def _set_raw(self, key, value): kv = KVStoreModel.objects.get_or_create(key=key)[0] kv.value = value kv.save() cache.set(key, value, settings.THUMBNAIL_CACHE_TIMEOUT) def _delete_raw(self, *keys): KVStoreModel.objects.filter(key__in=keys).delete() for key in keys: cache.delete(key) def _find_keys_raw(self, prefix): qs = KVStoreModel.objects.filter(key__startswith=prefix) return qs.values_list('key', flat=True) sorl-thumbnail-11.12/sorl/thumbnail/kvstores/base.py0000644000175000017500000001471211470234017021576 0ustar mikkomikkofrom sorl.thumbnail.conf import settings from sorl.thumbnail.helpers import serialize, deserialize, ThumbnailError from sorl.thumbnail.images import serialize_image_file, deserialize_image_file def add_prefix(key, identity='image'): """ Adds prefixes to the key """ return '||'.join([settings.THUMBNAIL_KEY_PREFIX, identity, key]) def del_prefix(key): """ Removes prefixes from the key """ return key.split('||')[-1] class KVStoreBase(object): def get(self, image_file): """ Gets the ``image_file`` from store. Returns ``None`` if not found. """ return self._get(image_file.key) def set(self, image_file, source=None): """ Updates store for the `image_file`. Makes sure the `image_file` has a size set. """ image_file.set_size() # make sure its got a size self._set(image_file.key, image_file) if source is not None: if not self.get(source): # make sure the source is in kvstore raise ThumbnailError('Cannot add thumbnails for source: `%s` ' 'that is not in kvstore.' % source.name) # Update the list of thumbnails for source. thumbnails = self._get(source.key, identity='thumbnails') or [] thumbnails = set(thumbnails) thumbnails.add(image_file.key) self._set(source.key, list(thumbnails), identity='thumbnails') def get_or_set(self, image_file): cached = self.get(image_file) if cached is not None: return cached self.set(image_file) return image_file def delete(self, image_file, delete_thumbnails=True): """ Deletes the referense to the ``image_file`` and deletes the references to thumbnails as well as thumbnail files if ``delete_thumbnails`` is `True``. Does not delete the ``image_file`` is self. """ if delete_thumbnails: self.delete_thumbnails(image_file) self._delete(image_file.key) def delete_thumbnails(self, image_file): """ Deletes references to thumbnails as well as thumbnail ``image_files``. """ thumbnail_keys = self._get(image_file.key, identity='thumbnails') if thumbnail_keys: # Delete all thumbnail keys from store and delete the # thumbnail ImageFiles. for key in thumbnail_keys: thumbnail = self._get(key) if thumbnail: self.delete(thumbnail) thumbnail.delete() # delete the actual file # Delete the thumbnails key from store self._delete(image_file.key, identity='thumbnails') def cleanup(self): """ Cleans up the key value store. In detail: 1. Deletes all key store references for image_files that do not exist and all key references for its thumbnails *and* their image_files. 2. Deletes or updates all invalid thumbnail keys """ for key in self._find_keys(identity='image'): image_file = self._get(key) if image_file and not image_file.exists(): self.delete(image_file) for key in self._find_keys(identity='thumbnails'): # We do not need to check for file existence in here since we # already did that above for all image references image_file = self._get(key) if image_file: # if there is an image_file then we check all of its thumbnails # for existence thumbnail_keys = self._get(key, identity='thumbnails') or [] thumbnail_keys_set = set(thumbnail_keys) for thumbnail_key in thumbnail_keys: if not self._get(thumbnail_key): thumbnail_keys_set.remove(thumbnail_key) thumbnail_keys = list(thumbnail_keys_set) if thumbnail_keys: self._set(key, thumbnail_keys, identity='thumbnails') return # if there is no image_file then this thumbnails key is just # hangin' loose, If the thumbnail_keys ended up empty there is no # reason for keeping it either self._delete(key, identity='thumbnails') def clear(self): """ Brutely clears the key value store for keys with THUMBNAIL_KEY_PREFIX prefix. Use this in emergency situations. Normally you would probably want to use the ``cleanup`` method instead. """ all_keys = self._find_keys_raw(settings.THUMBNAIL_KEY_PREFIX) self._delete_raw(*all_keys) def _get(self, key, identity='image'): """ Deserializing, prefix wrapper for _get_raw """ value = self._get_raw(add_prefix(key, identity)) if value is None: return None if identity == 'image': return deserialize_image_file(value) return deserialize(value) def _set(self, key, value, identity='image'): """ Serializing, prefix wrapper for _set_raw """ if identity == 'image': s = serialize_image_file(value) else: s = serialize(value) self._set_raw(add_prefix(key, identity), s) def _delete(self, key, identity='image'): """ Prefix wrapper for _delete_raw """ self._delete_raw(add_prefix(key, identity)) def _find_keys(self, identity='image'): """ Finds and returns all keys for identity, """ prefix = add_prefix('', identity) raw_keys = self._find_keys_raw(prefix) or [] for raw_key in raw_keys: yield del_prefix(raw_key) # # Methods which key-value stores need to implement # def _get_raw(self, key): """ Gets the value from keystore, returns `None` if not found. """ raise NotImplemented() def _set_raw(self, key, value): """ Sets value associated to key. Key is expected to be shorter than 200 chars. Value is a ``unicode`` object with an unknown (reasonable) length. """ raise NotImplemented() def _delete_raw(self, *keys): """ Deletes the keys. Silent failure for missing keys. """ raise NotImplemented() def _find_keys_raw(self, prefix): """ Finds all keys with prefix """ raise NotImplemented() sorl-thumbnail-11.12/sorl/thumbnail/images.py0000644000175000017500000001135611666137404020263 0ustar mikkomikkoimport re import urllib2 from django.core.files.base import File, ContentFile from django.core.files.storage import Storage, default_storage from django.core.urlresolvers import reverse from django.utils.encoding import force_unicode from django.utils.functional import LazyObject from django.utils import simplejson from sorl.thumbnail.conf import settings from sorl.thumbnail.helpers import ThumbnailError, tokey, get_module_class from sorl.thumbnail import default from sorl.thumbnail.parsers import parse_geometry url_pat = re.compile(r'^(https?|ftp):\/\/') def serialize_image_file(image_file): if image_file.size is None: raise ThumbnailError('Trying to serialize an ``ImageFile`` with a ' '``None`` size.') data = { 'name': image_file.name, 'storage': image_file.serialize_storage(), 'size': image_file.size, } return simplejson.dumps(data) def deserialize_image_file(s): data = simplejson.loads(s) class LazyStorage(LazyObject): def _setup(self): self._wrapped = get_module_class(data['storage'])() image_file = ImageFile(data['name'], LazyStorage()) image_file.set_size(data['size']) return image_file class BaseImageFile(object): def exists(self): raise NotImplemented() @property def width(self): return self.size[0] x = width @property def height(self): return self.size[1] y = height def is_portrait(self): return self.y > self.x @property def ratio(self): return float(self.x) / self.y @property def url(self): raise NotImplemented() src = url class ImageFile(BaseImageFile): _size = None def __init__(self, file_, storage=None): if not file_: raise ThumbnailError('File is empty.') # figure out name if hasattr(file_, 'name'): self.name = file_.name else: self.name = force_unicode(file_) # figure out storage if storage is not None: self.storage = storage elif hasattr(file_, 'storage'): self.storage = file_.storage elif url_pat.match(self.name): self.storage = UrlStorage() else: self.storage = default_storage def __unicode__(self): return self.name def exists(self): return self.storage.exists(self.name) def set_size(self, size=None): # set the size if given if size is not None: pass # Don't try to set the size the expensive way if it already has a # value. elif self._size is not None: return elif hasattr(self.storage, 'image_size'): # Storage backends can implement ``image_size`` method that # optimizes this. size = self.storage.image_size(self.name) else: # This is the worst case scenario image = default.engine.get_image(self) size = default.engine.get_image_size(image) self._size = list(size) @property def size(self): return self._size @property def url(self): return self.storage.url(self.name) def read(self): return self.storage.open(self.name).read() def write(self, content): if not isinstance(content, File): content = ContentFile(content) self._size = None return self.storage.save(self.name, content) def delete(self): return self.storage.delete(self.name) def serialize_storage(self): if isinstance(self.storage, LazyObject): # if storage is wrapped in a lazy object we need to get the real # thing. self.storage._setup() cls = self.storage._wrapped.__class__ else: cls = self.storage.__class__ return '%s.%s' % (cls.__module__, cls.__name__) @property def key(self): return tokey(self.name, self.serialize_storage()) def serialize(self): return serialize_image_file(self) class DummyImageFile(BaseImageFile): def __init__(self, geometry_string): self.size = parse_geometry( geometry_string, settings.THUMBNAIL_DUMMY_RATIO, ) def exists(self): return True @property def url(self): return settings.THUMBNAIL_DUMMY_SOURCE % ( {'width': self.x, 'height': self.y} ) class UrlStorage(Storage): def open(self, name): return urllib2.urlopen(name) def exists(self, name): try: self.open(name) except urllib2.URLError: return False return True def url(self, name): return name def delete(self, name): pass sorl-thumbnail-11.12/sorl/thumbnail/admin/0000755000175000017500000000000011677075433017533 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/admin/__init__.py0000644000175000017500000000034411635604732021637 0ustar mikkomikkotry: from django.forms import ClearableFileInput except ImportError: from .compat import AdminImageMixin else: from .current import AdminImageMixin AdminInlineImageMixin = AdminImageMixin # backwards compatibility sorl-thumbnail-11.12/sorl/thumbnail/admin/compat.py0000644000175000017500000000574611635604732021376 0ustar mikkomikko""" This is for Django < 1.3 I hate this shit and hopefully I will never have to touch this ever again. """ from django import forms from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from sorl.thumbnail.fields import ImageField, ImageFormField from sorl.thumbnail.shortcuts import get_thumbnail class ClearableImageFormField(forms.MultiValueField): def __init__(self, max_length=None, **kwargs): fields = ( ImageFormField(max_length=max_length, **kwargs), forms.BooleanField() ) super(ClearableImageFormField, self).__init__(fields, **kwargs) def compress(self, data_list): if data_list: if not data_list[0] and data_list[1]: return False return data_list[0] class AdminImageWidget(forms.FileInput): """ An ImageField Widget for django.contrib.admin that shows a thumbnailed image as well as a link to the current one if it hase one. """ def render(self, name, value, attrs=None): output = super(AdminImageWidget, self).render(name, value, attrs) if value and hasattr(value, 'url'): try: mini = get_thumbnail(value, 'x80', upscale=False) except Exception: pass else: output = ( u'
' u'' u'%s
' ) % (mini.width, value.url, mini.url, output) return mark_safe(output) class AdminClearWidget(forms.CheckboxInput): def render(self, name, value, attrs=None): output = super(AdminClearWidget, self).render(name, value, attrs) output = ( u'
' u'%s' u'
' ) % (name, _('Clear image'), output) return mark_safe(output) class AdminClearableImageWidget(forms.MultiWidget): def __init__(self, attrs=None): widgets = (AdminImageWidget(attrs=attrs), AdminClearWidget()) super(AdminClearableImageWidget, self).__init__(widgets, attrs) def decompress(self, value): if value: return (value, False) return (None, None) class AdminImageMixin(object): """ This is a mix-in for ModelAdmin subclasses to make ``ImageField`` show nicer form class and widget """ def formfield_for_dbfield(self, db_field, **kwargs): if isinstance(db_field, ImageField): if not db_field.blank: return db_field.formfield(widget=AdminImageWidget) return db_field.formfield( form_class=ClearableImageFormField, widget=AdminClearableImageWidget, ) sup = super(AdminImageMixin, self) return sup.formfield_for_dbfield(db_field, **kwargs) sorl-thumbnail-11.12/sorl/thumbnail/admin/current.py0000644000175000017500000000320111635604732021555 0ustar mikkomikkofrom django import forms from django.utils.safestring import mark_safe from sorl.thumbnail.fields import ImageField from sorl.thumbnail.shortcuts import get_thumbnail class AdminImageWidget(forms.ClearableFileInput): """ An ImageField Widget for django.contrib.admin that shows a thumbnailed image as well as a link to the current one if it hase one. """ template_with_initial = u'%(clear_template)s
%(input_text)s: %(input)s' template_with_clear = u'%(clear)s ' def render(self, name, value, attrs=None): output = super(AdminImageWidget, self).render(name, value, attrs) if value and hasattr(value, 'url'): try: mini = get_thumbnail(value, 'x80', upscale=False) except Exception: pass else: output = ( u'
' u'' u'%s
' ) % (mini.width, value.url, mini.url, output) return mark_safe(output) class AdminImageMixin(object): """ This is a mix-in for InlineModelAdmin subclasses to make ``ImageField`` show nicer form widget """ def formfield_for_dbfield(self, db_field, **kwargs): if isinstance(db_field, ImageField): return db_field.formfield(widget=AdminImageWidget) sup = super(AdminImageMixin, self) return sup.formfield_for_dbfield(db_field, **kwargs) sorl-thumbnail-11.12/sorl/thumbnail/base.py0000644000175000017500000000770211666137404017730 0ustar mikkomikkofrom sorl.thumbnail.conf import settings, defaults as default_settings from sorl.thumbnail.helpers import tokey, serialize from sorl.thumbnail.images import ImageFile from sorl.thumbnail import default from sorl.thumbnail.parsers import parse_geometry EXTENSIONS = { 'JPEG': 'jpg', 'PNG': 'png', } class ThumbnailBackend(object): """ The main class for sorl-thumbnail, you can subclass this if you for example want to change the way destination filename is generated. """ default_options = { 'format': settings.THUMBNAIL_FORMAT, 'quality': settings.THUMBNAIL_QUALITY, 'colorspace': settings.THUMBNAIL_COLORSPACE, 'upscale': settings.THUMBNAIL_UPSCALE, 'crop': False, } extra_options = ( ('progressive', 'THUMBNAIL_PROGRESSIVE'), ('orientation', 'THUMBNAIL_ORIENTATION'), ) def get_thumbnail(self, file_, geometry_string, **options): """ Returns thumbnail as an ImageFile instance for file with geometry and options given. First it will try to get it from the key value store, secondly it will create it. """ source = ImageFile(file_) for key, value in self.default_options.iteritems(): options.setdefault(key, value) # For the future I think it is better to add options only if they # differ from the default settings as below. This will ensure the same # filenames beeing generated for new options at default. for key, attr in self.extra_options: value = getattr(settings, attr) if value != getattr(default_settings, attr): options.setdefault(key, value) name = self._get_thumbnail_filename(source, geometry_string, options) thumbnail = ImageFile(name, default.storage) cached = default.kvstore.get(thumbnail) if cached: return cached if not thumbnail.exists(): # We have to check exists() because the Storage backend does not # overwrite in some implementations. source_image = default.engine.get_image(source) # We might as well set the size since we have the image in memory size = default.engine.get_image_size(source_image) source.set_size(size) self._create_thumbnail(source_image, geometry_string, options, thumbnail) # If the thumbnail exists we don't create it, the other option is # to delete and write but this could lead to race conditions so I # will just leave that out for now. default.kvstore.get_or_set(source) default.kvstore.set(thumbnail, source) return thumbnail def delete(self, file_, delete_file=True): """ Deletes file_ references in Key Value store and optionally the file_ it self. """ image_file = ImageFile(file_) if delete_file: image_file.delete() default.kvstore.delete(image_file) def _create_thumbnail(self, source_image, geometry_string, options, thumbnail): """ Creates the thumbnail by using default.engine """ ratio = default.engine.get_image_ratio(source_image) geometry = parse_geometry(geometry_string, ratio) image = default.engine.create(source_image, geometry, options) default.engine.write(image, options, thumbnail) # It's much cheaper to set the size here size = default.engine.get_image_size(image) thumbnail.set_size(size) def _get_thumbnail_filename(self, source, geometry_string, options): """ Computes the destination filename. """ key = tokey(source.key, geometry_string, serialize(options)) # make some subdirs path = '%s/%s/%s' % (key[:2], key[2:4], key) return '%s%s.%s' % (settings.THUMBNAIL_PREFIX, path, EXTENSIONS[options['format']]) sorl-thumbnail-11.12/sorl/thumbnail/conf/0000755000175000017500000000000011677075433017370 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/conf/__init__.py0000644000175000017500000000073111666137404021475 0ustar mikkomikkofrom django.conf import settings as user_settings from django.utils.functional import LazyObject from sorl.thumbnail.conf import defaults class Settings(object): pass class LazySettings(LazyObject): def _setup(self): self._wrapped = Settings() for obj in (defaults, user_settings): for attr in dir(obj): if attr == attr.upper(): setattr(self, attr, getattr(obj, attr)) settings = LazySettings() sorl-thumbnail-11.12/sorl/thumbnail/conf/defaults.py0000644000175000017500000000511511666137507021552 0ustar mikkomikkofrom django.conf import settings # When True ThumbnailNode.render can raise errors THUMBNAIL_DEBUG = False # Backend THUMBNAIL_BACKEND = 'sorl.thumbnail.base.ThumbnailBackend' # Key-value store, ships with: # sorl.thumbnail.kvstores.cached_db_kvstore.KVStore # sorl.thumbnail.kvstores.redis_kvstore.KVStore # Redis requires some more work, see docs THUMBNAIL_KVSTORE = 'sorl.thumbnail.kvstores.cached_db_kvstore.KVStore' # Change this to something else for MSSQL THUMBNAIL_KEY_DBCOLUMN = 'key' # Engine, ships with: # sorl.thumbnail.engines.convert_engine.Engine # sorl.thumbnail.engines.pil_engine.Engine # sorl.thumbnail.engines.pgmagick_engine.Engine # convert is preferred but requires imagemagick or graphicsmagick, se docs THUMBNAIL_ENGINE = 'sorl.thumbnail.engines.pil_engine.Engine' # Path to Imagemagick or Graphicsmagick ``convert`` and ``identify``. THUMBNAIL_CONVERT = 'convert' THUMBNAIL_IDENTIFY = 'identify' # Storage for the generated thumbnails THUMBNAIL_STORAGE = settings.DEFAULT_FILE_STORAGE # Redis settings THUMBNAIL_REDIS_DB = 0 THUMBNAIL_REDIS_PASSWORD = '' THUMBNAIL_REDIS_HOST = 'localhost' THUMBNAIL_REDIS_PORT = 6379 THUMBNAIL_REDIS_UNIX_SOCKET_PATH = None # Cache timeout for ``cached_db`` store. You should probably keep this at # maximum or ``0`` if your caching backend can handle that as infinate. THUMBNAIL_CACHE_TIMEOUT = 3600 * 24 * 365 * 10 # 10 years # Key prefix used by the key value store THUMBNAIL_KEY_PREFIX = 'sorl-thumbnail' # Thumbnail filename prefix THUMBNAIL_PREFIX = 'cache/' # Image format, common formats are: JPEG, PNG # Make sure the backend can handle the format you specify THUMBNAIL_FORMAT = 'JPEG' # Colorspace, backends are required to implement: RGB, GRAY # Setting this to None will keep the original colorspace. THUMBNAIL_COLORSPACE = 'RGB' # Should we upscale images by default THUMBNAIL_UPSCALE = True # Quality, 0-100 THUMBNAIL_QUALITY = 95 # Save as progressive when saving as jpeg THUMBNAIL_PROGRESSIVE = True # Orientate the thumbnail with respect to source EXIF orientation tag THUMBNAIL_ORIENTATION = True # This means sorl.thumbnail will generate and serve a generated dummy image # regardless of the thumbnail source content THUMBNAIL_DUMMY = False # Thumbnail dummy (placeholder) source. Some you might try are: # http://placekitten.com/%(width)s/%(height)s # http://placekitten.com/g/%(width)s/%(height)s # http://placehold.it/%(width)sx%(height)s THUMBNAIL_DUMMY_SOURCE = 'http://dummyimage.com/%(width)sx%(height)s' # Sets the source image ratio for dummy generation of images with only width # or height given THUMBNAIL_DUMMY_RATIO = 1.5 sorl-thumbnail-11.12/sorl/thumbnail/templatetags/0000755000175000017500000000000011677075433021135 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/templatetags/__init__.py0000644000175000017500000000000011467213637023231 0ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/templatetags/thumbnail.py0000644000175000017500000001145111476142267023471 0ustar mikkomikkoimport logging import re import sys from django.template import Library, Node, NodeList, TemplateSyntaxError from django.utils.encoding import smart_str from functools import wraps from sorl.thumbnail.conf import settings from sorl.thumbnail.images import ImageFile, DummyImageFile from sorl.thumbnail import default from sorl.thumbnail.parsers import parse_geometry register = Library() kw_pat = re.compile(r'^(?P[\w]+)=(?P.+)$') logger = logging.getLogger('sorl.thumbnail') def safe_filter(error_output=''): """ A safe filter decorator only raising errors when ``THUMBNAIL_DEBUG`` is ``True`` otherwise returning ``error_output``. """ def inner(f): @wraps(f) def wrapper(*args, **kwargs): try: return f(*args, **kwargs) except Exception: if settings.THUMBNAIL_DEBUG: raise logger.error('Thumbnail filter failed:', exc_info=sys.exc_info()) return error_output return wrapper return inner class ThumbnailNodeBase(Node): """ A Node that renders safely """ nodelist_empty = NodeList() def render(self, context): try: return self._render(context) except Exception: if settings.THUMBNAIL_DEBUG: raise logger.error('Thumbnail tag failed:', exc_info=sys.exc_info()) return self.nodelist_empty.render(context) def _render(self, context): raise NotImplemented() #@register.tag('thumbnail') class ThumbnailNode(ThumbnailNodeBase): child_nodelists = ('nodelist_file', 'nodelist_empty') error_msg = ('Syntax error. Expected: ``thumbnail source geometry ' '[key1=val1 key2=val2...] as var``') def __init__(self, parser, token): bits = token.split_contents() if len(bits) < 5 or bits[-2] != 'as': raise TemplateSyntaxError(self.error_msg) self.file_ = parser.compile_filter(bits[1]) self.geometry = parser.compile_filter(bits[2]) self.options = [] for bit in bits[3:-2]: m = kw_pat.match(bit) if not m: raise TemplateSyntaxError(self.error_msg) key = smart_str(m.group('key')) expr = parser.compile_filter(m.group('value')) self.options.append((key, expr)) self.as_var = bits[-1] self.nodelist_file = parser.parse(('empty', 'endthumbnail',)) if parser.next_token().contents == 'empty': self.nodelist_empty = parser.parse(('endthumbnail',)) parser.delete_first_token() def _render(self, context): file_ = self.file_.resolve(context) geometry = self.geometry.resolve(context) options = {} for key, expr in self.options: noresolve = {u'True': True, u'False': False, u'None': None} value = noresolve.get(unicode(expr), expr.resolve(context)) if key == 'options': options.update(value) else: options[key] = value if settings.THUMBNAIL_DUMMY: thumbnail = DummyImageFile(geometry) elif file_: thumbnail = default.backend.get_thumbnail( file_, geometry, **options ) else: return self.nodelist_empty.render(context) context.push() context[self.as_var] = thumbnail output = self.nodelist_file.render(context) context.pop() return output def __repr__(self): return "" def __iter__(self): for node in self.nodelist_file: yield node for node in self.nodelist_empty: yield node @register.tag def thumbnail(parser, token): return ThumbnailNode(parser, token) @safe_filter(error_output=False) @register.filter def is_portrait(file_): """ A very handy filter to determine if an image is portrait or landscape. """ if settings.THUMBNAIL_DUMMY: return settings.THUMBNAIL_DUMMY_RATIO < 1 if not file_: return False image_file = default.kvstore.get_or_set(ImageFile(file_)) return image_file.is_portrait() @safe_filter(error_output='auto') @register.filter def margin(file_, geometry_string): """ Returns the calculated margin for an image and geometry """ if not file_ or settings.THUMBNAIL_DUMMY: return 'auto' margin = [0, 0, 0, 0] image_file = default.kvstore.get_or_set(ImageFile(file_)) x, y = parse_geometry(geometry_string, image_file.ratio) ex = x - image_file.x margin[3] = ex / 2 margin[1] = ex / 2 if ex % 2: margin[1] += 1 ey = y - image_file.y margin[0] = ey / 2 margin[2] = ey / 2 if ey % 2: margin[2] += 1 return ' '.join([ '%spx' % n for n in margin ]) sorl-thumbnail-11.12/sorl/thumbnail/shortcuts.py0000644000175000017500000000061611502163456021044 0ustar mikkomikkofrom sorl.thumbnail import default def get_thumbnail(file_, geometry_string, **options): """ A shortcut for the Backend ``get_thumbnail`` method """ return default.backend.get_thumbnail(file_, geometry_string, **options) def delete(file_, delete_file=True): """ A shortcut for the Backend ``delete`` method """ return default.backend.delete(file_, delete_file) sorl-thumbnail-11.12/sorl/thumbnail/default.py0000644000175000017500000000130311470234017020420 0ustar mikkomikkofrom django.utils.functional import LazyObject from sorl.thumbnail.conf import settings from sorl.thumbnail.helpers import get_module_class class Backend(LazyObject): def _setup(self): self._wrapped = get_module_class(settings.THUMBNAIL_BACKEND)() class KVStore(LazyObject): def _setup(self): self._wrapped = get_module_class(settings.THUMBNAIL_KVSTORE)() class Engine(LazyObject): def _setup(self): self._wrapped = get_module_class(settings.THUMBNAIL_ENGINE)() class Storage(LazyObject): def _setup(self): self._wrapped = get_module_class(settings.THUMBNAIL_STORAGE)() backend = Backend() kvstore = KVStore() engine = Engine() storage = Storage() sorl-thumbnail-11.12/sorl/thumbnail/parsers.py0000644000175000017500000000502611467213637020474 0ustar mikkomikko#coding=utf-8 import re from sorl.thumbnail.helpers import ThumbnailError, toint bgpos_pat = re.compile(r'^(?P\d+)(?P%|px)$') geometry_pat = re.compile(r'^(?P\d+)?(?:x(?P\d+))?$') class ThumbnailParseError(ThumbnailError): pass def parse_geometry(geometry, ratio=None): """ Parses a geometry string syntax and returns a (width, height) tuple """ m = geometry_pat.match(geometry) def syntax_error(): return ThumbnailParseError('Geometry does not have the correct ' 'syntax: %s' % geometry) if not m: raise syntax_error() x = m.group('x') y = m.group('y') if x is None and y is None: raise syntax_error() if x is not None: x = int(x) if y is not None: y = int(y) # calculate x or y proportionally if not set but we need the image ratio # for this if ratio is not None: ratio = float(ratio) if x is None: x = toint(y * ratio) elif y is None: y = toint(x / ratio) return x, y def parse_crop(crop, xy_image, xy_window): """ Returns x, y offsets for cropping. The window area should fit inside image but it works out anyway """ def syntax_error(): raise ThumbnailParseError('Unrecognized crop option: %s' % crop) x_alias_percent = { 'left': '0%', 'center': '50%', 'right': '100%', } y_alias_percent = { 'top': '0%', 'center': '50%', 'bottom': '100%', } xy_crop = crop.split(' ') if len(xy_crop) == 1: if crop in x_alias_percent: x_crop = x_alias_percent[crop] y_crop = '50%' elif crop in y_alias_percent: y_crop = y_alias_percent[crop] x_crop = '50%' else: x_crop, y_crop = crop, crop elif len(xy_crop) == 2: x_crop, y_crop = xy_crop x_crop = x_alias_percent.get(x_crop, x_crop) y_crop = y_alias_percent.get(y_crop, y_crop) else: syntax_error() def get_offset(crop, epsilon): m = bgpos_pat.match(crop) if not m: syntax_error() value = int(m.group('value')) # we only take ints in the regexp unit = m.group('unit') if unit == '%': value = epsilon * value / 100.0 # return ∈ [0, epsilon] return int(max(0, min(value, epsilon))) offset_x = get_offset(x_crop, xy_image[0] - xy_window[0]) offset_y = get_offset(y_crop, xy_image[1] - xy_window[1]) return offset_x, offset_y sorl-thumbnail-11.12/sorl/thumbnail/helpers.py0000644000175000017500000000264011476142267020456 0ustar mikkomikkoimport hashlib from django.core.exceptions import ImproperlyConfigured from django.utils.encoding import smart_str from django.utils.importlib import import_module from django.utils import simplejson class ThumbnailError(Exception): pass class SortedJSONEncoder(simplejson.JSONEncoder): """ A json encoder that sorts the dict keys """ def __init__(self, **kwargs): kwargs['sort_keys'] = True super(SortedJSONEncoder, self).__init__(**kwargs) def toint(number): """ Helper to return rounded int for a float or just the int it self. """ if isinstance(number, float): number = round(number, 0) return int(number) def tokey(*args): """ Computes a (hopefully) unique key from arguments given. """ salt = '||'.join([smart_str(arg) for arg in args]) hash_ = hashlib.md5(salt) return hash_.hexdigest() def serialize(obj): return simplejson.dumps(obj, cls=SortedJSONEncoder) def deserialize(s): return simplejson.loads(s) def get_module_class(class_path): """ imports and returns module class from ``path.to.module.Class`` argument """ try: mod_name, cls_name = class_path.rsplit('.', 1) mod = import_module(mod_name) except ImportError, e: raise ImproperlyConfigured(('Error importing module %s: "%s"' % (mod_name, e))) return getattr(mod, cls_name) sorl-thumbnail-11.12/sorl/thumbnail/management/0000755000175000017500000000000011677075433020557 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/management/__init__.py0000644000175000017500000000000011470234017022640 0ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/management/commands/0000755000175000017500000000000011677075433022360 5ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/management/commands/__init__.py0000644000175000017500000000000011470234017024441 0ustar mikkomikkosorl-thumbnail-11.12/sorl/thumbnail/management/commands/thumbnail.py0000644000175000017500000000127011470234017024677 0ustar mikkomikkofrom django.core.management.base import BaseCommand, CommandError from sorl.thumbnail.conf import settings from sorl.thumbnail import default class Command(BaseCommand): help = ( u'Handles thumbnails and key value store' ) args = '[cleanup, clear]' option_list = BaseCommand.option_list def handle(self, cmd, *args, **kwargs): if cmd not in ['cleanup', 'clear']: raise CommandError('`%s` is not a valid argument' % cmd) if cmd == 'cleanup': default.kvstore.cleanup() print 'Cleanup thumbnails done.' if cmd == 'clear': default.kvstore.clear() print 'Cleared the Key Value Store.' sorl-thumbnail-11.12/PKG-INFO0000644000175000017500000000262011677075433014576 0ustar mikkomikkoMetadata-Version: 1.0 Name: sorl-thumbnail Version: 11.12 Summary: Thumbnails for Django Home-page: https://github.com/sorl/sorl-thumbnail Author: Mikko Hellsing Author-email: mikko@aino.se License: BSD Description: sorl-thumbnail ============== Thumbnails for Django. Totally rewritten. Features at a glance -------------------- - Storage support - Pluggable Engine support (ImageMagick, PIL, pgmagick included) - Pluggable Key Value Store support (cached db, redis) - Pluggable Backend support - Admin integration with possibility to delete - Dummy generation (placeholders) - Flexible, simple syntax, generates no html - ImageField for model that deletes thumbnails - CSS style cropping options - Margin calculation for vertical positioning Read more in `the documentation (latest version) `_ Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Multimedia :: Graphics Classifier: Framework :: Django sorl-thumbnail-11.12/setup.cfg0000644000175000017500000000007311677075433015322 0ustar mikkomikko[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0