From db4df4f82cd87a3147b394ab31585dea3587adfa Mon Sep 17 00:00:00 2001 From: Clay McClure Date: Tue, 27 Mar 2012 17:17:54 -0400 Subject: [PATCH 01/14] Add SpecFile.__unicode__ SpecFile is based after django.core.files.base.ContentFile, which lacks a __unicode__ method. This leads to an AttributeError when SpecFile.__repr__ is called. This is easily resolved by giving SpecFile a proper __unicode__ method. --- imagekit/generators.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index a53bf09..39f64f4 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -4,6 +4,7 @@ import os from StringIO import StringIO from django.core.files.base import ContentFile +from django.utils.encoding import smart_str, smart_unicode from .processors import ProcessorPipeline, AutoConvert from .utils import img_to_fobj, open_image, \ @@ -25,7 +26,10 @@ class SpecFile(ContentFile): self.file.content_type = None def __str__(self): - return self.file.name + return smart_str(self.file.name or '') + + def __unicode__(self): + return smart_unicode(self.file.name or u'') class SpecFileGenerator(object): From 6e4a8d1b5870031738192b0b2590966926794be6 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 9 Apr 2012 21:25:46 -0400 Subject: [PATCH 02/14] Woah, globals --- imagekit/generators.py | 4 ++-- imagekit/models/fields/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index 39f64f4..bdda2c1 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -33,11 +33,11 @@ class SpecFile(ContentFile): class SpecFileGenerator(object): - def __init__(self, processors=None, format=None, options={}, + def __init__(self, processors=None, format=None, options=None, autoconvert=True, storage=None): self.processors = processors self.format = format - self.options = options + self.options = options or {} self.autoconvert = autoconvert self.storage = storage diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 505ea7e..990e5c3 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -15,7 +15,7 @@ class ImageSpecField(object): variants of uploaded images to your models. """ - def __init__(self, processors=None, format=None, options={}, + def __init__(self, processors=None, format=None, options=None, image_field=None, pre_cache=None, storage=None, cache_to=None, autoconvert=True, image_cache_backend=None): """ @@ -146,7 +146,7 @@ class ProcessedImageField(models.ImageField): """ attr_class = ProcessedImageFieldFile - def __init__(self, processors=None, format=None, options={}, + def __init__(self, processors=None, format=None, options=None, verbose_name=None, name=None, width_field=None, height_field=None, autoconvert=True, **kwargs): """ From 8044b97a33a51fce0cb365b4c9062cdd487698e7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 19 Apr 2012 21:18:42 -0400 Subject: [PATCH 03/14] Extract suggest_extension util from generator --- imagekit/generators.py | 27 ++------------------------- imagekit/models/fields/__init__.py | 3 ++- imagekit/models/fields/files.py | 7 ++++--- imagekit/utils.py | 24 ++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index bdda2c1..a104772 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -7,9 +7,8 @@ from django.core.files.base import ContentFile from django.utils.encoding import smart_str, smart_unicode from .processors import ProcessorPipeline, AutoConvert -from .utils import img_to_fobj, open_image, \ - format_to_extension, extension_to_format, UnknownFormatError, \ - UnknownExtensionError +from .utils import (img_to_fobj, open_image, extension_to_format, + UnknownExtensionError) class SpecFile(ContentFile): @@ -76,28 +75,6 @@ class SpecFileGenerator(object): content = SpecFile(filename, 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 diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 990e5c3..b04443f 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -7,6 +7,7 @@ from ...imagecache import get_default_image_cache_backend from ...generators import SpecFileGenerator from .files import ImageSpecFieldFile, ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta +from ...utils import suggest_extension class ImageSpecField(object): @@ -169,7 +170,7 @@ class ProcessedImageField(models.ImageField): filename = os.path.normpath(self.storage.get_valid_name( os.path.basename(filename))) name, ext = os.path.splitext(filename) - ext = self.generator.suggest_extension(filename) + ext = suggest_extension(filename, self.generator.format) return u'%s%s' % (name, ext) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 1e1d3e4..153c69f 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -4,6 +4,8 @@ import datetime from django.db.models.fields.files import ImageField, ImageFieldFile from django.utils.encoding import force_unicode, smart_str +from ...utils import suggest_extension + class ImageSpecFieldFile(ImageFieldFile): def __init__(self, instance, field, attname): @@ -118,9 +120,8 @@ class ImageSpecFieldFile(ImageFieldFile): raise Exception('No cache_to or default_cache_to value' ' specified') if callable(cache_to): - suggested_extension = \ - self.field.generator.suggest_extension( - self.source_file.name) + suggested_extension = suggest_extension( + self.source_file.name, self.field.generator.format) new_filename = force_unicode( datetime.datetime.now().strftime( smart_str(cache_to(self.instance, diff --git a/imagekit/utils.py b/imagekit/utils.py index 305bdd4..5d21e71 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,3 +1,4 @@ +import os import tempfile import types @@ -165,3 +166,26 @@ def validate_app_cache(apps, force_revalidation=False): if force_revalidation: f.invalidate() f.validate() + + +def suggest_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 From 7d5937ebe64d1f26f103df3040b15588bc4556a4 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 19 Apr 2012 21:26:42 -0400 Subject: [PATCH 04/14] Rename SpecFile and move it to utils --- imagekit/generators.py | 28 ++-------------------------- imagekit/utils.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index a104772..e6cfb2f 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -1,36 +1,12 @@ -import mimetypes import os from StringIO import StringIO -from django.core.files.base import ContentFile -from django.utils.encoding import smart_str, smart_unicode - from .processors import ProcessorPipeline, AutoConvert -from .utils import (img_to_fobj, open_image, extension_to_format, +from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format, UnknownExtensionError) -class SpecFile(ContentFile): - """ - Wraps a ContentFile in a file-like object with a filename - and a content_type. - """ - def __init__(self, filename, content): - self.file = ContentFile(content) - self.file.name = filename - try: - self.file.content_type = mimetypes.guess_type(filename)[0] - except IndexError: - self.file.content_type = None - - def __str__(self): - return smart_str(self.file.name or '') - - def __unicode__(self): - return smart_unicode(self.file.name or u'') - - class SpecFileGenerator(object): def __init__(self, processors=None, format=None, options=None, autoconvert=True, storage=None): @@ -72,7 +48,7 @@ class SpecFileGenerator(object): options.items()) imgfile = img_to_fobj(img, format, **options) - content = SpecFile(filename, imgfile.read()) + content = IKContentFile(filename, imgfile.read()) return img, content def generate_file(self, filename, source_file, save=True): diff --git a/imagekit/utils.py b/imagekit/utils.py index 5d21e71..11f4a4d 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,13 +1,36 @@ import os +import mimetypes import tempfile import types +from django.core.files.base import ContentFile from django.db.models.loading import cache from django.utils.functional import wraps +from django.utils.encoding import smart_str, smart_unicode from imagekit.lib import Image, ImageFile +class IKContentFile(ContentFile): + """ + Wraps a ContentFile in a file-like object with a filename + and a content_type. + """ + def __init__(self, filename, content): + self.file = ContentFile(content) + self.file.name = filename + try: + self.file.content_type = mimetypes.guess_type(filename)[0] + except IndexError: + self.file.content_type = None + + def __str__(self): + return smart_str(self.file.name or '') + + def __unicode__(self): + return smart_unicode(self.file.name or u'') + + def img_to_fobj(img, format, **kwargs): tmp = tempfile.TemporaryFile() try: From e0c9708e63b0b312aab9b82c248875901847d8b3 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 19 Apr 2012 23:01:07 -0400 Subject: [PATCH 05/14] Extract reusable save_image function --- imagekit/generators.py | 12 ++------- imagekit/utils.py | 57 +++++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index e6cfb2f..4d24bce 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -1,8 +1,7 @@ import os - from StringIO import StringIO -from .processors import ProcessorPipeline, AutoConvert +from .processors import ProcessorPipeline from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format, UnknownExtensionError) @@ -38,14 +37,7 @@ class SpecFileGenerator(object): 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()) + format = format or img.format or original_format or 'JPEG' imgfile = img_to_fobj(img, format, **options) content = IKContentFile(filename, imgfile.read()) diff --git a/imagekit/utils.py b/imagekit/utils.py index 11f4a4d..c54ff3f 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -8,7 +8,8 @@ from django.db.models.loading import cache from django.utils.functional import wraps from django.utils.encoding import smart_str, smart_unicode -from imagekit.lib import Image, ImageFile +from .lib import Image, ImageFile +from .processors import AutoConvert class IKContentFile(ContentFile): @@ -31,22 +32,8 @@ class IKContentFile(ContentFile): return smart_unicode(self.file.name or u'') -def img_to_fobj(img, format, **kwargs): - tmp = tempfile.TemporaryFile() - try: - img.save(tmp, format, **kwargs) - except IOError: - # PIL can have problems saving large JPEGs if MAXBLOCK isn't big enough, - # So if we have a problem saving, we temporarily increase it. See - # http://github.com/jdriscoll/django-imagekit/issues/50 - old_maxblock = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = img.size[0] * img.size[1] - try: - img.save(tmp, format, **kwargs) - finally: - ImageFile.MAXBLOCK = old_maxblock - tmp.seek(0) - return tmp +def img_to_fobj(img, format, autoconvert=True, **options): + return save_image(img, tempfile.TemporaryFile(), format, options, autoconvert) def get_spec_files(instance): @@ -212,3 +199,39 @@ def suggest_extension(name, format): else: extension = suggested_extension return extension + + +def save_image(img, outfile, format, options=None, autoconvert=True): + options = options or {} + + if autoconvert: + autoconvert_processor = AutoConvert(format) + img = autoconvert_processor.process(img) + options = dict(autoconvert_processor.save_kwargs.items() + + options.items()) + + # Attempt to reset the file pointer. + try: + outfile.seek(0) + except AttributeError: + pass + + try: + img.save(outfile, format, **options) + except IOError: + # PIL can have problems saving large JPEGs if MAXBLOCK isn't big enough, + # So if we have a problem saving, we temporarily increase it. See + # http://github.com/jdriscoll/django-imagekit/issues/50 + old_maxblock = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = img.size[0] * img.size[1] + try: + img.save(outfile, format, **options) + finally: + ImageFile.MAXBLOCK = old_maxblock + + try: + outfile.seek(0) + except AttributeError: + pass + + return outfile From 6f8a22c5bfc02930d869f8f5637b1d980a44fbb1 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 19 Apr 2012 23:35:49 -0400 Subject: [PATCH 06/14] Use StringIO instead of temp file --- imagekit/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imagekit/utils.py b/imagekit/utils.py index c54ff3f..8cdaec1 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,6 +1,6 @@ import os import mimetypes -import tempfile +from StringIO import StringIO import types from django.core.files.base import ContentFile @@ -33,7 +33,7 @@ class IKContentFile(ContentFile): def img_to_fobj(img, format, autoconvert=True, **options): - return save_image(img, tempfile.TemporaryFile(), format, options, autoconvert) + return save_image(img, StringIO(), format, options, autoconvert) def get_spec_files(instance): From 5fdf9d6a91074ef41147eea9fd9767556368d2e0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 19 Apr 2012 23:59:20 -0400 Subject: [PATCH 07/14] PIL bug workaround --- imagekit/processors/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/imagekit/processors/base.py b/imagekit/processors/base.py index afc0f4f..1be6736 100644 --- a/imagekit/processors/base.py +++ b/imagekit/processors/base.py @@ -208,6 +208,11 @@ class AutoConvert(object): # In other words, a P-format image converted to an # RGBA-formatted image by a processor and then saved as a # P-format image will give the expected results. + + # Work around a bug in PIL: split() doesn't check to see if + # img is loaded. + img.load() + alpha = img.split()[-1] mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, From b6f629d644f75651b90da9b0e07aa04c29f5227a Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 01:51:52 -0400 Subject: [PATCH 08/14] Kill PIL's chattiness; fixes #91 --- imagekit/utils.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/imagekit/utils.py b/imagekit/utils.py index 8cdaec1..6a4d4a2 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,6 +1,7 @@ import os import mimetypes from StringIO import StringIO +import sys import types from django.core.files.base import ContentFile @@ -217,7 +218,8 @@ def save_image(img, outfile, format, options=None, autoconvert=True): pass try: - img.save(outfile, format, **options) + with quiet(): + img.save(outfile, format, **options) except IOError: # PIL can have problems saving large JPEGs if MAXBLOCK isn't big enough, # So if we have a problem saving, we temporarily increase it. See @@ -235,3 +237,20 @@ def save_image(img, outfile, format, options=None, autoconvert=True): pass return outfile + + +class quiet(object): + """ + A context manager for suppressing the stderr activity of PIL's C libraries. + Based on http://stackoverflow.com/a/978264/155370 + + """ + def __enter__(self): + self.stderr_fd = sys.__stderr__.fileno() + self.null_fd = os.open(os.devnull, os.O_RDWR) + self.old = os.dup(self.stderr_fd) + os.dup2(self.null_fd, self.stderr_fd) + + def __exit__(self, *args, **kwargs): + os.dup2(self.old, self.stderr_fd) + os.close(self.null_fd) From 222c9ba22a351c26284f79dbedcc2e64cd1d00e0 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 21:23:06 -0400 Subject: [PATCH 09/14] Docstring for save_image --- imagekit/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/imagekit/utils.py b/imagekit/utils.py index 6a4d4a2..9807f7a 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -203,6 +203,15 @@ def suggest_extension(name, format): def save_image(img, outfile, format, options=None, autoconvert=True): + """ + Wraps PIL's ``Image.save()`` method. There are two main benefits of using + this function over PIL's: + + 1. It gracefully handles the infamous "Suspension not allowed here" errors. + 2. It incorporates the AutoConvert processor, which will do some + common-sense processing given the target format. + + """ options = options or {} if autoconvert: From 667e265c9401206201c60e39cba92efe20aedd84 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 21:37:43 -0400 Subject: [PATCH 10/14] Refactored AutoConvert into prepare_image Because of its need to return kwargs for ``Image.save()``, it never really fit the mold of a processor. --- imagekit/models/fields/__init__.py | 4 +- imagekit/processors/base.py | 93 -------------------------- imagekit/utils.py | 104 +++++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 102 deletions(-) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index b04443f..eb7b8d4 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -48,8 +48,8 @@ class ImageSpecField(object): based on that format. if not, the extension of the original file will be passed. You do not have to use this extension, it's only a recommendation. - :param autoconvert: Specifies whether the AutoConvert processor - should be run before saving. + :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 diff --git a/imagekit/processors/base.py b/imagekit/processors/base.py index 1be6736..06239e2 100644 --- a/imagekit/processors/base.py +++ b/imagekit/processors/base.py @@ -1,10 +1,6 @@ from imagekit.lib import Image, ImageColor, ImageEnhance -RGBA_TRANSPARENCY_FORMATS = ['PNG'] -PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF'] - - class ProcessorPipeline(list): """ A :class:`list` of other processors. This class allows any object that @@ -173,95 +169,6 @@ class Transpose(object): return img -class AutoConvert(object): - """A processor that does some common-sense conversions based on the target - format. This includes things like preserving transparency and quantizing. - This processors is used automatically by ``ImageSpecField`` and - ``ProcessedImageField`` immediately before saving the image unless you - specify ``autoconvert=False``. - - """ - - def __init__(self, format): - self.format = format - - def process(self, img): - matte = False - self.save_kwargs = {} - self.rgba_ = img.mode == 'RGBA' - if self.rgba_: - if self.format in RGBA_TRANSPARENCY_FORMATS: - pass - elif self.format in PALETTE_TRANSPARENCY_FORMATS: - # If you're going from a format with alpha transparency to one - # with palette transparency, transparency values will be - # snapped: pixels that are more opaque than not will become - # fully opaque; pixels that are more transparent than not will - # become fully transparent. This will not produce a good-looking - # result if your image contains varying levels of opacity; in - # that case, you'll probably want to use a processor to matte - # the image on a solid color. The reason we don't matte by - # default is because not doing so allows processors to treat - # RGBA-format images as a super-type of P-format images: if you - # have an RGBA-format image with only a single transparent - # color, and save it as a GIF, it will retain its transparency. - # In other words, a P-format image converted to an - # RGBA-formatted image by a processor and then saved as a - # P-format image will give the expected results. - - # Work around a bug in PIL: split() doesn't check to see if - # img is loaded. - img.load() - - alpha = img.split()[-1] - mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) - img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, - colors=255) - img.paste(255, mask) - self.save_kwargs['transparency'] = 255 - else: - # Simply converting an RGBA-format image to an RGB one creates a - # gross result, so we matte the image on a white background. If - # that's not what you want, that's fine: use a processor to deal - # with the transparency however you want. This is simply a - # sensible default that will always produce something that looks - # good. Or at least, it will look better than just a straight - # conversion. - matte = True - elif img.mode == 'P': - if self.format in PALETTE_TRANSPARENCY_FORMATS: - try: - self.save_kwargs['transparency'] = img.info['transparency'] - except KeyError: - pass - elif self.format in RGBA_TRANSPARENCY_FORMATS: - # Currently PIL doesn't support any RGBA-mode formats that - # aren't also P-mode formats, so this will never happen. - img = img.convert('RGBA') - else: - matte = True - else: - img = img.convert('RGB') - - # GIFs are always going to be in palette mode, so we can do a little - # optimization. Note that the RGBA sources also use adaptive - # quantization (above). Images that are already in P mode don't need - # any quantization because their colors are already limited. - if self.format == 'GIF': - img = img.convert('P', palette=Image.ADAPTIVE) - - if matte: - img = img.convert('RGBA') - bg = Image.new('RGBA', img.size, (255, 255, 255)) - bg.paste(img, img) - img = bg.convert('RGB') - - if self.format == 'JPEG': - self.save_kwargs['optimize'] = True - - return img - - class Anchor(object): """ Defines all the anchor points needed by the various processor classes. diff --git a/imagekit/utils.py b/imagekit/utils.py index 9807f7a..0890feb 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -10,7 +10,10 @@ from django.utils.functional import wraps from django.utils.encoding import smart_str, smart_unicode from .lib import Image, ImageFile -from .processors import AutoConvert + + +RGBA_TRANSPARENCY_FORMATS = ['PNG'] +PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF'] class IKContentFile(ContentFile): @@ -208,17 +211,15 @@ def save_image(img, outfile, format, options=None, autoconvert=True): this function over PIL's: 1. It gracefully handles the infamous "Suspension not allowed here" errors. - 2. It incorporates the AutoConvert processor, which will do some - common-sense processing given the target format. + 2. It prepares the image for saving using ``prepare_image()``, which will do + some common-sense processing given the target format. """ options = options or {} if autoconvert: - autoconvert_processor = AutoConvert(format) - img = autoconvert_processor.process(img) - options = dict(autoconvert_processor.save_kwargs.items() + - options.items()) + img, save_kwargs = prepare_image(img, format) + options = dict(save_kwargs.items() + options.items()) # Attempt to reset the file pointer. try: @@ -263,3 +264,92 @@ class quiet(object): def __exit__(self, *args, **kwargs): os.dup2(self.old, self.stderr_fd) os.close(self.null_fd) + + +def prepare_image(img, format): + """ + Prepares the image for saving to the provided format by doing some + common-sense conversions. This includes things like preserving transparency + and quantizing. This function is used automatically by ``save_image()`` + (and classes like ``ImageSpecField`` and ``ProcessedImageField``) + immediately before saving unless you specify ``autoconvert=False``. It is + provided as a utility for those doing their own processing. + + :param img: The image to prepare for saving. + :param format: The format that the image will be saved to. + + """ + matte = False + save_kwargs = {} + + if img.mode == 'RGBA': + if format in RGBA_TRANSPARENCY_FORMATS: + pass + elif format in PALETTE_TRANSPARENCY_FORMATS: + # If you're going from a format with alpha transparency to one + # with palette transparency, transparency values will be + # snapped: pixels that are more opaque than not will become + # fully opaque; pixels that are more transparent than not will + # become fully transparent. This will not produce a good-looking + # result if your image contains varying levels of opacity; in + # that case, you'll probably want to use a processor to matte + # the image on a solid color. The reason we don't matte by + # default is because not doing so allows processors to treat + # RGBA-format images as a super-type of P-format images: if you + # have an RGBA-format image with only a single transparent + # color, and save it as a GIF, it will retain its transparency. + # In other words, a P-format image converted to an + # RGBA-formatted image by a processor and then saved as a + # P-format image will give the expected results. + + # Work around a bug in PIL: split() doesn't check to see if + # img is loaded. + img.load() + + alpha = img.split()[-1] + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, + colors=255) + img.paste(255, mask) + save_kwargs['transparency'] = 255 + else: + # Simply converting an RGBA-format image to an RGB one creates a + # gross result, so we matte the image on a white background. If + # that's not what you want, that's fine: use a processor to deal + # with the transparency however you want. This is simply a + # sensible default that will always produce something that looks + # good. Or at least, it will look better than just a straight + # conversion. + matte = True + elif img.mode == 'P': + if format in PALETTE_TRANSPARENCY_FORMATS: + try: + save_kwargs['transparency'] = img.info['transparency'] + except KeyError: + pass + elif format in RGBA_TRANSPARENCY_FORMATS: + # Currently PIL doesn't support any RGBA-mode formats that + # aren't also P-mode formats, so this will never happen. + img = img.convert('RGBA') + else: + matte = True + else: + img = img.convert('RGB') + + # GIFs are always going to be in palette mode, so we can do a little + # optimization. Note that the RGBA sources also use adaptive + # quantization (above). Images that are already in P mode don't need + # any quantization because their colors are already limited. + if format == 'GIF': + img = img.convert('P', palette=Image.ADAPTIVE) + + if matte: + img = img.convert('RGBA') + bg = Image.new('RGBA', img.size, (255, 255, 255)) + bg.paste(img, img) + img = bg.convert('RGB') + + if format == 'JPEG': + save_kwargs['optimize'] = True + + return img, save_kwargs From af7c12cb684eb44ebdb0ea2f5eb420ca5b48f044 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 21:43:59 -0400 Subject: [PATCH 11/14] Tell people to import fields from the models module --- README.rst | 6 +++--- docs/apireference.rst | 2 +- imagekit/models/__init__.py | 4 ++-- imagekit/models/fields/__init__.py | 2 +- tests/core/models.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 5a2ea01..3e6d21c 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ Much like ``django.db.models.ImageField``, Specs are defined as properties of a model class:: from django.db import models - from imagekit.models.fields import ImageSpecField + from imagekit.models import ImageSpecField class Photo(models.Model): original_image = models.ImageField(upload_to='photos') @@ -49,7 +49,7 @@ an ImageFile-like object (just like with a normal photo.original_image.url # > '/media/photos/birthday.tiff' photo.formatted_image.url # > '/media/cache/photos/birthday_formatted_image.jpeg' -Check out ``imagekit.models.fields.ImageSpecField`` for more information. +Check out ``imagekit.models.ImageSpecField`` for more information. Processors @@ -60,7 +60,7 @@ something to it, and return the result. By providing a list of processors to your spec, you can expose different versions of the original image:: from django.db import models - from imagekit.models.fields import ImageSpecField + from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFill, Adjust class Photo(models.Model): diff --git a/docs/apireference.rst b/docs/apireference.rst index d4a2ed8..df4b5c7 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -5,7 +5,7 @@ API Reference :mod:`models` Module -------------------- -.. automodule:: imagekit.models.fields +.. automodule:: imagekit.models :members: diff --git a/imagekit/models/__init__.py b/imagekit/models/__init__.py index 97798e1..c5ef221 100644 --- a/imagekit/models/__init__.py +++ b/imagekit/models/__init__.py @@ -5,6 +5,6 @@ import warnings class ImageSpec(ImageSpecField): def __init__(self, *args, **kwargs): warnings.warn('ImageSpec has been moved to' - ' imagekit.models.fields.ImageSpecField. Please use that' - ' instead.', DeprecationWarning) + ' 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 eb7b8d4..b2cceda 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -154,7 +154,7 @@ class ProcessedImageField(models.ImageField): The ProcessedImageField constructor accepts all of the arguments that the :class:`django.db.models.ImageField` constructor accepts, as well as the ``processors``, ``format``, and ``options`` arguments of - :class:`imagekit.models.fields.ImageSpecField`. + :class:`imagekit.models.ImageSpecField`. """ if 'quality' in kwargs: diff --git a/tests/core/models.py b/tests/core/models.py index 35ffffb..2c3d8e4 100644 --- a/tests/core/models.py +++ b/tests/core/models.py @@ -1,6 +1,6 @@ from django.db import models -from imagekit.models.fields import ImageSpecField +from imagekit.models import ImageSpecField from imagekit.processors import Adjust from imagekit.processors import ResizeToFill from imagekit.processors import SmartCrop From 7b34716d9e8f9a2deac9efcde85181418218e737 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 23:26:16 -0400 Subject: [PATCH 12/14] Don't get extension of empty filename --- imagekit/generators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index 4d24bce..5c2f004 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -29,7 +29,7 @@ class SpecFileGenerator(object): # Determine the format. format = self.format - if not format: + if filename and not format: # Try to guess the format from the extension. extension = os.path.splitext(filename)[1].lower() if extension: @@ -37,7 +37,7 @@ class SpecFileGenerator(object): format = extension_to_format(extension) except UnknownExtensionError: pass - format = format or img.format or original_format or 'JPEG' + format = format or img.format or original_format or 'JPEG' imgfile = img_to_fobj(img, format, **options) content = IKContentFile(filename, imgfile.read()) From b466ff3723ebba28afef94839e9158ca88e69842 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 23:27:35 -0400 Subject: [PATCH 13/14] Additional mimetype utils --- imagekit/utils.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/imagekit/utils.py b/imagekit/utils.py index 0890feb..5614b59 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -122,6 +122,19 @@ def _format_to_extension(format): return None +def extension_to_mimetype(ext): + try: + filename = 'a%s' % (ext or '') # guess_type requires a full filename, not just an extension + mimetype = mimetypes.guess_type(filename)[0] + except IndexError: + mimetype = None + return mimetype + + +def format_to_mimetype(format): + return extension_to_mimetype(format_to_extension(format)) + + def extension_to_format(extension): """Returns the format that corresponds to the provided extension. From 89eb05668e121c4853461b31a162049b942d778b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 20 Apr 2012 23:30:30 -0400 Subject: [PATCH 14/14] IKContentFile accepts format hint --- imagekit/generators.py | 2 +- imagekit/utils.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/imagekit/generators.py b/imagekit/generators.py index 5c2f004..50d04ac 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -40,7 +40,7 @@ class SpecFileGenerator(object): format = format or img.format or original_format or 'JPEG' imgfile = img_to_fobj(img, format, **options) - content = IKContentFile(filename, imgfile.read()) + content = IKContentFile(filename, imgfile.read(), format=format) return img, content def generate_file(self, filename, source_file, save=True): diff --git a/imagekit/utils.py b/imagekit/utils.py index 5614b59..d175b65 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -18,16 +18,21 @@ PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF'] class IKContentFile(ContentFile): """ - Wraps a ContentFile in a file-like object with a filename - and a content_type. + Wraps a ContentFile in a file-like object with a filename and a + content_type. A PIL image format can be optionally be provided as a content + type hint. + """ - def __init__(self, filename, content): + def __init__(self, filename, content, format=None): self.file = ContentFile(content) self.file.name = filename - try: - self.file.content_type = mimetypes.guess_type(filename)[0] - except IndexError: - self.file.content_type = None + mimetype = getattr(self.file, 'content_type', None) + if format and not mimetype: + mimetype = format_to_mimetype(format) + if not mimetype: + ext = os.path.splitext(filename or '')[1] + mimetype = extension_to_mimetype(ext) + self.file.content_type = mimetype def __str__(self): return smart_str(self.file.name or '')