diff --git a/imagekit/generators.py b/imagekit/generators.py new file mode 100644 index 0000000..d064d80 --- /dev/null +++ b/imagekit/generators.py @@ -0,0 +1,100 @@ +import os +from StringIO import StringIO + +from django.core.files.base import ContentFile + +from .processors import ProcessorPipeline, AutoConvert +from .utils import img_to_fobj, open_image, \ + format_to_extension, extension_to_format, UnknownFormatError, \ + UnknownExtensionError + + +class SpecFileGenerator(object): + def __init__(self, processors=None, format=None, options={}, + autoconvert=True, storage=None): + self.processors = processors + self.format = format + self.options = options + self.autoconvert = autoconvert + self.storage = storage + + def process_content(self, content, filename=None, source_file=None): + img = open_image(content) + original_format = img.format + + # Run the processors + processors = self.processors + if callable(processors): + processors = processors(source_file) + img = ProcessorPipeline(processors or []).process(img) + + options = dict(self.options or {}) + + # Determine the format. + format = self.format + if not format: + # Try to guess the format from the extension. + extension = os.path.splitext(filename)[1].lower() + if extension: + try: + format = extension_to_format(extension) + except UnknownExtensionError: + pass + format = format or img.format or original_format or 'JPEG' + + # Run the AutoConvert processor + if self.autoconvert: + autoconvert_processor = AutoConvert(format) + img = autoconvert_processor.process(img) + options = dict(autoconvert_processor.save_kwargs.items() + \ + options.items()) + + imgfile = img_to_fobj(img, format, **options) + content = ContentFile(imgfile.read()) + return img, content + + def suggest_extension(self, name): + original_extension = os.path.splitext(name)[1] + try: + suggested_extension = format_to_extension(self.format) + except UnknownFormatError: + extension = original_extension + else: + if suggested_extension.lower() == original_extension.lower(): + extension = original_extension + else: + try: + original_format = extension_to_format(original_extension) + except UnknownExtensionError: + extension = suggested_extension + else: + # If the formats match, give precedence to the original extension. + if self.format.lower() == original_format.lower(): + extension = original_extension + else: + extension = suggested_extension + return extension + + def generate_file(self, filename, source_file, save=True): + """ + Generates a new image file by processing the source file and returns + the content of the result, ready for saving. + + """ + if source_file: # TODO: Should we error here or something if the source_file doesn't exist? + # Process the original image file. + + try: + fp = source_file.storage.open(source_file.name) + except IOError: + return + fp.seek(0) + fp = StringIO(fp.read()) + + img, content = self.process_content(fp, filename, source_file) + + if save: + storage = self.storage or source_file.storage + storage.save(filename, content) + + return content diff --git a/imagekit/models/fields.py b/imagekit/models/fields.py index f2f012b..b7f3862 100644 --- a/imagekit/models/fields.py +++ b/imagekit/models/fields.py @@ -1,34 +1,13 @@ import os import datetime -from StringIO import StringIO -from django.core.files.base import ContentFile from django.db import models from django.db.models.fields.files import ImageFieldFile from django.db.models.signals import post_init, post_save, post_delete from django.utils.encoding import force_unicode, smart_str -from imagekit.utils import img_to_fobj, open_image, \ - format_to_extension, extension_to_format, UnknownFormatError, \ - UnknownExtensionError -from imagekit.processors import ProcessorPipeline, AutoConvert from ..imagecache import get_default_image_cache_backend - - -class _ImageSpecFieldMixin(object): - def __init__(self, processors=None, format=None, options={}, - autoconvert=True): - self.processors = processors - self.format = format - self.options = options - self.autoconvert = autoconvert - - def process(self, image, file, instance): - processors = self.processors - if callable(processors): - processors = processors(instance=instance, file=file) - processors = ProcessorPipeline(processors or []) - return processors.process(image.copy()) +from ..generators import SpecFileGenerator class BoundImageKitMeta(object): @@ -54,14 +33,12 @@ class ImageKitMeta(object): return ik -class ImageSpecField(_ImageSpecFieldMixin): +class ImageSpecField(object): """ The heart and soul of the ImageKit library, ImageSpecField allows you to add variants of uploaded images to your models. """ - _upload_to_attr = 'cache_to' - def __init__(self, processors=None, format=None, options={}, image_field=None, pre_cache=None, storage=None, cache_to=None, autoconvert=True, image_cache_backend=None): @@ -106,10 +83,16 @@ class ImageSpecField(_ImageSpecFieldMixin): raise Exception('The pre_cache argument has been removed in favor' ' of cache state backends.') - _ImageSpecFieldMixin.__init__(self, processors, format=format, - options=options, autoconvert=autoconvert) + # The generator accepts a callable value for processors, but it + # takes different arguments than the callable that ImageSpecField + # expects, so we create a partial application and pass that instead. + # TODO: Should we change the signatures to match? Even if `instance` is not part of the signature, it's accessible through the source file object's instance property. + p = lambda file: processors(instance=file.instance, file=file) if \ + callable(processors) else processors + + self.generator = SpecFileGenerator(p, format=format, options=options, + autoconvert=autoconvert, storage=storage) self.image_field = image_field - self.pre_cache = pre_cache self.storage = storage self.cache_to = cache_to self.image_cache_backend = image_cache_backend or \ @@ -170,62 +153,7 @@ class ImageSpecField(_ImageSpecFieldMixin): ImageSpecField._update_source_hashes(instance) -def _get_suggested_extension(name, format): - original_extension = os.path.splitext(name)[1] - try: - suggested_extension = format_to_extension(format) - except UnknownFormatError: - extension = original_extension - else: - if suggested_extension.lower() == original_extension.lower(): - extension = original_extension - else: - try: - original_format = extension_to_format(original_extension) - except UnknownExtensionError: - extension = suggested_extension - else: - # If the formats match, give precedence to the original extension. - if format.lower() == original_format.lower(): - extension = original_extension - else: - extension = suggested_extension - return extension - - -class _ImageSpecFieldFileMixin(object): - def _process_content(self, filename, content): - img = open_image(content) - original_format = img.format - img = self.field.process(img, self, self.instance) - options = dict(self.field.options or {}) - - # 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. - try: - format = extension_to_format(extension) - except UnknownExtensionError: - pass - format = format or img.format or original_format or 'JPEG' - - # Run the AutoConvert processor - if getattr(self.field, 'autoconvert', True): - autoconvert_processor = AutoConvert(format) - img = autoconvert_processor.process(img) - options = dict(autoconvert_processor.save_kwargs.items() + \ - options.items()) - - imgfile = img_to_fobj(img, format, **options) - content = ContentFile(imgfile.read()) - return img, content - - -class ImageSpecFieldFile(_ImageSpecFieldFileMixin, ImageFieldFile): +class ImageSpecFieldFile(ImageFieldFile): def __init__(self, instance, field, attname): ImageFieldFile.__init__(self, instance, field, None) self.attname = attname @@ -278,22 +206,8 @@ class ImageSpecFieldFile(_ImageSpecFieldFileMixin, ImageFieldFile): the content of the result, ready for saving. """ - source_file = self.source_file - if source_file: # TODO: Should we error here or something if the source_file doesn't exist? - # Process the original image file. - try: - fp = source_file.storage.open(source_file.name) - except IOError: - return - fp.seek(0) - fp = StringIO(fp.read()) - - img, content = self._process_content(self.name, fp) - - if save: - self.storage.save(self.name, content) - - return content + return self.field.generator.generate_file(self.name, self.source_file, + save) @property def url(self): @@ -331,10 +245,6 @@ class ImageSpecFieldFile(_ImageSpecFieldFileMixin, ImageFieldFile): if save: self.instance.save() - @property - def _suggested_extension(self): - 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 @@ -362,13 +272,21 @@ class ImageSpecFieldFile(_ImageSpecFieldFileMixin, ImageFieldFile): 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') + 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.source_file.name, - self.attname, self._suggested_extension)))) + suggested_extension = \ + self.field.generator.suggest_extension( + self.source_file.name) + new_filename = force_unicode( + datetime.datetime.now().strftime( + smart_str(cache_to(self.instance, + self.source_file.name, self.attname, + suggested_extension)))) else: - dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to)))) + 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) @@ -417,14 +335,15 @@ def _post_delete_handler(sender, instance=None, **kwargs): spec_file.delete(save=False) -class ProcessedImageFieldFile(ImageFieldFile, _ImageSpecFieldFileMixin): +class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): new_filename = self.field.generate_filename(self.instance, name) - img, content = self._process_content(new_filename, content) + img, content = self.field.generator.process_content(content, + new_filename, self) return super(ProcessedImageFieldFile, self).save(name, content, save) -class ProcessedImageField(models.ImageField, _ImageSpecFieldMixin): +class ProcessedImageField(models.ImageField): """ ProcessedImageField is an ImageField that runs processors on the uploaded image *before* saving it to storage. This is in contrast to specs, which @@ -432,7 +351,6 @@ class ProcessedImageField(models.ImageField, _ImageSpecFieldMixin): within a reasonable size. """ - _upload_to_attr = 'upload_to' attr_class = ProcessedImageFieldFile def __init__(self, processors=None, format=None, options={}, @@ -449,15 +367,16 @@ class ProcessedImageField(models.ImageField, _ImageSpecFieldMixin): raise Exception('The "quality" keyword argument has been' """ deprecated. Use `options={'quality': %s}` instead.""" \ % kwargs['quality']) - _ImageSpecFieldMixin.__init__(self, processors, format=format, - options=options, autoconvert=autoconvert) models.ImageField.__init__(self, verbose_name, name, width_field, height_field, **kwargs) + self.generator = SpecFileGenerator(processors, format=format, + options=options, autoconvert=autoconvert) def get_filename(self, filename): - filename = os.path.normpath(self.storage.get_valid_name(os.path.basename(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) + ext = self.generator.suggest_extension(filename) return '%s%s' % (name, ext)