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