diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 55a5d04..ee903d9 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,3 +1,7 @@ +from . import conf +from .base import ImageSpec + + __title__ = 'django-imagekit' __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' __version__ = (2, 0, 1, 'final', 0) diff --git a/imagekit/generators.py b/imagekit/base.py similarity index 51% rename from imagekit/generators.py rename to imagekit/base.py index e848981..30b34cc 100644 --- a/imagekit/generators.py +++ b/imagekit/base.py @@ -2,20 +2,26 @@ from django.conf import settings from hashlib import md5 import os import pickle +from .exceptions import UnknownExtensionError +from .imagecache.backends import get_default_image_cache_backend +from .imagecache.strategies import StrategyWrapper from .lib import StringIO from .processors import ProcessorPipeline -from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format, - suggest_extension, UnknownExtensionError) +from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj, + suggest_extension) -class SpecFileGenerator(object): - def __init__(self, processors=None, format=None, options=None, - autoconvert=True, storage=None): - self.processors = processors - self.format = format - self.options = options or {} - self.autoconvert = autoconvert - self.storage = storage +class BaseImageSpec(object): + processors = None + format = None + options = None + autoconvert = True + + def __init__(self, processors=None, format=None, options=None, autoconvert=None): + self.processors = processors or self.processors or [] + self.format = format or self.format + self.options = options or self.options + self.autoconvert = self.autoconvert if autoconvert is None else autoconvert def get_processors(self, source_file): processors = self.processors @@ -23,6 +29,28 @@ class SpecFileGenerator(object): processors = processors(source_file) return processors + def get_hash(self, source_file): + return md5(''.join([ + source_file.name, + pickle.dumps(self.get_processors(source_file)), + self.format, + pickle.dumps(self.options), + str(self.autoconvert), + ]).encode('utf-8')).hexdigest() + + def generate_filename(self, source_file): + source_filename = source_file.name + filename = None + if source_filename: + hash = self.get_hash(source_file) + extension = suggest_extension(source_filename, self.format) + filename = os.path.normpath(os.path.join( + settings.IMAGEKIT_CACHE_DIR, + os.path.splitext(source_filename)[0], + '%s%s' % (hash, extension))) + + return filename + def process_content(self, content, filename=None, source_file=None): img = open_image(content) original_format = img.format @@ -49,27 +77,44 @@ class SpecFileGenerator(object): content = IKContentFile(filename, imgfile.read(), format=format) return img, content - def get_hash(self, source_file): - return md5(''.join([ - source_file.name, - pickle.dumps(self.get_processors(source_file)), - self.format, - pickle.dumps(self.options), - str(self.autoconvert), - ]).encode('utf-8')).hexdigest() - def generate_filename(self, source_file): - source_filename = source_file.name - filename = None - if source_filename: - hash = self.get_hash(source_file) - extension = suggest_extension(source_filename, self.format) - filename = os.path.normpath(os.path.join( - settings.IMAGEKIT_CACHE_DIR, - os.path.splitext(source_filename)[0], - '%s%s' % (hash, extension))) +class ImageSpec(BaseImageSpec): + storage = None + image_cache_backend = None + image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY - return filename + def __init__(self, processors=None, format=None, options=None, + storage=None, autoconvert=None, image_cache_backend=None, + image_cache_strategy=None): + """ + :param processors: A list of processors to run on the original image. + :param format: The format of the output file. If not provided, + ImageSpecField will try to guess the appropriate format based on the + extension of the filename and the format of the input image. + :param options: A dictionary that will be passed to PIL's + ``Image.save()`` method as keyword arguments. Valid options vary + between formats, but some examples include ``quality``, + ``optimize``, and ``progressive`` for JPEGs. See the PIL + documentation for others. + :param autoconvert: Specifies whether automatic conversion using + ``prepare_image()`` should be performed prior to saving. + :param image_field: The name of the model property that contains the + original image. + :param storage: A Django storage system to use to save the generated + image. + :param image_cache_backend: An object responsible for managing the state + of cached files. Defaults to an instance of + ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + :param image_cache_strategy: A dictionary containing callbacks that + allow you to customize how and when the image cache is validated. + Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` + + """ + super(ImageSpec, self).__init__(processors=processors, format=format, + options=options, autoconvert=autoconvert) + self.storage = storage or self.storage + self.image_cache_backend = image_cache_backend or self.image_cache_backend or get_default_image_cache_backend() + self.image_cache_strategy = StrategyWrapper(image_cache_strategy or self.image_cache_strategy) def generate_file(self, filename, source_file, save=True): """ diff --git a/imagekit/conf.py b/imagekit/conf.py index e43d04e..2767e6b 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -7,4 +7,4 @@ class ImageKitConf(AppConf): CACHE_BACKEND = None CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'ik-' - DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' + DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' diff --git a/imagekit/exceptions.py b/imagekit/exceptions.py new file mode 100644 index 0000000..d3595b4 --- /dev/null +++ b/imagekit/exceptions.py @@ -0,0 +1,8 @@ + + +class UnknownExtensionError(Exception): + pass + + +class UnknownFormatError(Exception): + pass diff --git a/imagekit/files.py b/imagekit/files.py index c2ec36c..0d9837f 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -2,7 +2,6 @@ import os from django.db.models.fields.files import ImageFieldFile -from .generators import SpecFileGenerator from .utils import SpecWrapper, suggest_extension @@ -11,9 +10,6 @@ class ImageSpecFile(ImageFieldFile): spec = SpecWrapper(spec) self.storage = spec.storage or source_file.storage - self.generator = SpecFileGenerator(processors=spec.processors, - format=spec.format, options=spec.options, - autoconvert=spec.autoconvert, storage=self.storage) self.spec = spec self.source_file = source_file @@ -44,11 +40,11 @@ class ImageSpecFile(ImageFieldFile): source_filename = self.source_file.name filepath, basename = os.path.split(source_filename) filename = os.path.splitext(basename)[0] - extension = suggest_extension(source_filename, self.generator.format) + extension = suggest_extension(source_filename, self.spec.format) new_name = '%s%s' % (filename, extension) cache_filename = ['cache', 'iktt'] + self.spec_id.split(':') + \ [filepath, new_name] return os.path.join(*cache_filename) def generate(self, save=True): - return self.generator.generate_file(self.name, self.source_file, save) + return self.spec.generate_file(self.name, self.source_file, save) diff --git a/imagekit/models/__init__.py b/imagekit/models/__init__.py index 4207987..4c3dad7 100644 --- a/imagekit/models/__init__.py +++ b/imagekit/models/__init__.py @@ -1,11 +1,2 @@ from .. import conf from .fields import ImageSpecField, ProcessedImageField -import warnings - - -class ImageSpec(ImageSpecField): - def __init__(self, *args, **kwargs): - warnings.warn('ImageSpec has been moved to' - ' imagekit.models.ImageSpecField. Please use that instead.', - DeprecationWarning) - super(ImageSpec, self).__init__(*args, **kwargs) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index fdb4475..9685218 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,14 +1,11 @@ import os -from django.conf import settings from django.db import models from django.db.models.signals import post_init, post_save, post_delete -from ...imagecache.backends import get_default_image_cache_backend -from ...imagecache.strategies import StrategyWrapper -from ...generators import SpecFileGenerator -from .files import ImageSpecFieldFile, ProcessedImageFieldFile -from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta +from .files import ProcessedImageFieldFile +from .utils import ImageSpecFileDescriptor, ImageKitMeta +from ...base import ImageSpec from ...utils import suggest_extension @@ -19,53 +16,31 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, - image_field=None, pre_cache=None, storage=None, autoconvert=True, + image_field=None, storage=None, autoconvert=True, image_cache_backend=None, image_cache_strategy=None): - """ - :param processors: A list of processors to run on the original image. - :param format: The format of the output file. If not provided, - ImageSpecField will try to guess the appropriate format based on the - extension of the filename and the format of the input image. - :param options: A dictionary that will be passed to PIL's - ``Image.save()`` method as keyword arguments. Valid options vary - between formats, but some examples include ``quality``, - ``optimize``, and ``progressive`` for JPEGs. See the PIL - documentation for others. - :param image_field: The name of the model property that contains the - original image. - :param storage: A Django storage system to use to save the generated - image. - :param autoconvert: Specifies whether automatic conversion using - ``prepare_image()`` should be performed prior to saving. - :param image_cache_backend: An object responsible for managing the state - of cached files. Defaults to an instance of - ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` - :param image_cache_strategy: A dictionary containing callbacks that - allow you to customize how and when the image cache is validated. - Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` - """ - - if pre_cache is not None: - raise Exception('The pre_cache argument has been removed in favor' - ' of cache state backends.') - - # The generator accepts a callable value for processors, but it + # The spec 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.spec = ImageSpec( + processors=p, + format=format, + options=options, + storage=storage, + autoconvert=autoconvert, + image_cache_backend=image_cache_backend, + image_cache_strategy=image_cache_strategy, + ) + self.image_field = image_field - self.storage = storage - self.image_cache_backend = image_cache_backend or \ - get_default_image_cache_backend() - if image_cache_strategy is None: - image_cache_strategy = settings.IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY - self.image_cache_strategy = StrategyWrapper(image_cache_strategy) + + @property + def storage(self): + return self.spec.storage def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) @@ -94,7 +69,7 @@ class ImageSpecField(object): # Register the field with the image_cache_backend try: - self.image_cache_backend.register_field(cls, self, name) + self.spec.image_cache_backend.register_field(cls, self, name) except AttributeError: pass @@ -106,9 +81,9 @@ class ImageSpecField(object): for attname in instance._ik.spec_fields: file = getattr(instance, attname) if created: - file.field.image_cache_strategy.invoke_callback('source_create', file) + file.field.spec.image_cache_strategy.invoke_callback('source_create', file) elif old_hashes[attname] != new_hashes[attname]: - file.field.image_cache_strategy.invoke_callback('source_change', file) + file.field.spec.image_cache_strategy.invoke_callback('source_change', file) @staticmethod def _update_source_hashes(instance): @@ -125,7 +100,7 @@ class ImageSpecField(object): @staticmethod def _post_delete_receiver(sender, instance=None, **kwargs): for spec_file in instance._ik.spec_files: - spec_file.field.image_cache_strategy.invoke_callback('source_delete', spec_file) + spec_file.field.spec.image_cache_strategy.invoke_callback('source_delete', spec_file) @staticmethod def _post_init_receiver(sender, instance, **kwargs): @@ -152,20 +127,16 @@ class ProcessedImageField(models.ImageField): :class:`imagekit.models.ImageSpecField`. """ - if 'quality' in kwargs: - raise Exception('The "quality" keyword argument has been' - """ deprecated. Use `options={'quality': %s}` instead.""" \ - % kwargs['quality']) models.ImageField.__init__(self, verbose_name, name, width_field, height_field, **kwargs) - self.generator = SpecFileGenerator(processors, format=format, - options=options, autoconvert=autoconvert) + self.spec = ImageSpec(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))) name, ext = os.path.splitext(filename) - ext = suggest_extension(filename, self.generator.format) + ext = suggest_extension(filename, self.spec.format) return u'%s%s' % (name, ext) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 2d58845..691ce6b 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -7,7 +7,7 @@ class ImageSpecFieldFile(ImageFieldFile): self.attname = attname def get_hash(self): - return self.field.generator.get_hash(self.source_file) + return self.field.spec.get_hash(self.source_file) @property def source_file(self): @@ -34,16 +34,16 @@ class ImageSpecFieldFile(ImageFieldFile): 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) - self.field.image_cache_strategy.invoke_callback('access', self) + self.field.spec.image_cache_strategy.invoke_callback('access', self) def clear(self): - return self.field.image_cache_backend.clear(self) + return self.field.spec.image_cache_backend.clear(self) def invalidate(self): - return self.field.image_cache_backend.invalidate(self) + return self.field.spec.image_cache_backend.invalidate(self) def validate(self): - return self.field.image_cache_backend.validate(self) + return self.field.spec.image_cache_backend.validate(self) def generate(self, save=True): """ @@ -51,7 +51,7 @@ class ImageSpecFieldFile(ImageFieldFile): the content of the result, ready for saving. """ - return self.field.generator.generate_file(self.name, self.source_file, + return self.field.spec.generate_file(self.name, self.source_file, save) def delete(self, save=False): @@ -91,7 +91,7 @@ class ImageSpecFieldFile(ImageFieldFile): Specifies the filename that the cached image will use. """ - return self.field.generator.generate_filename(self.source_file) + return self.field.spec.generate_filename(self.source_file) @name.setter def name(self, value): @@ -123,7 +123,7 @@ class ImageSpecFieldFile(ImageFieldFile): class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): - new_filename = self.field.generate_filename(self.instance, name) - img, content = self.field.generator.process_content(content, + new_filename = self.field.spec.generate_filename(self.instance, name) + img, content = self.field.spec.process_content(content, new_filename, self) return super(ProcessedImageFieldFile, self).save(name, content, save) diff --git a/imagekit/utils.py b/imagekit/utils.py index ef549f3..fb60035 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -10,6 +10,7 @@ from django.utils.encoding import smart_str, smart_unicode from django.utils.functional import wraps from django.utils.importlib import import_module +from .exceptions import UnknownExtensionError, UnknownFormatError from .lib import Image, ImageFile, StringIO @@ -76,14 +77,6 @@ def _wrap_copy(f): return copy -class UnknownExtensionError(Exception): - pass - - -class UnknownFormatError(Exception): - pass - - _pil_init = 0 diff --git a/tests/core/tests.py b/tests/core/tests.py index b4884c8..4998ce5 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -5,6 +5,7 @@ import os from django.test import TestCase from imagekit import utils +from imagekit.exceptions import UnknownFormatError from .models import (Photo, AbstractImageModel, ConcreteImageModel1, ConcreteImageModel2) from .testutils import create_photo, pickleback @@ -59,7 +60,7 @@ class IKUtilsTest(TestCase): self.assertEqual(utils.format_to_extension('PNG'), '.png') self.assertEqual(utils.format_to_extension('ICO'), '.ico') - with self.assertRaises(utils.UnknownFormatError): + with self.assertRaises(UnknownFormatError): utils.format_to_extension('TXT')