From d632fc70fab17d962f26369b4f2e0460430f4298 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:27:21 -0500 Subject: [PATCH 01/12] Copy contents to NamedTemporaryFile if generated file has no name --- imagekit/files.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/imagekit/files.py b/imagekit/files.py index 9916c2c..b4efd84 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -3,6 +3,7 @@ from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode import os +from tempfile import NamedTemporaryFile from .signals import before_access from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, get_singleton) @@ -116,7 +117,16 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): def generate(self): # Generate the file content = self.generator.generate() - actual_name = self.storage.save(self.name, content) + + # 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 + + actual_name = self.storage.save(self.name, File(content)) if actual_name != self.name: get_logger().warning('The storage backend %s did not save the file' From eef1e41448ad11e0a0e78f3515123226388fde46 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:28:23 -0500 Subject: [PATCH 02/12] Remove code that used old filename kwarg --- imagekit/specs/__init__.py | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 9bfab2c..e05aac8 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,15 +1,14 @@ from django.conf import settings +from django.core.files import File 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 @@ -140,9 +139,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 +147,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 From 6ff1d35fbea771a692fe7e650ed6889e922fe819 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:31:53 -0500 Subject: [PATCH 03/12] Remove unused import --- imagekit/specs/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index e05aac8..062f239 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.core.files import File from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os From 4737ac64c43a2af4a892f93fe6df19b4c9288407 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:35:38 -0500 Subject: [PATCH 04/12] Specs no longer accept arbitrary kwargs Only the source. --- imagekit/management/commands/warmimagecache.py | 2 +- imagekit/models/fields/utils.py | 2 +- imagekit/registry.py | 2 +- imagekit/specs/__init__.py | 10 ++++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/imagekit/management/commands/warmimagecache.py b/imagekit/management/commands/warmimagecache.py index 6c9822d..2d147f5 100644 --- a/imagekit/management/commands/warmimagecache.py +++ b/imagekit/management/commands/warmimagecache.py @@ -22,7 +22,7 @@ class Command(BaseCommand): for source_group in source_group_registry.get(spec_id): for source in source_group.files(): if source: - spec = generator_registry.get(spec_id, source=source) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.) + spec = generator_registry.get(spec_id, source=source) self.stdout.write(' %s\n' % source) try: # TODO: Allow other validation actions through command option 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/registry.py b/imagekit/registry.py index 8d082cd..9c0f4b8 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -102,7 +102,7 @@ class SourceGroupRegistry(object): if source_group not in self._source_groups: return - for spec in (generator_registry.get(id, source=source, **info) + for spec in (generator_registry.get(id, source=source) for id in self._source_groups[source_group]): event_name = { source_created: 'source_created', diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 062f239..d2b154b 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -36,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) @@ -83,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 @@ -128,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, @@ -212,7 +210,7 @@ class SpecHost(object): self.spec_id = id register.spec(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 @@ -222,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) From d52b9c810006c3551e286739c0238f268c958a6f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 21:47:54 -0500 Subject: [PATCH 05/12] Add utility for extracting field info --- imagekit/utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/imagekit/utils.py b/imagekit/utils.py index ac93cf6..e6c2943 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -382,3 +382,22 @@ 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), + ) From 9dd7bef709a53cd8757fb04ee07eaa61740d7fed Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:07:31 -0500 Subject: [PATCH 06/12] Simplify import --- tests/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/models.py b/tests/models.py index 199fdc0..1aa55e4 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,9 +1,7 @@ from django.db import models 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 Photo(models.Model): From 234082e63cba8b86066d63554b85b9f3d594d411 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:34:29 -0500 Subject: [PATCH 07/12] Extract generate() util, to make files Django likes --- imagekit/files.py | 15 +++------------ imagekit/utils.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/imagekit/files.py b/imagekit/files.py index b4efd84..1dd8976 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -3,10 +3,9 @@ from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.utils.encoding import smart_str, smart_unicode import os -from tempfile import NamedTemporaryFile from .signals import before_access from .utils import (format_to_mimetype, extension_to_mimetype, get_logger, - get_singleton) + get_singleton, generate) class BaseIKFile(File): @@ -116,17 +115,9 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): def generate(self): # Generate the file - content = self.generator.generate() + content = generate(self.generator) - # 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 - - actual_name = self.storage.save(self.name, File(content)) + actual_name = self.storage.save(self.name, content) if actual_name != self.name: get_logger().warning('The storage backend %s did not save the file' diff --git a/imagekit/utils.py b/imagekit/utils.py index e6c2943..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 @@ -401,3 +403,22 @@ def get_field_info(field_file): 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) From c6f2c2e7a7377eec830e30680ce69a23c5e1c89c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:35:57 -0500 Subject: [PATCH 08/12] Add test for ProcessedImageField --- tests/models.py | 6 ++++++ tests/test_fields.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/test_fields.py diff --git a/tests/models.py b/tests/models.py index 1aa55e4..0b997ea 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,6 @@ from django.db import models +from imagekit.models import ProcessedImageField from imagekit.models import ImageSpecField from imagekit.processors import Adjust, ResizeToFill, SmartCrop @@ -16,6 +17,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..ee7301c --- /dev/null +++ b/tests/test_fields.py @@ -0,0 +1,15 @@ +from django.core.files.base import File +from nose.tools import eq_ +from . import imagespecs # noqa +from .models import ProcessedImageFieldModel +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) From 84b30e990fdb6a24aa48f8d36b998d2df8b07b89 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:37:00 -0500 Subject: [PATCH 09/12] Fix imagekit.models.fields.ProcessedImageField --- imagekit/models/fields/files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) From f4917ab7ca267c446420209b98812c5ae8b59d55 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:39:14 -0500 Subject: [PATCH 10/12] Clean up util method --- tests/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 From b45a22abe6f472b6fc875b442fdc90e3a4330284 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:54:08 -0500 Subject: [PATCH 11/12] Add test for imagekit.forms.fields.ProcessedImageField --- tests/models.py | 4 ++++ tests/test_fields.py | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/models.py b/tests/models.py index 0b997ea..17e887b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -5,6 +5,10 @@ from imagekit.models import ImageSpecField from imagekit.processors import Adjust, ResizeToFill, SmartCrop +class ImageModel(models.Model): + image = models.ImageField(upload_to='b') + + class Photo(models.Model): original_image = models.ImageField(upload_to='photos') diff --git a/tests/test_fields.py b/tests/test_fields.py index ee7301c..bce294f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,7 +1,11 @@ +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 +from .models import ProcessedImageFieldModel, ImageModel from .utils import get_image_file @@ -13,3 +17,20 @@ def test_model_processedimagefield(): 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) From c202234e827b294f7a7d418d868f0c6f95715929 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 23 Jan 2013 22:54:25 -0500 Subject: [PATCH 12/12] Fix imagekit.forms.fields.ProcessedImageField --- imagekit/forms/fields.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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