diff --git a/src/imagekit/__init__.py b/src/imagekit/__init__.py index ee4964e..e34d4c3 100644 --- a/src/imagekit/__init__.py +++ b/src/imagekit/__init__.py @@ -6,5 +6,19 @@ Author: Justin Driscoll Version: 1.0 """ -from models import IKModel -from specs import ImageSpec \ No newline at end of file +# Required PIL classes may or may not be available from the root namespace +# depending on the installation method used. +try: + import Image + import ImageFile + import ImageFilter + import ImageEnhance +except ImportError: + try: + from PIL import Image + from PIL import ImageFile + from PIL import ImageFilter + from PIL import ImageEnhance + except ImportError: + raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.') + \ No newline at end of file diff --git a/src/imagekit/config.py b/src/imagekit/config.py index ad92aea..f0386b1 100644 --- a/src/imagekit/config.py +++ b/src/imagekit/config.py @@ -5,7 +5,7 @@ from imagekit import processors class ResizeThumbnail(processors.Resize): width = 100 height = 75 - crop = True + crop = ('center', 'center') class EnhanceSmall(processors.Adjustment): contrast = 1.2 diff --git a/src/imagekit/models.py b/src/imagekit/models.py index 8d5b590..48cb929 100644 --- a/src/imagekit/models.py +++ b/src/imagekit/models.py @@ -19,16 +19,17 @@ class IKModelBase(ModelBase): user_opts = getattr(cls, 'IK', None) opts = Options(user_opts) - setattr(cls, '_ik', opts) - try: module = __import__(opts.config_module, {}, {}, ['']) except ImportError: raise ImportError('Unable to load imagekit config module: %s' % opts.config_module) for spec in [spec for spec in module.__dict__.values() if \ - issubclass(spec, specs.ImageSpec)]: - setattr(cls, spec.name, specs.Descriptor(spec)) + isinstance(spec, type) and issubclass(spec, specs.ImageSpec)]: + setattr(cls, spec.name(), specs.Descriptor(spec)) + opts.specs.append(spec) + + setattr(cls, '_ik', opts) class IKModel(models.Model): @@ -39,117 +40,66 @@ class IKModel(models.Model): """ __metaclass__ = IKModelBase - - CROP_X_NONE = 0 - CROP_X_LEFT = 1 - CROP_X_CENTER = 2 - CROP_X_RIGHT = 3 - - CROP_Y_NONE = 0 - CROP_Y_TOP = 1 - CROP_Y_CENTER = 2 - CROP_Y_BOTTOM = 3 - - CROP_X_CHOICES = ( - (CROP_X_NONE, 'None'), - (CROP_X_LEFT, 'Left'), - (CROP_X_CENTER, 'Center'), - (CROP_X_RIGHT, 'Right'), - ) - - CROP_Y_CHOICES = ( - (CROP_Y_NONE, 'None'), - (CROP_Y_TOP, 'Top'), - (CROP_Y_CENTER, 'Center'), - (CROP_Y_BOTTOM, 'Bottom'), - ) image = models.ImageField(_('image'), upload_to='photos') - crop_x = models.PositiveSmallIntegerField(choices=CROP_X_CHOICES, - default=CROP_X_CENTER) - crop_y = models.PositiveSmallIntegerField(choices=CROP_Y_CHOICES, - default=CROP_Y_CENTER) class Meta: abstract = True class IK: pass + + def admin_thumbnail_view(self): + prop = getattr(self, 'admin_thumbnail', None) + if prop is None: + return 'An "admin_thumbnail" image spec has not been defined.' + else: + if hasattr(self, 'get_absolute_url'): + return u'' % \ + (self.get_absolute_url(), prop.url) + else: + return u'' % \ + (self.image.url, prop.url) + admin_thumbnail_view.short_description = _('Thumbnail') + admin_thumbnail_view.allow_tags = True @property def cache_dir(self): """ Returns the path to the image cache directory """ - return os.path.join(os.path.dirname(self._obj.image.path), + return os.path.join(os.path.dirname(self.image.path), self._ik.cache_dir_name) @property def cache_url(self): """ Returns a url pointing to the image cache directory """ - return '/'.join([os.path.dirname(self._obj.image.url), + return '/'.join([os.path.dirname(self.image.url), self._ik.cache_dir_name]) - def _cache_spec(self, spec): - if self._file_exists(spec): - return - - # create cache directory if it does not exist - if not os.path.isdir(self._cache_path()): - os.makedirs(self._cache_path()) - - img = Image.open(self.image.path) - - if img.size != spec.size and spec.size != (0, 0): - resized = resize_image(img, spec) - - output_filename = self._spec_filename(spec) - - try: - if img.format == 'JPEG': - resized.save(output_filename, img.format, quality=int(spec.quality)) - else: - try: - im.save(im_filename) - except KeyError: - pass - except IOError, e: - if os.path.isfile(output_filename): - os.unlink(output_filename) - raise e - - def _delete_spec(self, spec, remove_dirs=True): - if not self._file_exists(spec): - return - accessor = getattr(self, spec.name) - if os.path.isfile(accessor.path): - os.remove(accessor.path) - if remove_dirs: - self._cleanupget_cache_dirs - def _cleanup_cache_dirs(self): try: - os.removedirs(self._cache_path()) + os.removedirs(self.cache_path) except: pass def _clear_cache(self): - cache = SpecCache() - for photosize in cache.sizes.values(): - self._delete_spec(spec, False) + for spec in self._ik.specs: + prop = getattr(self, spec.name()) + prop.delete() self._cleanup_cache_dirs() def _pre_cache(self): - cache = SpecCache() - for spec in cache.specs.values(): - if spec.cache_on_save: - self._cache_spec(spec) + for spec in self._ik.specs: + if spec.pre_cache: + prop = getattr(self, spec.name()) + prop.create() def save(self, *args, **kwargs): - #if self._get_pk_val(): - # self._clear_cache() + if self._get_pk_val(): + self._clear_cache() super(IKModel, self).save(*args, **kwargs) - #self._pre_cache() + self._pre_cache() def delete(self): assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname) - self._clear_cache() - super(ImageModel, self).delete() + #self._clear_cache() + super(IKModel, self).delete() diff --git a/src/imagekit/options.py b/src/imagekit/options.py index 3f7d803..ebde0f8 100644 --- a/src/imagekit/options.py +++ b/src/imagekit/options.py @@ -15,6 +15,7 @@ class Options(object): cache_filename_format = "%(filename)s_%(specname)s.%(extension)s" # Configuration options coded in the models itself config_module = 'imagekit.config' + specs = [] def __init__(self, opts): for key, value in opts.__dict__.iteritems(): diff --git a/src/imagekit/processors.py b/src/imagekit/processors.py index 93e99a4..13a37be 100644 --- a/src/imagekit/processors.py +++ b/src/imagekit/processors.py @@ -6,6 +6,7 @@ the class properties as settings. The process method can be overridden as well a own effects/processes entirely. """ +from imagekit import * class ImageProcessor(object): """ Base image processor class """ @@ -17,9 +18,47 @@ class ImageProcessor(object): class Resize(ImageProcessor): width = None height = None - crop = False + crop = False # ('top', 'left') upscale = False + @classmethod + def process(cls, image): + cur_width, cur_height = image.size + if cls.crop: + ratio = max(float(cls.width)/cur_width, float(cls.height)/cur_height) + resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) + crop_x, crop_y = (abs(cls.width - resize_x), abs(cls.height - resize_y)) + x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) + box_upper, box_lower = { + 'top': (9, cls.height), + 'center': (int(y_diff), int(y_diff + cls.height)), + 'bottom': (int(crop_y), int(resize_y)), + }[cls.crop[0]] + box_left, box_right = { + 'left': (0, cls.width), + 'center': (int(x_diff), int(x_diff +cls.width)), + 'right': (int(crop_x), int(resize_x)), + }[cls.crop[1]] + box = (box_left, box_upper, box_right, box_lower) + image = image.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) + else: + if not cls.width == 0 and not cls.height == 0: + ratio = min(float(cls.width)/cur_width, + float(cls.height)/cur_height) + else: + if cls.width == 0: + ratio = float(cls.height)/cur_height + else: + ratio = float(cls.width)/cur_width + new_dimensions = (int(round(cur_width*ratio)), + int(round(cur_height*ratio))) + if new_dimensions[0] > cur_width or \ + new_dimensions[1] > cur_height: + if not cls.upscale: + return image + image = image.resize(new_dimensions, Image.ANTIALIAS) + return image + class Transpose(ImageProcessor): """ Rotates or flips the image diff --git a/src/imagekit/specs.py b/src/imagekit/specs.py index 22f8c73..112eed0 100644 --- a/src/imagekit/specs.py +++ b/src/imagekit/specs.py @@ -5,13 +5,15 @@ inheriting from IKModel will be modified with a descriptor/accessor for each spec found. """ +import os +from imagekit import Image + class ImageSpec(object): - cache_on_save = False + pre_cache = False output_quality = 70 increment_count = False processors = [] - @property @classmethod def name(cls): return getattr(cls, 'access_as', cls.__name__.lower()) @@ -34,8 +36,8 @@ class ImageSpec(object): quality=int(cls.output_quality), optimize=True) except IOError, e: - if os.path.isfile(filename): - os.unlink(filename) + if os.path.isfile(save_as): + os.remove(save_as) raise e return processed_image @@ -48,7 +50,9 @@ class Accessor(object): self.spec = spec def create(self): - self._img = self.spec.process(self.image, save_as=self.path) + if not os.path.isdir(self._obj.cache_dir): + os.makedirs(self._obj.cache_dir) + self._img = self.spec.process(Image.open(self._obj.image.path), save_as=self.path) def delete(self): if self.exists: @@ -58,14 +62,14 @@ class Accessor(object): @property def name(self): filename, ext = os.path.splitext(os.path.basename(self._obj.image.path)) - return self.spec._ik.cache_filename_format % \ + return self._obj._ik.cache_filename_format % \ {'filename': filename, - 'sizename': self.spec.name, + 'specname': self.spec.name(), 'extension': ext.lstrip('.')} @property def path(self): - return os.abspath(os.path.join(self._obj.cache_dir, self.name) + return os.path.abspath(os.path.join(self._obj.cache_dir, self.name)) @property def url(self): @@ -85,7 +89,7 @@ class Accessor(object): @property def image(self): if self._img is None: - if not self.exists(): + if not self.exists: self.create() else: self._img = Image.open(self.path) diff --git a/src/imagekit/tests.py b/src/imagekit/tests.py index 78a5cb0..a0801f9 100644 --- a/src/imagekit/tests.py +++ b/src/imagekit/tests.py @@ -3,27 +3,15 @@ import StringIO import unittest from django.conf import settings from django.core.files.base import ContentFile +from django.db import models from django.test import TestCase from models import IKModel from specs import ImageSpec +from imagekit import Image -# Required PIL classes may or may not be available from the root namespace -# depending on the installation method used. -try: - import Image - import ImageFile - import ImageFilter - import ImageEnhance -except ImportError: - try: - from PIL import Image - from PIL import ImageFile - from PIL import ImageFilter - from PIL import ImageEnhance - except ImportError: - raise ImportError(_('Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')) +IMG_PATH = os.path.join(os.path.dirname(__file__), 'test.jpg') class TestPhoto(IKModel): """ Minimal ImageModel class for testing """ @@ -33,13 +21,10 @@ class TestPhoto(IKModel): class PLTest(TestCase): """ Base TestCase class """ def setUp(self): - imgfile = StringIO.StringIO() - Image.new('RGB', (100, 100)).save(imgfile, 'JPEG') - - content_file = ContentFile(imgfile.read()) - + Image.new('RGB', (100, 100)).save(IMG_PATH, 'JPEG') self.p = TestPhoto(name='landscape') - self.p.image.save('image.jpeg', content_file) + self.p.image.save(os.path.basename(IMG_PATH), + ContentFile(open(IMG_PATH, 'rb').read())) self.p.save() def test_setup(self): @@ -47,14 +32,10 @@ class PLTest(TestCase): self.assertEqual(self.p.image.width, 100 ) def test_accessor(self): - pass - self.assertEqual(self.p.thumbnail.siz) + self.assertEqual(self.p.admin_thumbnail.width, 100) def tearDown(self): + os.remove(IMG_PATH) path = self.p.image.path - - os.remove(path) - return - self.p.delete() self.failIf(os.path.isfile(path))