diff --git a/imagekit/files.py b/imagekit/files.py index 9916c2c..1dd8976 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -5,7 +5,7 @@ from django.utils.encoding import smart_str, smart_unicode import os from .signals import before_access from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, - get_singleton) + get_singleton, generate) class BaseIKFile(File): @@ -115,7 +115,8 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): def generate(self): # Generate the file - content = self.generator.generate() + content = generate(self.generator) + actual_name = self.storage.save(self.name, content) if actual_name != self.name: diff --git a/imagekit/forms/fields.py b/imagekit/forms/fields.py index 40bb5b5..903f6ae 100644 --- a/imagekit/forms/fields.py +++ b/imagekit/forms/fields.py @@ -1,5 +1,6 @@ from django.forms import ImageField from ..specs import SpecHost +from ..utils import generate class ProcessedImageField(ImageField, SpecHost): @@ -22,7 +23,7 @@ class ProcessedImageField(ImageField, SpecHost): data = super(ProcessedImageField, self).clean(data, initial) if data: - spec = self.get_spec() # HINTS?!?!?!?!?! - data = spec.apply(data, data.name) + spec = self.get_spec(source=data) + data = generate(spec) return data diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 26037fd..0fbbad6 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -1,13 +1,13 @@ from django.db.models.fields.files import ImageFieldFile import os -from ...utils import suggest_extension +from ...utils import suggest_extension, generate class ProcessedImageFieldFile(ImageFieldFile): def save(self, name, content, save=True): filename, ext = os.path.splitext(name) - spec = self.field.get_spec() # TODO: What "hints"? + spec = self.field.get_spec(source=content) ext = suggest_extension(name, spec.format) new_name = '%s%s' % (filename, ext) - content = spec.apply(content, new_name) + content = generate(spec) return super(ProcessedImageFieldFile, self).save(new_name, content, save) diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 39dbc52..2324a33 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -29,7 +29,7 @@ class ImageSpecFileDescriptor(object): self.attname)) else: source = image_fields[0] - spec = self.field.get_spec(source=source) # TODO: What "hints" should we pass here? + spec = self.field.get_spec(source=source) file = GeneratedImageCacheFile(spec) instance.__dict__[self.attname] = file return file diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 015d4a6..01c7131 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,13 +3,11 @@ from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle -from ..exceptions import UnknownExtensionError -from ..files import GeneratedImageCacheFile, IKContentFile +from ..files import GeneratedImageCacheFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline -from ..utils import (open_image, extension_to_format, img_to_fobj, - suggest_extension) +from ..utils import open_image, img_to_fobj, suggest_extension from ..registry import generator_registry, register @@ -38,7 +36,7 @@ class BaseImageSpec(object): """ - def __init__(self, **kwargs): + def __init__(self): self.image_cache_backend = self.image_cache_backend or get_default_image_cache_backend() self.image_cache_strategy = StrategyWrapper(self.image_cache_strategy) @@ -85,10 +83,9 @@ class ImageSpec(BaseImageSpec): """ - def __init__(self, source, **kwargs): + def __init__(self, source): self.source = source self.processors = self.processors or [] - self.kwargs = kwargs super(ImageSpec, self).__init__() @property @@ -130,7 +127,6 @@ class ImageSpec(BaseImageSpec): def get_hash(self): return md5(pickle.dumps([ self.source.name, - self.kwargs, self.processors, self.format, self.options, @@ -140,9 +136,7 @@ class ImageSpec(BaseImageSpec): def generate(self): # TODO: Move into a generator base class # TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.) - source = self.source - filename = self.kwargs.get('filename') - img = open_image(source) + img = open_image(self.source) original_format = img.format # Run the processors @@ -150,22 +144,8 @@ class ImageSpec(BaseImageSpec): img = ProcessorPipeline(processors or []).process(img) options = dict(self.options or {}) - - # Determine the format. - format = self.format - if filename and 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' - - imgfile = img_to_fobj(img, format, **options) - # TODO: Is this the right place to wrap the file? Can we use a mixin instead? Is it even still having the desired effect? Re: #111 - content = IKContentFile(filename, imgfile.read(), format=format) + format = self.format or img.format or original_format or 'JPEG' + content = img_to_fobj(img, format, **options) return content @@ -230,7 +210,7 @@ class SpecHost(object): self.spec_id = id register.generator(id, self._original_spec) - def get_spec(self, **kwargs): + def get_spec(self, source): """ Look up the spec by the spec id. We do this (instead of storing the spec as an attribute) so that users can override apps' specs--without @@ -240,4 +220,4 @@ class SpecHost(object): """ if not getattr(self, 'spec_id', None): raise Exception('Object %s has no spec id.' % self) - return generator_registry.get(self.spec_id, **kwargs) + return generator_registry.get(self.spec_id, source=source) diff --git a/imagekit/utils.py b/imagekit/utils.py index ac93cf6..82a2c02 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -2,9 +2,11 @@ import logging import os import mimetypes import sys +from tempfile import NamedTemporaryFile import types from django.core.exceptions import ImproperlyConfigured +from django.core.files import File from django.db.models.loading import cache from django.utils.functional import wraps from django.utils.importlib import import_module @@ -382,3 +384,41 @@ def get_logger(logger_name='imagekit', add_null_handler=True): if add_null_handler: logger.addHandler(logging.NullHandler()) return logger + + +def get_field_info(field_file): + """ + A utility for easily extracting information about the host model from a + Django FileField (or subclass). This is especially useful for when you want + to alter processors based on a property of the source model. For example:: + + class MySpec(ImageSpec): + def __init__(self, source): + instance, attname = get_field_info(source) + self.processors = [SmartResize(instance.thumbnail_width, + instance.thumbnail_height)] + + """ + return ( + getattr(field_file, 'instance', None), + getattr(getattr(field_file, 'field', None), 'attname', None), + ) + + +def generate(generator): + """ + Calls the ``generate()`` method of a generator instance, and then wraps the + result in a Django File object so Django knows how to save it. + + """ + content = generator.generate() + + # If the file doesn't have a name, Django will raise an Exception while + # trying to save it, so we create a named temporary file. + if not getattr(content, 'name', None): + f = NamedTemporaryFile() + f.write(content.read()) + f.seek(0) + content = f + + return File(content) diff --git a/tests/models.py b/tests/models.py index 199fdc0..17e887b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,9 +1,12 @@ from django.db import models +from imagekit.models import ProcessedImageField from imagekit.models import ImageSpecField -from imagekit.processors import Adjust -from imagekit.processors import ResizeToFill -from imagekit.processors import SmartCrop +from imagekit.processors import Adjust, ResizeToFill, SmartCrop + + +class ImageModel(models.Model): + image = models.ImageField(upload_to='b') class Photo(models.Model): @@ -18,6 +21,11 @@ class Photo(models.Model): format='JPEG', options={'quality': 90}) +class ProcessedImageFieldModel(models.Model): + processed = ProcessedImageField([SmartCrop(50, 50)], format='JPEG', + options={'quality': 90}, upload_to='p') + + class AbstractImageModel(models.Model): original_image = models.ImageField(upload_to='photos') abstract_class_spec = ImageSpecField() diff --git a/tests/test_fields.py b/tests/test_fields.py new file mode 100644 index 0000000..bce294f --- /dev/null +++ b/tests/test_fields.py @@ -0,0 +1,36 @@ +from django import forms +from django.core.files.base import File +from django.core.files.uploadedfile import SimpleUploadedFile +from imagekit import forms as ikforms +from imagekit.processors import SmartCrop +from nose.tools import eq_ +from . import imagespecs # noqa +from .models import ProcessedImageFieldModel, ImageModel +from .utils import get_image_file + + +def test_model_processedimagefield(): + instance = ProcessedImageFieldModel() + file = File(get_image_file()) + instance.processed.save('whatever.jpeg', file) + instance.save() + + eq_(instance.processed.width, 50) + eq_(instance.processed.height, 50) + + +def test_form_processedimagefield(): + class TestForm(forms.ModelForm): + image = ikforms.ProcessedImageField(spec_id='tests:testform_image', + processors=[SmartCrop(50, 50)], format='JPEG') + + class Meta: + model = ImageModel + + upload_file = get_image_file() + file_dict = {'image': SimpleUploadedFile('abc.jpg', upload_file.read())} + form = TestForm({}, file_dict) + instance = form.save() + + eq_(instance.image.width, 50) + eq_(instance.image.height, 50) diff --git a/tests/utils.py b/tests/utils.py index 1a4e3fc..2763f8f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ from bs4 import BeautifulSoup import os from django.conf import settings -from django.core.files.base import ContentFile +from django.core.files import File from django.template import Context, Template from imagekit.lib import Image, StringIO import pickle @@ -26,10 +26,8 @@ def create_image(): def create_instance(model_class, image_name): instance = model_class() - img = get_image_file() - file = ContentFile(img.read()) - instance.original_image = file - instance.original_image.save(image_name, file) + img = File(get_image_file()) + instance.original_image.save(image_name, img) instance.save() img.close() return instance