From a71b3ca337201a77fb2face9abdd0f970a5363d9 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 20 Sep 2011 21:04:35 -0400 Subject: [PATCH] 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))