Create ImageSpecs; remove generators

This commit is contained in:
Matthew Tretter 2012-10-04 21:37:20 -04:00
parent 675fd02afb
commit d2087aa168
10 changed files with 127 additions and 118 deletions

View file

@ -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)

View file

@ -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):
"""

View file

@ -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'

8
imagekit/exceptions.py Normal file
View file

@ -0,0 +1,8 @@
class UnknownExtensionError(Exception):
pass
class UnknownFormatError(Exception):
pass

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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')