django-imagekit/imagekit/specs.py

225 lines
8.3 KiB
Python
Raw Normal View History

from django.conf import settings
from hashlib import md5
2012-02-12 04:48:54 +00:00
import os
import pickle
2012-10-05 02:02:29 +00:00
from .exceptions import UnknownExtensionError, AlreadyRegistered, NotRegistered
2012-10-05 01:37:20 +00:00
from .imagecache.backends import get_default_image_cache_backend
from .imagecache.strategies import StrategyWrapper
2012-05-12 19:45:08 +00:00
from .lib import StringIO
2012-04-20 03:01:07 +00:00
from .processors import ProcessorPipeline
2012-10-05 01:37:20 +00:00
from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj,
suggest_extension)
2012-02-12 04:48:54 +00:00
2012-10-05 02:02:29 +00:00
class SpecRegistry(object):
def __init__(self):
self._specs = {}
def register(self, id, spec):
if id in self._specs:
raise AlreadyRegistered('The spec with id %s is already registered' % id)
self._specs[id] = spec
def unregister(self, id, spec):
try:
del self._specs[id]
except KeyError:
raise NotRegistered('The spec with id %s is not registered' % id)
def get_spec(self, id):
try:
return self._specs[id]
except KeyError:
raise NotRegistered('The spec with id %s is not registered' % id)
spec_registry = SpecRegistry()
2012-10-05 01:37:20 +00:00
class BaseImageSpec(object):
processors = None
format = None
options = None
autoconvert = True
def __init__(self, processors=None, format=None, options=None, autoconvert=None):
self.processors = processors or self.processors or []
self.format = format or self.format
self.options = options or self.options
self.autoconvert = self.autoconvert if autoconvert is None else autoconvert
2012-02-12 04:48:54 +00:00
def get_processors(self, source_file):
processors = self.processors
if callable(processors):
processors = processors(source_file)
return processors
2012-10-05 01:37:20 +00:00
def get_hash(self, source_file):
return md5(''.join([
source_file.name,
pickle.dumps(self.get_processors(source_file)),
self.format,
pickle.dumps(self.options),
str(self.autoconvert),
]).encode('utf-8')).hexdigest()
def generate_filename(self, source_file):
source_filename = source_file.name
filename = None
if source_filename:
hash = self.get_hash(source_file)
extension = suggest_extension(source_filename, self.format)
filename = os.path.normpath(os.path.join(
settings.IMAGEKIT_CACHE_DIR,
os.path.splitext(source_filename)[0],
'%s%s' % (hash, extension)))
return filename
2012-02-12 04:48:54 +00:00
def process_content(self, content, filename=None, source_file=None):
img = open_image(content)
original_format = img.format
# Run the processors
processors = self.get_processors(source_file)
2012-02-12 04:48:54 +00:00
img = ProcessorPipeline(processors or []).process(img)
options = dict(self.options or {})
# Determine the format.
format = self.format
2012-04-21 03:26:16 +00:00
if filename and not format:
2012-02-12 04:48:54 +00:00
# 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
2012-04-21 03:26:16 +00:00
format = format or img.format or original_format or 'JPEG'
2012-02-12 04:48:54 +00:00
imgfile = img_to_fobj(img, format, **options)
2012-04-21 03:30:30 +00:00
content = IKContentFile(filename, imgfile.read(), format=format)
2012-02-12 04:48:54 +00:00
return img, content
2012-09-06 04:07:40 +00:00
2012-10-05 01:37:20 +00:00
class ImageSpec(BaseImageSpec):
storage = None
image_cache_backend = None
image_cache_strategy = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_STRATEGY
2012-10-05 01:37:20 +00:00
def __init__(self, processors=None, format=None, options=None,
storage=None, autoconvert=None, image_cache_backend=None,
image_cache_strategy=None):
"""
:param processors: A list of processors to run on the original image.
:param format: The format of the output file. If not provided,
ImageSpecField will try to guess the appropriate format based on the
extension of the filename and the format of the input image.
:param options: A dictionary that will be passed to PIL's
``Image.save()`` method as keyword arguments. Valid options vary
between formats, but some examples include ``quality``,
``optimize``, and ``progressive`` for JPEGs. See the PIL
documentation for others.
:param autoconvert: Specifies whether automatic conversion using
``prepare_image()`` should be performed prior to saving.
:param image_field: The name of the model property that contains the
original image.
:param storage: A Django storage system to use to save the generated
image.
:param image_cache_backend: An object responsible for managing the state
of cached files. Defaults to an instance of
``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND``
:param image_cache_strategy: A dictionary containing callbacks that
allow you to customize how and when the image cache is validated.
Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY``
"""
super(ImageSpec, self).__init__(processors=processors, format=format,
options=options, autoconvert=autoconvert)
self.storage = storage or self.storage
self.image_cache_backend = image_cache_backend or self.image_cache_backend or get_default_image_cache_backend()
self.image_cache_strategy = StrategyWrapper(image_cache_strategy or self.image_cache_strategy)
2012-02-12 04:48:54 +00:00
def generate_file(self, filename, source_file, save=True):
"""
Generates a new image file by processing the source file and returns
the content of the result, ready for saving.
"""
if source_file: # TODO: Should we error here or something if the source_file doesn't exist?
# Process the original image file.
try:
fp = source_file.storage.open(source_file.name)
except IOError:
return
fp.seek(0)
fp = StringIO(fp.read())
img, content = self.process_content(fp, filename, source_file)
if save:
storage = self.storage or source_file.storage
storage.save(filename, content)
2012-02-14 02:16:45 +00:00
return content
class SpecHost(object):
"""
An object that ostensibly has a spec attribute but really delegates to the
spec registry.
"""
def __init__(self, processors=None, format=None, options=None,
storage=None, autoconvert=None, image_cache_backend=None,
image_cache_strategy=None, spec=None, id=None):
if spec:
if any([processors, format, options, storage, autoconvert,
image_cache_backend, image_cache_strategy]):
raise TypeError('You can provide either an image spec or'
' arguments for the ImageSpec constructor, but not both.')
else:
spec = ImageSpec(
processors=processors,
format=format,
options=options,
storage=storage,
autoconvert=autoconvert,
image_cache_backend=image_cache_backend,
image_cache_strategy=image_cache_strategy,
)
self._original_spec = spec
self.spec_id = None
if id:
# If an id is given, register the spec immediately.
self.register_spec(id)
def register_spec(self, id):
"""
Registers the spec with the specified id. Useful for when the id isn't
known when the instance is constructed (e.g. for ImageSpecFields whose
generated `spec_id`s are only known when they are contributed to a
class)
"""
self.spec_id = id
spec_registry.register(id, self._original_spec)
@property
def spec(self):
"""
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
having to edit model definitions--simply by registering another spec
with the same id.
"""
if not getattr(self, 'spec_id', None):
raise Exception('Object %s has no spec id.' % self)
return spec_registry.get_spec(self.spec_id)