From a9895f335a5323b4bee7d6cbcffffbe826645c03 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 08:30:54 -0400 Subject: [PATCH 01/68] Using spec properties from ImageModel. --- imagekit/models.py | 41 ++++++++++++++++++----------------------- imagekit/options.py | 1 - imagekit/specs.py | 10 ++++++---- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 3241d09..d8a5a2e 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -8,7 +8,7 @@ from django.db.models.signals import post_delete from django.utils.html import conditional_escape as escape from django.utils.translation import ugettext_lazy as _ -from imagekit import specs +from imagekit.specs import ImageSpec, Descriptor from imagekit.lib import * from imagekit.options import Options from imagekit.utils import img_to_fobj @@ -38,25 +38,20 @@ class ImageModelBase(ModelBase): module. """ - def __init__(cls, name, bases, attrs): - parents = [b for b in bases if isinstance(b, ImageModelBase)] - if not parents: - return - user_opts = getattr(cls, 'IKOptions', None) - opts = Options(user_opts) - if not opts.specs: - try: - module = __import__(opts.spec_module, {}, {}, ['']) - except ImportError: - raise ImportError('Unable to load imagekit config module: %s' \ - % opts.spec_module) - opts.specs.extend([spec for spec in module.__dict__.values() \ - if isinstance(spec, type) \ - and issubclass(spec, specs.ImageSpec) \ - and spec != specs.ImageSpec]) - for spec in opts.specs: - setattr(cls, spec.name(), specs.Descriptor(spec)) - setattr(cls, '_ik', opts) + def __init__(self, name, bases, attrs): + if [b for b in bases if isinstance(b, ImageModelBase)]: + user_opts = getattr(self, 'IKOptions', None) + + specs = [] + for k, v in attrs.items(): + if isinstance(v, ImageSpec): + setattr(self, k, Descriptor(v, k)) + specs.append(v) + + user_opts.specs = specs + opts = Options(user_opts) + setattr(self, '_ik', opts) + ModelBase.__init__(self, name, bases, attrs) class ImageModel(models.Model): @@ -72,9 +67,6 @@ class ImageModel(models.Model): class Meta: abstract = True - class IKOptions: - pass - def admin_thumbnail_view(self): if not self._imgfield: return None @@ -92,6 +84,9 @@ class ImageModel(models.Model): admin_thumbnail_view.short_description = _('Thumbnail') admin_thumbnail_view.allow_tags = True + class IKOptions: + pass + @property def _imgfield(self): return getattr(self, self._ik.image_field) diff --git a/imagekit/options.py b/imagekit/options.py index 7170799..455eb41 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -16,7 +16,6 @@ class Options(object): cache_filename_fields = ['pk', ] cache_filename_format = "%(filename)s_%(specname)s.%(extension)s" admin_thumbnail_spec = 'admin_thumbnail' - spec_module = 'imagekit.defaults' specs = None #storage = defaults to image_field.storage diff --git a/imagekit/specs.py b/imagekit/specs.py index 0332187..cc6fc86 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -34,11 +34,12 @@ class ImageSpec(object): class Accessor(object): - def __init__(self, obj, spec): + def __init__(self, obj, spec, property_name): self._img = None self._fmt = None self._obj = obj self.spec = spec + self.property_name = property_name def _get_imgfile(self): format = self._img.format or 'JPEG' @@ -86,7 +87,7 @@ class Accessor(object): if issubclass(processor, processors.Format): extension = processor.extension filename_format_dict = {'filename': filename, - 'specname': self.spec.name(), + 'specname': self.property_name, 'extension': extension.lstrip('.')} cache_filename_fields = self._obj._ik.cache_filename_fields filename_format_dict.update(dict(zip( @@ -138,8 +139,9 @@ class Accessor(object): class Descriptor(object): - def __init__(self, spec): + def __init__(self, spec, property_name): + self._property_name = property_name self._spec = spec def __get__(self, obj, type=None): - return Accessor(obj, self._spec) + return Accessor(obj, self._spec, self._property_name) From cd3395b68f612a6dd3ce62f224a63d491024907c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 09:15:08 -0400 Subject: [PATCH 02/68] Processors are now instance-based. --- imagekit/processors.py | 86 +++++++++++++++++++++--------------------- imagekit/specs.py | 19 ++++------ 2 files changed, 50 insertions(+), 55 deletions(-) diff --git a/imagekit/processors.py b/imagekit/processors.py index 4988f60..0ea7efa 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -11,22 +11,22 @@ from imagekit.lib import * class ImageProcessor(object): """ Base image processor class """ - @classmethod - def process(cls, img, fmt, obj): + def process(self, img, fmt, obj): return img, fmt -class Adjustment(ImageProcessor): - color = 1.0 - brightness = 1.0 - contrast = 1.0 - sharpness = 1.0 +class Adjust(ImageProcessor): + + def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0): + self.color = color + self.brightness = brightness + self.contrast = contrast + self.sharpness = sharpness - @classmethod - def process(cls, img, fmt, obj): + def process(self, img, fmt, obj): img = img.convert('RGB') for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: - factor = getattr(cls, name.lower()) + factor = getattr(self, name.lower()) if factor != 1.0: try: img = getattr(ImageEnhance, name)(img).enhance(factor) @@ -39,9 +39,8 @@ class Format(ImageProcessor): format = 'JPEG' extension = 'jpg' - @classmethod - def process(cls, img, fmt, obj): - return img, cls.format + def process(self, img, fmt, obj): + return img, self.format class Reflection(ImageProcessor): @@ -49,10 +48,9 @@ class Reflection(ImageProcessor): size = 0.0 opacity = 0.6 - @classmethod - def process(cls, img, fmt, obj): + def process(self, img, fmt, obj): # convert bgcolor string to rgb value - background_color = ImageColor.getrgb(cls.background_color) + background_color = ImageColor.getrgb(self.background_color) # handle palleted images img = img.convert('RGB') # copy orignial image and flip the orientation @@ -60,8 +58,8 @@ class Reflection(ImageProcessor): # create a new image filled with the bgcolor the same size background = Image.new("RGB", img.size, background_color) # calculate our alpha mask - start = int(255 - (255 * cls.opacity)) # The start of our gradient - steps = int(255 * cls.size) # the number of intermedite values + start = int(255 - (255 * self.opacity)) # The start of our gradient + steps = int(255 * self.size) # the number of intermedite values increment = (255 - start) / float(steps) mask = Image.new('L', (1, 255)) for y in range(255): @@ -74,7 +72,7 @@ class Reflection(ImageProcessor): # merge the reflection onto our background color using the alpha mask reflection = Image.composite(background, reflection, alpha_mask) # crop the reflection - reflection_height = int(img.size[1] * cls.size) + reflection_height = int(img.size[1] * self.size) reflection = reflection.crop((0, 0, img.size[0], reflection_height)) # create new image sized to hold both the original image and the reflection composite = Image.new("RGB", (img.size[0], img.size[1]+reflection_height), background_color) @@ -88,47 +86,48 @@ class Reflection(ImageProcessor): class Resize(ImageProcessor): - width = None - height = None - crop = False - upscale = False + + def __init__(self, width, height, crop=False, upscale=False): + self.width = width + self.height = height + self.crop = crop + self.upscale = upscale - @classmethod - def process(cls, img, fmt, obj): + def process(self, img, fmt, obj): cur_width, cur_height = img.size - if cls.crop: + if self.crop: crop_horz = getattr(obj, obj._ik.crop_horz_field, 1) crop_vert = getattr(obj, obj._ik.crop_vert_field, 1) - ratio = max(float(cls.width)/cur_width, float(cls.height)/cur_height) + ratio = max(float(self.width)/cur_width, float(self.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)) + crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) box_left, box_right = { - 0: (0, cls.width), - 1: (int(x_diff), int(x_diff + cls.width)), + 0: (0, self.width), + 1: (int(x_diff), int(x_diff + self.width)), 2: (int(crop_x), int(resize_x)), }[crop_horz] box_upper, box_lower = { - 0: (0, cls.height), - 1: (int(y_diff), int(y_diff + cls.height)), + 0: (0, self.height), + 1: (int(y_diff), int(y_diff + self.height)), 2: (int(crop_y), int(resize_y)), }[crop_vert] box = (box_left, box_upper, box_right, box_lower) img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) else: - if not cls.width is None and not cls.height is None: - ratio = min(float(cls.width)/cur_width, - float(cls.height)/cur_height) + if not self.width is None and not self.height is None: + ratio = min(float(self.width)/cur_width, + float(self.height)/cur_height) else: - if cls.width is None: - ratio = float(cls.height)/cur_height + if self.width is None: + ratio = float(self.height)/cur_height else: - ratio = float(cls.width)/cur_width + ratio = float(self.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: + if not self.upscale: return img, fmt img = img.resize(new_dimensions, Image.ANTIALIAS) return img, fmt @@ -162,16 +161,15 @@ class Transpose(ImageProcessor): method = 'auto' - @classmethod - def process(cls, img, fmt, obj): - if cls.method == 'auto': + def process(self, img, fmt, obj): + if self.method == 'auto': try: orientation = Image.open(obj._imgfield.file)._getexif()[0x0112] - ops = cls.EXIF_ORIENTATION_STEPS[orientation] + ops = self.EXIF_ORIENTATION_STEPS[orientation] except: ops = [] else: - ops = [cls.method] + ops = [self.method] for method in ops: img = img.transpose(getattr(Image, method)) return img, fmt diff --git a/imagekit/specs.py b/imagekit/specs.py index cc6fc86..905639b 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -14,20 +14,17 @@ from django.core.files.base import ContentFile class ImageSpec(object): - pre_cache = False - quality = 70 - increment_count = False - processors = [] - @classmethod - def name(cls): - return getattr(cls, 'access_as', cls.__name__.lower()) + def __init__(self, processors, pre_cache=False, quality=70, increment_count=False): + self.processors = list(processors or []) + self.pre_cache = pre_cache + self.quality = quality + self.increment_count = increment_count - @classmethod - def process(cls, image, obj): + def process(self, image, obj): fmt = image.format img = image.copy() - for proc in cls.processors: + for proc in self.processors: img, fmt = proc.process(img, fmt, obj) img.format = fmt return img, fmt @@ -84,7 +81,7 @@ class Accessor(object): filepath, basename = os.path.split(self._obj._imgfield.name) filename, extension = os.path.splitext(basename) for processor in self.spec.processors: - if issubclass(processor, processors.Format): + if isinstance(processor, processors.Format): extension = processor.extension filename_format_dict = {'filename': filename, 'specname': self.property_name, From e32ccb617d7cafa7791d180138900b144a905ac7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 09:15:31 -0400 Subject: [PATCH 03/68] Resize split into Crop and Fit. --- imagekit/processors.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/imagekit/processors.py b/imagekit/processors.py index 0ea7efa..7e7af0b 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -85,7 +85,7 @@ class Reflection(ImageProcessor): return composite, fmt -class Resize(ImageProcessor): +class _Resize(ImageProcessor): def __init__(self, width, height, crop=False, upscale=False): self.width = width @@ -133,6 +133,16 @@ class Resize(ImageProcessor): return img, fmt +class Crop(_Resize): + def __init__(self, width, height): + super(Crop, self).__init__(width, height, True) + + +class Fit(_Resize): + def __init__(self, width, height, upscale=False): + super(Fit, self).__init__(width, height, False, upscale) + + class Transpose(ImageProcessor): """ Rotates or flips the image From def8dea23ff4a405d9841f3b33eb1a4f28390b21 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 09:15:58 -0400 Subject: [PATCH 04/68] Storage moved onto ImageSpec. --- imagekit/models.py | 4 ---- imagekit/specs.py | 14 +++++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index d8a5a2e..4308540 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -91,10 +91,6 @@ class ImageModel(models.Model): def _imgfield(self): return getattr(self, self._ik.image_field) - @property - def _storage(self): - return getattr(self._ik, 'storage', self._imgfield.storage) - def _clear_cache(self): for spec in self._ik.specs: prop = getattr(self, spec.name()) diff --git a/imagekit/specs.py b/imagekit/specs.py index 905639b..6971a8b 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -62,18 +62,18 @@ class Accessor(object): self._img, self._fmt = self.spec.process(Image.open(fp), self._obj) # save the new image to the cache content = ContentFile(self._get_imgfile().read()) - self._obj._storage.save(self.name, content) + self._storage.save(self.name, content) def _delete(self): if self._obj._imgfield: try: - self._obj._storage.delete(self.name) + self._storage.delete(self.name) except (NotImplementedError, IOError): return def _exists(self): if self._obj._imgfield: - return self._obj._storage.exists(self.name) + return self._storage.exists(self.name) @property def name(self): @@ -101,6 +101,10 @@ class Accessor(object): return os.path.join(self._obj._ik.cache_dir, filepath, cache_filename) + @property + def _storage(self): + return getattr(self._obj._ik, 'storage', self._obj._imgfield.storage) + @property def url(self): if not self.spec.pre_cache: @@ -111,12 +115,12 @@ class Accessor(object): current_count = getattr(self._obj, fieldname) setattr(self._obj, fieldname, current_count + 1) self._obj.save(clear_cache=False) - return self._obj._storage.url(self.name) + return self._storage.url(self.name) @property def file(self): self._create() - return self._obj._storage.open(self.name) + return self._storage.open(self.name) @property def image(self): From 0b0942921b1cfca5ec85a1f7cafaa4f5da870616 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 14:13:22 -0400 Subject: [PATCH 05/68] Updated README to reflect the new API. --- README.rst | 77 ++++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/README.rst b/README.rst index 28b543e..9ecde9f 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ django-imagekit =============== -ImageKit In 7 Steps +ImageKit In 6 Steps =================== Step 1 @@ -17,7 +17,7 @@ Step 1 Step 2 ****** -Add ImageKit to your models. +Create an ImageModel subclass and add specs to it. :: @@ -25,60 +25,57 @@ Add ImageKit to your models. from django.db import models from imagekit.models import ImageModel + from imagekit.specs import ImageSpec + from imagekit.processors import Crop, Fit, Adjust class Photo(ImageModel): name = models.CharField(max_length=100) original_image = models.ImageField(upload_to='photos') num_views = models.PositiveIntegerField(editable=False, default=0) - class IKOptions: - # This inner class is where we define the ImageKit options for the model - spec_module = 'myapp.specs' - cache_dir = 'photos' - image_field = 'original_image' - save_count_as = 'num_views' + thumbnail_image = ImageSpec([Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)], quality=90, pre_cache=True, image_field='original_image', cache_dir='photos') + display = ImageSpec([Fit(600)], quality=90, increment_count=True, image_field='original_image', cache_dir='photos', save_count_as='num_views') -Step 3 -****** -Create your specifications. +Of course, you don't have to define your ImageSpecs inline if you don't want to: :: # myapp/specs.py from imagekit.specs import ImageSpec - from imagekit import processors + from imagekit.processors import Crop, Fit, Adjust - # first we define our thumbnail resize processor - class ResizeThumb(processors.Resize): - width = 100 - height = 75 - crop = True + class _BaseSpec(ImageSpec): + quality = 90 + image_field = 'original_image' + cache_dir = 'photos' - # now we define a display size resize processor - class ResizeDisplay(processors.Resize): - width = 600 - - # now let's create an adjustment processor to enhance the image at small sizes - class EnchanceThumb(processors.Adjustment): - contrast = 1.2 - sharpness = 1.1 - - # now we can define our thumbnail spec - class Thumbnail(ImageSpec): - quality = 90 # defaults to 70 - access_as = 'thumbnail_image' + class DisplaySpec(_BaseSpec): pre_cache = True - processors = [ResizeThumb, EnchanceThumb] - - # and our display spec - class Display(ImageSpec): - quality = 90 # defaults to 70 increment_count = True - processors = [ResizeDisplay] + save_count_as = 'num_views' + processors = [Fit(600)] -Step 4 + class ThumbnailSpec(_BaseSpec): + processors = [Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)] + + # myapp/models.py + + from django.db import models + from imagekit.models import ImageModel + from myapp.specs import DisplaySpec, ThumbnailSpec + + class Photo(ImageModel): + name = models.CharField(max_length=100) + original_image = models.ImageField(upload_to='photos') + num_views = models.PositiveIntegerField(editable=False, default=0) + + thumbnail_image = ThumbnailSpec() + display = DisplaySpec() + + +Step 3 ****** Flush the cache and pre-generate thumbnails (ImageKit has to be added to ``INSTALLED_APPS`` for management command to work). @@ -87,7 +84,7 @@ Flush the cache and pre-generate thumbnails (ImageKit has to be added to ``INSTA $ python manage.py ikflush myapp -Step 5 +Step 4 ****** Use your new model in templates. @@ -108,7 +105,7 @@ Use your new model in templates. {% endfor %} -Step 6 +Step 5 ****** Play with the API. @@ -131,7 +128,7 @@ Play with the API. >>> p.display.spec -Step 7 +Step 6 ****** Enjoy a nice beverage. From db4d704f71940889bd7acc39e0405abc66c97919 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 15:52:09 -0400 Subject: [PATCH 06/68] Changed ImageSpec constructor so you can use static properties. For example: class MyImageSpec(ImageSpec): quality = 100 class Photo(ImageModel): display = MyImageSpec() --- imagekit/specs.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/imagekit/specs.py b/imagekit/specs.py index 6971a8b..0bdef69 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -15,11 +15,15 @@ from django.core.files.base import ContentFile class ImageSpec(object): - def __init__(self, processors, pre_cache=False, quality=70, increment_count=False): - self.processors = list(processors or []) - self.pre_cache = pre_cache - self.quality = quality - self.increment_count = increment_count + processors = [] + pre_cache = False + quality = 70 + increment_count = False + + def __init__(self, processors=None, **kwargs): + if processors: + self.processors = processors + self.__dict__.update(kwargs) def process(self, image, obj): fmt = image.format From a1f11facbef096fa9c5f8bdaf97359a8d0d45af9 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 16:49:44 -0400 Subject: [PATCH 07/68] Processors now use static properties. --- imagekit/defaults.py | 5 ++--- imagekit/processors.py | 27 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/imagekit/defaults.py b/imagekit/defaults.py index 5e6c249..4423357 100644 --- a/imagekit/defaults.py +++ b/imagekit/defaults.py @@ -3,12 +3,11 @@ from imagekit.specs import ImageSpec from imagekit import processors -class ResizeThumbnail(processors.Resize): +class ResizeThumbnail(processors.Crop): width = 100 height = 50 - crop = True -class EnhanceSmall(processors.Adjustment): +class EnhanceSmall(processors.Adjust): contrast = 1.2 sharpness = 1.1 diff --git a/imagekit/processors.py b/imagekit/processors.py index 7e7af0b..484ddb1 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -87,11 +87,20 @@ class Reflection(ImageProcessor): class _Resize(ImageProcessor): - def __init__(self, width, height, crop=False, upscale=False): - self.width = width - self.height = height - self.crop = crop - self.upscale = upscale + width = None + height = None + crop = False + upscale = False + + def __init__(self, width=None, height=None, crop=None, upscale=None): + if width is not None: + self.width = width + if height is not None: + self.height = height + if crop is not None: + self.crop = crop + if upscale is not None: + self.upscale = upscale def process(self, img, fmt, obj): cur_width, cur_height = img.size @@ -134,13 +143,13 @@ class _Resize(ImageProcessor): class Crop(_Resize): - def __init__(self, width, height): - super(Crop, self).__init__(width, height, True) + def __init__(self, width=None, height=None): + super(Crop, self).__init__(width, height, crop=True) class Fit(_Resize): - def __init__(self, width, height, upscale=False): - super(Fit, self).__init__(width, height, False, upscale) + def __init__(self, width=None, height=None, upscale=None): + super(Fit, self).__init__(width, height, crop=False, upscale=upscale) class Transpose(ImageProcessor): From 5e00de5204ff2f0a18db91f710bf1888a6e45057 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 8 Sep 2011 16:50:06 -0400 Subject: [PATCH 08/68] Admin thumbnails. --- imagekit/defaults.py | 3 +-- imagekit/models.py | 32 +++++++++++++++++--------------- imagekit/options.py | 5 ++++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/imagekit/defaults.py b/imagekit/defaults.py index 4423357..bc197b3 100644 --- a/imagekit/defaults.py +++ b/imagekit/defaults.py @@ -20,5 +20,4 @@ class PNGFormat(processors.Format): extension = 'png' class DjangoAdminThumbnail(ImageSpec): - access_as = 'admin_thumbnail' - processors = [ResizeThumbnail, EnhanceSmall, SampleReflection, PNGFormat] + processors = [ResizeThumbnail(), EnhanceSmall(), SampleReflection(), PNGFormat()] diff --git a/imagekit/models.py b/imagekit/models.py index 4308540..382636b 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -12,6 +12,7 @@ from imagekit.specs import ImageSpec, Descriptor from imagekit.lib import * from imagekit.options import Options from imagekit.utils import img_to_fobj +from imagekit import defaults # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) @@ -39,18 +40,17 @@ class ImageModelBase(ModelBase): """ def __init__(self, name, bases, attrs): - if [b for b in bases if isinstance(b, ImageModelBase)]: - user_opts = getattr(self, 'IKOptions', None) - - specs = [] - for k, v in attrs.items(): - if isinstance(v, ImageSpec): - setattr(self, k, Descriptor(v, k)) - specs.append(v) - - user_opts.specs = specs - opts = Options(user_opts) - setattr(self, '_ik', opts) + user_opts = getattr(self, 'IKOptions', None) + + specs = [] + for k, v in attrs.items(): + if isinstance(v, ImageSpec): + setattr(self, k, Descriptor(v, k)) + specs.append(v) + + user_opts.specs = specs + opts = Options(user_opts) + setattr(self, '_ik', opts) ModelBase.__init__(self, name, bases, attrs) @@ -64,16 +64,18 @@ class ImageModel(models.Model): """ __metaclass__ = ImageModelBase + admin_thumbnail = defaults.DjangoAdminThumbnail() + class Meta: abstract = True def admin_thumbnail_view(self): if not self._imgfield: return None - prop = getattr(self, self._ik.admin_thumbnail_spec, None) + prop = getattr(self, self._ik.admin_thumbnail_property, None) if prop is None: - return 'An "%s" image spec has not been defined.' % \ - self._ik.admin_thumbnail_spec + return 'The property "%s" has not been defined.' % \ + self._ik.admin_thumbnail_property else: if hasattr(self, 'get_absolute_url'): return u'' % \ diff --git a/imagekit/options.py b/imagekit/options.py index 455eb41..d0deaea 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -7,6 +7,10 @@ class Options(object): """ Class handling per-model imagekit options """ + + admin_thumbnail_property = 'admin_thumbnail' + """The name of the spec to be used by the admin_thumbnail_view""" + image_field = 'image' crop_horz_field = 'crop_horz' crop_vert_field = 'crop_vert' @@ -15,7 +19,6 @@ class Options(object): save_count_as = None cache_filename_fields = ['pk', ] cache_filename_format = "%(filename)s_%(specname)s.%(extension)s" - admin_thumbnail_spec = 'admin_thumbnail' specs = None #storage = defaults to image_field.storage From 4c78f2d24c4125b6df42a9f4a8a7fdede3f46bcd Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 10 Sep 2011 00:24:37 -0400 Subject: [PATCH 09/68] _imgfield is now a property of ImageSpec Moved _imgfield from ImageModel to ImageSpec. Theoretically, this will allow you to have specs that use different image fields on the same model. --- imagekit/models.py | 43 ++++++++++++++++++++++-------------------- imagekit/options.py | 2 +- imagekit/processors.py | 14 +++++++------- imagekit/specs.py | 24 +++++++++++++++-------- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 382636b..879bdbe 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -70,7 +70,7 @@ class ImageModel(models.Model): abstract = True def admin_thumbnail_view(self): - if not self._imgfield: + if not self._imgfields: return None prop = getattr(self, self._ik.admin_thumbnail_property, None) if prop is None: @@ -82,17 +82,13 @@ class ImageModel(models.Model): (escape(self.get_absolute_url()), escape(prop.url)) else: return u'' % \ - (escape(self._imgfield.url), escape(prop.url)) + (escape(self._get_imgfield(self).url), escape(prop.url)) admin_thumbnail_view.short_description = _('Thumbnail') admin_thumbnail_view.allow_tags = True class IKOptions: pass - @property - def _imgfield(self): - return getattr(self, self._ik.image_field) - def _clear_cache(self): for spec in self._ik.specs: prop = getattr(self, spec.name()) @@ -105,14 +101,20 @@ class ImageModel(models.Model): prop._create() def save_image(self, name, image, save=True, replace=True): - if self._imgfield and replace: - self._imgfield.delete(save=False) - if hasattr(image, 'read'): - data = image.read() - else: - data = image - content = ContentFile(data) - self._imgfield.save(name, content, save) + imgfields = self._imgfields + for imgfield in imgfields: + if imgfield and replace: + imgfield.delete(save=False) + if hasattr(image, 'read'): + data = image.read() + else: + data = image + content = ContentFile(data) + imgfield.save(name, content, save) + + @property + def _imgfields(self): + return set([spec._get_imgfield(self) for spec in self._ik.specs]) def save(self, clear_cache=True, *args, **kwargs): super(ImageModel, self).save(*args, **kwargs) @@ -121,10 +123,11 @@ class ImageModel(models.Model): if is_new_object: clear_cache = False - if self._imgfield: + imgfields = self._imgfields + for imgfield in imgfields: spec = self._ik.preprocessor_spec if spec is not None: - newfile = self._imgfield.storage.open(str(self._imgfield)) + newfile = imgfield.storage.open(str(imgfield)) img = Image.open(newfile) img, format = spec.process(img, self) if format != 'JPEG': @@ -135,10 +138,10 @@ class ImageModel(models.Model): optimize=True) content = ContentFile(imgfile.read()) newfile.close() - name = str(self._imgfield) - self._imgfield.storage.delete(name) - self._imgfield.storage.save(name, content) - if self._imgfield: + name = str(imgfield) + imgfield.storage.delete(name) + imgfield.storage.save(name, content) + if self._imgfields: if clear_cache: self._clear_cache() self._pre_cache() diff --git a/imagekit/options.py b/imagekit/options.py index d0deaea..92e25cb 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -7,7 +7,7 @@ class Options(object): """ Class handling per-model imagekit options """ - + admin_thumbnail_property = 'admin_thumbnail' """The name of the spec to be used by the admin_thumbnail_view""" diff --git a/imagekit/processors.py b/imagekit/processors.py index 484ddb1..26beaa5 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -11,7 +11,7 @@ from imagekit.lib import * class ImageProcessor(object): """ Base image processor class """ - def process(self, img, fmt, obj): + def process(self, img, fmt, obj, spec): return img, fmt @@ -23,7 +23,7 @@ class Adjust(ImageProcessor): self.contrast = contrast self.sharpness = sharpness - def process(self, img, fmt, obj): + def process(self, img, fmt, obj, spec): img = img.convert('RGB') for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: factor = getattr(self, name.lower()) @@ -39,7 +39,7 @@ class Format(ImageProcessor): format = 'JPEG' extension = 'jpg' - def process(self, img, fmt, obj): + def process(self, img, fmt, obj, spec): return img, self.format @@ -48,7 +48,7 @@ class Reflection(ImageProcessor): size = 0.0 opacity = 0.6 - def process(self, img, fmt, obj): + def process(self, img, fmt, obj, spec): # convert bgcolor string to rgb value background_color = ImageColor.getrgb(self.background_color) # handle palleted images @@ -102,7 +102,7 @@ class _Resize(ImageProcessor): if upscale is not None: self.upscale = upscale - def process(self, img, fmt, obj): + def process(self, img, fmt, obj, spec): cur_width, cur_height = img.size if self.crop: crop_horz = getattr(obj, obj._ik.crop_horz_field, 1) @@ -180,10 +180,10 @@ class Transpose(ImageProcessor): method = 'auto' - def process(self, img, fmt, obj): + def process(self, img, fmt, obj, spec): if self.method == 'auto': try: - orientation = Image.open(obj._imgfield.file)._getexif()[0x0112] + orientation = Image.open(spec._get_imgfield(obj).file)._getexif()[0x0112] ops = self.EXIF_ORIENTATION_STEPS[orientation] except: ops = [] diff --git a/imagekit/specs.py b/imagekit/specs.py index 0bdef69..c99b627 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -15,6 +15,7 @@ from django.core.files.base import ContentFile class ImageSpec(object): + image_field = 'original_image' # TODO: Get rid of this. It can be specified in a SpecDefaults nested class. processors = [] pre_cache = False quality = 70 @@ -25,11 +26,14 @@ class ImageSpec(object): self.processors = processors self.__dict__.update(kwargs) + def _get_imgfield(self, obj): + return getattr(obj, self.image_field) + def process(self, image, obj): fmt = image.format img = image.copy() for proc in self.processors: - img, fmt = proc.process(img, fmt, obj) + img, fmt = proc.process(img, fmt, obj, self) img.format = fmt return img, fmt @@ -52,13 +56,17 @@ class Accessor(object): optimize=True) return imgfile + @property + def _imgfield(self): + return self.spec._get_imgfield(self._obj) + def _create(self): - if self._obj._imgfield: + if self._imgfield: if self._exists(): return # process the original image file try: - fp = self._obj._imgfield.storage.open(self._obj._imgfield.name) + fp = self._imgfield.storage.open(self._imgfield.name) except IOError: return fp.seek(0) @@ -69,20 +77,20 @@ class Accessor(object): self._storage.save(self.name, content) def _delete(self): - if self._obj._imgfield: + if self._imgfield: try: self._storage.delete(self.name) except (NotImplementedError, IOError): return def _exists(self): - if self._obj._imgfield: + if self._imgfield: return self._storage.exists(self.name) @property def name(self): - if self._obj._imgfield.name: - filepath, basename = os.path.split(self._obj._imgfield.name) + if self._imgfield.name: + filepath, basename = os.path.split(self._imgfield.name) filename, extension = os.path.splitext(basename) for processor in self.spec.processors: if isinstance(processor, processors.Format): @@ -107,7 +115,7 @@ class Accessor(object): @property def _storage(self): - return getattr(self._obj._ik, 'storage', self._obj._imgfield.storage) + return getattr(self._obj._ik, 'storage', self._imgfield.storage) @property def url(self): From 57a28091c559c5a4f07140901a21b7b0904d124b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 10 Sep 2011 00:25:27 -0400 Subject: [PATCH 10/68] Added default_image_field This works kind of like Django's models' _default_manager. If your specs don't specify an image_field, and your IKOptions don't specify a default_image_field, the first ImageField your model defines will be used. --- imagekit/models.py | 8 ++++++-- imagekit/options.py | 12 ++++++++++-- imagekit/specs.py | 5 +++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 879bdbe..defaafb 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -41,14 +41,18 @@ class ImageModelBase(ModelBase): """ def __init__(self, name, bases, attrs): user_opts = getattr(self, 'IKOptions', None) - specs = [] + default_image_field = getattr(user_opts, 'default_image_field', None) + for k, v in attrs.items(): if isinstance(v, ImageSpec): setattr(self, k, Descriptor(v, k)) specs.append(v) - + elif not default_image_field and isinstance(v, models.ImageField): + default_image_field = k + user_opts.specs = specs + user_opts.default_image_field = default_image_field opts = Options(user_opts) setattr(self, '_ik', opts) ModelBase.__init__(self, name, bases, attrs) diff --git a/imagekit/options.py b/imagekit/options.py index 92e25cb..132f5c6 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -10,8 +10,16 @@ class Options(object): admin_thumbnail_property = 'admin_thumbnail' """The name of the spec to be used by the admin_thumbnail_view""" - - image_field = 'image' + + default_image_field = None + """The name of the image field property on the model. + Can be overridden on a per-spec basis by setting the image_field property on + the spec. If you don't define default_image_field on your IKOptions class, + it will be automatically populated with the name of the first ImageField the + model defines. + + """ + crop_horz_field = 'crop_horz' crop_vert_field = 'crop_vert' preprocessor_spec = None diff --git a/imagekit/specs.py b/imagekit/specs.py index c99b627..4467082 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -15,7 +15,7 @@ from django.core.files.base import ContentFile class ImageSpec(object): - image_field = 'original_image' # TODO: Get rid of this. It can be specified in a SpecDefaults nested class. + image_field = None processors = [] pre_cache = False quality = 70 @@ -27,7 +27,8 @@ class ImageSpec(object): self.__dict__.update(kwargs) def _get_imgfield(self, obj): - return getattr(obj, self.image_field) + field_name = getattr(self, 'image_field', None) or obj._ik.default_image_field + return getattr(obj, field_name) def process(self, image, obj): fmt = image.format From 8cfe485a5a5a97c553096ed19b9a36e8c9bdd9a4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 9 Sep 2011 23:23:27 -0400 Subject: [PATCH 11/68] Storage can be specified on a per-spec basis. If not defined on the spec, IKOptions.default_storage will be used. If that's not defined, it will fall back to the image field's storage. --- imagekit/options.py | 7 ++++++- imagekit/specs.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/imagekit/options.py b/imagekit/options.py index 132f5c6..ab636ec 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -20,6 +20,12 @@ class Options(object): """ + default_storage = None + """Storage used for specs that don't define their own storage explicitly. + If neither is specified, the image field's storage will be used. + + """ + crop_horz_field = 'crop_horz' crop_vert_field = 'crop_vert' preprocessor_spec = None @@ -28,7 +34,6 @@ class Options(object): cache_filename_fields = ['pk', ] cache_filename_format = "%(filename)s_%(specname)s.%(extension)s" specs = None - #storage = defaults to image_field.storage def __init__(self, opts): for key, value in opts.__dict__.iteritems(): diff --git a/imagekit/specs.py b/imagekit/specs.py index 4467082..ae92312 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -20,6 +20,7 @@ class ImageSpec(object): pre_cache = False quality = 70 increment_count = False + storage = None def __init__(self, processors=None, **kwargs): if processors: @@ -116,7 +117,9 @@ class Accessor(object): @property def _storage(self): - return getattr(self._obj._ik, 'storage', self._imgfield.storage) + return getattr(self.spec, 'storage', None) or \ + getattr(self._obj._ik, 'default_storage', None) or \ + self._imgfield.storage @property def url(self): From 82348d4931f0e651bce4281a5eeb93d27b368c4d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 18 Sep 2011 21:08:49 -0400 Subject: [PATCH 12/68] Changed how cache files are named. Removed the cache_dir, cache_filename_fields and cache_filename_format properties of IKOptions. While these were very powerful, I felt that it was unnecessarily confusing to have two properties (cache_dir and cache_filename_format) that determine the filename. The new cache_to property is modeled after ImageField's upload_to and behaves almost identically (the only exception being that a callable value receives different arguments). In addition, I felt that the interpolation of model properties provided by cache_filename_fields, though useful, would be better handled by a utility function outside of this library. --- README.rst | 7 ++++--- imagekit/options.py | 17 ++++++++++++++--- imagekit/specs.py | 42 +++++++++++++++++++++--------------------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/README.rst b/README.rst index 9ecde9f..abf2e43 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,8 @@ Create an ImageModel subclass and add specs to it. original_image = models.ImageField(upload_to='photos') num_views = models.PositiveIntegerField(editable=False, default=0) - thumbnail_image = ImageSpec([Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)], quality=90, pre_cache=True, image_field='original_image', cache_dir='photos') - display = ImageSpec([Fit(600)], quality=90, increment_count=True, image_field='original_image', cache_dir='photos', save_count_as='num_views') + thumbnail_image = ImageSpec([Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)], quality=90, pre_cache=True, image_field='original_image', cache_to='cache/photos/thumbnails/') + display = ImageSpec([Fit(600)], quality=90, increment_count=True, image_field='original_image', cache_to='cache/photos/display/', save_count_as='num_views') Of course, you don't have to define your ImageSpecs inline if you don't want to: @@ -49,16 +49,17 @@ Of course, you don't have to define your ImageSpecs inline if you don't want to: class _BaseSpec(ImageSpec): quality = 90 image_field = 'original_image' - cache_dir = 'photos' class DisplaySpec(_BaseSpec): pre_cache = True increment_count = True save_count_as = 'num_views' processors = [Fit(600)] + cache_to = 'cache/photos/display/' class ThumbnailSpec(_BaseSpec): processors = [Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)] + cache_to = 'cache/photos/thumbnails/' # myapp/models.py diff --git a/imagekit/options.py b/imagekit/options.py index ab636ec..0bfadf2 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -1,6 +1,8 @@ # Imagekit options from imagekit import processors from imagekit.specs import ImageSpec +import os +import os.path class Options(object): @@ -26,13 +28,22 @@ class Options(object): """ + def default_cache_to(self, instance, path, specname, extension): + """Determines the filename to use for the transformed image. Can be + overridden on a per-spec basis by setting the cache_to property on the + spec, or on a per-model basis by defining default_cache_to on your own + IKOptions class. + + """ + filepath, basename = os.path.split(path) + filename = os.path.splitext(basename)[0] + new_name = '{0}_{1}.{2}'.format(filename, specname, extension) + return os.path.join(os.path.join('cache', filepath), new_name) + crop_horz_field = 'crop_horz' crop_vert_field = 'crop_vert' preprocessor_spec = None - cache_dir = 'cache' save_count_as = None - cache_filename_fields = ['pk', ] - cache_filename_format = "%(filename)s_%(specname)s.%(extension)s" specs = None def __init__(self, opts): diff --git a/imagekit/specs.py b/imagekit/specs.py index ae92312..7a15846 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -6,11 +6,13 @@ spec found. """ import os +import datetime from StringIO import StringIO from imagekit import processors from imagekit.lib import * from imagekit.utils import img_to_fobj from django.core.files.base import ContentFile +from django.utils.encoding import force_unicode, smart_str class ImageSpec(object): @@ -91,29 +93,27 @@ class Accessor(object): @property def name(self): - if self._imgfield.name: - filepath, basename = os.path.split(self._imgfield.name) - filename, extension = os.path.splitext(basename) - for processor in self.spec.processors: - if isinstance(processor, processors.Format): - extension = processor.extension - filename_format_dict = {'filename': filename, - 'specname': self.property_name, - 'extension': extension.lstrip('.')} - cache_filename_fields = self._obj._ik.cache_filename_fields - filename_format_dict.update(dict(zip( - cache_filename_fields, - [getattr(self._obj, field) for - field in cache_filename_fields]))) - cache_filename = self._obj._ik.cache_filename_format % \ - filename_format_dict + filename = self._imgfield.name + if filename: + cache_to = getattr(self.spec, 'cache_to', None) or \ + getattr(self._obj._ik, 'default_cache_to', None) - if callable(self._obj._ik.cache_dir): - return self._obj._ik.cache_dir(self._obj, filepath, - cache_filename) + if not cache_to: + raise Exception('No cache_to or default_cache_to value specified') + if callable(cache_to): + extension = os.path.splitext(filename)[1] + for processor in self.spec.processors: + if isinstance(processor, processors.Format): + extension = processor.extension + new_filename = force_unicode(datetime.datetime.now().strftime( \ + smart_str(cache_to(self._obj, self._imgfield.name, \ + self.property_name, extension.lstrip('.'))))) else: - return os.path.join(self._obj._ik.cache_dir, filepath, - cache_filename) + dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) + filename = os.path.normpath(os.path.basename(filename)) + new_filename = os.path.join(dir_name, filename) + + return new_filename @property def _storage(self): From 501d3c7ad3cf7f9ef1d7140bd41c733569e2d6bb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 20 Sep 2011 15:42:28 -0400 Subject: [PATCH 13/68] Now using contribute_to_class. By creating the Descriptor using contribute_to_class (instead of in ImageModelBase's __init__), we take the first step towards eliminating the need to extend ImageModel at all. --- imagekit/models.py | 3 +-- imagekit/specs.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index defaafb..76145c1 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -8,7 +8,7 @@ from django.db.models.signals import post_delete from django.utils.html import conditional_escape as escape from django.utils.translation import ugettext_lazy as _ -from imagekit.specs import ImageSpec, Descriptor +from imagekit.specs import ImageSpec from imagekit.lib import * from imagekit.options import Options from imagekit.utils import img_to_fobj @@ -46,7 +46,6 @@ class ImageModelBase(ModelBase): for k, v in attrs.items(): if isinstance(v, ImageSpec): - setattr(self, k, Descriptor(v, k)) specs.append(v) elif not default_image_field and isinstance(v, models.ImageField): default_image_field = k diff --git a/imagekit/specs.py b/imagekit/specs.py index 7a15846..769c23c 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -41,6 +41,9 @@ class ImageSpec(object): img.format = fmt return img, fmt + def contribute_to_class(self, cls, name): + setattr(cls, name, Descriptor(self, name)) + class Accessor(object): def __init__(self, obj, spec, property_name): From 544d5b874aea5afff38b7d26cf52e2a22a53b405 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 20 Sep 2011 19:30:29 -0400 Subject: [PATCH 14/68] Added AdminThumbnailView field. You're no longer restricted to just one, special-case admin thumbnail. Make as many as you want by adding AdminThumbnailView properties to your model and including them in your admin class's `list_display` tuple. You can also provide a custom template. Note that (because this change introduces templates to imagekit), imagekit is now required in INSTALLED_APPS. Ideally we could get this stuff out of the model, but we'll have to look into whether that's possible without making things really complicated. --- imagekit/fields.py | 44 +++++++++++++++++++ imagekit/models.py | 19 -------- imagekit/options.py | 3 -- .../templates/imagekit/admin/thumbnail.html | 3 ++ 4 files changed, 47 insertions(+), 22 deletions(-) create mode 100755 imagekit/fields.py create mode 100644 imagekit/templates/imagekit/admin/thumbnail.html diff --git a/imagekit/fields.py b/imagekit/fields.py new file mode 100755 index 0000000..acc1420 --- /dev/null +++ b/imagekit/fields.py @@ -0,0 +1,44 @@ +from django.utils.translation import ugettext_lazy as _ +from django.template.loader import render_to_string + + +class BoundAdminThumbnailView(object): + short_description = _('Thumbnail') + allow_tags = True + + def __init__(self, model_instance, image_field, template=None): + self.model_instance = model_instance + self.image_field = image_field + self.template = template + + def __unicode__(self): + thumbnail = getattr(self.model_instance, self.image_field, None) + + if not thumbnail: + raise Exception('The property {0} is not defined on {1}.'.format( + self.model_instance, self.image_field)) + + original_image = getattr(thumbnail, '_imgfield', None) or thumbnail + template = self.template or 'imagekit/admin/thumbnail.html' + + return render_to_string(template, { + 'model': self.model_instance, + 'thumbnail': thumbnail, + 'original_image': original_image, + }) + + +class AdminThumbnailView(object): + def __init__(self, image_field, template=None): + """ + Keyword arguments: + image_field -- the name of the ImageField or ImageSpec on the model to + use for the thumbnail. + template -- the template with which to render the thumbnail + + """ + self.image_field = image_field + self.template = template + + def __get__(self, obj, type=None): + return BoundAdminThumbnailView(obj, self.image_field, self.template) diff --git a/imagekit/models.py b/imagekit/models.py index 76145c1..3f557ea 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -67,28 +67,9 @@ class ImageModel(models.Model): """ __metaclass__ = ImageModelBase - admin_thumbnail = defaults.DjangoAdminThumbnail() - class Meta: abstract = True - def admin_thumbnail_view(self): - if not self._imgfields: - return None - prop = getattr(self, self._ik.admin_thumbnail_property, None) - if prop is None: - return 'The property "%s" has not been defined.' % \ - self._ik.admin_thumbnail_property - else: - if hasattr(self, 'get_absolute_url'): - return u'' % \ - (escape(self.get_absolute_url()), escape(prop.url)) - else: - return u'' % \ - (escape(self._get_imgfield(self).url), escape(prop.url)) - admin_thumbnail_view.short_description = _('Thumbnail') - admin_thumbnail_view.allow_tags = True - class IKOptions: pass diff --git a/imagekit/options.py b/imagekit/options.py index 0bfadf2..bd83c1d 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -10,9 +10,6 @@ class Options(object): """ - admin_thumbnail_property = 'admin_thumbnail' - """The name of the spec to be used by the admin_thumbnail_view""" - default_image_field = None """The name of the image field property on the model. Can be overridden on a per-spec basis by setting the image_field property on diff --git a/imagekit/templates/imagekit/admin/thumbnail.html b/imagekit/templates/imagekit/admin/thumbnail.html new file mode 100644 index 0000000..6531391 --- /dev/null +++ b/imagekit/templates/imagekit/admin/thumbnail.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 80c785f2e54bdfafd3f2b9d646675a42792ee136 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 20 Sep 2011 19:56:55 -0400 Subject: [PATCH 15/68] No need for this. --- imagekit/defaults.py | 23 ----------------------- imagekit/models.py | 1 - 2 files changed, 24 deletions(-) delete mode 100644 imagekit/defaults.py diff --git a/imagekit/defaults.py b/imagekit/defaults.py deleted file mode 100644 index bc197b3..0000000 --- a/imagekit/defaults.py +++ /dev/null @@ -1,23 +0,0 @@ -""" Default ImageKit configuration """ - -from imagekit.specs import ImageSpec -from imagekit import processors - -class ResizeThumbnail(processors.Crop): - width = 100 - height = 50 - -class EnhanceSmall(processors.Adjust): - contrast = 1.2 - sharpness = 1.1 - -class SampleReflection(processors.Reflection): - size = 0.5 - background_color = "#000000" - -class PNGFormat(processors.Format): - format = 'PNG' - extension = 'png' - -class DjangoAdminThumbnail(ImageSpec): - processors = [ResizeThumbnail(), EnhanceSmall(), SampleReflection(), PNGFormat()] diff --git a/imagekit/models.py b/imagekit/models.py index 3f557ea..3a19983 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -12,7 +12,6 @@ from imagekit.specs import ImageSpec from imagekit.lib import * from imagekit.options import Options from imagekit.utils import img_to_fobj -from imagekit import defaults # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) From a71b3ca337201a77fb2face9abdd0f970a5363d9 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 20 Sep 2011 21:04:35 -0400 Subject: [PATCH 16/68] Removed Format processor The Format processor was really a special case and didn't do any processing at all. Instead, ImageSpec just knew to look for it and responded accordingly. Therefore, it's been replaced with a `format` property on ImageSpec. This warranted a deeper look at how the format and extension were being deduced (when not explicitly provided); the results are documented in-code, though the goal was "no surprises." --- imagekit/processors.py | 8 ----- imagekit/specs.py | 71 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/imagekit/processors.py b/imagekit/processors.py index 26beaa5..5623402 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -35,14 +35,6 @@ class Adjust(ImageProcessor): return img, fmt -class Format(ImageProcessor): - format = 'JPEG' - extension = 'jpg' - - def process(self, img, fmt, obj, spec): - return img, self.format - - class Reflection(ImageProcessor): background_color = '#FFFFFF' size = 0.0 diff --git a/imagekit/specs.py b/imagekit/specs.py index 769c23c..bb339ca 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -8,7 +8,6 @@ spec found. import os import datetime from StringIO import StringIO -from imagekit import processors from imagekit.lib import * from imagekit.utils import img_to_fobj from django.core.files.base import ContentFile @@ -23,6 +22,26 @@ class ImageSpec(object): quality = 70 increment_count = False storage = None + format = None + + cache_to = None + """Specifies the filename to use when saving the image cache file. This is + modeled after ImageField's `upload_to` and can accept either a string + (that specifies a directory) or a callable (that returns a filepath). + Callable values should accept the following arguments: + + instance -- the model instance this spec belongs to + path -- the path of the original image + specname -- the property name that the spec is bound to on the model instance + extension -- a recommended extension. If the format of the spec is set + explicitly, this suggestion will be based on that format. if not, + the extension of the original file will be passed. You do not have + to use this extension, it's only a recommendation. + + If you have not explicitly set a format on your ImageSpec, the extension of + the path returned by this function will be used to infer one. + + """ def __init__(self, processors=None, **kwargs): if processors: @@ -38,8 +57,9 @@ class ImageSpec(object): img = image.copy() for proc in self.processors: img, fmt = proc.process(img, fmt, obj, self) - img.format = fmt - return img, fmt + format = self.format or fmt + img.format = format + return img, format def contribute_to_class(self, cls, name): setattr(cls, name, Descriptor(self, name)) @@ -53,8 +73,24 @@ class Accessor(object): self.spec = spec self.property_name = property_name + @property + def _format(self): + """The format used to save the cache file. If the format is set + explicitly on the ImageSpec, that format will be used. Otherwise, the + format will be inferred from the extension of the cache filename (see + the `name` property). + + """ + format = self.spec.format + if not format: + # Get the real (not suggested) extension. + extension = os.path.splitext(self.name)[1].lower() + # Try to guess the format from the extension. + format = Image.EXTENSION.get(extension) + return format or self._img.format or 'JPEG' + def _get_imgfile(self): - format = self._img.format or 'JPEG' + format = self._format if format != 'JPEG': imgfile = img_to_fobj(self._img, format) else: @@ -94,8 +130,29 @@ class Accessor(object): if self._imgfield: return self._storage.exists(self.name) + @property + def _suggested_extension(self): + if self.spec.format: + # Try to look up an extension by the format + extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ + if v == self.spec.format.upper()] + else: + extensions = [] + original_extension = os.path.splitext(self._imgfield.name)[1].lstrip('.') + if not extensions or original_extension.lower() in extensions: + # If the original extension matches the format, use it. + extension = original_extension + else: + extension = extensions[0] + return extension + @property def name(self): + """ + Specifies the filename that the cached image will use. The user can + control this by providing a `cache_to` method to the ImageSpec. + + """ filename = self._imgfield.name if filename: cache_to = getattr(self.spec, 'cache_to', None) or \ @@ -104,13 +161,9 @@ class Accessor(object): if not cache_to: raise Exception('No cache_to or default_cache_to value specified') if callable(cache_to): - extension = os.path.splitext(filename)[1] - for processor in self.spec.processors: - if isinstance(processor, processors.Format): - extension = processor.extension new_filename = force_unicode(datetime.datetime.now().strftime( \ smart_str(cache_to(self._obj, self._imgfield.name, \ - self.property_name, extension.lstrip('.'))))) + self.property_name, self._suggested_extension)))) else: dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) filename = os.path.normpath(os.path.basename(filename)) From 2770be23eab80087be0d2d3e6b1cf7eec541ab36 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 09:39:06 -0400 Subject: [PATCH 17/68] Naming & implementation of bound fields are more consistent. Bound fields are now named as such (BoundBlah), extend their unbound counterparts, and their constructors accept an unbound instance. --- imagekit/fields.py | 40 ++++++++++++++++++++++------------------ imagekit/specs.py | 38 ++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index acc1420..9ec039d 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -2,15 +2,31 @@ from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string -class BoundAdminThumbnailView(object): +class AdminThumbnailView(object): short_description = _('Thumbnail') allow_tags = True - def __init__(self, model_instance, image_field, template=None): - self.model_instance = model_instance + def __init__(self, image_field, template=None): + """ + Keyword arguments: + image_field -- the name of the ImageField or ImageSpec on the model to + use for the thumbnail. + template -- the template with which to render the thumbnail + + """ self.image_field = image_field self.template = template + def __get__(self, obj, type=None): + return BoundAdminThumbnailView(obj, self) + + +class BoundAdminThumbnailView(AdminThumbnailView): + def __init__(self, model_instance, unbound_field): + super(BoundAdminThumbnailView, self).__init__(unbound_field.image_field, + unbound_field.template) + self.model_instance = model_instance + def __unicode__(self): thumbnail = getattr(self.model_instance, self.image_field, None) @@ -26,19 +42,7 @@ class BoundAdminThumbnailView(object): 'thumbnail': thumbnail, 'original_image': original_image, }) - - -class AdminThumbnailView(object): - def __init__(self, image_field, template=None): - """ - Keyword arguments: - image_field -- the name of the ImageField or ImageSpec on the model to - use for the thumbnail. - template -- the template with which to render the thumbnail - - """ - self.image_field = image_field - self.template = template - + def __get__(self, obj, type=None): - return BoundAdminThumbnailView(obj, self.image_field, self.template) + """Override AdminThumbnailView's implementation.""" + return self diff --git a/imagekit/specs.py b/imagekit/specs.py index bb339ca..4f30a83 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -62,15 +62,21 @@ class ImageSpec(object): return img, format def contribute_to_class(self, cls, name): - setattr(cls, name, Descriptor(self, name)) + setattr(cls, name, _ImageSpecDescriptor(self, name)) -class Accessor(object): - def __init__(self, obj, spec, property_name): +class BoundImageSpec(ImageSpec): + def __init__(self, obj, unbound_field, property_name): + super(BoundImageSpec, self).__init__(unbound_field.processors, + image_field=unbound_field.image_field, + pre_cache=unbound_field.pre_cache, + quality=unbound_field.quality, + increment_count=unbound_field.increment_count, + storage=unbound_field.storage, format=unbound_field.format, + cache_to=unbound_field.cache_to) self._img = None self._fmt = None self._obj = obj - self.spec = spec self.property_name = property_name @property @@ -81,7 +87,7 @@ class Accessor(object): the `name` property). """ - format = self.spec.format + format = self.format if not format: # Get the real (not suggested) extension. extension = os.path.splitext(self.name)[1].lower() @@ -95,13 +101,13 @@ class Accessor(object): imgfile = img_to_fobj(self._img, format) else: imgfile = img_to_fobj(self._img, format, - quality=int(self.spec.quality), + quality=int(self.quality), optimize=True) return imgfile @property def _imgfield(self): - return self.spec._get_imgfield(self._obj) + return self._get_imgfield(self._obj) def _create(self): if self._imgfield: @@ -114,7 +120,7 @@ class Accessor(object): return fp.seek(0) fp = StringIO(fp.read()) - self._img, self._fmt = self.spec.process(Image.open(fp), self._obj) + self._img, self._fmt = self.process(Image.open(fp), self._obj) # save the new image to the cache content = ContentFile(self._get_imgfile().read()) self._storage.save(self.name, content) @@ -132,10 +138,10 @@ class Accessor(object): @property def _suggested_extension(self): - if self.spec.format: + if self.format: # Try to look up an extension by the format extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ - if v == self.spec.format.upper()] + if v == self.format.upper()] else: extensions = [] original_extension = os.path.splitext(self._imgfield.name)[1].lstrip('.') @@ -155,7 +161,7 @@ class Accessor(object): """ filename = self._imgfield.name if filename: - cache_to = getattr(self.spec, 'cache_to', None) or \ + cache_to = self.cache_to or \ getattr(self._obj._ik, 'default_cache_to', None) if not cache_to: @@ -173,15 +179,15 @@ class Accessor(object): @property def _storage(self): - return getattr(self.spec, 'storage', None) or \ + return self.storage or \ getattr(self._obj._ik, 'default_storage', None) or \ self._imgfield.storage @property def url(self): - if not self.spec.pre_cache: + if not self.pre_cache: self._create() - if self.spec.increment_count: + if self.increment_count: fieldname = self._obj._ik.save_count_as if fieldname is not None: current_count = getattr(self._obj, fieldname) @@ -211,10 +217,10 @@ class Accessor(object): return self.image.size[1] -class Descriptor(object): +class _ImageSpecDescriptor(object): def __init__(self, spec, property_name): self._property_name = property_name self._spec = spec def __get__(self, obj, type=None): - return Accessor(obj, self._spec, self._property_name) + return BoundImageSpec(obj, self._spec, self._property_name) From 34e475885b9217a5a38aed09a2e615779cc4a297 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 09:52:38 -0400 Subject: [PATCH 18/68] Unbound fields are accessible from class. --- imagekit/fields.py | 9 ++++++--- imagekit/specs.py | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 9ec039d..2571742 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -17,8 +17,11 @@ class AdminThumbnailView(object): self.image_field = image_field self.template = template - def __get__(self, obj, type=None): - return BoundAdminThumbnailView(obj, self) + def __get__(self, instance, owner): + if instance is None: + return self + else: + return BoundAdminThumbnailView(instance, self) class BoundAdminThumbnailView(AdminThumbnailView): @@ -43,6 +46,6 @@ class BoundAdminThumbnailView(AdminThumbnailView): 'original_image': original_image, }) - def __get__(self, obj, type=None): + def __get__(self, instance, owner): """Override AdminThumbnailView's implementation.""" return self diff --git a/imagekit/specs.py b/imagekit/specs.py index 4f30a83..6cb4366 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -222,5 +222,8 @@ class _ImageSpecDescriptor(object): self._property_name = property_name self._spec = spec - def __get__(self, obj, type=None): - return BoundImageSpec(obj, self._spec, self._property_name) + def __get__(self, instance, owner): + if instance is None: + return self._spec + else: + return BoundImageSpec(instance, self._spec, self._property_name) From b1c5432310efa7a707acda9399fba289ab4f6929 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Tue, 20 Sep 2011 20:19:19 -0400 Subject: [PATCH 19/68] Implemented post_save and post_delete handlers for ImageSpecs. Removed the save and clear_cache methods from ImageModel (along with helpers). Now, whenever an ImageSpec is contributed to a model, handlers are created for the post_save and post_delete signals. The post_save handler does the work of running the ImageSpec processors and caching the resulting file, while the post_delete handler does the work cleaning up the cached files. --- imagekit/models.py | 62 ---------------------------------------------- imagekit/specs.py | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 62 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 3a19983..6d75ff9 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -72,68 +72,6 @@ class ImageModel(models.Model): class IKOptions: pass - def _clear_cache(self): - for spec in self._ik.specs: - prop = getattr(self, spec.name()) - prop._delete() - - def _pre_cache(self): - for spec in self._ik.specs: - if spec.pre_cache: - prop = getattr(self, spec.name()) - prop._create() - - def save_image(self, name, image, save=True, replace=True): - imgfields = self._imgfields - for imgfield in imgfields: - if imgfield and replace: - imgfield.delete(save=False) - if hasattr(image, 'read'): - data = image.read() - else: - data = image - content = ContentFile(data) - imgfield.save(name, content, save) - @property def _imgfields(self): return set([spec._get_imgfield(self) for spec in self._ik.specs]) - - def save(self, clear_cache=True, *args, **kwargs): - super(ImageModel, self).save(*args, **kwargs) - - is_new_object = self._get_pk_val() is None - if is_new_object: - clear_cache = False - - imgfields = self._imgfields - for imgfield in imgfields: - spec = self._ik.preprocessor_spec - if spec is not None: - newfile = imgfield.storage.open(str(imgfield)) - img = Image.open(newfile) - img, format = spec.process(img, self) - if format != 'JPEG': - imgfile = img_to_fobj(img, format) - else: - imgfile = img_to_fobj(img, format, - quality=int(spec.quality), - optimize=True) - content = ContentFile(imgfile.read()) - newfile.close() - name = str(imgfield) - imgfield.storage.delete(name) - imgfield.storage.save(name, content) - if self._imgfields: - if clear_cache: - self._clear_cache() - self._pre_cache() - - def clear_cache(self, **kwargs): - 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() - - -post_delete.connect(ImageModel.clear_cache, sender=ImageModel) - - diff --git a/imagekit/specs.py b/imagekit/specs.py index 6cb4366..8477650 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -12,6 +12,7 @@ from imagekit.lib import * from imagekit.utils import img_to_fobj from django.core.files.base import ContentFile from django.utils.encoding import force_unicode, smart_str +from django.db.models.signals import post_save, post_delete class ImageSpec(object): @@ -63,6 +64,16 @@ class ImageSpec(object): def contribute_to_class(self, cls, name): setattr(cls, name, _ImageSpecDescriptor(self, name)) + # Connect to the signals only once for this class's attribute. + uid = '%s.%s_%s' % (cls.__module__, cls.__name__, name) + post_save.connect(_create_post_save_handler(name), + sender=cls, + weak=False, + dispatch_uid='%s_save' % uid) + post_delete.connect(_create_post_delete_handler(name, uid), + sender=cls, + weak=False, + dispatch_uid='%s.delete' % uid) class BoundImageSpec(ImageSpec): @@ -227,3 +238,41 @@ class _ImageSpecDescriptor(object): return self._spec else: return BoundImageSpec(instance, self._spec, self._property_name) + + +def _create_post_save_handler(accessor_name): + def handler(sender, instance=None, created=False, raw=False, **kwargs): + if raw: + return + accessor = getattr(instance, accessor_name) + spec = accessor.spec + imgfield = spec._get_imgfield(instance) + newfile = imgfield.storage.open(str(imgfield)) + img = Image.open(newfile) + img, format = spec.process(img, instance) + if format != 'JPEG': + imgfile = img_to_fobj(img, format) + else: + imgfile = img_to_fobj(img, format, + quality=int(spec.quality), + optimize=True) + content = ContentFile(imgfile.read()) + newfile.close() + name = str(imgfield) + imgfield.storage.delete(name) + imgfield.storage.save(name, content) + if not created: + accessor._delete() + accessor._create() + return handler + + +def _create_post_delete_handler(accessor_name, uid): + def handler(sender, instance=None, **kwargs): + assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) + accessor = getattr(instance, accessor_name) + accessor._delete() + post_save.disconnect(dispatch_uid='%s_save' % uid) + post_delete.disconnect(dispatch_uid='%s.delete' % uid) + + return handler From fe2fb844af76f4e3c986d04aa4ac34f7404df910 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Wed, 21 Sep 2011 10:37:50 -0400 Subject: [PATCH 20/68] Changed the way post_save and post_delete signals are being handled. One handler is created per model instead of per bound image spec. This cuts down on the number of handlers created, and also offloads the policing of the handlers in memory to the signal framework. Since they are no longer being created per spec, the handlers can be weakly referenced. --- imagekit/specs.py | 57 ++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/imagekit/specs.py b/imagekit/specs.py index 8477650..271132d 100644 --- a/imagekit/specs.py +++ b/imagekit/specs.py @@ -64,15 +64,13 @@ class ImageSpec(object): def contribute_to_class(self, cls, name): setattr(cls, name, _ImageSpecDescriptor(self, name)) - # Connect to the signals only once for this class's attribute. - uid = '%s.%s_%s' % (cls.__module__, cls.__name__, name) - post_save.connect(_create_post_save_handler(name), + # Connect to the signals only once for this class. + uid = '%s.%s' % (cls.__module__, cls.__name__) + post_save.connect(_post_save_handler, sender=cls, - weak=False, dispatch_uid='%s_save' % uid) - post_delete.connect(_create_post_delete_handler(name, uid), + post_delete.connect(_post_delete_handler, sender=cls, - weak=False, dispatch_uid='%s.delete' % uid) @@ -240,21 +238,21 @@ class _ImageSpecDescriptor(object): return BoundImageSpec(instance, self._spec, self._property_name) -def _create_post_save_handler(accessor_name): - def handler(sender, instance=None, created=False, raw=False, **kwargs): - if raw: - return - accessor = getattr(instance, accessor_name) - spec = accessor.spec - imgfield = spec._get_imgfield(instance) +def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): + if raw: + return + bound_specs = _get_bound_specs(instance) + for bound_spec in bound_specs: + name = bound_spec.property_name + imgfield = bound_spec._get_imgfield(instance) newfile = imgfield.storage.open(str(imgfield)) img = Image.open(newfile) - img, format = spec.process(img, instance) + img, format = bound_spec.process(img, instance) if format != 'JPEG': imgfile = img_to_fobj(img, format) else: imgfile = img_to_fobj(img, format, - quality=int(spec.quality), + quality=int(bound_spec.quality), optimize=True) content = ContentFile(imgfile.read()) newfile.close() @@ -262,17 +260,24 @@ def _create_post_save_handler(accessor_name): imgfield.storage.delete(name) imgfield.storage.save(name, content) if not created: - accessor._delete() - accessor._create() - return handler + bound_spec._delete() + bound_spec._create() -def _create_post_delete_handler(accessor_name, uid): - def handler(sender, instance=None, **kwargs): - assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) - accessor = getattr(instance, accessor_name) - accessor._delete() - post_save.disconnect(dispatch_uid='%s_save' % uid) - post_delete.disconnect(dispatch_uid='%s.delete' % uid) +def _post_delete_handler(sender, instance=None, **kwargs): + assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) + bound_specs = _get_bound_specs(instance) + for bound_spec in bound_specs: + bound_spec._delete() - return handler + +def _get_bound_specs(instance): + bound_specs = [] + for key in dir(instance): + try: + value = getattr(instance, key) + except AttributeError: + continue + if isinstance(value, BoundImageSpec): + bound_specs.append(value) + return bound_specs From b5616d2f7502d36249891b89d52ac82ca0433743 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 11:37:29 -0400 Subject: [PATCH 21/68] Move ImageSpec to fields module. --- imagekit/fields.py | 276 +++++++++++++++++++++++ imagekit/management/commands/ikflush.py | 2 +- imagekit/models.py | 2 +- imagekit/options.py | 2 - imagekit/specs.py | 283 ------------------------ imagekit/tests.py | 2 +- 6 files changed, 279 insertions(+), 288 deletions(-) delete mode 100644 imagekit/specs.py diff --git a/imagekit/fields.py b/imagekit/fields.py index 2571742..970aee7 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -1,7 +1,283 @@ +import os +import datetime +from StringIO import StringIO +from imagekit.lib import * +from imagekit.utils import img_to_fobj +from django.core.files.base import ContentFile +from django.utils.encoding import force_unicode, smart_str +from django.db.models.signals import post_save, post_delete from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string +class ImageSpec(object): + + image_field = None + processors = [] + pre_cache = False + quality = 70 + increment_count = False + storage = None + format = None + + cache_to = None + """Specifies the filename to use when saving the image cache file. This is + modeled after ImageField's `upload_to` and can accept either a string + (that specifies a directory) or a callable (that returns a filepath). + Callable values should accept the following arguments: + + instance -- the model instance this spec belongs to + path -- the path of the original image + specname -- the property name that the spec is bound to on the model instance + extension -- a recommended extension. If the format of the spec is set + explicitly, this suggestion will be based on that format. if not, + the extension of the original file will be passed. You do not have + to use this extension, it's only a recommendation. + + If you have not explicitly set a format on your ImageSpec, the extension of + the path returned by this function will be used to infer one. + + """ + + def __init__(self, processors=None, **kwargs): + if processors: + self.processors = processors + self.__dict__.update(kwargs) + + def _get_imgfield(self, obj): + field_name = getattr(self, 'image_field', None) or obj._ik.default_image_field + return getattr(obj, field_name) + + def process(self, image, obj): + fmt = image.format + img = image.copy() + for proc in self.processors: + img, fmt = proc.process(img, fmt, obj, self) + format = self.format or fmt + img.format = format + return img, format + + def contribute_to_class(self, cls, name): + setattr(cls, name, _ImageSpecDescriptor(self, name)) + # Connect to the signals only once for this class. + uid = '%s.%s' % (cls.__module__, cls.__name__) + post_save.connect(_post_save_handler, + sender=cls, + dispatch_uid='%s_save' % uid) + post_delete.connect(_post_delete_handler, + sender=cls, + dispatch_uid='%s.delete' % uid) + + +class BoundImageSpec(ImageSpec): + def __init__(self, obj, unbound_field, property_name): + super(BoundImageSpec, self).__init__(unbound_field.processors, + image_field=unbound_field.image_field, + pre_cache=unbound_field.pre_cache, + quality=unbound_field.quality, + increment_count=unbound_field.increment_count, + storage=unbound_field.storage, format=unbound_field.format, + cache_to=unbound_field.cache_to) + self._img = None + self._fmt = None + self._obj = obj + self.property_name = property_name + + @property + def _format(self): + """The format used to save the cache file. If the format is set + explicitly on the ImageSpec, that format will be used. Otherwise, the + format will be inferred from the extension of the cache filename (see + the `name` property). + + """ + format = self.format + if not format: + # Get the real (not suggested) extension. + extension = os.path.splitext(self.name)[1].lower() + # Try to guess the format from the extension. + format = Image.EXTENSION.get(extension) + return format or self._img.format or 'JPEG' + + def _get_imgfile(self): + format = self._format + if format != 'JPEG': + imgfile = img_to_fobj(self._img, format) + else: + imgfile = img_to_fobj(self._img, format, + quality=int(self.quality), + optimize=True) + return imgfile + + @property + def _imgfield(self): + return self._get_imgfield(self._obj) + + def _create(self): + if self._imgfield: + if self._exists(): + return + # process the original image file + try: + fp = self._imgfield.storage.open(self._imgfield.name) + except IOError: + return + fp.seek(0) + fp = StringIO(fp.read()) + self._img, self._fmt = self.process(Image.open(fp), self._obj) + # save the new image to the cache + content = ContentFile(self._get_imgfile().read()) + self._storage.save(self.name, content) + + def _delete(self): + if self._imgfield: + try: + self._storage.delete(self.name) + except (NotImplementedError, IOError): + return + + def _exists(self): + if self._imgfield: + return self._storage.exists(self.name) + + @property + def _suggested_extension(self): + if self.format: + # Try to look up an extension by the format + extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ + if v == self.format.upper()] + else: + extensions = [] + original_extension = os.path.splitext(self._imgfield.name)[1].lstrip('.') + if not extensions or original_extension.lower() in extensions: + # If the original extension matches the format, use it. + extension = original_extension + else: + extension = extensions[0] + return extension + + @property + def name(self): + """ + Specifies the filename that the cached image will use. The user can + control this by providing a `cache_to` method to the ImageSpec. + + """ + filename = self._imgfield.name + if filename: + cache_to = self.cache_to or \ + getattr(self._obj._ik, 'default_cache_to', None) + + if not cache_to: + raise Exception('No cache_to or default_cache_to value specified') + if callable(cache_to): + new_filename = force_unicode(datetime.datetime.now().strftime( \ + smart_str(cache_to(self._obj, self._imgfield.name, \ + self.property_name, self._suggested_extension)))) + else: + dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) + filename = os.path.normpath(os.path.basename(filename)) + new_filename = os.path.join(dir_name, filename) + + return new_filename + + @property + def _storage(self): + return self.storage or \ + getattr(self._obj._ik, 'default_storage', None) or \ + self._imgfield.storage + + @property + def url(self): + if not self.pre_cache: + self._create() + if self.increment_count: + fieldname = self._obj._ik.save_count_as + if fieldname is not None: + current_count = getattr(self._obj, fieldname) + setattr(self._obj, fieldname, current_count + 1) + self._obj.save(clear_cache=False) + return self._storage.url(self.name) + + @property + def file(self): + self._create() + return self._storage.open(self.name) + + @property + def image(self): + if not self._img: + self._create() + if not self._img: + self._img = Image.open(self.file) + return self._img + + @property + def width(self): + return self.image.size[0] + + @property + def height(self): + return self.image.size[1] + + +class _ImageSpecDescriptor(object): + def __init__(self, spec, property_name): + self._property_name = property_name + self._spec = spec + + def __get__(self, instance, owner): + if instance is None: + return self._spec + else: + return BoundImageSpec(instance, self._spec, self._property_name) + + +def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): + if raw: + return + bound_specs = _get_bound_specs(instance) + for bound_spec in bound_specs: + name = bound_spec.property_name + imgfield = bound_spec._get_imgfield(instance) + newfile = imgfield.storage.open(str(imgfield)) + img = Image.open(newfile) + img, format = bound_spec.process(img, instance) + if format != 'JPEG': + imgfile = img_to_fobj(img, format) + else: + imgfile = img_to_fobj(img, format, + quality=int(bound_spec.quality), + optimize=True) + content = ContentFile(imgfile.read()) + newfile.close() + name = str(imgfield) + imgfield.storage.delete(name) + imgfield.storage.save(name, content) + if not created: + bound_spec._delete() + bound_spec._create() + + +def _post_delete_handler(sender, instance=None, **kwargs): + assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) + bound_specs = _get_bound_specs(instance) + for bound_spec in bound_specs: + bound_spec._delete() + + +def _get_bound_specs(instance): + bound_specs = [] + for key in dir(instance): + try: + value = getattr(instance, key) + except AttributeError: + continue + if isinstance(value, BoundImageSpec): + bound_specs.append(value) + return bound_specs + + class AdminThumbnailView(object): short_description = _('Thumbnail') allow_tags = True diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index ffda42b..ec1fb03 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -2,7 +2,7 @@ from django.db.models.loading import cache from django.core.management.base import BaseCommand, CommandError from optparse import make_option from imagekit.models import ImageModel -from imagekit.specs import ImageSpec +from imagekit.fields import ImageSpec class Command(BaseCommand): diff --git a/imagekit/models.py b/imagekit/models.py index 6d75ff9..8e22975 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -8,7 +8,7 @@ from django.db.models.signals import post_delete from django.utils.html import conditional_escape as escape from django.utils.translation import ugettext_lazy as _ -from imagekit.specs import ImageSpec +from imagekit.fields import ImageSpec from imagekit.lib import * from imagekit.options import Options from imagekit.utils import img_to_fobj diff --git a/imagekit/options.py b/imagekit/options.py index bd83c1d..f23683e 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -1,6 +1,4 @@ # Imagekit options -from imagekit import processors -from imagekit.specs import ImageSpec import os import os.path diff --git a/imagekit/specs.py b/imagekit/specs.py deleted file mode 100644 index 271132d..0000000 --- a/imagekit/specs.py +++ /dev/null @@ -1,283 +0,0 @@ -""" ImageKit image specifications - -All imagekit specifications must inherit from the ImageSpec class. Models -inheriting from ImageModel will be modified with a descriptor/accessor for each -spec found. - -""" -import os -import datetime -from StringIO import StringIO -from imagekit.lib import * -from imagekit.utils import img_to_fobj -from django.core.files.base import ContentFile -from django.utils.encoding import force_unicode, smart_str -from django.db.models.signals import post_save, post_delete - - -class ImageSpec(object): - - image_field = None - processors = [] - pre_cache = False - quality = 70 - increment_count = False - storage = None - format = None - - cache_to = None - """Specifies the filename to use when saving the image cache file. This is - modeled after ImageField's `upload_to` and can accept either a string - (that specifies a directory) or a callable (that returns a filepath). - Callable values should accept the following arguments: - - instance -- the model instance this spec belongs to - path -- the path of the original image - specname -- the property name that the spec is bound to on the model instance - extension -- a recommended extension. If the format of the spec is set - explicitly, this suggestion will be based on that format. if not, - the extension of the original file will be passed. You do not have - to use this extension, it's only a recommendation. - - If you have not explicitly set a format on your ImageSpec, the extension of - the path returned by this function will be used to infer one. - - """ - - def __init__(self, processors=None, **kwargs): - if processors: - self.processors = processors - self.__dict__.update(kwargs) - - def _get_imgfield(self, obj): - field_name = getattr(self, 'image_field', None) or obj._ik.default_image_field - return getattr(obj, field_name) - - def process(self, image, obj): - fmt = image.format - img = image.copy() - for proc in self.processors: - img, fmt = proc.process(img, fmt, obj, self) - format = self.format or fmt - img.format = format - return img, format - - def contribute_to_class(self, cls, name): - setattr(cls, name, _ImageSpecDescriptor(self, name)) - # Connect to the signals only once for this class. - uid = '%s.%s' % (cls.__module__, cls.__name__) - post_save.connect(_post_save_handler, - sender=cls, - dispatch_uid='%s_save' % uid) - post_delete.connect(_post_delete_handler, - sender=cls, - dispatch_uid='%s.delete' % uid) - - -class BoundImageSpec(ImageSpec): - def __init__(self, obj, unbound_field, property_name): - super(BoundImageSpec, self).__init__(unbound_field.processors, - image_field=unbound_field.image_field, - pre_cache=unbound_field.pre_cache, - quality=unbound_field.quality, - increment_count=unbound_field.increment_count, - storage=unbound_field.storage, format=unbound_field.format, - cache_to=unbound_field.cache_to) - self._img = None - self._fmt = None - self._obj = obj - self.property_name = property_name - - @property - def _format(self): - """The format used to save the cache file. If the format is set - explicitly on the ImageSpec, that format will be used. Otherwise, the - format will be inferred from the extension of the cache filename (see - the `name` property). - - """ - format = self.format - if not format: - # Get the real (not suggested) extension. - extension = os.path.splitext(self.name)[1].lower() - # Try to guess the format from the extension. - format = Image.EXTENSION.get(extension) - return format or self._img.format or 'JPEG' - - def _get_imgfile(self): - format = self._format - if format != 'JPEG': - imgfile = img_to_fobj(self._img, format) - else: - imgfile = img_to_fobj(self._img, format, - quality=int(self.quality), - optimize=True) - return imgfile - - @property - def _imgfield(self): - return self._get_imgfield(self._obj) - - def _create(self): - if self._imgfield: - if self._exists(): - return - # process the original image file - try: - fp = self._imgfield.storage.open(self._imgfield.name) - except IOError: - return - fp.seek(0) - fp = StringIO(fp.read()) - self._img, self._fmt = self.process(Image.open(fp), self._obj) - # save the new image to the cache - content = ContentFile(self._get_imgfile().read()) - self._storage.save(self.name, content) - - def _delete(self): - if self._imgfield: - try: - self._storage.delete(self.name) - except (NotImplementedError, IOError): - return - - def _exists(self): - if self._imgfield: - return self._storage.exists(self.name) - - @property - def _suggested_extension(self): - if self.format: - # Try to look up an extension by the format - extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ - if v == self.format.upper()] - else: - extensions = [] - original_extension = os.path.splitext(self._imgfield.name)[1].lstrip('.') - if not extensions or original_extension.lower() in extensions: - # If the original extension matches the format, use it. - extension = original_extension - else: - extension = extensions[0] - return extension - - @property - def name(self): - """ - Specifies the filename that the cached image will use. The user can - control this by providing a `cache_to` method to the ImageSpec. - - """ - filename = self._imgfield.name - if filename: - cache_to = self.cache_to or \ - getattr(self._obj._ik, 'default_cache_to', None) - - if not cache_to: - raise Exception('No cache_to or default_cache_to value specified') - if callable(cache_to): - new_filename = force_unicode(datetime.datetime.now().strftime( \ - smart_str(cache_to(self._obj, self._imgfield.name, \ - self.property_name, self._suggested_extension)))) - else: - dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) - filename = os.path.normpath(os.path.basename(filename)) - new_filename = os.path.join(dir_name, filename) - - return new_filename - - @property - def _storage(self): - return self.storage or \ - getattr(self._obj._ik, 'default_storage', None) or \ - self._imgfield.storage - - @property - def url(self): - if not self.pre_cache: - self._create() - if self.increment_count: - fieldname = self._obj._ik.save_count_as - if fieldname is not None: - current_count = getattr(self._obj, fieldname) - setattr(self._obj, fieldname, current_count + 1) - self._obj.save(clear_cache=False) - return self._storage.url(self.name) - - @property - def file(self): - self._create() - return self._storage.open(self.name) - - @property - def image(self): - if not self._img: - self._create() - if not self._img: - self._img = Image.open(self.file) - return self._img - - @property - def width(self): - return self.image.size[0] - - @property - def height(self): - return self.image.size[1] - - -class _ImageSpecDescriptor(object): - def __init__(self, spec, property_name): - self._property_name = property_name - self._spec = spec - - def __get__(self, instance, owner): - if instance is None: - return self._spec - else: - return BoundImageSpec(instance, self._spec, self._property_name) - - -def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): - if raw: - return - bound_specs = _get_bound_specs(instance) - for bound_spec in bound_specs: - name = bound_spec.property_name - imgfield = bound_spec._get_imgfield(instance) - newfile = imgfield.storage.open(str(imgfield)) - img = Image.open(newfile) - img, format = bound_spec.process(img, instance) - if format != 'JPEG': - imgfile = img_to_fobj(img, format) - else: - imgfile = img_to_fobj(img, format, - quality=int(bound_spec.quality), - optimize=True) - content = ContentFile(imgfile.read()) - newfile.close() - name = str(imgfield) - imgfield.storage.delete(name) - imgfield.storage.save(name, content) - if not created: - bound_spec._delete() - bound_spec._create() - - -def _post_delete_handler(sender, instance=None, **kwargs): - assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) - bound_specs = _get_bound_specs(instance) - for bound_spec in bound_specs: - bound_spec._delete() - - -def _get_bound_specs(instance): - bound_specs = [] - for key in dir(instance): - try: - value = getattr(instance, key) - except AttributeError: - continue - if isinstance(value, BoundImageSpec): - bound_specs.append(value) - return bound_specs diff --git a/imagekit/tests.py b/imagekit/tests.py index eb2ec6c..92025d1 100644 --- a/imagekit/tests.py +++ b/imagekit/tests.py @@ -8,7 +8,7 @@ from django.test import TestCase from imagekit import processors from imagekit.models import ImageModel -from imagekit.specs import ImageSpec +from imagekit.fields import ImageSpec from imagekit.lib import Image From e4c4fe02b35cbf1aceb19760d220d1bb7a46e277 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 18:24:02 -0400 Subject: [PATCH 22/68] Separated `Crop.process()` and `Fit.process()` They didn't have enough in common to warrant them being branches of the same method. --- imagekit/processors.py | 97 +++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/imagekit/processors.py b/imagekit/processors.py index 5623402..a4ae9a0 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -81,69 +81,70 @@ class _Resize(ImageProcessor): width = None height = None - crop = False - upscale = False - def __init__(self, width=None, height=None, crop=None, upscale=None): + def __init__(self, width=None, height=None): if width is not None: self.width = width if height is not None: self.height = height - if crop is not None: - self.crop = crop + + def process(self, img, fmt, obj, spec): + raise NotImplementedError('process must be overridden by subclasses.') + + +class Crop(_Resize): + def __init__(self, width=None, height=None): + super(Crop, self).__init__(width, height) + + def process(self, img, fmt, obj, spec): + cur_width, cur_height = img.size + crop_horz = getattr(obj, obj._ik.crop_horz_field, 1) + crop_vert = getattr(obj, obj._ik.crop_vert_field, 1) + ratio = max(float(self.width)/cur_width, float(self.height)/cur_height) + resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) + crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) + x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) + box_left, box_right = { + 0: (0, self.width), + 1: (int(x_diff), int(x_diff + self.width)), + 2: (int(crop_x), int(resize_x)), + }[crop_horz] + box_upper, box_lower = { + 0: (0, self.height), + 1: (int(y_diff), int(y_diff + self.height)), + 2: (int(crop_y), int(resize_y)), + }[crop_vert] + box = (box_left, box_upper, box_right, box_lower) + img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) + return img, fmt + + +class Fit(_Resize): + def __init__(self, width=None, height=None, upscale=None): + super(Fit, self).__init__(width, height) if upscale is not None: self.upscale = upscale def process(self, img, fmt, obj, spec): cur_width, cur_height = img.size - if self.crop: - crop_horz = getattr(obj, obj._ik.crop_horz_field, 1) - crop_vert = getattr(obj, obj._ik.crop_vert_field, 1) - ratio = max(float(self.width)/cur_width, float(self.height)/cur_height) - resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) - crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) - x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) - box_left, box_right = { - 0: (0, self.width), - 1: (int(x_diff), int(x_diff + self.width)), - 2: (int(crop_x), int(resize_x)), - }[crop_horz] - box_upper, box_lower = { - 0: (0, self.height), - 1: (int(y_diff), int(y_diff + self.height)), - 2: (int(crop_y), int(resize_y)), - }[crop_vert] - box = (box_left, box_upper, box_right, box_lower) - img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) + if not self.width is None and not self.height is None: + ratio = min(float(self.width)/cur_width, + float(self.height)/cur_height) else: - if not self.width is None and not self.height is None: - ratio = min(float(self.width)/cur_width, - float(self.height)/cur_height) + if self.width is None: + ratio = float(self.height)/cur_height else: - if self.width is None: - ratio = float(self.height)/cur_height - else: - ratio = float(self.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 self.upscale: - return img, fmt - img = img.resize(new_dimensions, Image.ANTIALIAS) + ratio = float(self.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 self.upscale: + return img, fmt + img = img.resize(new_dimensions, Image.ANTIALIAS) return img, fmt -class Crop(_Resize): - def __init__(self, width=None, height=None): - super(Crop, self).__init__(width, height, crop=True) - - -class Fit(_Resize): - def __init__(self, width=None, height=None, upscale=None): - super(Fit, self).__init__(width, height, crop=False, upscale=upscale) - - class Transpose(ImageProcessor): """ Rotates or flips the image From 4e23254e73d2d921116f6b7ad399a2c905a0f0cf Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 19:02:18 -0400 Subject: [PATCH 23/68] Crop processor accepts anchor argument. --- imagekit/models.py | 14 ------------- imagekit/options.py | 2 -- imagekit/processors.py | 46 ++++++++++++++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 8e22975..b9ace46 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -16,20 +16,6 @@ from imagekit.utils import img_to_fobj # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) -# Choice tuples for specifying the crop origin. -# These are provided for convenience. -CROP_HORZ_CHOICES = ( - (0, _('left')), - (1, _('center')), - (2, _('right')), -) - -CROP_VERT_CHOICES = ( - (0, _('top')), - (1, _('center')), - (2, _('bottom')), -) - class ImageModelBase(ModelBase): """ ImageModel metaclass diff --git a/imagekit/options.py b/imagekit/options.py index f23683e..833dd23 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -35,8 +35,6 @@ class Options(object): new_name = '{0}_{1}.{2}'.format(filename, specname, extension) return os.path.join(os.path.join('cache', filepath), new_name) - crop_horz_field = 'crop_horz' - crop_vert_field = 'crop_vert' preprocessor_spec = None save_count_as = None specs = None diff --git a/imagekit/processors.py b/imagekit/processors.py index a4ae9a0..ea75b31 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -93,27 +93,51 @@ class _Resize(ImageProcessor): class Crop(_Resize): - def __init__(self, width=None, height=None): + + TOP_LEFT = 'tl' + TOP = 't' + TOP_RIGHT = 'tr' + BOTTOM_LEFT = 'bl' + BOTTOM = 'b' + BOTTOM_RIGHT = 'br' + CENTER = 'c' + LEFT = 'l' + RIGHT = 'r' + + _ANCHOR_PTS = { + TOP_LEFT: (0, 0), + TOP: (0.5, 0), + TOP_RIGHT: (1, 0), + LEFT: (0, 0.5), + CENTER: (0.5, 0.5), + RIGHT: (1, 0.5), + BOTTOM_LEFT: (0, 1), + BOTTOM: (0.5, 1), + BOTTOM_RIGHT: (1, 1), + } + + def __init__(self, width=None, height=None, anchor=None): super(Crop, self).__init__(width, height) + self.anchor = anchor def process(self, img, fmt, obj, spec): cur_width, cur_height = img.size - crop_horz = getattr(obj, obj._ik.crop_horz_field, 1) - crop_vert = getattr(obj, obj._ik.crop_vert_field, 1) + horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ + Crop.CENTER] ratio = max(float(self.width)/cur_width, float(self.height)/cur_height) resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) box_left, box_right = { - 0: (0, self.width), - 1: (int(x_diff), int(x_diff + self.width)), - 2: (int(crop_x), int(resize_x)), - }[crop_horz] + 0: (0, self.width), + 0.5: (int(x_diff), int(x_diff + self.width)), + 1: (int(crop_x), int(resize_x)), + }[horizontal_anchor] box_upper, box_lower = { - 0: (0, self.height), - 1: (int(y_diff), int(y_diff + self.height)), - 2: (int(crop_y), int(resize_y)), - }[crop_vert] + 0: (0, self.height), + 0.5: (int(y_diff), int(y_diff + self.height)), + 1: (int(crop_y), int(resize_y)), + }[vertical_anchor] box = (box_left, box_upper, box_right, box_lower) img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) return img, fmt From 305d20569ca368192c9828cf6fc1c6ff992ad389 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 20:12:49 -0400 Subject: [PATCH 24/68] Moved get_bound_specs to utils. --- imagekit/fields.py | 18 +++--------------- imagekit/utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 970aee7..963f4d8 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -2,7 +2,7 @@ import os import datetime from StringIO import StringIO from imagekit.lib import * -from imagekit.utils import img_to_fobj +from imagekit.utils import img_to_fobj, get_bound_specs from django.core.files.base import ContentFile from django.utils.encoding import force_unicode, smart_str from django.db.models.signals import post_save, post_delete @@ -236,7 +236,7 @@ class _ImageSpecDescriptor(object): def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): if raw: return - bound_specs = _get_bound_specs(instance) + bound_specs = get_bound_specs(instance) for bound_spec in bound_specs: name = bound_spec.property_name imgfield = bound_spec._get_imgfield(instance) @@ -261,23 +261,11 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs def _post_delete_handler(sender, instance=None, **kwargs): assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) - bound_specs = _get_bound_specs(instance) + bound_specs = get_bound_specs(instance) for bound_spec in bound_specs: bound_spec._delete() -def _get_bound_specs(instance): - bound_specs = [] - for key in dir(instance): - try: - value = getattr(instance, key) - except AttributeError: - continue - if isinstance(value, BoundImageSpec): - bound_specs.append(value) - return bound_specs - - class AdminThumbnailView(object): short_description = _('Thumbnail') allow_tags = True diff --git a/imagekit/utils.py b/imagekit/utils.py index af8d40f..96f86d1 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -7,3 +7,16 @@ def img_to_fobj(img, format, **kwargs): img.convert('RGB').save(tmp, format, **kwargs) tmp.seek(0) return tmp + + +def get_bound_specs(instance): + from imagekit.fields import BoundImageSpec + bound_specs = [] + for key in dir(instance): + try: + value = getattr(instance, key) + except AttributeError: + continue + if isinstance(value, BoundImageSpec): + bound_specs.append(value) + return bound_specs From 71670162376c9ae215e4ec833d186e20d952ccbc Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 20:21:15 -0400 Subject: [PATCH 25/68] Removed specs list from opts. --- imagekit/management/commands/ikflush.py | 15 ++++++--------- imagekit/models.py | 10 +++------- imagekit/options.py | 2 -- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index ec1fb03..b87d171 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -1,8 +1,7 @@ from django.db.models.loading import cache from django.core.management.base import BaseCommand, CommandError from optparse import make_option -from imagekit.models import ImageModel -from imagekit.fields import ImageSpec +from imagekit.utils import get_bound_specs class Command(BaseCommand): @@ -22,15 +21,13 @@ def flush_cache(apps, options): if apps: for app_label in apps: app = cache.get_app(app_label) - models = [m for m in cache.get_models(app) if issubclass(m, ImageModel)] - for model in models: + for model in [m for m in cache.get_models(app)]: print 'Flushing cache for "%s.%s"' % (app_label, model.__name__) for obj in model.objects.order_by('-id'): - for spec in model._ik.specs: - prop = getattr(obj, spec.name(), None) - if prop is not None: - prop._delete() + for spec in get_bound_specs(obj): + if spec is not None: + spec._delete() if spec.pre_cache: - prop._create() + spec._create() else: print 'Please specify on or more app names' diff --git a/imagekit/models.py b/imagekit/models.py index b9ace46..56ea545 100644 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _ from imagekit.fields import ImageSpec from imagekit.lib import * from imagekit.options import Options -from imagekit.utils import img_to_fobj +from imagekit.utils import img_to_fobj, get_bound_specs # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) @@ -26,16 +26,12 @@ class ImageModelBase(ModelBase): """ def __init__(self, name, bases, attrs): user_opts = getattr(self, 'IKOptions', None) - specs = [] default_image_field = getattr(user_opts, 'default_image_field', None) for k, v in attrs.items(): - if isinstance(v, ImageSpec): - specs.append(v) - elif not default_image_field and isinstance(v, models.ImageField): + if not default_image_field and isinstance(v, models.ImageField): default_image_field = k - user_opts.specs = specs user_opts.default_image_field = default_image_field opts = Options(user_opts) setattr(self, '_ik', opts) @@ -60,4 +56,4 @@ class ImageModel(models.Model): @property def _imgfields(self): - return set([spec._get_imgfield(self) for spec in self._ik.specs]) + return set([spec._get_imgfield(self) for spec in get_bound_specs(self)]) diff --git a/imagekit/options.py b/imagekit/options.py index 833dd23..96e4b21 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -37,9 +37,7 @@ class Options(object): preprocessor_spec = None save_count_as = None - specs = None def __init__(self, opts): for key, value in opts.__dict__.iteritems(): setattr(self, key, value) - self.specs = list(self.specs or []) From fb53981ec80d160d7274622d12c8f036ab469ecb Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 21:02:12 -0400 Subject: [PATCH 26/68] No need to extend ImageModel. In fact, ImageModel doesn't exist anymore. Most of IKOptions have also been removed. --- imagekit/fields.py | 48 ++++++++++++++++++++++++++++-------- imagekit/models.py | 59 --------------------------------------------- imagekit/options.py | 27 --------------------- 3 files changed, 38 insertions(+), 96 deletions(-) delete mode 100644 imagekit/models.py diff --git a/imagekit/fields.py b/imagekit/fields.py index 963f4d8..496f919 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -3,13 +3,19 @@ import datetime from StringIO import StringIO from imagekit.lib import * from imagekit.utils import img_to_fobj, get_bound_specs +from django.conf import settings from django.core.files.base import ContentFile from django.utils.encoding import force_unicode, smart_str +from django.db import models from django.db.models.signals import post_save, post_delete from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string +# Modify image file buffer size. +ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) + + class ImageSpec(object): image_field = None @@ -44,10 +50,6 @@ class ImageSpec(object): self.processors = processors self.__dict__.update(kwargs) - def _get_imgfield(self, obj): - field_name = getattr(self, 'image_field', None) or obj._ik.default_image_field - return getattr(obj, field_name) - def process(self, image, obj): fmt = image.format img = image.copy() @@ -111,7 +113,25 @@ class BoundImageSpec(ImageSpec): @property def _imgfield(self): - return self._get_imgfield(self._obj) + field_name = getattr(self, 'image_field', None) + if field_name: + field = getattr(self._obj, field_name) + else: + image_fields = [getattr(self._obj, f.attname) for f in \ + self._obj.__class__._meta.fields if \ + isinstance(f, models.ImageField)] + if len(image_fields) == 0: + raise Exception('{0} does not define any ImageFields, so your ' + '{1} ImageSpec has no image to act on.'.format( + self._obj.__class__.__name__, self.property_name)) + elif len(image_fields) > 1: + raise Exception('{0} defines multiple ImageFields, but you have ' + 'not specified an image_field for your {1} ' + 'ImageSpec.'.format(self._obj.__class__.__name__, + self.property_name)) + else: + field = image_fields[0] + return field def _create(self): if self._imgfield: @@ -156,6 +176,17 @@ class BoundImageSpec(ImageSpec): extension = extensions[0] return extension + def _default_cache_to(self, instance, path, specname, extension): + """Determines the filename to use for the transformed image. Can be + overridden on a per-spec basis by setting the cache_to property on the + spec. + + """ + filepath, basename = os.path.split(path) + filename = os.path.splitext(basename)[0] + new_name = '{0}_{1}.{2}'.format(filename, specname, extension) + return os.path.join(os.path.join('cache', filepath), new_name) + @property def name(self): """ @@ -165,8 +196,7 @@ class BoundImageSpec(ImageSpec): """ filename = self._imgfield.name if filename: - cache_to = self.cache_to or \ - getattr(self._obj._ik, 'default_cache_to', None) + cache_to = self.cache_to or self._default_cache_to if not cache_to: raise Exception('No cache_to or default_cache_to value specified') @@ -183,9 +213,7 @@ class BoundImageSpec(ImageSpec): @property def _storage(self): - return self.storage or \ - getattr(self._obj._ik, 'default_storage', None) or \ - self._imgfield.storage + return self.storage or self._imgfield.storage @property def url(self): diff --git a/imagekit/models.py b/imagekit/models.py deleted file mode 100644 index 56ea545..0000000 --- a/imagekit/models.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -from datetime import datetime -from django.conf import settings -from django.core.files.base import ContentFile -from django.db import models -from django.db.models.base import ModelBase -from django.db.models.signals import post_delete -from django.utils.html import conditional_escape as escape -from django.utils.translation import ugettext_lazy as _ - -from imagekit.fields import ImageSpec -from imagekit.lib import * -from imagekit.options import Options -from imagekit.utils import img_to_fobj, get_bound_specs - -# Modify image file buffer size. -ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) - - -class ImageModelBase(ModelBase): - """ ImageModel metaclass - - This metaclass parses IKOptions and loads the specified specification - module. - - """ - def __init__(self, name, bases, attrs): - user_opts = getattr(self, 'IKOptions', None) - default_image_field = getattr(user_opts, 'default_image_field', None) - - for k, v in attrs.items(): - if not default_image_field and isinstance(v, models.ImageField): - default_image_field = k - - user_opts.default_image_field = default_image_field - opts = Options(user_opts) - setattr(self, '_ik', opts) - ModelBase.__init__(self, name, bases, attrs) - - -class ImageModel(models.Model): - """ Abstract base class implementing all core ImageKit functionality - - Subclasses of ImageModel are augmented with accessors for each defined - image specification and can override the inner IKOptions class to customize - storage locations and other options. - - """ - __metaclass__ = ImageModelBase - - class Meta: - abstract = True - - class IKOptions: - pass - - @property - def _imgfields(self): - return set([spec._get_imgfield(self) for spec in get_bound_specs(self)]) diff --git a/imagekit/options.py b/imagekit/options.py index 96e4b21..fce55a5 100644 --- a/imagekit/options.py +++ b/imagekit/options.py @@ -8,33 +8,6 @@ class Options(object): """ - default_image_field = None - """The name of the image field property on the model. - Can be overridden on a per-spec basis by setting the image_field property on - the spec. If you don't define default_image_field on your IKOptions class, - it will be automatically populated with the name of the first ImageField the - model defines. - - """ - - default_storage = None - """Storage used for specs that don't define their own storage explicitly. - If neither is specified, the image field's storage will be used. - - """ - - def default_cache_to(self, instance, path, specname, extension): - """Determines the filename to use for the transformed image. Can be - overridden on a per-spec basis by setting the cache_to property on the - spec, or on a per-model basis by defining default_cache_to on your own - IKOptions class. - - """ - filepath, basename = os.path.split(path) - filename = os.path.splitext(basename)[0] - new_name = '{0}_{1}.{2}'.format(filename, specname, extension) - return os.path.join(os.path.join('cache', filepath), new_name) - preprocessor_spec = None save_count_as = None From 788257b819f3418522fb446bead9a1608c623f9f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 21:04:40 -0400 Subject: [PATCH 27/68] `property_name` is now `attname` That's what Django calls it; that's what we'll call it. --- imagekit/fields.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 496f919..d446a0d 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -72,7 +72,7 @@ class ImageSpec(object): class BoundImageSpec(ImageSpec): - def __init__(self, obj, unbound_field, property_name): + def __init__(self, obj, unbound_field, attname): super(BoundImageSpec, self).__init__(unbound_field.processors, image_field=unbound_field.image_field, pre_cache=unbound_field.pre_cache, @@ -83,7 +83,7 @@ class BoundImageSpec(ImageSpec): self._img = None self._fmt = None self._obj = obj - self.property_name = property_name + self.attname = attname @property def _format(self): @@ -123,12 +123,12 @@ class BoundImageSpec(ImageSpec): if len(image_fields) == 0: raise Exception('{0} does not define any ImageFields, so your ' '{1} ImageSpec has no image to act on.'.format( - self._obj.__class__.__name__, self.property_name)) + self._obj.__class__.__name__, self.attname)) elif len(image_fields) > 1: raise Exception('{0} defines multiple ImageFields, but you have ' 'not specified an image_field for your {1} ' 'ImageSpec.'.format(self._obj.__class__.__name__, - self.property_name)) + self.attname)) else: field = image_fields[0] return field @@ -203,7 +203,7 @@ class BoundImageSpec(ImageSpec): if callable(cache_to): new_filename = force_unicode(datetime.datetime.now().strftime( \ smart_str(cache_to(self._obj, self._imgfield.name, \ - self.property_name, self._suggested_extension)))) + self.attname, self._suggested_extension)))) else: dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) filename = os.path.normpath(os.path.basename(filename)) @@ -250,15 +250,15 @@ class BoundImageSpec(ImageSpec): class _ImageSpecDescriptor(object): - def __init__(self, spec, property_name): - self._property_name = property_name + def __init__(self, spec, attname): + self._attname = attname self._spec = spec def __get__(self, instance, owner): if instance is None: return self._spec else: - return BoundImageSpec(instance, self._spec, self._property_name) + return BoundImageSpec(instance, self._spec, self._attname) def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): @@ -266,7 +266,7 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs return bound_specs = get_bound_specs(instance) for bound_spec in bound_specs: - name = bound_spec.property_name + name = bound_spec.attname imgfield = bound_spec._get_imgfield(instance) newfile = imgfield.storage.open(str(imgfield)) img = Image.open(newfile) From a60dfba31d258a1fdb5273d78aaddfc218e17f9a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 21:08:59 -0400 Subject: [PATCH 28/68] IKOptions no longer exists. --- imagekit/fields.py | 8 -------- imagekit/options.py | 16 ---------------- 2 files changed, 24 deletions(-) delete mode 100644 imagekit/options.py diff --git a/imagekit/fields.py b/imagekit/fields.py index d446a0d..027949b 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -22,7 +22,6 @@ class ImageSpec(object): processors = [] pre_cache = False quality = 70 - increment_count = False storage = None format = None @@ -77,7 +76,6 @@ class BoundImageSpec(ImageSpec): image_field=unbound_field.image_field, pre_cache=unbound_field.pre_cache, quality=unbound_field.quality, - increment_count=unbound_field.increment_count, storage=unbound_field.storage, format=unbound_field.format, cache_to=unbound_field.cache_to) self._img = None @@ -219,12 +217,6 @@ class BoundImageSpec(ImageSpec): def url(self): if not self.pre_cache: self._create() - if self.increment_count: - fieldname = self._obj._ik.save_count_as - if fieldname is not None: - current_count = getattr(self._obj, fieldname) - setattr(self._obj, fieldname, current_count + 1) - self._obj.save(clear_cache=False) return self._storage.url(self.name) @property diff --git a/imagekit/options.py b/imagekit/options.py deleted file mode 100644 index fce55a5..0000000 --- a/imagekit/options.py +++ /dev/null @@ -1,16 +0,0 @@ -# Imagekit options -import os -import os.path - - -class Options(object): - """ Class handling per-model imagekit options - - """ - - preprocessor_spec = None - save_count_as = None - - def __init__(self, opts): - for key, value in opts.__dict__.iteritems(): - setattr(self, key, value) From 98a5ca32b41cf3d06e30d6348d46b1471e55b998 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 21 Sep 2011 23:10:06 -0400 Subject: [PATCH 29/68] Fix bug when imgfield doesn't exist. --- imagekit/fields.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 027949b..1e5c355 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -259,24 +259,25 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs bound_specs = get_bound_specs(instance) for bound_spec in bound_specs: name = bound_spec.attname - imgfield = bound_spec._get_imgfield(instance) - newfile = imgfield.storage.open(str(imgfield)) - img = Image.open(newfile) - img, format = bound_spec.process(img, instance) - if format != 'JPEG': - imgfile = img_to_fobj(img, format) - else: - imgfile = img_to_fobj(img, format, - quality=int(bound_spec.quality), - optimize=True) - content = ContentFile(imgfile.read()) - newfile.close() - name = str(imgfield) - imgfield.storage.delete(name) - imgfield.storage.save(name, content) - if not created: - bound_spec._delete() - bound_spec._create() + imgfield = bound_spec._imgfield + if imgfield: + newfile = imgfield.storage.open(imgfield.name) + img = Image.open(newfile) + img, format = bound_spec.process(img, instance) + if format != 'JPEG': + imgfile = img_to_fobj(img, format) + else: + imgfile = img_to_fobj(img, format, + quality=int(bound_spec.quality), + optimize=True) + content = ContentFile(imgfile.read()) + newfile.close() + name = str(imgfield) + imgfield.storage.delete(name) + imgfield.storage.save(name, content) + if not created: + bound_spec._delete() + bound_spec._create() def _post_delete_handler(sender, instance=None, **kwargs): From 5718c304cfd3a8f0e59ba918ebf9e485d4edc738 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 22 Sep 2011 00:24:13 -0400 Subject: [PATCH 30/68] ProcessedImageField replaces preprocessor_spec --- imagekit/fields.py | 121 +++++++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 32 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 1e5c355..896ec85 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -10,20 +10,31 @@ from django.db import models from django.db.models.signals import post_save, post_delete from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string +from django.db.models.fields.files import ImageFieldFile # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) -class ImageSpec(object): +class _ImageSpecMixin(object): - image_field = None - processors = [] - pre_cache = False - quality = 70 - storage = None - format = None + def __init__(self, processors=None, quality=70, format=None): + self.processors = processors + self.quality = quality + self.format = format + + def process(self, image, obj): + fmt = image.format + img = image.copy() + for proc in self.processors: + img, fmt = proc.process(img, fmt, obj, self) + format = self.format or fmt + img.format = format + return img, format + + +class ImageSpec(_ImageSpecMixin): cache_to = None """Specifies the filename to use when saving the image cache file. This is @@ -44,19 +55,15 @@ class ImageSpec(object): """ - def __init__(self, processors=None, **kwargs): - if processors: - self.processors = processors - self.__dict__.update(kwargs) + def __init__(self, processors=None, quality=70, format=None, + image_field=None, pre_cache=False, storage=None, cache_to=None): - def process(self, image, obj): - fmt = image.format - img = image.copy() - for proc in self.processors: - img, fmt = proc.process(img, fmt, obj, self) - format = self.format or fmt - img.format = format - return img, format + _ImageSpecMixin.__init__(self, processors, quality=quality, + format=format) + self.image_field = image_field + self.pre_cache = pre_cache + self.storage = storage + self.cache_to = cache_to def contribute_to_class(self, cls, name): setattr(cls, name, _ImageSpecDescriptor(self, name)) @@ -70,6 +77,22 @@ class ImageSpec(object): dispatch_uid='%s.delete' % uid) +def _get_suggested_extension(name, format): + if format: + # Try to look up an extension by the format + extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ + if v == format.upper()] + else: + extensions = [] + original_extension = os.path.splitext(name)[1].lstrip('.') + if not extensions or original_extension.lower() in extensions: + # If the original extension matches the format, use it. + extension = original_extension + else: + extension = extensions[0] + return extension + + class BoundImageSpec(ImageSpec): def __init__(self, obj, unbound_field, attname): super(BoundImageSpec, self).__init__(unbound_field.processors, @@ -133,6 +156,7 @@ class BoundImageSpec(ImageSpec): def _create(self): if self._imgfield: + # TODO: Should we error here or something if the image doesn't exist? if self._exists(): return # process the original image file @@ -160,19 +184,7 @@ class BoundImageSpec(ImageSpec): @property def _suggested_extension(self): - if self.format: - # Try to look up an extension by the format - extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ - if v == self.format.upper()] - else: - extensions = [] - original_extension = os.path.splitext(self._imgfield.name)[1].lstrip('.') - if not extensions or original_extension.lower() in extensions: - # If the original extension matches the format, use it. - extension = original_extension - else: - extension = extensions[0] - return extension + return _get_suggested_extension(self._imgfield.name, self.format) def _default_cache_to(self, instance, path, specname, extension): """Determines the filename to use for the transformed image. Can be @@ -334,3 +346,48 @@ class BoundAdminThumbnailView(AdminThumbnailView): def __get__(self, instance, owner): """Override AdminThumbnailView's implementation.""" return self + + +class ProcessedImageFieldFile(ImageFieldFile): + def save(self, name, content, save=True): + new_filename = self.field.generate_filename(self.instance, name) + img = Image.open(content) + img, format = self.field.process(img, self.instance) + format = self._get_format(new_filename, format) + if format != 'JPEG': + imgfile = img_to_fobj(img, format) + else: + imgfile = img_to_fobj(img, format, + quality=int(self.field.quality), + optimize=True) + content = ContentFile(imgfile.read()) + return super(ProcessedImageFieldFile, self).save(name, content, save) + + def _get_format(self, name, fallback): + format = self.field.format + if not format: + if callable(self.field.upload_to): + # The extension is explicit, so assume they want the matching format. + extension = os.path.splitext(name)[1].lower() + # Try to guess the format from the extension. + format = Image.EXTENSION.get(extension) + return format or fallback + + +class ProcessedImageField(models.ImageField, _ImageSpecMixin): + attr_class = ProcessedImageFieldFile + + def __init__(self, processors=None, quality=70, format=None, + verbose_name=None, name=None, width_field=None, height_field=None, + **kwargs): + + _ImageSpecMixin.__init__(self, processors, quality=quality, + format=format) + models.ImageField.__init__(self, verbose_name, name, width_field, + height_field, **kwargs) + + def get_filename(self, filename): + filename = os.path.normpath(self.storage.get_valid_name(os.path.basename(filename))) + name, ext = os.path.splitext(filename) + ext = _get_suggested_extension(filename, self.format) + return '{0}.{1}'.format(name, ext) From bd7eb7284baeb439439835b0af596dd389a597cf Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 22 Sep 2011 09:20:37 -0400 Subject: [PATCH 31/68] BoundImageSpec is now ImageSpecFile In preparation for unifying the ImageSpecFile and ProcessedImageFieldFile interfaces. --- imagekit/fields.py | 65 ++++++++++++------------- imagekit/management/commands/ikflush.py | 12 ++--- imagekit/processors.py | 3 +- imagekit/utils.py | 12 ++--- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 896ec85..fdf9002 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -2,7 +2,7 @@ import os import datetime from StringIO import StringIO from imagekit.lib import * -from imagekit.utils import img_to_fobj, get_bound_specs +from imagekit.utils import img_to_fobj, get_spec_files from django.conf import settings from django.core.files.base import ContentFile from django.utils.encoding import force_unicode, smart_str @@ -93,14 +93,9 @@ def _get_suggested_extension(name, format): return extension -class BoundImageSpec(ImageSpec): - def __init__(self, obj, unbound_field, attname): - super(BoundImageSpec, self).__init__(unbound_field.processors, - image_field=unbound_field.image_field, - pre_cache=unbound_field.pre_cache, - quality=unbound_field.quality, - storage=unbound_field.storage, format=unbound_field.format, - cache_to=unbound_field.cache_to) +class ImageSpecFile(object): + def __init__(self, obj, spec, attname): + self._spec = spec self._img = None self._fmt = None self._obj = obj @@ -114,7 +109,7 @@ class BoundImageSpec(ImageSpec): the `name` property). """ - format = self.format + format = self._spec.format if not format: # Get the real (not suggested) extension. extension = os.path.splitext(self.name)[1].lower() @@ -128,13 +123,13 @@ class BoundImageSpec(ImageSpec): imgfile = img_to_fobj(self._img, format) else: imgfile = img_to_fobj(self._img, format, - quality=int(self.quality), + quality=int(self._spec.quality), optimize=True) return imgfile @property def _imgfield(self): - field_name = getattr(self, 'image_field', None) + field_name = getattr(self._spec, 'image_field', None) if field_name: field = getattr(self._obj, field_name) else: @@ -166,25 +161,25 @@ class BoundImageSpec(ImageSpec): return fp.seek(0) fp = StringIO(fp.read()) - self._img, self._fmt = self.process(Image.open(fp), self._obj) + self._img, self._fmt = self._spec.process(Image.open(fp), self._obj) # save the new image to the cache content = ContentFile(self._get_imgfile().read()) - self._storage.save(self.name, content) + self.storage.save(self.name, content) def _delete(self): if self._imgfield: try: - self._storage.delete(self.name) + self.storage.delete(self.name) except (NotImplementedError, IOError): return def _exists(self): if self._imgfield: - return self._storage.exists(self.name) + return self.storage.exists(self.name) @property def _suggested_extension(self): - return _get_suggested_extension(self._imgfield.name, self.format) + return _get_suggested_extension(self._imgfield.name, self._spec.format) def _default_cache_to(self, instance, path, specname, extension): """Determines the filename to use for the transformed image. Can be @@ -206,7 +201,7 @@ class BoundImageSpec(ImageSpec): """ filename = self._imgfield.name if filename: - cache_to = self.cache_to or self._default_cache_to + cache_to = self._spec.cache_to or self._default_cache_to if not cache_to: raise Exception('No cache_to or default_cache_to value specified') @@ -222,19 +217,19 @@ class BoundImageSpec(ImageSpec): return new_filename @property - def _storage(self): - return self.storage or self._imgfield.storage + def storage(self): + return self._spec.storage or self._imgfield.storage @property def url(self): - if not self.pre_cache: + if not self._spec.pre_cache: self._create() - return self._storage.url(self.name) + return self.storage.url(self.name) @property def file(self): self._create() - return self._storage.open(self.name) + return self.storage.open(self.name) @property def image(self): @@ -262,25 +257,25 @@ class _ImageSpecDescriptor(object): if instance is None: return self._spec else: - return BoundImageSpec(instance, self._spec, self._attname) + return ImageSpecFile(instance, self._spec, self._attname) def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): if raw: return - bound_specs = get_bound_specs(instance) - for bound_spec in bound_specs: - name = bound_spec.attname - imgfield = bound_spec._imgfield + spec_files = get_spec_files(instance) + for spec_file in spec_files: + name = spec_file.attname + imgfield = spec_file._imgfield if imgfield: newfile = imgfield.storage.open(imgfield.name) img = Image.open(newfile) - img, format = bound_spec.process(img, instance) + img, format = spec_file._spec.process(img, instance) if format != 'JPEG': imgfile = img_to_fobj(img, format) else: imgfile = img_to_fobj(img, format, - quality=int(bound_spec.quality), + quality=int(spec_file._spec.quality), optimize=True) content = ContentFile(imgfile.read()) newfile.close() @@ -288,15 +283,15 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs imgfield.storage.delete(name) imgfield.storage.save(name, content) if not created: - bound_spec._delete() - bound_spec._create() + spec_file._delete() + spec_file._create() def _post_delete_handler(sender, instance=None, **kwargs): assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) - bound_specs = get_bound_specs(instance) - for bound_spec in bound_specs: - bound_spec._delete() + spec_files = get_spec_files(instance) + for spec_file in spec_files: + spec_file._delete() class AdminThumbnailView(object): diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index b87d171..3dcd548 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -1,7 +1,7 @@ from django.db.models.loading import cache from django.core.management.base import BaseCommand, CommandError from optparse import make_option -from imagekit.utils import get_bound_specs +from imagekit.utils import get_spec_files class Command(BaseCommand): @@ -24,10 +24,10 @@ def flush_cache(apps, options): for model in [m for m in cache.get_models(app)]: print 'Flushing cache for "%s.%s"' % (app_label, model.__name__) for obj in model.objects.order_by('-id'): - for spec in get_bound_specs(obj): - if spec is not None: - spec._delete() - if spec.pre_cache: - spec._create() + for spec_file in get_spec_files(obj): + if spec_file is not None: + spec_file._delete() + if spec_file._spec.pre_cache: + spec_file._create() else: print 'Please specify on or more app names' diff --git a/imagekit/processors.py b/imagekit/processors.py index ea75b31..825f32e 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -146,8 +146,7 @@ class Crop(_Resize): class Fit(_Resize): def __init__(self, width=None, height=None, upscale=None): super(Fit, self).__init__(width, height) - if upscale is not None: - self.upscale = upscale + self.upscale = upscale def process(self, img, fmt, obj, spec): cur_width, cur_height = img.size diff --git a/imagekit/utils.py b/imagekit/utils.py index 96f86d1..6218123 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -9,14 +9,14 @@ def img_to_fobj(img, format, **kwargs): return tmp -def get_bound_specs(instance): - from imagekit.fields import BoundImageSpec - bound_specs = [] +def get_spec_files(instance): + from imagekit.fields import ImageSpecFile + spec_files = [] for key in dir(instance): try: value = getattr(instance, key) except AttributeError: continue - if isinstance(value, BoundImageSpec): - bound_specs.append(value) - return bound_specs + if isinstance(value, ImageSpecFile): + spec_files.append(value) + return spec_files From 0ef56e1aaa4c6f925df6f387ef96c9e873aa1d46 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 22 Sep 2011 17:31:26 -0400 Subject: [PATCH 32/68] `process()` accepts file In the old IK API, processors (like `Transpose`) were able to access the file by inspecting the model instance (which carried an options object that specified the attribute name of the ImageField from which the file could be extracted). Since the new API allows for multiple ImageFields (and because IKOptions have been removed), it became necessary to provide more information. Initially, this was accomplished by passing the spec to `process()`, however with the addition of ProcessedImageField, it became clear the a cleaner solution was to pass only the field file (ImageSpecFile or ProcessedImageFieldFile). This keeps the ORM stuff (fields, etc.) out of the `ImageProcessor` API but (because field files, not just regular files, are passed) the average hacker can still have their processor make use of model information by accessing the model through the file's `instance` property. --- imagekit/fields.py | 10 +++++----- imagekit/processors.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index fdf9002..3d3949e 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -24,11 +24,11 @@ class _ImageSpecMixin(object): self.quality = quality self.format = format - def process(self, image, obj): + def process(self, image, file): fmt = image.format img = image.copy() for proc in self.processors: - img, fmt = proc.process(img, fmt, obj, self) + img, fmt = proc.process(img, fmt, file) format = self.format or fmt img.format = format return img, format @@ -161,7 +161,7 @@ class ImageSpecFile(object): return fp.seek(0) fp = StringIO(fp.read()) - self._img, self._fmt = self._spec.process(Image.open(fp), self._obj) + self._img, self._fmt = self._spec.process(Image.open(fp), self) # save the new image to the cache content = ContentFile(self._get_imgfile().read()) self.storage.save(self.name, content) @@ -270,7 +270,7 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs if imgfield: newfile = imgfield.storage.open(imgfield.name) img = Image.open(newfile) - img, format = spec_file._spec.process(img, instance) + img, format = spec_file._spec.process(img, spec_file) if format != 'JPEG': imgfile = img_to_fobj(img, format) else: @@ -347,7 +347,7 @@ class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): new_filename = self.field.generate_filename(self.instance, name) img = Image.open(content) - img, format = self.field.process(img, self.instance) + img, format = self.field.process(img, self) format = self._get_format(new_filename, format) if format != 'JPEG': imgfile = img_to_fobj(img, format) diff --git a/imagekit/processors.py b/imagekit/processors.py index 825f32e..028a884 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -11,7 +11,7 @@ from imagekit.lib import * class ImageProcessor(object): """ Base image processor class """ - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): return img, fmt @@ -23,7 +23,7 @@ class Adjust(ImageProcessor): self.contrast = contrast self.sharpness = sharpness - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): img = img.convert('RGB') for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: factor = getattr(self, name.lower()) @@ -40,7 +40,7 @@ class Reflection(ImageProcessor): size = 0.0 opacity = 0.6 - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): # convert bgcolor string to rgb value background_color = ImageColor.getrgb(self.background_color) # handle palleted images @@ -88,7 +88,7 @@ class _Resize(ImageProcessor): if height is not None: self.height = height - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): raise NotImplementedError('process must be overridden by subclasses.') @@ -120,7 +120,7 @@ class Crop(_Resize): super(Crop, self).__init__(width, height) self.anchor = anchor - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): cur_width, cur_height = img.size horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ Crop.CENTER] @@ -148,7 +148,7 @@ class Fit(_Resize): super(Fit, self).__init__(width, height) self.upscale = upscale - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): cur_width, cur_height = img.size if not self.width is None and not self.height is None: ratio = min(float(self.width)/cur_width, @@ -196,10 +196,10 @@ class Transpose(ImageProcessor): method = 'auto' - def process(self, img, fmt, obj, spec): + def process(self, img, fmt, file): if self.method == 'auto': try: - orientation = Image.open(spec._get_imgfield(obj).file)._getexif()[0x0112] + orientation = Image.open(file.file)._getexif()[0x0112] ops = self.EXIF_ORIENTATION_STEPS[orientation] except: ops = [] From 3d810e7be5cbe05cfa05a19da97371452e64a25c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 22 Sep 2011 18:13:32 -0400 Subject: [PATCH 33/68] Rename ImageSpecFile properties `_obj` and `_spec` are now `instance` and `field`, to match FieldFile. --- imagekit/fields.py | 46 ++++++++++++------------- imagekit/management/commands/ikflush.py | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index 3d3949e..a6b6fff 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -94,11 +94,11 @@ def _get_suggested_extension(name, format): class ImageSpecFile(object): - def __init__(self, obj, spec, attname): - self._spec = spec + def __init__(self, instance, field, attname): + self.field = field self._img = None self._fmt = None - self._obj = obj + self.instance = instance self.attname = attname @property @@ -109,7 +109,7 @@ class ImageSpecFile(object): the `name` property). """ - format = self._spec.format + format = self.field.format if not format: # Get the real (not suggested) extension. extension = os.path.splitext(self.name)[1].lower() @@ -123,27 +123,27 @@ class ImageSpecFile(object): imgfile = img_to_fobj(self._img, format) else: imgfile = img_to_fobj(self._img, format, - quality=int(self._spec.quality), + quality=int(self.field.quality), optimize=True) return imgfile @property def _imgfield(self): - field_name = getattr(self._spec, 'image_field', None) + field_name = getattr(self.field, 'image_field', None) if field_name: - field = getattr(self._obj, field_name) + field = getattr(self.instance, field_name) else: - image_fields = [getattr(self._obj, f.attname) for f in \ - self._obj.__class__._meta.fields if \ + image_fields = [getattr(self.instance, f.attname) for f in \ + self.instance.__class__._meta.fields if \ isinstance(f, models.ImageField)] if len(image_fields) == 0: raise Exception('{0} does not define any ImageFields, so your ' '{1} ImageSpec has no image to act on.'.format( - self._obj.__class__.__name__, self.attname)) + self.instance.__class__.__name__, self.attname)) elif len(image_fields) > 1: raise Exception('{0} defines multiple ImageFields, but you have ' 'not specified an image_field for your {1} ' - 'ImageSpec.'.format(self._obj.__class__.__name__, + 'ImageSpec.'.format(self.instance.__class__.__name__, self.attname)) else: field = image_fields[0] @@ -161,7 +161,7 @@ class ImageSpecFile(object): return fp.seek(0) fp = StringIO(fp.read()) - self._img, self._fmt = self._spec.process(Image.open(fp), self) + self._img, self._fmt = self.field.process(Image.open(fp), self) # save the new image to the cache content = ContentFile(self._get_imgfile().read()) self.storage.save(self.name, content) @@ -179,7 +179,7 @@ class ImageSpecFile(object): @property def _suggested_extension(self): - return _get_suggested_extension(self._imgfield.name, self._spec.format) + return _get_suggested_extension(self._imgfield.name, self.field.format) def _default_cache_to(self, instance, path, specname, extension): """Determines the filename to use for the transformed image. Can be @@ -201,13 +201,13 @@ class ImageSpecFile(object): """ filename = self._imgfield.name if filename: - cache_to = self._spec.cache_to or self._default_cache_to + cache_to = self.field.cache_to or self._default_cache_to if not cache_to: raise Exception('No cache_to or default_cache_to value specified') if callable(cache_to): new_filename = force_unicode(datetime.datetime.now().strftime( \ - smart_str(cache_to(self._obj, self._imgfield.name, \ + smart_str(cache_to(self.instance, self._imgfield.name, \ self.attname, self._suggested_extension)))) else: dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) @@ -218,11 +218,11 @@ class ImageSpecFile(object): @property def storage(self): - return self._spec.storage or self._imgfield.storage + return self.field.storage or self._imgfield.storage @property def url(self): - if not self._spec.pre_cache: + if not self.field.pre_cache: self._create() return self.storage.url(self.name) @@ -249,15 +249,15 @@ class ImageSpecFile(object): class _ImageSpecDescriptor(object): - def __init__(self, spec, attname): + def __init__(self, field, attname): self._attname = attname - self._spec = spec + self.field = field def __get__(self, instance, owner): if instance is None: - return self._spec + return self.field else: - return ImageSpecFile(instance, self._spec, self._attname) + return ImageSpecFile(instance, self.field, self._attname) def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): @@ -270,12 +270,12 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs if imgfield: newfile = imgfield.storage.open(imgfield.name) img = Image.open(newfile) - img, format = spec_file._spec.process(img, spec_file) + img, format = spec_file.field.process(img, spec_file) if format != 'JPEG': imgfile = img_to_fobj(img, format) else: imgfile = img_to_fobj(img, format, - quality=int(spec_file._spec.quality), + quality=int(spec_file.field.quality), optimize=True) content = ContentFile(imgfile.read()) newfile.close() diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index 3dcd548..fd26dd4 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -27,7 +27,7 @@ def flush_cache(apps, options): for spec_file in get_spec_files(obj): if spec_file is not None: spec_file._delete() - if spec_file._spec.pre_cache: + if spec_file.field.pre_cache: spec_file._create() else: print 'Please specify on or more app names' From 77459eae73350adba1060e752ec04df0f898ab89 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Thu, 22 Sep 2011 19:44:47 -0400 Subject: [PATCH 34/68] Pared down the _post_save_handler. The original handler implementation ported code from the old ImageModel's save method, but ended up duplicating the efforts of the ImageSpecFile's _create method. --- imagekit/fields.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index a6b6fff..b3aabd7 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -265,26 +265,10 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs return spec_files = get_spec_files(instance) for spec_file in spec_files: - name = spec_file.attname - imgfield = spec_file._imgfield - if imgfield: - newfile = imgfield.storage.open(imgfield.name) - img = Image.open(newfile) - img, format = spec_file.field.process(img, spec_file) - if format != 'JPEG': - imgfile = img_to_fobj(img, format) - else: - imgfile = img_to_fobj(img, format, - quality=int(spec_file.field.quality), - optimize=True) - content = ContentFile(imgfile.read()) - newfile.close() - name = str(imgfield) - imgfield.storage.delete(name) - imgfield.storage.save(name, content) - if not created: - spec_file._delete() - spec_file._create() + if not created: + spec_file._delete() + if spec_file.field.pre_cache: + spec_file._create() def _post_delete_handler(sender, instance=None, **kwargs): From 3f7ca512af649c88ec0b87f19b4b2cfbeec70b65 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 23 Sep 2011 12:44:53 -0400 Subject: [PATCH 35/68] Extension argument to cache_to includes dot. It seems that's how we do it in python world. Who am I to argue? --- imagekit/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imagekit/fields.py b/imagekit/fields.py index b3aabd7..1965ed3 100755 --- a/imagekit/fields.py +++ b/imagekit/fields.py @@ -80,11 +80,11 @@ class ImageSpec(_ImageSpecMixin): def _get_suggested_extension(name, format): if format: # Try to look up an extension by the format - extensions = [k.lstrip('.') for k, v in Image.EXTENSION.iteritems() \ + extensions = [k for k, v in Image.EXTENSION.iteritems() \ if v == format.upper()] else: extensions = [] - original_extension = os.path.splitext(name)[1].lstrip('.') + original_extension = os.path.splitext(name)[1] if not extensions or original_extension.lower() in extensions: # If the original extension matches the format, use it. extension = original_extension @@ -189,7 +189,7 @@ class ImageSpecFile(object): """ filepath, basename = os.path.split(path) filename = os.path.splitext(basename)[0] - new_name = '{0}_{1}.{2}'.format(filename, specname, extension) + new_name = '{0}_{1}{2}'.format(filename, specname, extension) return os.path.join(os.path.join('cache', filepath), new_name) @property From e190e78df5dc10c9fef513d0d1b0b1f1a41dfa9d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 23 Sep 2011 18:06:28 -0400 Subject: [PATCH 36/68] Rename fields module to models. --- imagekit/{fields.py => models.py} | 0 imagekit/tests.py | 2 +- imagekit/utils.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename imagekit/{fields.py => models.py} (100%) diff --git a/imagekit/fields.py b/imagekit/models.py similarity index 100% rename from imagekit/fields.py rename to imagekit/models.py diff --git a/imagekit/tests.py b/imagekit/tests.py index 92025d1..6745c7c 100644 --- a/imagekit/tests.py +++ b/imagekit/tests.py @@ -8,7 +8,7 @@ from django.test import TestCase from imagekit import processors from imagekit.models import ImageModel -from imagekit.fields import ImageSpec +from imagekit.models import ImageSpec from imagekit.lib import Image diff --git a/imagekit/utils.py b/imagekit/utils.py index 6218123..b2be22d 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -10,7 +10,7 @@ def img_to_fobj(img, format, **kwargs): def get_spec_files(instance): - from imagekit.fields import ImageSpecFile + from imagekit.models import ImageSpecFile spec_files = [] for key in dir(instance): try: From 15e49be7199951dbd80c3ec77e63ee1b3ac25974 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 23 Sep 2011 18:55:12 -0400 Subject: [PATCH 37/68] Extracted ProcessorPipeline Pulled some functionality out of _ImageSpecMixin into the ProcessorPipeline class so it could be used independently of the model-related stuff. --- imagekit/models.py | 10 ++++------ imagekit/processors.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 1965ed3..98f88c6 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -3,6 +3,7 @@ import datetime from StringIO import StringIO from imagekit.lib import * from imagekit.utils import img_to_fobj, get_spec_files +from imagekit.processors import ProcessorPipeline from django.conf import settings from django.core.files.base import ContentFile from django.utils.encoding import force_unicode, smart_str @@ -18,7 +19,6 @@ ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) class _ImageSpecMixin(object): - def __init__(self, processors=None, quality=70, format=None): self.processors = processors self.quality = quality @@ -27,11 +27,9 @@ class _ImageSpecMixin(object): def process(self, image, file): fmt = image.format img = image.copy() - for proc in self.processors: - img, fmt = proc.process(img, fmt, file) - format = self.format or fmt - img.format = format - return img, format + processors = ProcessorPipeline(self.processors) + img, fmt = processors.process(img, fmt, file) + return img, self.format or fmt class ImageSpec(_ImageSpecMixin): diff --git a/imagekit/processors.py b/imagekit/processors.py index 028a884..0db5486 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -8,6 +8,7 @@ own effects/processes entirely. """ from imagekit.lib import * + class ImageProcessor(object): """ Base image processor class """ @@ -15,6 +16,18 @@ class ImageProcessor(object): return img, fmt +class ProcessorPipeline(ImageProcessor, list): + """A processor that just runs a bunch of other processors. This class allows + any object that knows how to deal with a single processor to deal with a + list of them. + + """ + def process(self, img, fmt, file): + for proc in self: + img, fmt = proc.process(img, fmt, file) + return img, fmt + + class Adjust(ImageProcessor): def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0): From 7e20b75ced0e9be0b1db6160692637dc5c91709a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 23 Sep 2011 20:11:18 -0400 Subject: [PATCH 38/68] Processors don't care about format. The process of choosing an image format has been cleaned up and Processors' role in determining the format has been removed. Previously, processors would return a tuple containing the modified image and the format. Other parts of IK overrode PIL's Image.format with the target format, although that had no effect on PIL and the fact that it didn't throw an error was just lucky. --- imagekit/models.py | 14 +++++--------- imagekit/processors.py | 44 ++++++++++++++++++++---------------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 98f88c6..e7b47a9 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -25,11 +25,8 @@ class _ImageSpecMixin(object): self.format = format def process(self, image, file): - fmt = image.format - img = image.copy() processors = ProcessorPipeline(self.processors) - img, fmt = processors.process(img, fmt, file) - return img, self.format or fmt + return processors.process(image.copy(), file) class ImageSpec(_ImageSpecMixin): @@ -95,7 +92,6 @@ class ImageSpecFile(object): def __init__(self, instance, field, attname): self.field = field self._img = None - self._fmt = None self.instance = instance self.attname = attname @@ -159,7 +155,7 @@ class ImageSpecFile(object): return fp.seek(0) fp = StringIO(fp.read()) - self._img, self._fmt = self.field.process(Image.open(fp), self) + self._img = self.field.process(Image.open(fp), self) # save the new image to the cache content = ContentFile(self._get_imgfile().read()) self.storage.save(self.name, content) @@ -329,8 +325,8 @@ class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): new_filename = self.field.generate_filename(self.instance, name) img = Image.open(content) - img, format = self.field.process(img, self) - format = self._get_format(new_filename, format) + img = self.field.process(img, self) + format = self._get_format(new_filename, img.format) if format != 'JPEG': imgfile = img_to_fobj(img, format) else: @@ -348,7 +344,7 @@ class ProcessedImageFieldFile(ImageFieldFile): extension = os.path.splitext(name)[1].lower() # Try to guess the format from the extension. format = Image.EXTENSION.get(extension) - return format or fallback + return format or fallback or 'JPEG' class ProcessedImageField(models.ImageField, _ImageSpecMixin): diff --git a/imagekit/processors.py b/imagekit/processors.py index 0db5486..654b8f3 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -1,9 +1,9 @@ """ Imagekit Image "ImageProcessors" -A processor defines a set of class variables (optional) and a -class method named "process" which processes the supplied image using -the class properties as settings. The process method can be overridden as well allowing user to define their -own effects/processes entirely. +A processor accepts an image, does some stuff, and returns a new image. +Processors can do anything with the image you want, but their responsibilities +should be limited to image manipulations--they should be completely decoupled +from both the filesystem and the ORM. """ from imagekit.lib import * @@ -12,8 +12,8 @@ from imagekit.lib import * class ImageProcessor(object): """ Base image processor class """ - def process(self, img, fmt, file): - return img, fmt + def process(self, img, file): + return img class ProcessorPipeline(ImageProcessor, list): @@ -22,10 +22,10 @@ class ProcessorPipeline(ImageProcessor, list): list of them. """ - def process(self, img, fmt, file): + def process(self, img, file): for proc in self: - img, fmt = proc.process(img, fmt, file) - return img, fmt + img = proc.process(img, file) + return img class Adjust(ImageProcessor): @@ -36,7 +36,7 @@ class Adjust(ImageProcessor): self.contrast = contrast self.sharpness = sharpness - def process(self, img, fmt, file): + def process(self, img, file): img = img.convert('RGB') for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: factor = getattr(self, name.lower()) @@ -45,7 +45,7 @@ class Adjust(ImageProcessor): img = getattr(ImageEnhance, name)(img).enhance(factor) except ValueError: pass - return img, fmt + return img class Reflection(ImageProcessor): @@ -53,7 +53,7 @@ class Reflection(ImageProcessor): size = 0.0 opacity = 0.6 - def process(self, img, fmt, file): + def process(self, img, file): # convert bgcolor string to rgb value background_color = ImageColor.getrgb(self.background_color) # handle palleted images @@ -84,10 +84,8 @@ class Reflection(ImageProcessor): # paste the orignal image and the reflection into the composite image composite.paste(img, (0, 0)) composite.paste(reflection, (0, img.size[1])) - # Save the file as a JPEG - fmt = 'JPEG' # return the image complete with reflection effect - return composite, fmt + return composite class _Resize(ImageProcessor): @@ -101,7 +99,7 @@ class _Resize(ImageProcessor): if height is not None: self.height = height - def process(self, img, fmt, file): + def process(self, img, file): raise NotImplementedError('process must be overridden by subclasses.') @@ -133,7 +131,7 @@ class Crop(_Resize): super(Crop, self).__init__(width, height) self.anchor = anchor - def process(self, img, fmt, file): + def process(self, img, file): cur_width, cur_height = img.size horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ Crop.CENTER] @@ -153,7 +151,7 @@ class Crop(_Resize): }[vertical_anchor] box = (box_left, box_upper, box_right, box_lower) img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) - return img, fmt + return img class Fit(_Resize): @@ -161,7 +159,7 @@ class Fit(_Resize): super(Fit, self).__init__(width, height) self.upscale = upscale - def process(self, img, fmt, file): + def process(self, img, file): cur_width, cur_height = img.size if not self.width is None and not self.height is None: ratio = min(float(self.width)/cur_width, @@ -176,9 +174,9 @@ class Fit(_Resize): if new_dimensions[0] > cur_width or \ new_dimensions[1] > cur_height: if not self.upscale: - return img, fmt + return img img = img.resize(new_dimensions, Image.ANTIALIAS) - return img, fmt + return img class Transpose(ImageProcessor): @@ -209,7 +207,7 @@ class Transpose(ImageProcessor): method = 'auto' - def process(self, img, fmt, file): + def process(self, img, file): if self.method == 'auto': try: orientation = Image.open(file.file)._getexif()[0x0112] @@ -220,4 +218,4 @@ class Transpose(ImageProcessor): ops = [self.method] for method in ops: img = img.transpose(getattr(Image, method)) - return img, fmt + return img From d99bf5327bc375023cb16f86b03c58e1212ac299 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Fri, 23 Sep 2011 21:01:20 -0400 Subject: [PATCH 39/68] Transpose processor has a new API. Transpose now takes transposition constants as arguments. Multiple transpositions can be sequenced together in one Transpose processor. Auto transposition is not yet supported (PIL strips the EXIF data, so need to find a workaround for getting that data to the processor). --- imagekit/processors.py | 65 ++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/imagekit/processors.py b/imagekit/processors.py index 654b8f3..71d55d7 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -182,40 +182,57 @@ class Fit(_Resize): class Transpose(ImageProcessor): """ Rotates or flips the image - Method should be one of the following strings: - - FLIP_LEFT RIGHT - - FLIP_TOP_BOTTOM - - ROTATE_90 - - ROTATE_270 - - ROTATE_180 - - auto + Possible arguments: + - Transpose.AUTO + - Transpose.FLIP_HORIZONTAL + - Transpose.FLIP_VERTICAL + - Transpose.ROTATE_90 + - Transpose.ROTATE_180 + - Transpose.ROTATE_270 + + The order of the arguments dictates the order in which the Transposition + steps are taken. - If method is set to 'auto' the processor will attempt to rotate the image - according to the EXIF Orientation data. + If Transpose.AUTO is present, all other arguments are ignored, and the + processor will attempt to rotate the image according to the + EXIF Orientation data. """ - EXIF_ORIENTATION_STEPS = { + AUTO = 'auto' + FLIP_HORIZONTAL = Image.FLIP_LEFT_RIGHT + FLIP_VERTICAL = Image.FLIP_TOP_BOTTOM + ROTATE_90 = Image.ROTATE_90 + ROTATE_180 = Image.ROTATE_180 + ROTATE_270 = Image.ROTATE_270 + + methods = [AUTO] + _EXIF_ORIENTATION_STEPS = { 1: [], - 2: ['FLIP_LEFT_RIGHT'], - 3: ['ROTATE_180'], - 4: ['FLIP_TOP_BOTTOM'], - 5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'], - 6: ['ROTATE_270'], - 7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'], - 8: ['ROTATE_90'], + 2: [FLIP_HORIZONTAL], + 3: [ROTATE_180], + 4: [FLIP_VERTICAL], + 5: [ROTATE_270, FLIP_HORIZONTAL], + 6: [ROTATE_270], + 7: [ROTATE_90, FLIP_HORIZONTAL], + 8: [ROTATE_90], } - method = 'auto' + def __init__(self, *args): + super(Transpose, self).__init__() + if args: + self.methods = args def process(self, img, file): - if self.method == 'auto': + if self.AUTO in self.methods: + raise Exception("AUTO is not yet supported. Sorry!") try: - orientation = Image.open(file.file)._getexif()[0x0112] - ops = self.EXIF_ORIENTATION_STEPS[orientation] - except: + orientation = img._getexif()[0x0112] + ops = self._EXIF_ORIENTATION_STEPS[orientation] + print 'GOT %s >>>> %s' % (orientation, ops) + except AttributeError: ops = [] else: - ops = [self.method] + ops = self.methods for method in ops: - img = img.transpose(getattr(Image, method)) + img = img.transpose(method) return img From 8a0bc084fe68ee238da3cb6f826586bbfc02ec2b Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Fri, 23 Sep 2011 21:25:47 -0400 Subject: [PATCH 40/68] Processors no longer take a file argument. They only get the image to process now. --- imagekit/models.py | 2 +- imagekit/processors.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index e7b47a9..f28042b 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -26,7 +26,7 @@ class _ImageSpecMixin(object): def process(self, image, file): processors = ProcessorPipeline(self.processors) - return processors.process(image.copy(), file) + return processors.process(image.copy()) class ImageSpec(_ImageSpecMixin): diff --git a/imagekit/processors.py b/imagekit/processors.py index 71d55d7..3ec7be3 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -12,7 +12,7 @@ from imagekit.lib import * class ImageProcessor(object): """ Base image processor class """ - def process(self, img, file): + def process(self, img): return img @@ -22,9 +22,9 @@ class ProcessorPipeline(ImageProcessor, list): list of them. """ - def process(self, img, file): + def process(self, img): for proc in self: - img = proc.process(img, file) + img = proc.process(img) return img @@ -36,7 +36,7 @@ class Adjust(ImageProcessor): self.contrast = contrast self.sharpness = sharpness - def process(self, img, file): + def process(self, img): img = img.convert('RGB') for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: factor = getattr(self, name.lower()) @@ -53,7 +53,7 @@ class Reflection(ImageProcessor): size = 0.0 opacity = 0.6 - def process(self, img, file): + def process(self, img): # convert bgcolor string to rgb value background_color = ImageColor.getrgb(self.background_color) # handle palleted images @@ -99,7 +99,7 @@ class _Resize(ImageProcessor): if height is not None: self.height = height - def process(self, img, file): + def process(self, img): raise NotImplementedError('process must be overridden by subclasses.') @@ -131,7 +131,7 @@ class Crop(_Resize): super(Crop, self).__init__(width, height) self.anchor = anchor - def process(self, img, file): + def process(self, img): cur_width, cur_height = img.size horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ Crop.CENTER] @@ -159,7 +159,7 @@ class Fit(_Resize): super(Fit, self).__init__(width, height) self.upscale = upscale - def process(self, img, file): + def process(self, img): cur_width, cur_height = img.size if not self.width is None and not self.height is None: ratio = min(float(self.width)/cur_width, @@ -222,7 +222,7 @@ class Transpose(ImageProcessor): if args: self.methods = args - def process(self, img, file): + def process(self, img): if self.AUTO in self.methods: raise Exception("AUTO is not yet supported. Sorry!") try: From cc96ba519804feeb0b3a3e4240a8e74020719708 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 23 Sep 2011 22:50:13 -0400 Subject: [PATCH 41/68] Moved _get_imgfile() into _create() That was the only place it was being called and it just led to (my) confusion with the image property. --- imagekit/models.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index f28042b..9403b93 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -111,16 +111,6 @@ class ImageSpecFile(object): format = Image.EXTENSION.get(extension) return format or self._img.format or 'JPEG' - def _get_imgfile(self): - format = self._format - if format != 'JPEG': - imgfile = img_to_fobj(self._img, format) - else: - imgfile = img_to_fobj(self._img, format, - quality=int(self.field.quality), - optimize=True) - return imgfile - @property def _imgfield(self): field_name = getattr(self.field, 'image_field', None) @@ -156,8 +146,16 @@ class ImageSpecFile(object): fp.seek(0) fp = StringIO(fp.read()) self._img = self.field.process(Image.open(fp), self) + # save the new image to the cache - content = ContentFile(self._get_imgfile().read()) + format = self._format + if format != 'JPEG': + imgfile = img_to_fobj(self._img, format) + else: + imgfile = img_to_fobj(self._img, format, + quality=int(self.field.quality), + optimize=True) + content = ContentFile(imgfile.read()) self.storage.save(self.name, content) def _delete(self): From f9c4d6b500888c059dcf4547a3442e6a7a582b27 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 23 Sep 2011 23:02:17 -0400 Subject: [PATCH 42/68] Cleanup. Centralized access of _img, tried to reduce re-calculation of some properties, renamed _imgfield to source_file to reflect the fact that it's an ImageFieldFile and not an ImageField. --- imagekit/models.py | 139 +++++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 9403b93..679e57e 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -89,89 +89,71 @@ def _get_suggested_extension(name, format): class ImageSpecFile(object): - def __init__(self, instance, field, attname): + def __init__(self, instance, field, attname, source_file): self.field = field self._img = None + self._file = None self.instance = instance self.attname = attname + self.source_file = source_file - @property - def _format(self): - """The format used to save the cache file. If the format is set - explicitly on the ImageSpec, that format will be used. Otherwise, the - format will be inferred from the extension of the cache filename (see - the `name` property). + def _create(self, lazy=False): + """Creates a new image by running the processors on the source file. + + Keyword Arguments: + lazy -- True if an already-existing image should be returned; False if + a new image should be created and the existing one overwritten. """ - format = self.field.format - if not format: - # Get the real (not suggested) extension. - extension = os.path.splitext(self.name)[1].lower() - # Try to guess the format from the extension. - format = Image.EXTENSION.get(extension) - return format or self._img.format or 'JPEG' - - @property - def _imgfield(self): - field_name = getattr(self.field, 'image_field', None) - if field_name: - field = getattr(self.instance, field_name) - else: - image_fields = [getattr(self.instance, f.attname) for f in \ - self.instance.__class__._meta.fields if \ - isinstance(f, models.ImageField)] - if len(image_fields) == 0: - raise Exception('{0} does not define any ImageFields, so your ' - '{1} ImageSpec has no image to act on.'.format( - self.instance.__class__.__name__, self.attname)) - elif len(image_fields) > 1: - raise Exception('{0} defines multiple ImageFields, but you have ' - 'not specified an image_field for your {1} ' - 'ImageSpec.'.format(self.instance.__class__.__name__, - self.attname)) - else: - field = image_fields[0] - return field - - def _create(self): - if self._imgfield: - # TODO: Should we error here or something if the image doesn't exist? - if self._exists(): - return + img = None + if lazy: + img = self._img + if not img and self.storage.exists(self.name): + img = Image.open(self.file) + if not img and self.source_file: # process the original image file try: - fp = self._imgfield.storage.open(self._imgfield.name) + fp = self.source_file.storage.open(self.source_file.name) except IOError: return fp.seek(0) fp = StringIO(fp.read()) - self._img = self.field.process(Image.open(fp), self) + img = self.field.process(Image.open(fp), self) + + # Determine the format. + format = self.field.format + if not format: + # Get the real (not suggested) extension. + extension = os.path.splitext(self.name)[1].lower() + # Try to guess the format from the extension. + format = Image.EXTENSION.get(extension) + format = format or img.format or 'JPEG' # save the new image to the cache - format = self._format if format != 'JPEG': - imgfile = img_to_fobj(self._img, format) + imgfile = img_to_fobj(img, format) else: - imgfile = img_to_fobj(self._img, format, + imgfile = img_to_fobj(img, format, quality=int(self.field.quality), optimize=True) content = ContentFile(imgfile.read()) - self.storage.save(self.name, content) + self._file = self.storage.save(self.name, content) + else: + # TODO: Should we error here or something if the imagefield doesn't exist? + img = None + self._img = img + return self._img def _delete(self): - if self._imgfield: + if self.source_file: try: self.storage.delete(self.name) except (NotImplementedError, IOError): return - def _exists(self): - if self._imgfield: - return self.storage.exists(self.name) - @property def _suggested_extension(self): - return _get_suggested_extension(self._imgfield.name, self.field.format) + return _get_suggested_extension(self.source_file.name, self.field.format) def _default_cache_to(self, instance, path, specname, extension): """Determines the filename to use for the transformed image. Can be @@ -191,7 +173,7 @@ class ImageSpecFile(object): control this by providing a `cache_to` method to the ImageSpec. """ - filename = self._imgfield.name + filename = self.source_file.name if filename: cache_to = self.field.cache_to or self._default_cache_to @@ -199,7 +181,7 @@ class ImageSpecFile(object): raise Exception('No cache_to or default_cache_to value specified') if callable(cache_to): new_filename = force_unicode(datetime.datetime.now().strftime( \ - smart_str(cache_to(self.instance, self._imgfield.name, \ + smart_str(cache_to(self.instance, self.source_file.name, self.attname, self._suggested_extension)))) else: dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) @@ -210,26 +192,25 @@ class ImageSpecFile(object): @property def storage(self): - return self.field.storage or self._imgfield.storage + return self.field.storage or self.source_file.storage @property def url(self): if not self.field.pre_cache: - self._create() + self._create(True) return self.storage.url(self.name) @property def file(self): - self._create() - return self.storage.open(self.name) + if not self._file: + if not self.storage.exists(self.name): + self._create() + self._file = self.storage.open(self.name) + return self._file @property def image(self): - if not self._img: - self._create() - if not self._img: - self._img = Image.open(self.file) - return self._img + return self._create(True) @property def width(self): @@ -242,14 +223,36 @@ class ImageSpecFile(object): class _ImageSpecDescriptor(object): def __init__(self, field, attname): - self._attname = attname + self.attname = attname self.field = field + def _get_image_field_file(self, instance): + field_name = getattr(self.field, 'image_field', None) + if field_name: + field = getattr(instance, field_name) + else: + image_fields = [getattr(instance, f.attname) for f in \ + instance.__class__._meta.fields if \ + isinstance(f, models.ImageField)] + if len(image_fields) == 0: + raise Exception('{0} does not define any ImageFields, so your ' + '{1} ImageSpec has no image to act on.'.format( + instance.__class__.__name__, self.attname)) + elif len(image_fields) > 1: + raise Exception('{0} defines multiple ImageFields, but you have ' + 'not specified an image_field for your {1} ' + 'ImageSpec.'.format(instance.__class__.__name__, + self.attname)) + else: + field = image_fields[0] + return field + def __get__(self, instance, owner): if instance is None: return self.field else: - return ImageSpecFile(instance, self.field, self._attname) + return ImageSpecFile(instance, self.field, self.attname, + self._get_image_field_file(instance)) def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): @@ -305,7 +308,7 @@ class BoundAdminThumbnailView(AdminThumbnailView): raise Exception('The property {0} is not defined on {1}.'.format( self.model_instance, self.image_field)) - original_image = getattr(thumbnail, '_imgfield', None) or thumbnail + original_image = getattr(thumbnail, 'source_file', None) or thumbnail template = self.template or 'imagekit/admin/thumbnail.html' return render_to_string(template, { From 6ae4f56ed7a7b99d529dbcae551b727737d81b4f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 24 Sep 2011 01:57:50 -0400 Subject: [PATCH 43/68] Eliminate repetition in File objects. --- imagekit/models.py | 76 +++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 679e57e..e8a92dd 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -31,6 +31,8 @@ class _ImageSpecMixin(object): class ImageSpec(_ImageSpecMixin): + _upload_to_attr = 'cache_to' + cache_to = None """Specifies the filename to use when saving the image cache file. This is modeled after ImageField's `upload_to` and can accept either a string @@ -88,7 +90,34 @@ def _get_suggested_extension(name, format): return extension -class ImageSpecFile(object): +class _ImageSpecFileMixin(object): + + def _process_content(self, filename, content): + img = Image.open(content) + original_format = img.format + img = self.field.process(img, self) + + # Determine the format. + format = self.field.format + if not format: + if callable(getattr(self.field, self.field._upload_to_attr)): + # The extension is explicit, so assume they want the matching format. + extension = os.path.splitext(filename)[1].lower() + # Try to guess the format from the extension. + format = Image.EXTENSION.get(extension) + format = format or img.format or original_format or 'JPEG' + + if format != 'JPEG': + imgfile = img_to_fobj(img, format) + else: + imgfile = img_to_fobj(img, format, + quality=int(self.field.quality), + optimize=True) + content = ContentFile(imgfile.read()) + return img, content + + +class ImageSpecFile(_ImageSpecFileMixin): def __init__(self, instance, field, attname, source_file): self.field = field self._img = None @@ -111,32 +140,15 @@ class ImageSpecFile(object): if not img and self.storage.exists(self.name): img = Image.open(self.file) if not img and self.source_file: - # process the original image file + # Process the original image file try: fp = self.source_file.storage.open(self.source_file.name) except IOError: return fp.seek(0) fp = StringIO(fp.read()) - img = self.field.process(Image.open(fp), self) - # Determine the format. - format = self.field.format - if not format: - # Get the real (not suggested) extension. - extension = os.path.splitext(self.name)[1].lower() - # Try to guess the format from the extension. - format = Image.EXTENSION.get(extension) - format = format or img.format or 'JPEG' - - # save the new image to the cache - if format != 'JPEG': - imgfile = img_to_fobj(img, format) - else: - imgfile = img_to_fobj(img, format, - quality=int(self.field.quality), - optimize=True) - content = ContentFile(imgfile.read()) + img, content = self._process_content(self.name, fp) self._file = self.storage.save(self.name, content) else: # TODO: Should we error here or something if the imagefield doesn't exist? @@ -322,33 +334,15 @@ class BoundAdminThumbnailView(AdminThumbnailView): return self -class ProcessedImageFieldFile(ImageFieldFile): +class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFileMixin): def save(self, name, content, save=True): new_filename = self.field.generate_filename(self.instance, name) - img = Image.open(content) - img = self.field.process(img, self) - format = self._get_format(new_filename, img.format) - if format != 'JPEG': - imgfile = img_to_fobj(img, format) - else: - imgfile = img_to_fobj(img, format, - quality=int(self.field.quality), - optimize=True) - content = ContentFile(imgfile.read()) + img, content = self._process_content(new_filename, content) return super(ProcessedImageFieldFile, self).save(name, content, save) - def _get_format(self, name, fallback): - format = self.field.format - if not format: - if callable(self.field.upload_to): - # The extension is explicit, so assume they want the matching format. - extension = os.path.splitext(name)[1].lower() - # Try to guess the format from the extension. - format = Image.EXTENSION.get(extension) - return format or fallback or 'JPEG' - class ProcessedImageField(models.ImageField, _ImageSpecMixin): + _upload_to_attr = 'upload_to' attr_class = ProcessedImageFieldFile def __init__(self, processors=None, quality=70, format=None, From c694ba644fb81bb9d1cee33f68c772cd76b0955a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 24 Sep 2011 01:58:19 -0400 Subject: [PATCH 44/68] Fix filename formatting. --- imagekit/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/models.py b/imagekit/models.py index e8a92dd..1665f86 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -358,4 +358,4 @@ class ProcessedImageField(models.ImageField, _ImageSpecMixin): filename = os.path.normpath(self.storage.get_valid_name(os.path.basename(filename))) name, ext = os.path.splitext(filename) ext = _get_suggested_extension(filename, self.format) - return '{0}.{1}'.format(name, ext) + return '{0}{1}'.format(name, ext) From f570bd0d7f57004550f54702c06fea1226f5be41 Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Sat, 24 Sep 2011 23:09:49 -0400 Subject: [PATCH 45/68] Transpose processor now supports auto EXIF orientation. Replaced calls to Image.open with an open_image utility function. The open_image utility calls Image.open, but then wraps the opened Image's copy method with a version that preserves EXIF data. This allows an ImageSpec to copy the original image, yet still provide all the original image's exif data to the processor pipeline. --- imagekit/models.py | 6 +++--- imagekit/processors.py | 2 -- imagekit/utils.py | 28 +++++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 1665f86..8d6d994 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -2,7 +2,7 @@ import os import datetime from StringIO import StringIO from imagekit.lib import * -from imagekit.utils import img_to_fobj, get_spec_files +from imagekit.utils import img_to_fobj, get_spec_files, open_image from imagekit.processors import ProcessorPipeline from django.conf import settings from django.core.files.base import ContentFile @@ -93,7 +93,7 @@ def _get_suggested_extension(name, format): class _ImageSpecFileMixin(object): def _process_content(self, filename, content): - img = Image.open(content) + img = open_image(content) original_format = img.format img = self.field.process(img, self) @@ -138,7 +138,7 @@ class ImageSpecFile(_ImageSpecFileMixin): if lazy: img = self._img if not img and self.storage.exists(self.name): - img = Image.open(self.file) + img = open_image(self.file) if not img and self.source_file: # Process the original image file try: diff --git a/imagekit/processors.py b/imagekit/processors.py index 3ec7be3..1e22ec5 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -224,11 +224,9 @@ class Transpose(ImageProcessor): def process(self, img): if self.AUTO in self.methods: - raise Exception("AUTO is not yet supported. Sorry!") try: orientation = img._getexif()[0x0112] ops = self._EXIF_ORIENTATION_STEPS[orientation] - print 'GOT %s >>>> %s' % (orientation, ops) except AttributeError: ops = [] else: diff --git a/imagekit/utils.py b/imagekit/utils.py index b2be22d..29d9da2 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,6 +1,9 @@ """ ImageKit utility functions """ -import tempfile +import tempfile, types +from django.utils.functional import wraps +from lib import Image + def img_to_fobj(img, format, **kwargs): tmp = tempfile.TemporaryFile() @@ -20,3 +23,26 @@ def get_spec_files(instance): if isinstance(value, ImageSpecFile): spec_files.append(value) return spec_files + + +def open_image(target): + img = Image.open(target) + img.copy = types.MethodType(_wrap_copy(img.copy), img, img.__class__) + return img + + +def _wrap_copy(f): + @wraps(f) + def copy(self): + img = f() + try: + img.app = self.app + except AttributeError: + pass + try: + img._getexif = self._getexif + except AttributeError: + pass + return img + return copy + From c00ea10b0a700e69003b860b5dbcc337d7b00e4d Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Sun, 25 Sep 2011 12:42:27 -0400 Subject: [PATCH 46/68] Bound fields are now cached on the model instance. ImageSpecFile and AdminThumbnailView are created on the first access, and then assigned as properties of the model instance for subsequent access. --- imagekit/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 8d6d994..03997ce 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -263,8 +263,10 @@ class _ImageSpecDescriptor(object): if instance is None: return self.field else: - return ImageSpecFile(instance, self.field, self.attname, - self._get_image_field_file(instance)) + img_spec_file = ImageSpecFile(instance, self.field, + self.attname, self._get_image_field_file(instance)) + setattr(instance, self.attname, img_spec_file) + return img_spec_file def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs): From 51144daeb6cdafe99211108327f8ca9a14ca1eb0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 25 Sep 2011 16:18:52 -0400 Subject: [PATCH 47/68] Fix bug Eric's fixes in c00ea10b0a700e69003b860b5dbcc337d7b00e4d meant that some of the code that already existed to reuse objects would actually be run for the first time. Naturally, there were some bugs; namely, I was storing a filename (instead of a File object) in `_file` and a bad else clause. --- imagekit/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 03997ce..bf6007b 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -139,7 +139,7 @@ class ImageSpecFile(_ImageSpecFileMixin): img = self._img if not img and self.storage.exists(self.name): img = open_image(self.file) - if not img and self.source_file: + if not img and self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? # Process the original image file try: fp = self.source_file.storage.open(self.source_file.name) @@ -149,10 +149,7 @@ class ImageSpecFile(_ImageSpecFileMixin): fp = StringIO(fp.read()) img, content = self._process_content(self.name, fp) - self._file = self.storage.save(self.name, content) - else: - # TODO: Should we error here or something if the imagefield doesn't exist? - img = None + self.storage.save(self.name, content) self._img = img return self._img From d6632c95f59e7b57ccd9aedf630c3b1efaf0b292 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 25 Sep 2011 20:38:43 -0400 Subject: [PATCH 48/68] Documentation! --- docs/Makefile | 153 ++++++++++++++ docs/apireference.rst | 15 ++ docs/conf.py | 243 +++++++++++++++++++++++ docs/index.rst | 129 ++++++++++++ docs/make.bat | 190 ++++++++++++++++++ imagekit/__init__.py | 9 - imagekit/management/commands/__init__.py | 1 - imagekit/management/commands/ikflush.py | 7 +- imagekit/models.py | 77 ++++--- imagekit/processors.py | 99 +++++++-- 10 files changed, 866 insertions(+), 57 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/apireference.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..5e9e72d --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python $(shell which sphinx-build) +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ImageKit.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ImageKit.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/ImageKit" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ImageKit" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/apireference.rst b/docs/apireference.rst new file mode 100644 index 0000000..b203568 --- /dev/null +++ b/docs/apireference.rst @@ -0,0 +1,15 @@ +API Reference +============= + + +:mod:`models` Module +-------------------- + +.. automodule:: imagekit.models + :members: + +:mod:`processors` Module +------------------------ + +.. automodule:: imagekit.processors + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..2f32935 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# +# ImageKit documentation build configuration file, created by +# sphinx-quickstart on Sun Sep 25 17:05:55 2011. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'ImageKit' +copyright = u'2011, Matthew Tretter' + +# 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 = '1.0alpha' +# The full version, including alpha/beta/rc tags. +release = '1.0alpha' + +# 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 = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ImageKitdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('imagekit', 'ImageKit.tex', u'ImageKit Documentation', + u'Matthew Tretter', '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 + +# 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 = [ + ('imagekit', 'imagekit', u'ImageKit Documentation', + [u'Matthew Tretter'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('imagekit', 'ImageKit', u'ImageKit Documentation', u'Matthew Tretter', + 'ImageKit', 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +autoclass_content = 'both' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..2a0c3cf --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,129 @@ +Getting Started +=============== + +ImageKit is a Django app that helps you to add variations of uploaded images to +your models. These variations are called "specs" and can include things like +different sizes (e.g. thumbnails) and black and white versions. + + +Adding Specs to a Model +----------------------- + +Much like :class:`django.db.models.ImageField`, Specs are defined as properties +of a model class:: + + from django.db import models + from imagekit.models import ImageSpec + + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + formatted_image = ImageSpec(image_field='original_image', format='JPEG', + quality=90) + +Accessing the spec through a model instance will create the image and return an +ImageFile-like object (just like with a normal +:class:`django.db.models.ImageField`):: + + photo = Photo.objects.all()[0] + photo.original_image.url # > '/media/photos/birthday.tiff' + photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg' + +Check out :class:`imagekit.models.ImageSpec` for more information. + + +Processors +---------- + +The real power of ImageKit comes from processors. Processors take an image, do +something to it, and return the result. By providing a list of processors to +your spec, you can expose different versions of the original image:: + + from django.db import models + from imagekit.models import ImageSpec + from imagekit.processors import Crop, Adjust + + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)], + image_field='original_image', format='JPEG', quality=90) + +The ``thumbnail`` property will now return a cropped image:: + + photo = Photo.objects.all()[0] + photo.thumbnail.url # > '/media/cache/photos/birthday_thumbnail.jpeg' + photo.thumbnail.width # > 50 + photo.original_image.width # > 1000 + +The original image is not modified; ``thumbnail`` is a new file that is the +result of running the :class:`imagekit.processors.Crop` processor on the +original. + +The :mod:`imagekit.processors` module contains processors for many common +image manipulations, like resizing, rotating, and color adjustments. However, if +they aren't up to the task, you can create your own. All you have to do is +implement a ``process()`` method:: + + class Watermark(object): + def process(self, image): + # Code for adding the watermark goes here. + return image + + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + watermarked_image = ImageSpec([Watermark()], image_field='original_image', + format='JPEG', quality=90) + + +Admin +----- + +ImageKit also contains a class named :class:`imagekit.models.AdminThumbnailView` +for displaying specs (or even regular ImageFields) in the +`Django admin change list`__. Like :class:`imagekit.models.ImageSpec`, +AdminThumbnailView is used as a property on Django model classes:: + + from django.db import models + from imagekit.models import ImageSpec + from imagekit.processors import Crop, AdminThumbnailView + + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + thumbnail = ImageSpec([Crop(50, 50)], image_field='original_image') + admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') + +You can then then add this property to the `list_display`__ field of your admin +class:: + + from django.contrib import admin + from .models import Photo + + + class PhotoAdmin(admin.ModelAdmin): + list_display = ('__str__', 'admin_thumbnail_view') + + + admin.site.register(Photo, PhotoAdmin) + +AdminThumbnailView can even use a custom template. For more information, see +:class:`imagekit.models.AdminThumbnailView`. + + +Commands +-------- + +.. automodule:: imagekit.management.commands.ikflush + + + + + +Digging Deeper +-------------- + +.. toctree:: + + apireference + + +__ https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list +__ https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..aad0d30 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +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. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + 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 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + 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 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ImageKit.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ImageKit.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + 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 + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 0540878..9a68e39 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,11 +1,2 @@ -""" -Django ImageKit - -Author: Justin Driscoll -Version: 0.3.6 - -""" __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett' __version__ = (0, 3, 6) - - diff --git a/imagekit/management/commands/__init__.py b/imagekit/management/commands/__init__.py index 8b13789..e69de29 100644 --- a/imagekit/management/commands/__init__.py +++ b/imagekit/management/commands/__init__.py @@ -1 +0,0 @@ - diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index fd26dd4..b1799cc 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -1,3 +1,7 @@ +"""Flushes the cached ImageKit images. + +""" + from django.db.models.loading import cache from django.core.management.base import BaseCommand, CommandError from optparse import make_option @@ -14,9 +18,6 @@ class Command(BaseCommand): return flush_cache(args, options) def flush_cache(apps, options): - """ Clears the image cache - - """ apps = [a.strip(',') for a in apps] if apps: for app_label in apps: diff --git a/imagekit/models.py b/imagekit/models.py index bf6007b..e3471f2 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -30,30 +30,45 @@ class _ImageSpecMixin(object): class ImageSpec(_ImageSpecMixin): - - _upload_to_attr = 'cache_to' - - cache_to = None - """Specifies the filename to use when saving the image cache file. This is - modeled after ImageField's `upload_to` and can accept either a string - (that specifies a directory) or a callable (that returns a filepath). - Callable values should accept the following arguments: - - instance -- the model instance this spec belongs to - path -- the path of the original image - specname -- the property name that the spec is bound to on the model instance - extension -- a recommended extension. If the format of the spec is set - explicitly, this suggestion will be based on that format. if not, - the extension of the original file will be passed. You do not have - to use this extension, it's only a recommendation. - - If you have not explicitly set a format on your ImageSpec, the extension of - the path returned by this function will be used to infer one. + """The heart and soul of the ImageKit library, ImageSpec allows you to add + variants of uploaded images to your models. """ + _upload_to_attr = 'cache_to' + def __init__(self, processors=None, quality=70, format=None, image_field=None, pre_cache=False, storage=None, cache_to=None): + """ + :param processors: A list of processors to run on the original image. + :param quality: The quality of the output image. This option is only + used for the JPEG format. + :param format: The format of the output file. If not provided, ImageSpec + will try to guess the appropriate format based on the extension + of the filename and the format of the input image. + :param image_field: The name of the model property that contains the + original image. + :param pre_cache: A boolean that specifies whether the image should be + generated immediately (True) or on demand (False). + :param storage: A Django storage system to use to save the generated + image. + :param cache_to: Specifies the filename to use when saving the image + cache file. This is modeled after ImageField's ``upload_to`` and + can be either a string (that specifies a directory) or a + callable (that returns a filepath). Callable values should + accept the following arguments: + + - instance -- The model instance this spec belongs to + - path -- The path of the original image + - specname -- the property name that the spec is bound to on + the model instance + - extension -- A recommended extension. If the format of the + spec is set explicitly, this suggestion will be + based on that format. if not, the extension of the + original file will be passed. You do not have to use + this extension, it's only a recommendation. + + """ _ImageSpecMixin.__init__(self, processors, quality=quality, format=format) @@ -285,15 +300,18 @@ def _post_delete_handler(sender, instance=None, **kwargs): class AdminThumbnailView(object): + """A convenience utility for adding thumbnails to the Django admin change + list. + + """ short_description = _('Thumbnail') allow_tags = True def __init__(self, image_field, template=None): """ - Keyword arguments: - image_field -- the name of the ImageField or ImageSpec on the model to - use for the thumbnail. - template -- the template with which to render the thumbnail + :param image_field: The name of the ImageField or ImageSpec on the model + to use for the thumbnail. + :param template: The template with which to render the thumbnail """ self.image_field = image_field @@ -341,12 +359,25 @@ class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFileMixin): class ProcessedImageField(models.ImageField, _ImageSpecMixin): + """ProcessedImageField is an ImageField that runs processors on the uploaded + image *before* saving it to storage. This is in contrast to specs, which + maintain the original. Useful for coercing fileformats or keeping images + within a reasonable size. + + """ _upload_to_attr = 'upload_to' attr_class = ProcessedImageFieldFile def __init__(self, processors=None, quality=70, format=None, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): + """ + The ProcessedImageField constructor accepts all of the arguments that + the :class:`django.db.models.ImageField` constructor accepts, as well as + the ``processors``, ``format``, and ``quality`` arguments of + :class:`imagekit.models.ImageSpec`. + + """ _ImageSpecMixin.__init__(self, processors, quality=quality, format=format) diff --git a/imagekit/processors.py b/imagekit/processors.py index 1e22ec5..38f374d 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -10,16 +10,16 @@ from imagekit.lib import * class ImageProcessor(object): - """ Base image processor class """ - def process(self, img): return img class ProcessorPipeline(ImageProcessor, list): - """A processor that just runs a bunch of other processors. This class allows - any object that knows how to deal with a single processor to deal with a - list of them. + """A :class:`list` of other processors. This class allows any object that + knows how to deal with a single processor to deal with a list of them. + For example:: + + processed_image = ProcessorPipeline([ProcessorA(), ProcessorB()]).process(image) """ def process(self, img): @@ -29,8 +29,28 @@ class ProcessorPipeline(ImageProcessor, list): class Adjust(ImageProcessor): - + """Performs color, brightness, contrast, and sharpness enhancements on the + image. See :mod:`PIL.ImageEnhance` for more imformation. + + """ def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0): + """ + :param color: A number between 0 and 1 that specifies the saturation of + the image. 0 corresponds to a completely desaturated image + (black and white) and 1 to the original color. + See :class:`PIL.ImageEnhance.Color` + :param brightness: A number representing the brightness; 0 results in a + completely black image whereas 1 corresponds to the brightness + of the original. See :class:`PIL.ImageEnhance.Brightness` + :param contrast: A number representing the contrast; 0 results in a + completely gray image whereas 1 corresponds to the contrast of + the original. See :class:`PIL.ImageEnhance.Contrast` + :param sharpness: A number representing the sharpness; 0 results in a + blurred image; 1 corresponds to the original sharpness; 2 + results in a sharpened image. See + :class:`PIL.ImageEnhance.Sharpness` + + """ self.color = color self.brightness = brightness self.contrast = contrast @@ -49,6 +69,9 @@ class Adjust(ImageProcessor): class Reflection(ImageProcessor): + """Creates an image with a reflection. + + """ background_color = '#FFFFFF' size = 0.0 opacity = 0.6 @@ -104,6 +127,9 @@ class _Resize(ImageProcessor): class Crop(_Resize): + """Resizes an image , cropping it to the specified width and height. + + """ TOP_LEFT = 'tl' TOP = 't' @@ -128,6 +154,23 @@ class Crop(_Resize): } def __init__(self, width=None, height=None, anchor=None): + """ + :param width: The target width, in pixels. + :param height: The target height, in pixels. + :param anchor: Specifies which part of the image should be retained when + cropping. Valid values are: + + - Crop.TOP_LEFT + - Crop.TOP + - Crop.TOP_RIGHT + - Crop.LEFT + - Crop.CENTER + - Crop.RIGHT + - Crop.BOTTOM_LEFT + - Crop.BOTTOM + - Crop.BOTTOM_RIGHT + + """ super(Crop, self).__init__(width, height) self.anchor = anchor @@ -155,7 +198,19 @@ class Crop(_Resize): class Fit(_Resize): + """Resizes an image to fit within the specified dimensions. + + """ + def __init__(self, width=None, height=None, upscale=None): + """ + :param width: The maximum width of the desired image. + :param height: The maximum height of the desired image. + :param upscale: A boolean value specifying whether the image should be + enlarged if its dimensions are smaller than the target + dimensions. + + """ super(Fit, self).__init__(width, height) self.upscale = upscale @@ -182,21 +237,6 @@ class Fit(_Resize): class Transpose(ImageProcessor): """ Rotates or flips the image - Possible arguments: - - Transpose.AUTO - - Transpose.FLIP_HORIZONTAL - - Transpose.FLIP_VERTICAL - - Transpose.ROTATE_90 - - Transpose.ROTATE_180 - - Transpose.ROTATE_270 - - The order of the arguments dictates the order in which the Transposition - steps are taken. - - If Transpose.AUTO is present, all other arguments are ignored, and the - processor will attempt to rotate the image according to the - EXIF Orientation data. - """ AUTO = 'auto' FLIP_HORIZONTAL = Image.FLIP_LEFT_RIGHT @@ -218,6 +258,23 @@ class Transpose(ImageProcessor): } def __init__(self, *args): + """ + Possible arguments: + - Transpose.AUTO + - Transpose.FLIP_HORIZONTAL + - Transpose.FLIP_VERTICAL + - Transpose.ROTATE_90 + - Transpose.ROTATE_180 + - Transpose.ROTATE_270 + + The order of the arguments dictates the order in which the Transposition + steps are taken. + + If Transpose.AUTO is present, all other arguments are ignored, and the + processor will attempt to rotate the image according to the + EXIF Orientation data. + + """ super(Transpose, self).__init__() if args: self.methods = args From 6adbef3475675f261afa42f9d103ae4dd3adc182 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 25 Sep 2011 23:24:44 -0400 Subject: [PATCH 49/68] Embracing duck typing. ImageProcessor didn't do anything. I'd rather get it out of there to reduce the temptation for future IK contributors to do type checking and mess up peoples' custom processors. --- imagekit/processors.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/imagekit/processors.py b/imagekit/processors.py index 38f374d..4c9c88a 100644 --- a/imagekit/processors.py +++ b/imagekit/processors.py @@ -1,6 +1,6 @@ -""" Imagekit Image "ImageProcessors" +"""Imagekit image processors. -A processor accepts an image, does some stuff, and returns a new image. +A processor accepts an image, does some stuff, and returns the result. Processors can do anything with the image you want, but their responsibilities should be limited to image manipulations--they should be completely decoupled from both the filesystem and the ORM. @@ -9,12 +9,7 @@ from both the filesystem and the ORM. from imagekit.lib import * -class ImageProcessor(object): - def process(self, img): - return img - - -class ProcessorPipeline(ImageProcessor, list): +class ProcessorPipeline(list): """A :class:`list` of other processors. This class allows any object that knows how to deal with a single processor to deal with a list of them. For example:: @@ -28,7 +23,7 @@ class ProcessorPipeline(ImageProcessor, list): return img -class Adjust(ImageProcessor): +class Adjust(object): """Performs color, brightness, contrast, and sharpness enhancements on the image. See :mod:`PIL.ImageEnhance` for more imformation. @@ -68,7 +63,7 @@ class Adjust(ImageProcessor): return img -class Reflection(ImageProcessor): +class Reflection(object): """Creates an image with a reflection. """ @@ -111,7 +106,7 @@ class Reflection(ImageProcessor): return composite -class _Resize(ImageProcessor): +class _Resize(object): width = None height = None @@ -234,7 +229,7 @@ class Fit(_Resize): return img -class Transpose(ImageProcessor): +class Transpose(object): """ Rotates or flips the image """ From 32c6f0c05fa16347684691c9c2f033b5000ad52a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sun, 25 Sep 2011 23:38:23 -0400 Subject: [PATCH 50/68] Installation instructions. --- docs/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 2a0c3cf..78af5ec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,14 @@ your models. These variations are called "specs" and can include things like different sizes (e.g. thumbnails) and black and white versions. +Installation +------------ + +1. ``pip install django-imagekit`` + (or clone the source and put the imagekit module on your path) +2. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py + + Adding Specs to a Model ----------------------- From 9b9d9ec1c40a94483a1ffd478e7d932ae0cdcba8 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 26 Sep 2011 00:06:05 -0400 Subject: [PATCH 51/68] Typo fix. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 78af5ec..13d707b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,7 +24,7 @@ of a model class:: from imagekit.models import ImageSpec class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') + original_image = models.ImageField(upload_to='photos') formatted_image = ImageSpec(image_field='original_image', format='JPEG', quality=90) From 6751d785040351f1811a82e45836a91e65ce32b7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 26 Sep 2011 11:36:08 -0400 Subject: [PATCH 52/68] More docs edits. Added us to the authors. Reorganized some of the documentation so that sphinx and the project landing page on github can share it. --- AUTHORS | 39 +++++--- README.rst | 210 +++++++++++++++++++------------------------ docs/conf.py | 2 +- docs/index.rst | 121 +------------------------ imagekit/__init__.py | 2 +- 5 files changed, 124 insertions(+), 250 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5d6d4c3..82f8157 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,16 +1,29 @@ -Original Author: +ImageKit was originally written by `Justin Driscoll`_. -* Justin Driscoll (jdriscoll) +The field-based API was written by the bright minds at HZDG_. + +Maintainers +~~~~~~~~~~~ + +* `Bryan Veloso`_ +* `Chris Drackett`_ +* `Greg Newman`_ + +Contributors +~~~~~~~~~~~~ + +* `Josh Ourisman`_ +* `Jonathan Slenders`_ +* `Matthew Tretter`_ +* `Eric Eldredge`_ -Maintainers: - -* Bryan Veloso (bryanveloso) -* Chris Drackett (chrisdrackett) -* Greg Newman (gregnewman) - - -Contributors: - -* Josh Ourisman (joshourisman) -* Jonathan Slenders (jonathanslenders) +.. _Justin Driscoll: http://github.com/jdriscoll +.. _HZDG: http://hzdg.com +.. _Bryan Veloso: http://github.com/bryanveloso +.. _Chris Drackett: http://github.com/chrisdrackett +.. _Greg Newman: http://github.com/gregnewman +.. _Josh Ourisman: http://github.com/joshourisman +.. _Jonathan Slenders: http://github.com/jonathanslenders +.. _Matthew Tretter: http://github.com/matthewwithanm +.. _Eric Eldredge: http://github.com/lettertwo diff --git a/README.rst b/README.rst index abf2e43..5c10590 100644 --- a/README.rst +++ b/README.rst @@ -1,143 +1,117 @@ -=============== -django-imagekit -=============== +ImageKit is a Django app that helps you to add variations of uploaded images to +your models. These variations are called "specs" and can include things like +different sizes (e.g. thumbnails) and black and white versions. -ImageKit In 6 Steps -=================== -Step 1 -****** +Installation +------------ -:: +1. ``pip install django-imagekit`` + (or clone the source and put the imagekit module on your path) +2. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py - $ pip install django-imagekit -(or clone the source and put the imagekit module on your path) +Adding Specs to a Model +----------------------- -Step 2 -****** - -Create an ImageModel subclass and add specs to it. - -:: - - # myapp/models.py +Much like ``django.db.models.ImageField``, Specs are defined as properties +of a model class:: from django.db import models - from imagekit.models import ImageModel - from imagekit.specs import ImageSpec - from imagekit.processors import Crop, Fit, Adjust + from imagekit.models import ImageSpec - class Photo(ImageModel): - name = models.CharField(max_length=100) + class Photo(models.Model): original_image = models.ImageField(upload_to='photos') - num_views = models.PositiveIntegerField(editable=False, default=0) + formatted_image = ImageSpec(image_field='original_image', format='JPEG', + quality=90) - thumbnail_image = ImageSpec([Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)], quality=90, pre_cache=True, image_field='original_image', cache_to='cache/photos/thumbnails/') - display = ImageSpec([Fit(600)], quality=90, increment_count=True, image_field='original_image', cache_to='cache/photos/display/', save_count_as='num_views') +Accessing the spec through a model instance will create the image and return an +ImageFile-like object (just like with a normal +``django.db.models.ImageField``):: + + photo = Photo.objects.all()[0] + photo.original_image.url # > '/media/photos/birthday.tiff' + photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg' + +Check out ``imagekit.models.ImageSpec`` for more information. -Of course, you don't have to define your ImageSpecs inline if you don't want to: +Processors +---------- -:: - - # myapp/specs.py - - from imagekit.specs import ImageSpec - from imagekit.processors import Crop, Fit, Adjust - - class _BaseSpec(ImageSpec): - quality = 90 - image_field = 'original_image' - - class DisplaySpec(_BaseSpec): - pre_cache = True - increment_count = True - save_count_as = 'num_views' - processors = [Fit(600)] - cache_to = 'cache/photos/display/' - - class ThumbnailSpec(_BaseSpec): - processors = [Crop(100, 75), Adjust(contrast=1.2, sharpness=1.1)] - cache_to = 'cache/photos/thumbnails/' - - # myapp/models.py +The real power of ImageKit comes from processors. Processors take an image, do +something to it, and return the result. By providing a list of processors to +your spec, you can expose different versions of the original image:: from django.db import models - from imagekit.models import ImageModel - from myapp.specs import DisplaySpec, ThumbnailSpec + from imagekit.models import ImageSpec + from imagekit.processors import Crop, Adjust - class Photo(ImageModel): - name = models.CharField(max_length=100) - original_image = models.ImageField(upload_to='photos') - num_views = models.PositiveIntegerField(editable=False, default=0) + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)], + image_field='original_image', format='JPEG', quality=90) - thumbnail_image = ThumbnailSpec() - display = DisplaySpec() - +The ``thumbnail`` property will now return a cropped image:: -Step 3 -****** + photo = Photo.objects.all()[0] + photo.thumbnail.url # > '/media/cache/photos/birthday_thumbnail.jpeg' + photo.thumbnail.width # > 50 + photo.original_image.width # > 1000 -Flush the cache and pre-generate thumbnails (ImageKit has to be added to ``INSTALLED_APPS`` for management command to work). +The original image is not modified; ``thumbnail`` is a new file that is the +result of running the ``imagekit.processors.Crop`` processor on the +original. -:: +The ``imagekit.processors`` module contains processors for many common +image manipulations, like resizing, rotating, and color adjustments. However, if +they aren't up to the task, you can create your own. All you have to do is +implement a ``process()`` method:: - $ python manage.py ikflush myapp + class Watermark(object): + def process(self, image): + # Code for adding the watermark goes here. + return image -Step 4 -****** - -Use your new model in templates. - -:: - -
- {{ photo.name }} -
- -
- {{ photo.name }} -
- -
- {% for p in photos %} - {{ p.name }} - {% endfor %} -
- -Step 5 -****** - -Play with the API. - -:: - - >>> from myapp.models import Photo - >>> p = Photo.objects.all()[0] - - >>> p.display.url - u'/static/photos/myphoto_display.jpg' - >>> p.display.width - 600 - >>> p.display.height - 420 - >>> p.display.image - - >>> p.display.file - - >>> p.display.spec - - -Step 6 -****** - -Enjoy a nice beverage. - -:: - - from refrigerator import beer - - beer.enjoy() + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + watermarked_image = ImageSpec([Watermark()], image_field='original_image', + format='JPEG', quality=90) +Admin +----- + +ImageKit also contains a class named ``imagekit.models.AdminThumbnailView`` +for displaying specs (or even regular ImageFields) in the +`Django admin change list`__. Like ``imagekit.models.ImageSpec``, +AdminThumbnailView is used as a property on Django model classes:: + + from django.db import models + from imagekit.models import ImageSpec + from imagekit.processors import Crop, AdminThumbnailView + + class Photo(models.Model): + original_image = models.ImageField(upload_to'photos') + thumbnail = ImageSpec([Crop(50, 50)], image_field='original_image') + admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') + +You can then then add this property to the `list_display`__ field of your admin +class:: + + from django.contrib import admin + from .models import Photo + + + class PhotoAdmin(admin.ModelAdmin): + list_display = ('__str__', 'admin_thumbnail_view') + + + admin.site.register(Photo, PhotoAdmin) + +AdminThumbnailView can even use a custom template. For more information, see +``imagekit.models.AdminThumbnailView``. + + +__ https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list +__ https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display diff --git a/docs/conf.py b/docs/conf.py index 2f32935..4588a69 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -153,7 +153,7 @@ html_static_path = ['_static'] #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +html_show_copyright = False # 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 diff --git a/docs/index.rst b/docs/index.rst index 13d707b..5bc1fba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,119 +1,7 @@ Getting Started =============== -ImageKit is a Django app that helps you to add variations of uploaded images to -your models. These variations are called "specs" and can include things like -different sizes (e.g. thumbnails) and black and white versions. - - -Installation ------------- - -1. ``pip install django-imagekit`` - (or clone the source and put the imagekit module on your path) -2. Add ``'imagekit'`` to your ``INSTALLED_APPS`` list in your project's settings.py - - -Adding Specs to a Model ------------------------ - -Much like :class:`django.db.models.ImageField`, Specs are defined as properties -of a model class:: - - from django.db import models - from imagekit.models import ImageSpec - - class Photo(models.Model): - original_image = models.ImageField(upload_to='photos') - formatted_image = ImageSpec(image_field='original_image', format='JPEG', - quality=90) - -Accessing the spec through a model instance will create the image and return an -ImageFile-like object (just like with a normal -:class:`django.db.models.ImageField`):: - - photo = Photo.objects.all()[0] - photo.original_image.url # > '/media/photos/birthday.tiff' - photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg' - -Check out :class:`imagekit.models.ImageSpec` for more information. - - -Processors ----------- - -The real power of ImageKit comes from processors. Processors take an image, do -something to it, and return the result. By providing a list of processors to -your spec, you can expose different versions of the original image:: - - from django.db import models - from imagekit.models import ImageSpec - from imagekit.processors import Crop, Adjust - - class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') - thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)], - image_field='original_image', format='JPEG', quality=90) - -The ``thumbnail`` property will now return a cropped image:: - - photo = Photo.objects.all()[0] - photo.thumbnail.url # > '/media/cache/photos/birthday_thumbnail.jpeg' - photo.thumbnail.width # > 50 - photo.original_image.width # > 1000 - -The original image is not modified; ``thumbnail`` is a new file that is the -result of running the :class:`imagekit.processors.Crop` processor on the -original. - -The :mod:`imagekit.processors` module contains processors for many common -image manipulations, like resizing, rotating, and color adjustments. However, if -they aren't up to the task, you can create your own. All you have to do is -implement a ``process()`` method:: - - class Watermark(object): - def process(self, image): - # Code for adding the watermark goes here. - return image - - class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') - watermarked_image = ImageSpec([Watermark()], image_field='original_image', - format='JPEG', quality=90) - - -Admin ------ - -ImageKit also contains a class named :class:`imagekit.models.AdminThumbnailView` -for displaying specs (or even regular ImageFields) in the -`Django admin change list`__. Like :class:`imagekit.models.ImageSpec`, -AdminThumbnailView is used as a property on Django model classes:: - - from django.db import models - from imagekit.models import ImageSpec - from imagekit.processors import Crop, AdminThumbnailView - - class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') - thumbnail = ImageSpec([Crop(50, 50)], image_field='original_image') - admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') - -You can then then add this property to the `list_display`__ field of your admin -class:: - - from django.contrib import admin - from .models import Photo - - - class PhotoAdmin(admin.ModelAdmin): - list_display = ('__str__', 'admin_thumbnail_view') - - - admin.site.register(Photo, PhotoAdmin) - -AdminThumbnailView can even use a custom template. For more information, see -:class:`imagekit.models.AdminThumbnailView`. +.. include:: ../README.rst Commands @@ -122,7 +10,10 @@ Commands .. automodule:: imagekit.management.commands.ikflush +Authors +------- +.. include:: ../AUTHORS Digging Deeper @@ -131,7 +22,3 @@ Digging Deeper .. toctree:: apireference - - -__ https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list -__ https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display \ No newline at end of file diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 9a68e39..4ec94b1 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,2 +1,2 @@ -__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett' +__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' __version__ = (0, 3, 6) From da00e2a5da1733f330561af1587a923691d77325 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 26 Sep 2011 14:40:48 -0400 Subject: [PATCH 53/68] Moved Crop and Fit to resize module. Crop doesn't necessarily imply the any scaling is taking place. Several ideas were discussed, from renaming Crop to combining both processors into a single Resize processor (as they were in the original IK), but those solutions were felt to either precluded future extension (alternative resize modes) or make the API too verbose. --- README.rst | 13 +- docs/apireference.rst | 3 + .../{processors.py => processors/__init__.py} | 123 ----------------- imagekit/processors/resize.py | 124 ++++++++++++++++++ 4 files changed, 134 insertions(+), 129 deletions(-) rename imagekit/{processors.py => processors/__init__.py} (60%) create mode 100644 imagekit/processors/resize.py diff --git a/README.rst b/README.rst index 5c10590..b895761 100644 --- a/README.rst +++ b/README.rst @@ -45,12 +45,13 @@ your spec, you can expose different versions of the original image:: from django.db import models from imagekit.models import ImageSpec - from imagekit.processors import Crop, Adjust + from imagekit.processors import resize, Adjust class Photo(models.Model): original_image = models.ImageField(upload_to'photos') - thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)], - image_field='original_image', format='JPEG', quality=90) + thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), + resize.Crop(50, 50)], image_field='original_image', + format='JPEG', quality=90) The ``thumbnail`` property will now return a cropped image:: @@ -60,7 +61,7 @@ The ``thumbnail`` property will now return a cropped image:: photo.original_image.width # > 1000 The original image is not modified; ``thumbnail`` is a new file that is the -result of running the ``imagekit.processors.Crop`` processor on the +result of running the ``imagekit.processors.resize.Crop`` processor on the original. The ``imagekit.processors`` module contains processors for many common @@ -89,11 +90,11 @@ AdminThumbnailView is used as a property on Django model classes:: from django.db import models from imagekit.models import ImageSpec - from imagekit.processors import Crop, AdminThumbnailView + from imagekit.processors import resize, AdminThumbnailView class Photo(models.Model): original_image = models.ImageField(upload_to'photos') - thumbnail = ImageSpec([Crop(50, 50)], image_field='original_image') + thumbnail = ImageSpec([resize.Crop(50, 50)], image_field='original_image') admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') You can then then add this property to the `list_display`__ field of your admin diff --git a/docs/apireference.rst b/docs/apireference.rst index b203568..93ad370 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -13,3 +13,6 @@ API Reference .. automodule:: imagekit.processors :members: + +.. automodule:: imagekit.processors.resize + :members: diff --git a/imagekit/processors.py b/imagekit/processors/__init__.py similarity index 60% rename from imagekit/processors.py rename to imagekit/processors/__init__.py index 4c9c88a..e721876 100644 --- a/imagekit/processors.py +++ b/imagekit/processors/__init__.py @@ -106,129 +106,6 @@ class Reflection(object): return composite -class _Resize(object): - - width = None - height = None - - def __init__(self, width=None, height=None): - if width is not None: - self.width = width - if height is not None: - self.height = height - - def process(self, img): - raise NotImplementedError('process must be overridden by subclasses.') - - -class Crop(_Resize): - """Resizes an image , cropping it to the specified width and height. - - """ - - TOP_LEFT = 'tl' - TOP = 't' - TOP_RIGHT = 'tr' - BOTTOM_LEFT = 'bl' - BOTTOM = 'b' - BOTTOM_RIGHT = 'br' - CENTER = 'c' - LEFT = 'l' - RIGHT = 'r' - - _ANCHOR_PTS = { - TOP_LEFT: (0, 0), - TOP: (0.5, 0), - TOP_RIGHT: (1, 0), - LEFT: (0, 0.5), - CENTER: (0.5, 0.5), - RIGHT: (1, 0.5), - BOTTOM_LEFT: (0, 1), - BOTTOM: (0.5, 1), - BOTTOM_RIGHT: (1, 1), - } - - def __init__(self, width=None, height=None, anchor=None): - """ - :param width: The target width, in pixels. - :param height: The target height, in pixels. - :param anchor: Specifies which part of the image should be retained when - cropping. Valid values are: - - - Crop.TOP_LEFT - - Crop.TOP - - Crop.TOP_RIGHT - - Crop.LEFT - - Crop.CENTER - - Crop.RIGHT - - Crop.BOTTOM_LEFT - - Crop.BOTTOM - - Crop.BOTTOM_RIGHT - - """ - super(Crop, self).__init__(width, height) - self.anchor = anchor - - def process(self, img): - cur_width, cur_height = img.size - horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ - Crop.CENTER] - ratio = max(float(self.width)/cur_width, float(self.height)/cur_height) - resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) - crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) - x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) - box_left, box_right = { - 0: (0, self.width), - 0.5: (int(x_diff), int(x_diff + self.width)), - 1: (int(crop_x), int(resize_x)), - }[horizontal_anchor] - box_upper, box_lower = { - 0: (0, self.height), - 0.5: (int(y_diff), int(y_diff + self.height)), - 1: (int(crop_y), int(resize_y)), - }[vertical_anchor] - box = (box_left, box_upper, box_right, box_lower) - img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) - return img - - -class Fit(_Resize): - """Resizes an image to fit within the specified dimensions. - - """ - - def __init__(self, width=None, height=None, upscale=None): - """ - :param width: The maximum width of the desired image. - :param height: The maximum height of the desired image. - :param upscale: A boolean value specifying whether the image should be - enlarged if its dimensions are smaller than the target - dimensions. - - """ - super(Fit, self).__init__(width, height) - self.upscale = upscale - - def process(self, img): - cur_width, cur_height = img.size - if not self.width is None and not self.height is None: - ratio = min(float(self.width)/cur_width, - float(self.height)/cur_height) - else: - if self.width is None: - ratio = float(self.height)/cur_height - else: - ratio = float(self.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 self.upscale: - return img - img = img.resize(new_dimensions, Image.ANTIALIAS) - return img - - class Transpose(object): """ Rotates or flips the image diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py new file mode 100644 index 0000000..bc0729e --- /dev/null +++ b/imagekit/processors/resize.py @@ -0,0 +1,124 @@ +from imagekit.lib import * + + +class _Resize(object): + + width = None + height = None + + def __init__(self, width=None, height=None): + if width is not None: + self.width = width + if height is not None: + self.height = height + + def process(self, img): + raise NotImplementedError('process must be overridden by subclasses.') + + +class Crop(_Resize): + """Resizes an image , cropping it to the specified width and height. + + """ + + TOP_LEFT = 'tl' + TOP = 't' + TOP_RIGHT = 'tr' + BOTTOM_LEFT = 'bl' + BOTTOM = 'b' + BOTTOM_RIGHT = 'br' + CENTER = 'c' + LEFT = 'l' + RIGHT = 'r' + + _ANCHOR_PTS = { + TOP_LEFT: (0, 0), + TOP: (0.5, 0), + TOP_RIGHT: (1, 0), + LEFT: (0, 0.5), + CENTER: (0.5, 0.5), + RIGHT: (1, 0.5), + BOTTOM_LEFT: (0, 1), + BOTTOM: (0.5, 1), + BOTTOM_RIGHT: (1, 1), + } + + def __init__(self, width=None, height=None, anchor=None): + """ + :param width: The target width, in pixels. + :param height: The target height, in pixels. + :param anchor: Specifies which part of the image should be retained when + cropping. Valid values are: + + - Crop.TOP_LEFT + - Crop.TOP + - Crop.TOP_RIGHT + - Crop.LEFT + - Crop.CENTER + - Crop.RIGHT + - Crop.BOTTOM_LEFT + - Crop.BOTTOM + - Crop.BOTTOM_RIGHT + + """ + super(Crop, self).__init__(width, height) + self.anchor = anchor + + def process(self, img): + cur_width, cur_height = img.size + horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ + Crop.CENTER] + ratio = max(float(self.width)/cur_width, float(self.height)/cur_height) + resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) + crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) + x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) + box_left, box_right = { + 0: (0, self.width), + 0.5: (int(x_diff), int(x_diff + self.width)), + 1: (int(crop_x), int(resize_x)), + }[horizontal_anchor] + box_upper, box_lower = { + 0: (0, self.height), + 0.5: (int(y_diff), int(y_diff + self.height)), + 1: (int(crop_y), int(resize_y)), + }[vertical_anchor] + box = (box_left, box_upper, box_right, box_lower) + img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box) + return img + + +class Fit(_Resize): + """Resizes an image to fit within the specified dimensions. + + """ + + def __init__(self, width=None, height=None, upscale=None): + """ + :param width: The maximum width of the desired image. + :param height: The maximum height of the desired image. + :param upscale: A boolean value specifying whether the image should be + enlarged if its dimensions are smaller than the target + dimensions. + + """ + super(Fit, self).__init__(width, height) + self.upscale = upscale + + def process(self, img): + cur_width, cur_height = img.size + if not self.width is None and not self.height is None: + ratio = min(float(self.width)/cur_width, + float(self.height)/cur_height) + else: + if self.width is None: + ratio = float(self.height)/cur_height + else: + ratio = float(self.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 self.upscale: + return img + img = img.resize(new_dimensions, Image.ANTIALIAS) + return img From 6412e40c695c4239c0f77fee45e97434330d9b56 Mon Sep 17 00:00:00 2001 From: Chris McKenzie Date: Mon, 26 Sep 2011 16:38:55 -0400 Subject: [PATCH 54/68] adding test for new api --- imagekit/tests.py | 110 ++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 77 deletions(-) diff --git a/imagekit/tests.py b/imagekit/tests.py index 6745c7c..ba31797 100644 --- a/imagekit/tests.py +++ b/imagekit/tests.py @@ -1,49 +1,21 @@ import os import tempfile -import unittest -from django.conf import settings +import Image from django.core.files.base import ContentFile from django.db import models from django.test import TestCase - -from imagekit import processors -from imagekit.models import ImageModel -from imagekit.models import ImageSpec -from imagekit.lib import Image +from imagekit.models import ImageSpec, AdminThumbnailView +from imagekit.processors.resize import Crop +from imagekit.processors import Adjust -class ResizeToWidth(processors.Resize): - width = 100 - -class ResizeToHeight(processors.Resize): - height = 100 - -class ResizeToFit(processors.Resize): - width = 100 - height = 100 - -class ResizeCropped(ResizeToFit): - crop = ('center', 'center') - -class TestResizeToWidth(ImageSpec): - access_as = 'to_width' - processors = [ResizeToWidth] - -class TestResizeToHeight(ImageSpec): - access_as = 'to_height' - processors = [ResizeToHeight] - -class TestResizeCropped(ImageSpec): - access_as = 'cropped' - processors = [ResizeCropped] - -class TestPhoto(ImageModel): - """ Minimal ImageModel class for testing """ - image = models.ImageField(upload_to='images') - - class IKOptions: - spec_module = 'imagekit.tests' +class Photo(models.Model): + original_image = models.ImageField(upload_to='photos') + thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)], + image_field='original_image', format='JPEG', quality=90) + admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') + class IKTest(TestCase): """ Base TestCase class """ @@ -52,48 +24,32 @@ class IKTest(TestCase): Image.new('RGB', (800, 600)).save(tmp, 'JPEG') tmp.seek(0) return tmp - + def setUp(self): - self.p = TestPhoto() + self.photo = Photo() img = self.generate_image() - self.p.save_image('test.jpeg', ContentFile(img.read())) - self.p.save() + file = ContentFile(img.read()) + self.photo.original_image = file + self.photo.original_image.save('test.jpeg', file) + self.photo.save() img.close() - + def test_save_image(self): - img = self.generate_image() - path = self.p.image.path - self.p.save_image('test2.jpeg', ContentFile(img.read())) - self.failIf(os.path.isfile(path)) - path = self.p.image.path - img.seek(0) - self.p.save_image('test.jpeg', ContentFile(img.read())) - self.failIf(os.path.isfile(path)) - img.close() - + photo = Photo.objects.get(id=self.photo.id) + self.assertTrue(os.path.isfile(photo.original_image.path)) + def test_setup(self): - self.assertEqual(self.p.image.width, 800) - self.assertEqual(self.p.image.height, 600) + self.assertEqual(self.photo.original_image.width, 800) + self.assertEqual(self.photo.original_image.height, 600) + + def test_thumbnail_creation(self): + photo = Photo.objects.get(id=self.photo.id) + self.assertTrue(os.path.isfile(photo.thumbnail.file.name)) - def test_to_width(self): - self.assertEqual(self.p.to_width.width, 100) - self.assertEqual(self.p.to_width.height, 75) - - def test_to_height(self): - self.assertEqual(self.p.to_height.width, 133) - self.assertEqual(self.p.to_height.height, 100) - - def test_crop(self): - self.assertEqual(self.p.cropped.width, 100) - self.assertEqual(self.p.cropped.height, 100) - - def test_url(self): - tup = (settings.MEDIA_URL, self.p._ik.cache_dir, - 'images/test_to_width.jpeg') - self.assertEqual(self.p.to_width.url, "%s%s/%s" % tup) - - def tearDown(self): - # make sure image file is deleted - path = self.p.image.path - self.p.delete() - self.failIf(os.path.isfile(path)) + def test_thumbnail_size(self): + self.assertEqual(self.photo.thumbnail.width, 50) + self.assertEqual(self.photo.thumbnail.height, 50) + + def test_thumbnail_source_file(self): + self.assertEqual( + self.photo.thumbnail.source_file, self.photo.original_image) \ No newline at end of file From 9e9665f6261a8944ef441d525116ca0405a0c2d4 Mon Sep 17 00:00:00 2001 From: Chris McKenzie Date: Mon, 26 Sep 2011 16:39:45 -0400 Subject: [PATCH 55/68] adding name to AUTHORS --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 82f8157..b76fa7b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Contributors * `Jonathan Slenders`_ * `Matthew Tretter`_ * `Eric Eldredge`_ +* `Chris McKenzie`_ .. _Justin Driscoll: http://github.com/jdriscoll @@ -27,3 +28,4 @@ Contributors .. _Jonathan Slenders: http://github.com/jonathanslenders .. _Matthew Tretter: http://github.com/matthewwithanm .. _Eric Eldredge: http://github.com/lettertwo +.. _Chris McKenzie: http://github.com/kenzic \ No newline at end of file From 492febf7ecaabbb76aabd0624f160d2da32beedb Mon Sep 17 00:00:00 2001 From: Chris McKenzie Date: Mon, 26 Sep 2011 16:40:38 -0400 Subject: [PATCH 56/68] fixing bad import in docs --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b895761..cf19596 100644 --- a/README.rst +++ b/README.rst @@ -89,8 +89,8 @@ for displaying specs (or even regular ImageFields) in the AdminThumbnailView is used as a property on Django model classes:: from django.db import models - from imagekit.models import ImageSpec - from imagekit.processors import resize, AdminThumbnailView + from imagekit.models import ImageSpec, AdminThumbnailView + from imagekit.processors import resize class Photo(models.Model): original_image = models.ImageField(upload_to'photos') From 67477a6e15504396e47c7a20f1fa293486a63a2b Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Mon, 26 Sep 2011 16:45:45 -0400 Subject: [PATCH 57/68] Adds explicit import of resize module to processors This way users can write 'from imagekit.processors import *' and also use the resize processors like so: 'resize.Crop(50, 50)' --- imagekit/processors/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index e721876..7c0d04d 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -7,6 +7,7 @@ from both the filesystem and the ORM. """ from imagekit.lib import * +from imagekit.processors import resize class ProcessorPipeline(list): From 067217e40e3fa6899f2f1ae51a43ef283d99c303 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 26 Sep 2011 16:52:35 -0400 Subject: [PATCH 58/68] Docs typo fix --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf19596..908037d 100644 --- a/README.rst +++ b/README.rst @@ -97,7 +97,7 @@ AdminThumbnailView is used as a property on Django model classes:: thumbnail = ImageSpec([resize.Crop(50, 50)], image_field='original_image') admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') -You can then then add this property to the `list_display`__ field of your admin +You can then add this property to the `list_display`__ field of your admin class:: from django.contrib import admin From b9aa69e0c01a90bc55ba229942ee01702cfc0f7f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 29 Sep 2011 23:49:05 -0400 Subject: [PATCH 59/68] AdminThumbnailView is now AdminThumbnail I never liked that the "AdminThumbnailView" was supposed to be defined on the model, but never looked into it. This commit puts the definition back where it belongs: in the admin. Instead of requiring you to add a field (with view logic) to your model, you now just add a property to your admin class and specify that property in the `list_display` list. --- README.rst | 26 +++++++-------------- docs/apireference.rst | 8 +++++++ imagekit/admin.py | 37 +++++++++++++++++++++++++++++ imagekit/models.py | 54 ------------------------------------------- imagekit/tests.py | 5 ++-- 5 files changed, 55 insertions(+), 75 deletions(-) create mode 100644 imagekit/admin.py diff --git a/README.rst b/README.rst index 908037d..f3cf0c1 100644 --- a/README.rst +++ b/README.rst @@ -83,35 +83,25 @@ implement a ``process()`` method:: Admin ----- -ImageKit also contains a class named ``imagekit.models.AdminThumbnailView`` +ImageKit also contains a class named ``imagekit.admin.AdminThumbnail`` for displaying specs (or even regular ImageFields) in the -`Django admin change list`__. Like ``imagekit.models.ImageSpec``, -AdminThumbnailView is used as a property on Django model classes:: - - from django.db import models - from imagekit.models import ImageSpec, AdminThumbnailView - from imagekit.processors import resize - - class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') - thumbnail = ImageSpec([resize.Crop(50, 50)], image_field='original_image') - admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') - -You can then add this property to the `list_display`__ field of your admin -class:: +`Django admin change list`__. AdminThumbnail is used as a property on +Django admin classes:: from django.contrib import admin + from imagekit.admin import AdminThumbnail from .models import Photo class PhotoAdmin(admin.ModelAdmin): - list_display = ('__str__', 'admin_thumbnail_view') + list_display = ('__str__', 'admin_thumbnail') + admin_thumbnail = AdminThumbnail(image_field='thumbnail') admin.site.register(Photo, PhotoAdmin) -AdminThumbnailView can even use a custom template. For more information, see -``imagekit.models.AdminThumbnailView``. +AdminThumbnail can even use a custom template. For more information, see +``imagekit.admin.AdminThumbnail``. __ https://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-change-list diff --git a/docs/apireference.rst b/docs/apireference.rst index 93ad370..c997026 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -8,6 +8,7 @@ API Reference .. automodule:: imagekit.models :members: + :mod:`processors` Module ------------------------ @@ -16,3 +17,10 @@ API Reference .. automodule:: imagekit.processors.resize :members: + + +:mod:`admin` Module +-------------------- + +.. automodule:: imagekit.admin + :members: diff --git a/imagekit/admin.py b/imagekit/admin.py new file mode 100644 index 0000000..87bd459 --- /dev/null +++ b/imagekit/admin.py @@ -0,0 +1,37 @@ +from django.utils.translation import ugettext_lazy as _ +from django.template.loader import render_to_string + + +class AdminThumbnail(object): + """A convenience utility for adding thumbnails to the Django admin change + list. + + """ + short_description = _('Thumbnail') + allow_tags = True + + def __init__(self, image_field, template=None): + """ + :param image_field: The name of the ImageField or ImageSpec on the model + to use for the thumbnail. + :param template: The template with which to render the thumbnail + + """ + self.image_field = image_field + self.template = template + + def __call__(self, obj): + thumbnail = getattr(obj, self.image_field, None) + + if not thumbnail: + raise Exception('The property {0} is not defined on {1}.'.format( + obj, self.image_field)) + + original_image = getattr(thumbnail, 'source_file', None) or thumbnail + template = self.template or 'imagekit/admin/thumbnail.html' + + return render_to_string(template, { + 'model': obj, + 'thumbnail': thumbnail, + 'original_image': original_image, + }) diff --git a/imagekit/models.py b/imagekit/models.py index e3471f2..28efc75 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -9,8 +9,6 @@ from django.core.files.base import ContentFile from django.utils.encoding import force_unicode, smart_str from django.db import models from django.db.models.signals import post_save, post_delete -from django.utils.translation import ugettext_lazy as _ -from django.template.loader import render_to_string from django.db.models.fields.files import ImageFieldFile @@ -299,58 +297,6 @@ def _post_delete_handler(sender, instance=None, **kwargs): spec_file._delete() -class AdminThumbnailView(object): - """A convenience utility for adding thumbnails to the Django admin change - list. - - """ - short_description = _('Thumbnail') - allow_tags = True - - def __init__(self, image_field, template=None): - """ - :param image_field: The name of the ImageField or ImageSpec on the model - to use for the thumbnail. - :param template: The template with which to render the thumbnail - - """ - self.image_field = image_field - self.template = template - - def __get__(self, instance, owner): - if instance is None: - return self - else: - return BoundAdminThumbnailView(instance, self) - - -class BoundAdminThumbnailView(AdminThumbnailView): - def __init__(self, model_instance, unbound_field): - super(BoundAdminThumbnailView, self).__init__(unbound_field.image_field, - unbound_field.template) - self.model_instance = model_instance - - def __unicode__(self): - thumbnail = getattr(self.model_instance, self.image_field, None) - - if not thumbnail: - raise Exception('The property {0} is not defined on {1}.'.format( - self.model_instance, self.image_field)) - - original_image = getattr(thumbnail, 'source_file', None) or thumbnail - template = self.template or 'imagekit/admin/thumbnail.html' - - return render_to_string(template, { - 'model': self.model_instance, - 'thumbnail': thumbnail, - 'original_image': original_image, - }) - - def __get__(self, instance, owner): - """Override AdminThumbnailView's implementation.""" - return self - - class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFileMixin): def save(self, name, content, save=True): new_filename = self.field.generate_filename(self.instance, name) diff --git a/imagekit/tests.py b/imagekit/tests.py index ba31797..cab98cf 100644 --- a/imagekit/tests.py +++ b/imagekit/tests.py @@ -4,7 +4,7 @@ import Image from django.core.files.base import ContentFile from django.db import models from django.test import TestCase -from imagekit.models import ImageSpec, AdminThumbnailView +from imagekit.models import ImageSpec from imagekit.processors.resize import Crop from imagekit.processors import Adjust @@ -14,8 +14,7 @@ class Photo(models.Model): original_image = models.ImageField(upload_to='photos') thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)], image_field='original_image', format='JPEG', quality=90) - admin_thumbnail_view = AdminThumbnailView(image_field='thumbnail') - + class IKTest(TestCase): """ Base TestCase class """ From b8e57dccd6731381e4cad2cb3e0d70ea2b886c5e Mon Sep 17 00:00:00 2001 From: Eric Eldredge Date: Mon, 3 Oct 2011 22:51:03 -0400 Subject: [PATCH 60/68] A list of ImageSpec names are now stored on the model. Previously, ImageSpecFile instances were retrieved (for saving and deleting, among other possibilities) by iterating over the model instance's attributes. This change adds ImageSpecFile names to a list (spec_file_names) on an imagekit meta object on the model (_ik), making later retrieval much cheaper and more straightforward. --- imagekit/models.py | 7 +++++++ imagekit/utils.py | 16 ++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/imagekit/models.py b/imagekit/models.py index 28efc75..b247b21 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -77,6 +77,13 @@ class ImageSpec(_ImageSpecMixin): def contribute_to_class(self, cls, name): setattr(cls, name, _ImageSpecDescriptor(self, name)) + try: + ik = getattr(cls, '_ik') + except AttributeError: + ik = type('ImageKitMeta', (object,), {'spec_file_names': []}) + setattr(cls, '_ik', ik) + ik.spec_file_names.append(name) + # Connect to the signals only once for this class. uid = '%s.%s' % (cls.__module__, cls.__name__) post_save.connect(_post_save_handler, diff --git a/imagekit/utils.py b/imagekit/utils.py index 29d9da2..3c9c7c3 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -13,16 +13,12 @@ def img_to_fobj(img, format, **kwargs): def get_spec_files(instance): - from imagekit.models import ImageSpecFile - spec_files = [] - for key in dir(instance): - try: - value = getattr(instance, key) - except AttributeError: - continue - if isinstance(value, ImageSpecFile): - spec_files.append(value) - return spec_files + try: + ik = getattr(instance, '_ik') + except AttributeError: + return [] + else: + return [getattr(instance, n) for n in ik.spec_file_names] def open_image(target): From 7f7141ef2789cb469b96d27c979c9d9c0bd31a6f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 10 Oct 2011 15:59:39 -0400 Subject: [PATCH 61/68] Typo fix --- imagekit/management/commands/ikflush.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index b1799cc..d3d680a 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -31,4 +31,4 @@ def flush_cache(apps, options): if spec_file.field.pre_cache: spec_file._create() else: - print 'Please specify on or more app names' + print 'Please specify one or more app names' From 8147cb85df162b1731490970a156e07ab90366cc Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 10 Oct 2011 17:00:41 -0400 Subject: [PATCH 62/68] ImageSpecFile is a proper File Previously, ImageSpecFile was a file-like object, but didn't actually extend any of the file classes. Because of this, some of Django's file- handling code was duplicated. More importantly, instances didn't always behave as one might expect (if one were familiar with ImageFields), particularly when the source image was empty. This could have been especially confusing in templates. (For example, because ImageSpecFields whose source images didn't exist were still truthy.) --- imagekit/management/commands/ikflush.py | 2 +- imagekit/models.py | 109 +++++++++++++----------- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index d3d680a..dd551d9 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -27,7 +27,7 @@ def flush_cache(apps, options): for obj in model.objects.order_by('-id'): for spec_file in get_spec_files(obj): if spec_file is not None: - spec_file._delete() + spec_file.delete(save=False) if spec_file.field.pre_cache: spec_file._create() else: diff --git a/imagekit/models.py b/imagekit/models.py index b247b21..82aa842 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -137,15 +137,28 @@ class _ImageSpecFileMixin(object): return img, content -class ImageSpecFile(_ImageSpecFileMixin): +class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): def __init__(self, instance, field, attname, source_file): - self.field = field - self._img = None - self._file = None - self.instance = instance + ImageFieldFile.__init__(self, instance, field, None) + self.storage = field.storage or source_file.storage self.attname = attname self.source_file = source_file + def _require_file(self): + if not self.source_file: + raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) + + def _get_file(self): + self._create(True) + return super(ImageFieldFile, self).file + + file = property(_get_file, ImageFieldFile._set_file, ImageFieldFile._del_file) + + @property + def url(self): + self._create(True) + return super(ImageFieldFile, self).url + def _create(self, lazy=False): """Creates a new image by running the processors on the source file. @@ -154,12 +167,10 @@ class ImageSpecFile(_ImageSpecFileMixin): a new image should be created and the existing one overwritten. """ - img = None - if lazy: - img = self._img - if not img and self.storage.exists(self.name): - img = open_image(self.file) - if not img and self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? + if lazy and (getattr(self, '_file', None) or self.storage.exists(self.name)): + return + + if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? # Process the original image file try: fp = self.source_file.storage.open(self.source_file.name) @@ -170,15 +181,35 @@ class ImageSpecFile(_ImageSpecFileMixin): img, content = self._process_content(self.name, fp) self.storage.save(self.name, content) - self._img = img - return self._img - def _delete(self): - if self.source_file: - try: - self.storage.delete(self.name) - except (NotImplementedError, IOError): - return + def delete(self, save=False): + """Pulled almost verbatim from ``ImageFieldFile.delete()`` and + ``FieldFile.delete()`` but with the attempts to reset the instance + property removed. + + """ + # Clear the image dimensions cache + if hasattr(self, '_dimensions_cache'): + del self._dimensions_cache + + # Only close the file if it's already open, which we know by the + # presence of self._file + if hasattr(self, '_file'): + self.close() + del self.file + + try: + self.storage.delete(self.name) + except (NotImplementedError, IOError): + pass + + # Delete the filesize cache + if hasattr(self, '_size'): + del self._size + self._committed = False + + if save: + self.instance.save() @property def _suggested_extension(self): @@ -219,35 +250,13 @@ class ImageSpecFile(_ImageSpecFileMixin): return new_filename - @property - def storage(self): - return self.field.storage or self.source_file.storage - - @property - def url(self): - if not self.field.pre_cache: - self._create(True) - return self.storage.url(self.name) - - @property - def file(self): - if not self._file: - if not self.storage.exists(self.name): - self._create() - self._file = self.storage.open(self.name) - return self._file - - @property - def image(self): - return self._create(True) - - @property - def width(self): - return self.image.size[0] - - @property - def height(self): - return self.image.size[1] + @name.setter + def name(self, value): + # TODO: Figure out a better way to handle this. We really don't want to + # allow anybody to set the name, but ``File.__init__`` (which is called + # by ``ImageSpecFile.__init__``) does, so we have to allow it at least + # that one time. + pass class _ImageSpecDescriptor(object): @@ -292,7 +301,7 @@ def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs spec_files = get_spec_files(instance) for spec_file in spec_files: if not created: - spec_file._delete() + spec_file.delete(save=False) if spec_file.field.pre_cache: spec_file._create() @@ -301,7 +310,7 @@ def _post_delete_handler(sender, instance=None, **kwargs): assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname) spec_files = get_spec_files(instance) for spec_file in spec_files: - spec_file._delete() + spec_file.delete(save=False) class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFileMixin): From 79a6e8f1ab027d5f92295499a483596dd248830a Mon Sep 17 00:00:00 2001 From: Chris McKenzie Date: Mon, 17 Oct 2011 10:27:29 -0400 Subject: [PATCH 63/68] fixing typo --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f3cf0c1..40e136b 100644 --- a/README.rst +++ b/README.rst @@ -48,7 +48,7 @@ your spec, you can expose different versions of the original image:: from imagekit.processors import resize, Adjust class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') + original_image = models.ImageField(upload_to='photos') thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), resize.Crop(50, 50)], image_field='original_image', format='JPEG', quality=90) @@ -75,7 +75,7 @@ implement a ``process()`` method:: return image class Photo(models.Model): - original_image = models.ImageField(upload_to'photos') + original_image = models.ImageField(upload_to='photos') watermarked_image = ImageSpec([Watermark()], image_field='original_image', format='JPEG', quality=90) From 6adadafc74991c20d855a01dd8d5a2defc49be2b Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Thu, 20 Oct 2011 12:12:47 +0900 Subject: [PATCH 64/68] PEP8 and import tweaks. --- imagekit/admin.py | 2 +- imagekit/management/commands/ikflush.py | 4 ++-- imagekit/models.py | 19 ++++++++++--------- imagekit/processors/resize.py | 22 +++++++++++----------- imagekit/utils.py | 7 +++---- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/imagekit/admin.py b/imagekit/admin.py index 87bd459..7c1475d 100644 --- a/imagekit/admin.py +++ b/imagekit/admin.py @@ -22,7 +22,7 @@ class AdminThumbnail(object): def __call__(self, obj): thumbnail = getattr(obj, self.image_field, None) - + if not thumbnail: raise Exception('The property {0} is not defined on {1}.'.format( obj, self.image_field)) diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index 9120af6..b0f62e3 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -3,8 +3,8 @@ """ from django.db.models.loading import cache -from django.core.management.base import BaseCommand, CommandError -from optparse import make_option +from django.core.management.base import BaseCommand + from imagekit.utils import get_spec_files diff --git a/imagekit/models.py b/imagekit/models.py index 6cd7d95..e0a2a1a 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -1,16 +1,17 @@ import os import datetime from StringIO import StringIO -from imagekit.lib import * -from imagekit.utils import img_to_fobj, get_spec_files, open_image -from imagekit.processors import ProcessorPipeline + from django.conf import settings from django.core.files.base import ContentFile -from django.utils.encoding import force_unicode, smart_str from django.db import models -from django.db.models.signals import post_save, post_delete from django.db.models.fields.files import ImageFieldFile +from django.db.models.signals import post_save, post_delete +from django.utils.encoding import force_unicode, smart_str +from imagekit.lib import Image, ImageFile +from imagekit.utils import img_to_fobj, get_spec_files, open_image +from imagekit.processors import ProcessorPipeline # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10) @@ -170,7 +171,7 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): if lazy and (getattr(self, '_file', None) or self.storage.exists(self.name)): return - if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? + if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? # Process the original image file try: fp = self.source_file.storage.open(self.source_file.name) @@ -244,9 +245,9 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): smart_str(cache_to(self.instance, self.source_file.name, self.attname, self._suggested_extension)))) else: - dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) - filename = os.path.normpath(os.path.basename(filename)) - new_filename = os.path.join(dir_name, filename) + dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) + filename = os.path.normpath(os.path.basename(filename)) + new_filename = os.path.join(dir_name, filename) return new_filename diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index bc0729e..7b02559 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -1,11 +1,11 @@ -from imagekit.lib import * +from imagekit.lib import Image class _Resize(object): - + width = None height = None - + def __init__(self, width=None, height=None): if width is not None: self.width = width @@ -68,7 +68,7 @@ class Crop(_Resize): cur_width, cur_height = img.size horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ Crop.CENTER] - ratio = max(float(self.width)/cur_width, float(self.height)/cur_height) + ratio = max(float(self.width) / cur_width, float(self.height)/cur_height) resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) @@ -91,7 +91,7 @@ class Fit(_Resize): """Resizes an image to fit within the specified dimensions. """ - + def __init__(self, width=None, height=None, upscale=None): """ :param width: The maximum width of the desired image. @@ -107,15 +107,15 @@ class Fit(_Resize): def process(self, img): cur_width, cur_height = img.size if not self.width is None and not self.height is None: - ratio = min(float(self.width)/cur_width, - float(self.height)/cur_height) + ratio = min(float(self.width) / cur_width, + float(self.height) / cur_height) else: if self.width is None: - ratio = float(self.height)/cur_height + ratio = float(self.height) / cur_height else: - ratio = float(self.width)/cur_width - new_dimensions = (int(round(cur_width*ratio)), - int(round(cur_height*ratio))) + ratio = float(self.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 self.upscale: diff --git a/imagekit/utils.py b/imagekit/utils.py index cf829e2..637433e 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,9 +1,9 @@ -""" ImageKit utility functions """ +import tempfile +import types -import tempfile, types from django.utils.functional import wraps -from lib import Image +from imagekit.lib import Image def img_to_fobj(img, format, **kwargs): @@ -49,4 +49,3 @@ def _wrap_copy(f): pass return img return copy - From 714ff8ae1d7888ecf4ee32b13a00c36cf6fb8062 Mon Sep 17 00:00:00 2001 From: Chris McKenzie Date: Mon, 24 Oct 2011 17:58:38 -0400 Subject: [PATCH 65/68] adding introspection rule for users with south --- imagekit/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/imagekit/models.py b/imagekit/models.py index e0a2a1a..3294092 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -352,3 +352,11 @@ class ProcessedImageField(models.ImageField, _ImageSpecMixin): name, ext = os.path.splitext(filename) ext = _get_suggested_extension(filename, self.format) return '{0}{1}'.format(name, ext) + + +try: + from south.modelsinspector import add_introspection_rules +except ImportError: + pass +else: + add_introspection_rules([], [r'^imagekit\.models\.ProcessedImageField$']) From a45f3af2a5f496a2b7bf9e4fc892268c26f0ea18 Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Mon, 31 Oct 2011 23:01:41 +0900 Subject: [PATCH 66/68] Requiring versiontools and patching up our setup.py. --- imagekit/__init__.py | 4 ++-- setup.py | 21 ++++++++------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 7f52c7a..c12553b 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,5 +1,5 @@ __title__ = 'django-imagekit' -__version__ = '1.0.0.alpha' -__build__ = 0x001000 __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' +__version__ = (1, 0, 0, 'final', 0) +__build__ = 0x001000 __license__ = 'BSD' diff --git a/setup.py b/setup.py index 2e3939f..8abc884 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,8 @@ #/usr/bin/env python import os import sys -import imagekit -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +from setuptools import setup, find_packages if 'publish' in sys.argv: os.system('python setup.py sdist upload') @@ -14,7 +10,7 @@ if 'publish' in sys.argv: setup( name='django-imagekit', - version=imagekit.__version__, + version=':versiontools:imagekit:', description='Automated image processing for Django models.', author='Justin Driscoll', author_email='justin@driscolldev.com', @@ -22,13 +18,9 @@ setup( maintainer_email='bryan@revyver.com', license='BSD', url='http://github.com/jdriscoll/django-imagekit/', - packages=[ - 'imagekit', - 'imagekit.management', - 'imagekit.management.commands' - ], + packages=find_packages(), classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', @@ -38,5 +30,8 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Utilities' - ] + ], + setup_requires=[ + 'versiontools >= 1.8', + ], ) From 06c1c678b641344bdc5758b15cce97c7ac37ea2b Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Mon, 31 Oct 2011 23:12:03 +0900 Subject: [PATCH 67/68] PEP8-ing and whitespacing. --- imagekit/admin.py | 10 +-- imagekit/management/__init__.py | 1 - imagekit/management/commands/ikflush.py | 4 +- imagekit/models.py | 89 +++++++++++++------------ imagekit/processors/__init__.py | 82 ++++++++++++----------- imagekit/processors/resize.py | 21 +++--- tests/core/specs.py | 6 ++ 7 files changed, 113 insertions(+), 100 deletions(-) diff --git a/imagekit/admin.py b/imagekit/admin.py index 7c1475d..cc24d29 100644 --- a/imagekit/admin.py +++ b/imagekit/admin.py @@ -3,8 +3,8 @@ from django.template.loader import render_to_string class AdminThumbnail(object): - """A convenience utility for adding thumbnails to the Django admin change - list. + """ + A convenience utility for adding thumbnails to Django's admin change list. """ short_description = _('Thumbnail') @@ -12,8 +12,8 @@ class AdminThumbnail(object): def __init__(self, image_field, template=None): """ - :param image_field: The name of the ImageField or ImageSpec on the model - to use for the thumbnail. + :param image_field: The name of the ImageField or ImageSpec on the + model to use for the thumbnail. :param template: The template with which to render the thumbnail """ @@ -25,7 +25,7 @@ class AdminThumbnail(object): if not thumbnail: raise Exception('The property {0} is not defined on {1}.'.format( - obj, self.image_field)) + obj, self.image_field)) original_image = getattr(thumbnail, 'source_file', None) or thumbnail template = self.template or 'imagekit/admin/thumbnail.html' diff --git a/imagekit/management/__init__.py b/imagekit/management/__init__.py index 8b13789..e69de29 100644 --- a/imagekit/management/__init__.py +++ b/imagekit/management/__init__.py @@ -1 +0,0 @@ - diff --git a/imagekit/management/commands/ikflush.py b/imagekit/management/commands/ikflush.py index b0f62e3..d718992 100644 --- a/imagekit/management/commands/ikflush.py +++ b/imagekit/management/commands/ikflush.py @@ -1,7 +1,7 @@ -"""Flushes the cached ImageKit images. +""" +Flushes and re-caches all images under ImageKit. """ - from django.db.models.loading import cache from django.core.management.base import BaseCommand diff --git a/imagekit/models.py b/imagekit/models.py index 3294092..815a972 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -29,11 +29,11 @@ class _ImageSpecMixin(object): class ImageSpec(_ImageSpecMixin): - """The heart and soul of the ImageKit library, ImageSpec allows you to add + """ + The heart and soul of the ImageKit library, ImageSpec allows you to add variants of uploaded images to your models. """ - _upload_to_attr = 'cache_to' def __init__(self, processors=None, quality=70, format=None, @@ -41,31 +41,31 @@ class ImageSpec(_ImageSpecMixin): """ :param processors: A list of processors to run on the original image. :param quality: The quality of the output image. This option is only - used for the JPEG format. - :param format: The format of the output file. If not provided, ImageSpec - will try to guess the appropriate format based on the extension - of the filename and the format of the input image. + used for the JPEG format. + :param format: The format of the output file. If not provided, + ImageSpec will try to guess the appropriate format based on the + extension of the filename and the format of the input image. :param image_field: The name of the model property that contains the - original image. - :param pre_cache: A boolean that specifies whether the image should be - generated immediately (True) or on demand (False). + original image. + :param pre_cache: A boolean that specifies whether the image should + be generated immediately (True) or on demand (False). :param storage: A Django storage system to use to save the generated - image. + image. :param cache_to: Specifies the filename to use when saving the image - cache file. This is modeled after ImageField's ``upload_to`` and - can be either a string (that specifies a directory) or a - callable (that returns a filepath). Callable values should - accept the following arguments: + cache file. This is modeled after ImageField's ``upload_to`` and + can be either a string (that specifies a directory) or a + callable (that returns a filepath). Callable values should + accept the following arguments: - - instance -- The model instance this spec belongs to - - path -- The path of the original image - - specname -- the property name that the spec is bound to on - the model instance - - extension -- A recommended extension. If the format of the - spec is set explicitly, this suggestion will be - based on that format. if not, the extension of the - original file will be passed. You do not have to use - this extension, it's only a recommendation. + - instance -- The model instance this spec belongs to + - path -- The path of the original image + - specname -- the property name that the spec is bound to on + the model instance + - extension -- A recommended extension. If the format of the + spec is set explicitly, this suggestion will be + based on that format. if not, the extension of the + original file will be passed. You do not have to use + this extension, it's only a recommendation. """ @@ -97,7 +97,7 @@ class ImageSpec(_ImageSpecMixin): def _get_suggested_extension(name, format): if format: - # Try to look up an extension by the format + # Try to look up an extension by the format. extensions = [k for k, v in Image.EXTENSION.iteritems() \ if v == format.upper()] else: @@ -112,7 +112,6 @@ def _get_suggested_extension(name, format): class _ImageSpecFileMixin(object): - def _process_content(self, filename, content): img = open_image(content) original_format = img.format @@ -161,18 +160,20 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): return super(ImageFieldFile, self).url def _create(self, lazy=False): - """Creates a new image by running the processors on the source file. + """ + Creates a new image by running the processors on the source file. Keyword Arguments: - lazy -- True if an already-existing image should be returned; False if - a new image should be created and the existing one overwritten. + lazy -- True if an already-existing image should be returned; + False if a new image should be created and the existing + one overwritten. """ if lazy and (getattr(self, '_file', None) or self.storage.exists(self.name)): return if self.source_file: # TODO: Should we error here or something if the source_file doesn't exist? - # Process the original image file + # Process the original image file. try: fp = self.source_file.storage.open(self.source_file.name) except IOError: @@ -184,7 +185,8 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): self.storage.save(self.name, content) def delete(self, save=False): - """Pulled almost verbatim from ``ImageFieldFile.delete()`` and + """ + Pulled almost verbatim from ``ImageFieldFile.delete()`` and ``FieldFile.delete()`` but with the attempts to reset the instance property removed. @@ -194,7 +196,7 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): del self._dimensions_cache # Only close the file if it's already open, which we know by the - # presence of self._file + # presence of self._file. if hasattr(self, '_file'): self.close() del self.file @@ -204,7 +206,7 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): except (NotImplementedError, IOError): pass - # Delete the filesize cache + # Delete the filesize cache. if hasattr(self, '_size'): del self._size self._committed = False @@ -217,9 +219,10 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): return _get_suggested_extension(self.source_file.name, self.field.format) def _default_cache_to(self, instance, path, specname, extension): - """Determines the filename to use for the transformed image. Can be - overridden on a per-spec basis by setting the cache_to property on the - spec. + """ + Determines the filename to use for the transformed image. Can be + overridden on a per-spec basis by setting the cache_to property on + the spec. """ filepath, basename = os.path.split(path) @@ -253,10 +256,10 @@ class ImageSpecFile(_ImageSpecFileMixin, ImageFieldFile): @name.setter def name(self, value): - # TODO: Figure out a better way to handle this. We really don't want to - # allow anybody to set the name, but ``File.__init__`` (which is called - # by ``ImageSpecFile.__init__``) does, so we have to allow it at least - # that one time. + # TODO: Figure out a better way to handle this. We really don't want + # to allow anybody to set the name, but ``File.__init__`` (which is + # called by ``ImageSpecFile.__init__``) does, so we have to allow it + # at least that one time. pass @@ -322,7 +325,8 @@ class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFileMixin): class ProcessedImageField(models.ImageField, _ImageSpecMixin): - """ProcessedImageField is an ImageField that runs processors on the uploaded + """ + ProcessedImageField is an ImageField that runs processors on the uploaded image *before* saving it to storage. This is in contrast to specs, which maintain the original. Useful for coercing fileformats or keeping images within a reasonable size. @@ -336,12 +340,11 @@ class ProcessedImageField(models.ImageField, _ImageSpecMixin): **kwargs): """ The ProcessedImageField constructor accepts all of the arguments that - the :class:`django.db.models.ImageField` constructor accepts, as well as - the ``processors``, ``format``, and ``quality`` arguments of + the :class:`django.db.models.ImageField` constructor accepts, as well + as the ``processors``, ``format``, and ``quality`` arguments of :class:`imagekit.models.ImageSpec`. """ - _ImageSpecMixin.__init__(self, processors, quality=quality, format=format) models.ImageField.__init__(self, verbose_name, name, width_field, diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index 7c0d04d..72e7f8d 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -1,4 +1,5 @@ -"""Imagekit image processors. +""" +Imagekit image processors. A processor accepts an image, does some stuff, and returns the result. Processors can do anything with the image you want, but their responsibilities @@ -6,12 +7,13 @@ should be limited to image manipulations--they should be completely decoupled from both the filesystem and the ORM. """ -from imagekit.lib import * +from imagekit.lib import Image, ImageColor, ImageEnhance from imagekit.processors import resize class ProcessorPipeline(list): - """A :class:`list` of other processors. This class allows any object that + """ + A :class:`list` of other processors. This class allows any object that knows how to deal with a single processor to deal with a list of them. For example:: @@ -25,27 +27,28 @@ class ProcessorPipeline(list): class Adjust(object): - """Performs color, brightness, contrast, and sharpness enhancements on the + """ + Performs color, brightness, contrast, and sharpness enhancements on the image. See :mod:`PIL.ImageEnhance` for more imformation. """ def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0): """ - :param color: A number between 0 and 1 that specifies the saturation of - the image. 0 corresponds to a completely desaturated image - (black and white) and 1 to the original color. - See :class:`PIL.ImageEnhance.Color` - :param brightness: A number representing the brightness; 0 results in a - completely black image whereas 1 corresponds to the brightness - of the original. See :class:`PIL.ImageEnhance.Brightness` + :param color: A number between 0 and 1 that specifies the saturation + of the image. 0 corresponds to a completely desaturated image + (black and white) and 1 to the original color. + See :class:`PIL.ImageEnhance.Color` + :param brightness: A number representing the brightness; 0 results in + a completely black image whereas 1 corresponds to the brightness + of the original. See :class:`PIL.ImageEnhance.Brightness` :param contrast: A number representing the contrast; 0 results in a - completely gray image whereas 1 corresponds to the contrast of - the original. See :class:`PIL.ImageEnhance.Contrast` + completely gray image whereas 1 corresponds to the contrast of + the original. See :class:`PIL.ImageEnhance.Contrast` :param sharpness: A number representing the sharpness; 0 results in a - blurred image; 1 corresponds to the original sharpness; 2 - results in a sharpened image. See - :class:`PIL.ImageEnhance.Sharpness` - + blurred image; 1 corresponds to the original sharpness; 2 + results in a sharpened image. See + :class:`PIL.ImageEnhance.Sharpness` + """ self.color = color self.brightness = brightness @@ -65,25 +68,26 @@ class Adjust(object): class Reflection(object): - """Creates an image with a reflection. - + """ + Creates an image with a reflection. + """ background_color = '#FFFFFF' size = 0.0 opacity = 0.6 def process(self, img): - # convert bgcolor string to rgb value + # Convert bgcolor string to RGB value. background_color = ImageColor.getrgb(self.background_color) - # handle palleted images + # Handle palleted images. img = img.convert('RGB') - # copy orignial image and flip the orientation + # Copy orignial image and flip the orientation. reflection = img.copy().transpose(Image.FLIP_TOP_BOTTOM) - # create a new image filled with the bgcolor the same size + # Create a new image filled with the bgcolor the same size. background = Image.new("RGB", img.size, background_color) - # calculate our alpha mask - start = int(255 - (255 * self.opacity)) # The start of our gradient - steps = int(255 * self.size) # the number of intermedite values + # Calculate our alpha mask. + start = int(255 - (255 * self.opacity)) # The start of our gradient. + steps = int(255 * self.size) # The number of intermedite values. increment = (255 - start) / float(steps) mask = Image.new('L', (1, 255)) for y in range(255): @@ -93,22 +97,24 @@ class Reflection(object): val = 255 mask.putpixel((0, y), val) alpha_mask = mask.resize(img.size) - # merge the reflection onto our background color using the alpha mask + # Merge the reflection onto our background color using the alpha mask. reflection = Image.composite(background, reflection, alpha_mask) - # crop the reflection + # Crop the reflection. reflection_height = int(img.size[1] * self.size) reflection = reflection.crop((0, 0, img.size[0], reflection_height)) - # create new image sized to hold both the original image and the reflection - composite = Image.new("RGB", (img.size[0], img.size[1]+reflection_height), background_color) - # paste the orignal image and the reflection into the composite image + # Create new image sized to hold both the original image and + # the reflection. + composite = Image.new("RGB", (img.size[0], img.size[1] + reflection_height), background_color) + # Paste the orignal image and the reflection into the composite image. composite.paste(img, (0, 0)) composite.paste(reflection, (0, img.size[1])) - # return the image complete with reflection effect + # Return the image complete with reflection effect. return composite class Transpose(object): - """ Rotates or flips the image + """ + Rotates or flips the image. """ AUTO = 'auto' @@ -133,18 +139,18 @@ class Transpose(object): def __init__(self, *args): """ Possible arguments: - - Transpose.AUTO + - Transpose.AUTO - Transpose.FLIP_HORIZONTAL - Transpose.FLIP_VERTICAL - Transpose.ROTATE_90 - Transpose.ROTATE_180 - Transpose.ROTATE_270 - The order of the arguments dictates the order in which the Transposition - steps are taken. + The order of the arguments dictates the order in which the + Transposition steps are taken. - If Transpose.AUTO is present, all other arguments are ignored, and the - processor will attempt to rotate the image according to the + If Transpose.AUTO is present, all other arguments are ignored, and + the processor will attempt to rotate the image according to the EXIF Orientation data. """ diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 7b02559..e2788d5 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -2,7 +2,6 @@ from imagekit.lib import Image class _Resize(object): - width = None height = None @@ -17,10 +16,10 @@ class _Resize(object): class Crop(_Resize): - """Resizes an image , cropping it to the specified width and height. + """ + Resizes an image , cropping it to the specified width and height. """ - TOP_LEFT = 'tl' TOP = 't' TOP_RIGHT = 'tr' @@ -47,8 +46,8 @@ class Crop(_Resize): """ :param width: The target width, in pixels. :param height: The target height, in pixels. - :param anchor: Specifies which part of the image should be retained when - cropping. Valid values are: + :param anchor: Specifies which part of the image should be retained + when cropping. Valid values are: - Crop.TOP_LEFT - Crop.TOP @@ -68,7 +67,7 @@ class Crop(_Resize): cur_width, cur_height = img.size horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \ Crop.CENTER] - ratio = max(float(self.width) / cur_width, float(self.height)/cur_height) + ratio = max(float(self.width) / cur_width, float(self.height) / cur_height) resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio)) crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y)) x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2)) @@ -88,17 +87,17 @@ class Crop(_Resize): class Fit(_Resize): - """Resizes an image to fit within the specified dimensions. + """ + Resizes an image to fit within the specified dimensions. """ - def __init__(self, width=None, height=None, upscale=None): """ :param width: The maximum width of the desired image. :param height: The maximum height of the desired image. - :param upscale: A boolean value specifying whether the image should be - enlarged if its dimensions are smaller than the target - dimensions. + :param upscale: A boolean value specifying whether the image should + be enlarged if its dimensions are smaller than the target + dimensions. """ super(Fit, self).__init__(width, height) diff --git a/tests/core/specs.py b/tests/core/specs.py index 5b38cc8..503f5ac 100644 --- a/tests/core/specs.py +++ b/tests/core/specs.py @@ -5,24 +5,30 @@ from imagekit.specs import ImageSpec class ResizeToWidth(processors.Resize): width = 100 + class ResizeToHeight(processors.Resize): height = 100 + class ResizeToFit(processors.Resize): width = 100 height = 100 + class ResizeCropped(ResizeToFit): crop = ('center', 'center') + class TestResizeToWidth(ImageSpec): access_as = 'to_width' processors = [ResizeToWidth] + class TestResizeToHeight(ImageSpec): access_as = 'to_height' processors = [ResizeToHeight] + class TestResizeCropped(ImageSpec): access_as = 'cropped' processors = [ResizeCropped] From f47c4f9e8bc3e4d68e85a5e13b7989a4140c45e5 Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Mon, 31 Oct 2011 23:23:50 +0900 Subject: [PATCH 68/68] Tests now run again. --- tests/core/models.py | 14 -------------- tests/core/specs.py | 34 ---------------------------------- tests/core/tests.py | 6 ++++-- tests/settings.py | 9 +++++++++ 4 files changed, 13 insertions(+), 50 deletions(-) delete mode 100644 tests/core/specs.py diff --git a/tests/core/models.py b/tests/core/models.py index 2f153bd..e69de29 100644 --- a/tests/core/models.py +++ b/tests/core/models.py @@ -1,14 +0,0 @@ -from django.db import models - -from imagekit.models import ImageModel - - -class TestPhoto(ImageModel): - """ - Minimal ImageModel class for testing. - - """ - image = models.ImageField(upload_to='images') - - class IKOptions: - spec_module = 'core.specs' diff --git a/tests/core/specs.py b/tests/core/specs.py deleted file mode 100644 index 503f5ac..0000000 --- a/tests/core/specs.py +++ /dev/null @@ -1,34 +0,0 @@ -from imagekit import processors -from imagekit.specs import ImageSpec - - -class ResizeToWidth(processors.Resize): - width = 100 - - -class ResizeToHeight(processors.Resize): - height = 100 - - -class ResizeToFit(processors.Resize): - width = 100 - height = 100 - - -class ResizeCropped(ResizeToFit): - crop = ('center', 'center') - - -class TestResizeToWidth(ImageSpec): - access_as = 'to_width' - processors = [ResizeToWidth] - - -class TestResizeToHeight(ImageSpec): - access_as = 'to_height' - processors = [ResizeToHeight] - - -class TestResizeCropped(ImageSpec): - access_as = 'cropped' - processors = [ResizeCropped] diff --git a/tests/core/tests.py b/tests/core/tests.py index 0591ab1..dd83fce 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -1,12 +1,14 @@ import os import tempfile -import Image + from django.core.files.base import ContentFile from django.db import models from django.test import TestCase + +from imagekit.lib import Image from imagekit.models import ImageSpec -from imagekit.processors.resize import Crop from imagekit.processors import Adjust +from imagekit.processors.resize import Crop class Photo(models.Model): diff --git a/tests/settings.py b/tests/settings.py index 082adb9..f034e9c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -8,10 +8,19 @@ BASE_PATH = os.path.abspath(os.path.dirname(__file__)) MEDIA_ROOT = os.path.normpath(os.path.join(BASE_PATH, 'media')) +# Django <= 1.2 DATABASE_ENGINE = 'sqlite3' DATABASE_NAME = 'imagekit.db' TEST_DATABASE_NAME = 'imagekit-test.db' +# Django >= 1.3 +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'imagekit.db', + }, +} + INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes',