Merge branch 'develop' into release/2.0

* develop:
  IKContentFile accepts format hint
  Additional mimetype utils
  Don't get extension of empty filename
  Tell people to import fields from the models module
  Refactored AutoConvert into prepare_image
  Docstring for save_image
  Kill PIL's chattiness; fixes #91
  PIL bug workaround
  Use StringIO instead of temp file
  Extract reusable save_image function
  Rename SpecFile and move it to utils
  Extract suggest_extension util from generator
  Woah, globals
  Add SpecFile.__unicode__
This commit is contained in:
Bryan Veloso 2012-04-24 13:59:28 -07:00
commit 209afac9e3
9 changed files with 249 additions and 180 deletions

View file

@ -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.
If you only want to save the processed image (without maintaining the original),
you can use a ``ProcessedImageField``::
@ -71,7 +71,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):

View file

@ -5,7 +5,7 @@ API Reference
:mod:`models` Module
--------------------
.. automodule:: imagekit.models.fields
.. automodule:: imagekit.models
:members:

View file

@ -1,39 +1,17 @@
import mimetypes
import os
from StringIO import StringIO
from django.core.files.base import ContentFile
from .processors import ProcessorPipeline, AutoConvert
from .utils import img_to_fobj, open_image, \
format_to_extension, extension_to_format, UnknownFormatError, \
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 self.file.name
from .processors import ProcessorPipeline
from .utils import (img_to_fobj, open_image, IKContentFile, extension_to_format,
UnknownExtensionError)
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
@ -51,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:
@ -61,39 +39,10 @@ class SpecFileGenerator(object):
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())
imgfile = img_to_fobj(img, format, **options)
content = SpecFile(filename, imgfile.read())
content = IKContentFile(filename, imgfile.read(), format=format)
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

View file

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

View file

@ -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):
@ -15,7 +16,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):
"""
@ -47,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
@ -146,14 +147,14 @@ 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):
"""
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:
@ -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)

View file

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

View file

@ -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,90 +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.
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.

View file

@ -1,28 +1,48 @@
import tempfile
import os
import mimetypes
from StringIO import StringIO
import sys
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
from .lib import Image, ImageFile
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
RGBA_TRANSPARENCY_FORMATS = ['PNG']
PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF']
class IKContentFile(ContentFile):
"""
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, format=None):
self.file = ContentFile(content)
self.file.name = filename
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 '')
def __unicode__(self):
return smart_unicode(self.file.name or u'')
def img_to_fobj(img, format, autoconvert=True, **options):
return save_image(img, StringIO(), format, options, autoconvert)
def get_spec_files(instance):
@ -107,6 +127,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.
@ -165,3 +198,176 @@ 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
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 prepares the image for saving using ``prepare_image()``, which will do
some common-sense processing given the target format.
"""
options = options or {}
if autoconvert:
img, save_kwargs = prepare_image(img, format)
options = dict(save_kwargs.items() + options.items())
# Attempt to reset the file pointer.
try:
outfile.seek(0)
except AttributeError:
pass
try:
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
# 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
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)
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

View file

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