2011-09-21 15:37:29 +00:00
import os
import datetime
from StringIO import StringIO
from imagekit . lib import *
2011-09-25 03:09:49 +00:00
from imagekit . utils import img_to_fobj , get_spec_files , open_image
2011-09-23 22:55:12 +00:00
from imagekit . processors import ProcessorPipeline
2011-09-22 01:02:12 +00:00
from django . conf import settings
2011-09-21 15:37:29 +00:00
from django . core . files . base import ContentFile
from django . utils . encoding import force_unicode , smart_str
2011-09-22 01:02:12 +00:00
from django . db import models
2011-09-21 15:37:29 +00:00
from django . db . models . signals import post_save , post_delete
2011-09-20 23:30:29 +00:00
from django . utils . translation import ugettext_lazy as _
from django . template . loader import render_to_string
2011-09-22 04:24:13 +00:00
from django . db . models . fields . files import ImageFieldFile
2011-09-20 23:30:29 +00:00
2011-09-22 01:02:12 +00:00
# Modify image file buffer size.
ImageFile . MAXBLOCK = getattr ( settings , ' PIL_IMAGEFILE_MAXBLOCK ' , 256 * 2 * * 10 )
2011-09-22 04:24:13 +00:00
class _ImageSpecMixin ( object ) :
def __init__ ( self , processors = None , quality = 70 , format = None ) :
self . processors = processors
self . quality = quality
self . format = format
2011-09-22 21:31:26 +00:00
def process ( self , image , file ) :
2011-09-23 22:55:12 +00:00
processors = ProcessorPipeline ( self . processors )
2011-09-24 01:25:47 +00:00
return processors . process ( image . copy ( ) )
2011-09-22 04:24:13 +00:00
class ImageSpec ( _ImageSpecMixin ) :
2011-09-26 00:38:43 +00:00
""" The heart and soul of the ImageKit library, ImageSpec allows you to add
variants of uploaded images to your models .
2011-09-21 15:37:29 +00:00
"""
2011-09-26 00:38:43 +00:00
_upload_to_attr = ' cache_to '
2011-09-22 04:24:13 +00:00
def __init__ ( self , processors = None , quality = 70 , format = None ,
image_field = None , pre_cache = False , storage = None , cache_to = None ) :
2011-09-26 00:38:43 +00:00
"""
: param processors : A list of processors to run on the original image .
: param quality : The quality of the output image . This option is only
used for the JPEG format .
: param format : The format of the output file . If not provided , ImageSpec
will try to guess the appropriate format based on the extension
of the filename and the format of the input image .
: param image_field : The name of the model property that contains the
original image .
: param pre_cache : A boolean that specifies whether the image should be
generated immediately ( True ) or on demand ( False ) .
: param storage : A Django storage system to use to save the generated
image .
: param cache_to : Specifies the filename to use when saving the image
cache file . This is modeled after ImageField ' s ``upload_to`` and
can be either a string ( that specifies a directory ) or a
callable ( that returns a filepath ) . Callable values should
accept the following arguments :
- instance - - The model instance this spec belongs to
- path - - The path of the original image
- specname - - the property name that the spec is bound to on
the model instance
- extension - - A recommended extension . If the format of the
spec is set explicitly , this suggestion will be
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.
"""
2011-09-21 15:37:29 +00:00
2011-09-22 04:24:13 +00:00
_ImageSpecMixin . __init__ ( self , processors , quality = quality ,
format = format )
self . image_field = image_field
self . pre_cache = pre_cache
self . storage = storage
self . cache_to = cache_to
2011-09-21 15:37:29 +00:00
def contribute_to_class ( self , cls , name ) :
setattr ( cls , name , _ImageSpecDescriptor ( self , name ) )
# Connect to the signals only once for this class.
uid = ' %s . %s ' % ( cls . __module__ , cls . __name__ )
post_save . connect ( _post_save_handler ,
sender = cls ,
dispatch_uid = ' %s _save ' % uid )
post_delete . connect ( _post_delete_handler ,
sender = cls ,
dispatch_uid = ' %s .delete ' % uid )
2011-09-22 04:24:13 +00:00
def _get_suggested_extension ( name , format ) :
if format :
# Try to look up an extension by the format
2011-09-23 16:44:53 +00:00
extensions = [ k for k , v in Image . EXTENSION . iteritems ( ) \
2011-09-22 04:24:13 +00:00
if v == format . upper ( ) ]
else :
extensions = [ ]
2011-09-23 16:44:53 +00:00
original_extension = os . path . splitext ( name ) [ 1 ]
2011-09-22 04:24:13 +00:00
if not extensions or original_extension . lower ( ) in extensions :
# If the original extension matches the format, use it.
extension = original_extension
else :
extension = extensions [ 0 ]
return extension
2011-09-24 05:57:50 +00:00
class _ImageSpecFileMixin ( object ) :
def _process_content ( self , filename , content ) :
2011-09-25 03:09:49 +00:00
img = open_image ( content )
2011-09-24 05:57:50 +00:00
original_format = img . format
img = self . field . process ( img , self )
# Determine the format.
format = self . field . format
if not format :
if callable ( getattr ( self . field , self . field . _upload_to_attr ) ) :
# The extension is explicit, so assume they want the matching format.
extension = os . path . splitext ( filename ) [ 1 ] . lower ( )
# Try to guess the format from the extension.
format = Image . EXTENSION . get ( extension )
format = format or img . format or original_format or ' JPEG '
if format != ' JPEG ' :
imgfile = img_to_fobj ( img , format )
else :
imgfile = img_to_fobj ( img , format ,
quality = int ( self . field . quality ) ,
optimize = True )
content = ContentFile ( imgfile . read ( ) )
return img , content
class ImageSpecFile ( _ImageSpecFileMixin ) :
2011-09-24 03:02:17 +00:00
def __init__ ( self , instance , field , attname , source_file ) :
2011-09-22 22:13:32 +00:00
self . field = field
2011-09-21 15:37:29 +00:00
self . _img = None
2011-09-24 03:02:17 +00:00
self . _file = None
2011-09-22 22:13:32 +00:00
self . instance = instance
2011-09-22 01:04:40 +00:00
self . attname = attname
2011-09-24 03:02:17 +00:00
self . source_file = source_file
2011-09-21 15:37:29 +00:00
2011-09-24 03:02:17 +00:00
def _create ( self , lazy = False ) :
""" Creates a new image by running the processors on the source file.
Keyword Arguments :
lazy - - True if an already - existing image should be returned ; False if
a new image should be created and the existing one overwritten .
2011-09-21 15:37:29 +00:00
"""
2011-09-24 03:02:17 +00:00
img = None
if lazy :
img = self . _img
if not img and self . storage . exists ( self . name ) :
2011-09-25 03:09:49 +00:00
img = open_image ( self . file )
2011-09-25 20:18:52 +00:00
if not img and self . source_file : # TODO: Should we error here or something if the source_file doesn't exist?
2011-09-24 05:57:50 +00:00
# Process the original image file
2011-09-21 15:37:29 +00:00
try :
2011-09-24 03:02:17 +00:00
fp = self . source_file . storage . open ( self . source_file . name )
2011-09-21 15:37:29 +00:00
except IOError :
return
fp . seek ( 0 )
fp = StringIO ( fp . read ( ) )
2011-09-24 03:02:17 +00:00
2011-09-24 05:57:50 +00:00
img , content = self . _process_content ( self . name , fp )
2011-09-25 20:18:52 +00:00
self . storage . save ( self . name , content )
2011-09-24 03:02:17 +00:00
self . _img = img
return self . _img
2011-09-21 15:37:29 +00:00
def _delete ( self ) :
2011-09-24 03:02:17 +00:00
if self . source_file :
2011-09-21 15:37:29 +00:00
try :
2011-09-22 13:20:37 +00:00
self . storage . delete ( self . name )
2011-09-21 15:37:29 +00:00
except ( NotImplementedError , IOError ) :
return
@property
def _suggested_extension ( self ) :
2011-09-24 03:02:17 +00:00
return _get_suggested_extension ( self . source_file . name , self . field . format )
2011-09-21 15:37:29 +00:00
2011-09-22 01:02:12 +00:00
def _default_cache_to ( self , instance , path , specname , extension ) :
""" Determines the filename to use for the transformed image. Can be
overridden on a per - spec basis by setting the cache_to property on the
spec .
"""
filepath , basename = os . path . split ( path )
filename = os . path . splitext ( basename ) [ 0 ]
2011-09-23 16:44:53 +00:00
new_name = ' {0} _ {1} {2} ' . format ( filename , specname , extension )
2011-09-22 01:02:12 +00:00
return os . path . join ( os . path . join ( ' cache ' , filepath ) , new_name )
2011-09-21 15:37:29 +00:00
@property
def name ( self ) :
"""
Specifies the filename that the cached image will use . The user can
control this by providing a ` cache_to ` method to the ImageSpec .
"""
2011-09-24 03:02:17 +00:00
filename = self . source_file . name
2011-09-21 15:37:29 +00:00
if filename :
2011-09-22 22:13:32 +00:00
cache_to = self . field . cache_to or self . _default_cache_to
2011-09-21 15:37:29 +00:00
if not cache_to :
raise Exception ( ' No cache_to or default_cache_to value specified ' )
if callable ( cache_to ) :
new_filename = force_unicode ( datetime . datetime . now ( ) . strftime ( \
2011-09-24 03:02:17 +00:00
smart_str ( cache_to ( self . instance , self . source_file . name ,
2011-09-22 01:04:40 +00:00
self . attname , self . _suggested_extension ) ) ) )
2011-09-21 15:37:29 +00:00
else :
dir_name = os . path . normpath ( force_unicode ( datetime . datetime . now ( ) . strftime ( smart_str ( cache_to ) ) ) )
filename = os . path . normpath ( os . path . basename ( filename ) )
new_filename = os . path . join ( dir_name , filename )
return new_filename
@property
2011-09-22 13:20:37 +00:00
def storage ( self ) :
2011-09-24 03:02:17 +00:00
return self . field . storage or self . source_file . storage
2011-09-21 15:37:29 +00:00
@property
def url ( self ) :
2011-09-22 22:13:32 +00:00
if not self . field . pre_cache :
2011-09-24 03:02:17 +00:00
self . _create ( True )
2011-09-22 13:20:37 +00:00
return self . storage . url ( self . name )
2011-09-21 15:37:29 +00:00
@property
def file ( self ) :
2011-09-24 03:02:17 +00:00
if not self . _file :
if not self . storage . exists ( self . name ) :
self . _create ( )
self . _file = self . storage . open ( self . name )
return self . _file
2011-09-21 15:37:29 +00:00
@property
def image ( self ) :
2011-09-24 03:02:17 +00:00
return self . _create ( True )
2011-09-21 15:37:29 +00:00
@property
def width ( self ) :
return self . image . size [ 0 ]
@property
def height ( self ) :
return self . image . size [ 1 ]
class _ImageSpecDescriptor ( object ) :
2011-09-22 22:13:32 +00:00
def __init__ ( self , field , attname ) :
2011-09-24 03:02:17 +00:00
self . attname = attname
2011-09-22 22:13:32 +00:00
self . field = field
2011-09-21 15:37:29 +00:00
2011-09-24 03:02:17 +00:00
def _get_image_field_file ( self , instance ) :
field_name = getattr ( self . field , ' image_field ' , None )
if field_name :
field = getattr ( instance , field_name )
else :
image_fields = [ getattr ( instance , f . attname ) for f in \
instance . __class__ . _meta . fields if \
isinstance ( f , models . ImageField ) ]
if len ( image_fields ) == 0 :
raise Exception ( ' {0} does not define any ImageFields, so your '
' {1} ImageSpec has no image to act on. ' . format (
instance . __class__ . __name__ , self . attname ) )
elif len ( image_fields ) > 1 :
raise Exception ( ' {0} defines multiple ImageFields, but you have '
' not specified an image_field for your {1} '
' ImageSpec. ' . format ( instance . __class__ . __name__ ,
self . attname ) )
else :
field = image_fields [ 0 ]
return field
2011-09-21 15:37:29 +00:00
def __get__ ( self , instance , owner ) :
if instance is None :
2011-09-22 22:13:32 +00:00
return self . field
2011-09-21 15:37:29 +00:00
else :
2011-09-25 16:42:27 +00:00
img_spec_file = ImageSpecFile ( instance , self . field ,
self . attname , self . _get_image_field_file ( instance ) )
setattr ( instance , self . attname , img_spec_file )
return img_spec_file
2011-09-21 15:37:29 +00:00
def _post_save_handler ( sender , instance = None , created = False , raw = False , * * kwargs ) :
if raw :
return
2011-09-22 13:20:37 +00:00
spec_files = get_spec_files ( instance )
for spec_file in spec_files :
2011-09-22 23:44:47 +00:00
if not created :
spec_file . _delete ( )
if spec_file . field . pre_cache :
spec_file . _create ( )
2011-09-21 15:37:29 +00:00
def _post_delete_handler ( sender , instance = None , * * kwargs ) :
assert instance . _get_pk_val ( ) is not None , " %s object can ' t be deleted because its %s attribute is set to None. " % ( instance . _meta . object_name , instance . _meta . pk . attname )
2011-09-22 13:20:37 +00:00
spec_files = get_spec_files ( instance )
for spec_file in spec_files :
spec_file . _delete ( )
2011-09-21 15:37:29 +00:00
2011-09-21 13:39:06 +00:00
class AdminThumbnailView ( object ) :
2011-09-26 00:38:43 +00:00
""" A convenience utility for adding thumbnails to the Django admin change
list .
"""
2011-09-20 23:30:29 +00:00
short_description = _ ( ' Thumbnail ' )
allow_tags = True
2011-09-21 13:39:06 +00:00
def __init__ ( self , image_field , template = None ) :
"""
2011-09-26 00:38:43 +00:00
: param image_field : The name of the ImageField or ImageSpec on the model
to use for the thumbnail .
: param template : The template with which to render the thumbnail
2011-09-21 13:39:06 +00:00
"""
2011-09-20 23:30:29 +00:00
self . image_field = image_field
self . template = template
2011-09-21 13:52:38 +00:00
def __get__ ( self , instance , owner ) :
if instance is None :
return self
else :
return BoundAdminThumbnailView ( instance , self )
2011-09-21 13:39:06 +00:00
class BoundAdminThumbnailView ( AdminThumbnailView ) :
def __init__ ( self , model_instance , unbound_field ) :
super ( BoundAdminThumbnailView , self ) . __init__ ( unbound_field . image_field ,
unbound_field . template )
self . model_instance = model_instance
2011-09-20 23:30:29 +00:00
def __unicode__ ( self ) :
thumbnail = getattr ( self . model_instance , self . image_field , None )
if not thumbnail :
raise Exception ( ' The property {0} is not defined on {1} . ' . format (
self . model_instance , self . image_field ) )
2011-09-24 03:02:17 +00:00
original_image = getattr ( thumbnail , ' source_file ' , None ) or thumbnail
2011-09-20 23:30:29 +00:00
template = self . template or ' imagekit/admin/thumbnail.html '
return render_to_string ( template , {
' model ' : self . model_instance ,
' thumbnail ' : thumbnail ,
' original_image ' : original_image ,
} )
2011-09-21 13:39:06 +00:00
2011-09-21 13:52:38 +00:00
def __get__ ( self , instance , owner ) :
2011-09-21 13:39:06 +00:00
""" Override AdminThumbnailView ' s implementation. """
return self
2011-09-22 04:24:13 +00:00
2011-09-24 05:57:50 +00:00
class ProcessedImageFieldFile ( ImageFieldFile , _ImageSpecFileMixin ) :
2011-09-22 04:24:13 +00:00
def save ( self , name , content , save = True ) :
new_filename = self . field . generate_filename ( self . instance , name )
2011-09-24 05:57:50 +00:00
img , content = self . _process_content ( new_filename , content )
2011-09-22 04:24:13 +00:00
return super ( ProcessedImageFieldFile , self ) . save ( name , content , save )
class ProcessedImageField ( models . ImageField , _ImageSpecMixin ) :
2011-09-26 00:38:43 +00:00
""" ProcessedImageField is an ImageField that runs processors on the uploaded
image * before * saving it to storage . This is in contrast to specs , which
maintain the original . Useful for coercing fileformats or keeping images
within a reasonable size .
"""
2011-09-24 05:57:50 +00:00
_upload_to_attr = ' upload_to '
2011-09-22 04:24:13 +00:00
attr_class = ProcessedImageFieldFile
def __init__ ( self , processors = None , quality = 70 , format = None ,
verbose_name = None , name = None , width_field = None , height_field = None ,
* * kwargs ) :
2011-09-26 00:38:43 +00:00
"""
The ProcessedImageField constructor accepts all of the arguments that
the : class : ` django . db . models . ImageField ` constructor accepts , as well as
the ` ` processors ` ` , ` ` format ` ` , and ` ` quality ` ` arguments of
: class : ` imagekit . models . ImageSpec ` .
"""
2011-09-22 04:24:13 +00:00
_ImageSpecMixin . __init__ ( self , processors , quality = quality ,
format = format )
models . ImageField . __init__ ( self , verbose_name , name , width_field ,
height_field , * * kwargs )
def get_filename ( self , filename ) :
filename = os . path . normpath ( self . storage . get_valid_name ( os . path . basename ( filename ) ) )
name , ext = os . path . splitext ( filename )
ext = _get_suggested_extension ( filename , self . format )
2011-09-24 05:58:19 +00:00
return ' {0} {1} ' . format ( name , ext )