2011-09-21 15:37:29 +00:00
import os
import datetime
from StringIO import StringIO
from imagekit . lib import *
2011-09-22 13:20:37 +00:00
from imagekit . utils import img_to_fobj , get_spec_files
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-21 15:37:29 +00:00
cache_to = None
""" Specifies the filename to use when saving the image cache file. This is
modeled after ImageField ' s `upload_to` and can accept 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.
If you have not explicitly set a format on your ImageSpec , the extension of
the path returned by this function will be used to infer one .
"""
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-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-22 13:20:37 +00:00
class ImageSpecFile ( object ) :
2011-09-22 22:13:32 +00:00
def __init__ ( self , instance , field , attname ) :
self . field = field
2011-09-21 15:37:29 +00:00
self . _img = 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-21 15:37:29 +00:00
@property
def _format ( self ) :
""" The format used to save the cache file. If the format is set
explicitly on the ImageSpec , that format will be used . Otherwise , the
format will be inferred from the extension of the cache filename ( see
the ` name ` property ) .
"""
2011-09-22 22:13:32 +00:00
format = self . field . format
2011-09-21 15:37:29 +00:00
if not format :
# Get the real (not suggested) extension.
extension = os . path . splitext ( self . name ) [ 1 ] . lower ( )
# Try to guess the format from the extension.
format = Image . EXTENSION . get ( extension )
return format or self . _img . format or ' JPEG '
def _get_imgfile ( self ) :
format = self . _format
if format != ' JPEG ' :
imgfile = img_to_fobj ( self . _img , format )
else :
imgfile = img_to_fobj ( self . _img , format ,
2011-09-22 22:13:32 +00:00
quality = int ( self . field . quality ) ,
2011-09-21 15:37:29 +00:00
optimize = True )
return imgfile
@property
def _imgfield ( self ) :
2011-09-22 22:13:32 +00:00
field_name = getattr ( self . field , ' image_field ' , None )
2011-09-22 01:02:12 +00:00
if field_name :
2011-09-22 22:13:32 +00:00
field = getattr ( self . instance , field_name )
2011-09-22 01:02:12 +00:00
else :
2011-09-22 22:13:32 +00:00
image_fields = [ getattr ( self . instance , f . attname ) for f in \
self . instance . __class__ . _meta . fields if \
2011-09-22 01:02:12 +00:00
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 (
2011-09-22 22:13:32 +00:00
self . instance . __class__ . __name__ , self . attname ) )
2011-09-22 01:02:12 +00:00
elif len ( image_fields ) > 1 :
raise Exception ( ' {0} defines multiple ImageFields, but you have '
' not specified an image_field for your {1} '
2011-09-22 22:13:32 +00:00
' ImageSpec. ' . format ( self . instance . __class__ . __name__ ,
2011-09-22 01:04:40 +00:00
self . attname ) )
2011-09-22 01:02:12 +00:00
else :
field = image_fields [ 0 ]
return field
2011-09-21 15:37:29 +00:00
def _create ( self ) :
if self . _imgfield :
2011-09-22 04:24:13 +00:00
# TODO: Should we error here or something if the image doesn't exist?
2011-09-21 15:37:29 +00:00
if self . _exists ( ) :
return
# process the original image file
try :
fp = self . _imgfield . storage . open ( self . _imgfield . name )
except IOError :
return
fp . seek ( 0 )
fp = StringIO ( fp . read ( ) )
2011-09-24 00:11:18 +00:00
self . _img = self . field . process ( Image . open ( fp ) , self )
2011-09-21 15:37:29 +00:00
# save the new image to the cache
content = ContentFile ( self . _get_imgfile ( ) . read ( ) )
2011-09-22 13:20:37 +00:00
self . storage . save ( self . name , content )
2011-09-21 15:37:29 +00:00
def _delete ( self ) :
if self . _imgfield :
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
def _exists ( self ) :
if self . _imgfield :
2011-09-22 13:20:37 +00:00
return self . storage . exists ( self . name )
2011-09-21 15:37:29 +00:00
@property
def _suggested_extension ( self ) :
2011-09-22 22:13:32 +00:00
return _get_suggested_extension ( self . _imgfield . 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 .
"""
filename = self . _imgfield . name
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-22 22:13:32 +00:00
smart_str ( cache_to ( self . instance , self . _imgfield . 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-22 22:13:32 +00:00
return self . field . storage or self . _imgfield . 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-21 15:37:29 +00:00
self . _create ( )
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 ) :
self . _create ( )
2011-09-22 13:20:37 +00:00
return self . storage . open ( self . name )
2011-09-21 15:37:29 +00:00
@property
def image ( self ) :
if not self . _img :
self . _create ( )
if not self . _img :
self . _img = Image . open ( self . file )
return self . _img
@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-22 01:04:40 +00:00
self . _attname = attname
2011-09-22 22:13:32 +00:00
self . field = 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-22 22:13:32 +00:00
return ImageSpecFile ( instance , self . field , self . _attname )
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-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 ) :
"""
Keyword arguments :
image_field - - the name of the ImageField or ImageSpec on the model to
use for the thumbnail .
template - - the template with which to render the thumbnail
"""
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 ) )
original_image = getattr ( thumbnail , ' _imgfield ' , None ) or thumbnail
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
class ProcessedImageFieldFile ( ImageFieldFile ) :
def save ( self , name , content , save = True ) :
new_filename = self . field . generate_filename ( self . instance , name )
img = Image . open ( content )
2011-09-24 00:11:18 +00:00
img = self . field . process ( img , self )
format = self . _get_format ( new_filename , img . format )
2011-09-22 04:24:13 +00:00
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 super ( ProcessedImageFieldFile , self ) . save ( name , content , save )
def _get_format ( self , name , fallback ) :
format = self . field . format
if not format :
if callable ( self . field . upload_to ) :
# The extension is explicit, so assume they want the matching format.
extension = os . path . splitext ( name ) [ 1 ] . lower ( )
# Try to guess the format from the extension.
format = Image . EXTENSION . get ( extension )
2011-09-24 00:11:18 +00:00
return format or fallback or ' JPEG '
2011-09-22 04:24:13 +00:00
class ProcessedImageField ( models . ImageField , _ImageSpecMixin ) :
attr_class = ProcessedImageFieldFile
def __init__ ( self , processors = None , quality = 70 , format = None ,
verbose_name = None , name = None , width_field = None , height_field = None ,
* * kwargs ) :
_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 )
return ' {0} . {1} ' . format ( name , ext )