mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-04-28 16:54:42 +00:00
Initial import
This commit is contained in:
commit
5b069cc289
5 changed files with 324 additions and 0 deletions
6
.hgignore
Normal file
6
.hgignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
syntax: glob
|
||||
|
||||
.svn
|
||||
*.pyc
|
||||
*.db
|
||||
.DS_Store
|
||||
0
src/imagekit/__init__.py
Normal file
0
src/imagekit/__init__.py
Normal file
74
src/imagekit/core.py
Normal file
74
src/imagekit/core.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
def get_crop_box(cur_size, new_size, x_pos, y_pos):
|
||||
""" Given a current image size, the target size and x,y positions this
|
||||
function returns a 4-tuple representing the left, upper, right and lower
|
||||
coordinates of the cropped image
|
||||
|
||||
"""
|
||||
cur_width, cur_height = cur_size
|
||||
new_width, new_height = new_size
|
||||
ratio = max(float(new_width)/cur_width, float(new_height)/cur_height)
|
||||
resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio))
|
||||
crop_x, crop_y = (abs(new_width - resize_x), abs(new_height - resize_y))
|
||||
x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2))
|
||||
box_left, box_right = {
|
||||
'left': (0, new_width)
|
||||
'center': (int(x_diff), int(x_diff +new_width))
|
||||
'right': (int(crop_x), int(resize_x))
|
||||
}[x_pos]
|
||||
box_upper, box_lower = {
|
||||
'top': (9, new_height),
|
||||
'center': (int(y_diff), int(y_diff + new_height))
|
||||
'bottom': (int(crop_y), int(resize_y))
|
||||
}[y_pos]
|
||||
return (resize_x, resize_y, (box_left, box_upper, box_right, box_lower))
|
||||
|
||||
|
||||
def resize_image(self, image, spec):
|
||||
cur_width, cur_height = image.size
|
||||
new_width, new_height = spec.size
|
||||
|
||||
if spec.crop:
|
||||
new_x, new_y, box = get_crop_box(new_width, new_height, spec.crop_x, spec.crop_y)
|
||||
new_image = image.resize(new_x, new_y).crop(box)
|
||||
|
||||
|
||||
def resize_image(self, im, photosize):
|
||||
cur_width, cur_height = im.size
|
||||
new_width, new_height = photosize.size
|
||||
if photosize.crop:
|
||||
ratio = max(float(new_width)/cur_width,float(new_height)/cur_height)
|
||||
x = (cur_width * ratio)
|
||||
y = (cur_height * ratio)
|
||||
xd = abs(new_width - x)
|
||||
yd = abs(new_height - y)
|
||||
x_diff = int(xd / 2)
|
||||
y_diff = int(yd / 2)
|
||||
if self.crop_from == 'top':
|
||||
box = (int(x_diff), 0, int(x_diff+new_width), new_height)
|
||||
elif self.crop_from == 'left':
|
||||
box = (0, int(y_diff), new_width, int(y_diff+new_height))
|
||||
elif self.crop_from == 'bottom':
|
||||
box = (int(x_diff), int(yd), int(x_diff+new_width), int(y)) # y - yd = new_height
|
||||
elif self.crop_from == 'right':
|
||||
box = (int(xd), int(y_diff), int(x), int(y_diff+new_height)) # x - xd = new_width
|
||||
else:
|
||||
box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height))
|
||||
im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
|
||||
else:
|
||||
if not new_width == 0 and not new_height == 0:
|
||||
ratio = min(float(new_width)/cur_width,
|
||||
float(new_height)/cur_height)
|
||||
else:
|
||||
if new_width == 0:
|
||||
ratio = float(new_height)/cur_height
|
||||
else:
|
||||
ratio = float(new_width)/cur_width
|
||||
new_dimensions = (int(round(cur_width*ratio)),
|
||||
int(round(cur_height*ratio)))
|
||||
if new_dimensions[0] > cur_width or \
|
||||
new_dimensions[1] > cur_height:
|
||||
if not photosize.upscale:
|
||||
return im
|
||||
im = im.resize(new_dimensions, Image.ANTIALIAS)
|
||||
return im
|
||||
243
src/imagekit/models.py
Normal file
243
src/imagekit/models.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
from datetime import datetime
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ImageSpec(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class SpecAccessor(object):
|
||||
def __init__(self, instance, spec):
|
||||
self._instance = instance
|
||||
self._spec = spec
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._instance._spec_url(spec)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._instance._spec_path(spec)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._instance._spec_size(spec)
|
||||
|
||||
@property
|
||||
def spec(self):
|
||||
return self._spec
|
||||
|
||||
"""
|
||||
|
||||
def _get_SIZE_photosize(self, size):
|
||||
return PhotoSizeCache().sizes.get(size)
|
||||
|
||||
def _get_SIZE_size(self, size):
|
||||
photosize = PhotoSizeCache().sizes.get(size)
|
||||
if not self.size_exists(photosize):
|
||||
self.create_size(photosize)
|
||||
return Image.open(self._get_SIZE_filename(size)).size
|
||||
|
||||
def _get_SIZE_url(self, size):
|
||||
photosize = PhotoSizeCache().sizes.get(size)
|
||||
if not self.size_exists(photosize):
|
||||
self.create_size(photosize)
|
||||
if photosize.increment_count:
|
||||
self.increment_count()
|
||||
return '/'.join([self.cache_url(), self._get_filename_for_size(photosize.name)])
|
||||
|
||||
def _get_SIZE_filename(self, size):
|
||||
photosize = PhotoSizeCache().sizes.get(size)
|
||||
return os.path.join(self.cache_path(),
|
||||
self._get_filename_for_size(photosize.name))
|
||||
|
||||
def add_accessor_methods(self, *args, **kwargs):
|
||||
for size in PhotoSizeCache().sizes.keys():
|
||||
setattr(self, 'get_%s_size' % size,
|
||||
curry(self._get_SIZE_size, size=size))
|
||||
setattr(self, 'get_%s_photosize' % size,
|
||||
curry(self._get_SIZE_photosize, size=size))
|
||||
setattr(self, 'get_%s_url' % size,
|
||||
curry(self._get_SIZE_url, size=size))
|
||||
setattr(self, 'get_%s_filename' % size,
|
||||
curry(self._get_SIZE_filename, size=size))
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ImageModel(models.Model):
|
||||
""" Abstract base class implementing all core ImageKit functionality
|
||||
|
||||
Subclasses of ImageModel can override the inner IKConfig class to customize
|
||||
storage locations and other options.
|
||||
|
||||
"""
|
||||
CROP_X_NONE = 0
|
||||
CROP_X_LEFT = 1
|
||||
CROP_X_CENTER = 2
|
||||
CROP_X_RIGHT = 3
|
||||
|
||||
CROP_Y_NONE = 0
|
||||
CROP_Y_TOP = 1
|
||||
CROP_Y_CENTER = 2
|
||||
CROP_Y_BOTTOM = 3
|
||||
|
||||
CROP_X_CHOICES = (
|
||||
(CROP_X_NONE, 'None'),
|
||||
(CROP_X_LEFT, 'Left'),
|
||||
(CROP_X_CENTER, 'Center'),
|
||||
(CROP_X_RIGHT, 'Right'),
|
||||
)
|
||||
|
||||
CROP_Y_CHOICES = (
|
||||
(CROP_Y_NONE, 'None'),
|
||||
(CROP_Y_TOP, 'Top'),
|
||||
(CROP_Y_CENTER, 'Center'),
|
||||
(CROP_Y_BOTTOM, 'Bottom'),
|
||||
)
|
||||
|
||||
image = models.ImageField(_('image'), upload_to=get_storage_path)
|
||||
crop_x = models.PositiveSmallIntegerField(choices=CROP_X_CHOICES,
|
||||
default=CROP_X_CENTER)
|
||||
crop_y = models.PositiveSmallIntegerField(choices=CROP_Y_CHOICES,
|
||||
default=CROP_Y_CENTER)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class IKConfig:
|
||||
""" Contains the default configuration for ImageModel subclasses.
|
||||
|
||||
Subclasses can inherit from this class to override configuration
|
||||
options.
|
||||
|
||||
"""
|
||||
max_image_size = (100, 100)
|
||||
image_dir = 'image'
|
||||
cache_dir = 'cache'
|
||||
save_count_as = None # Field name on subclass where count is stored
|
||||
cache_filename_format = "%(filename)s_%(specname)s.%(extension)s"
|
||||
|
||||
class SpecCache(object):
|
||||
""" A pseudo-singleton object that caches ImageSpec instances
|
||||
|
||||
Loads all ImageSpecs from the database and caches them to save on the
|
||||
total number of queries made.
|
||||
|
||||
"""
|
||||
__state = {"specs": {}}
|
||||
|
||||
def __init__(self):
|
||||
self.__dict__ = self.__state
|
||||
if not len(self.specs):
|
||||
specs = ImageSpec.objects.all()
|
||||
for spec in specs:
|
||||
self.specs[spec.name] = spec
|
||||
|
||||
def reset(self):
|
||||
self.specs = {}
|
||||
|
||||
def _image_basename(self):
|
||||
""" Returns the basename of the original image file """
|
||||
return os.path.basename(self.image.path)
|
||||
|
||||
def _cache_dir(self):
|
||||
""" Returns the path to the image cache directory """
|
||||
return os.path.join(os.path.dirname(self.image.path), self.IKConfig.cache_dir)
|
||||
|
||||
def _cache_url(self):
|
||||
""" Returns a url pointing to the image cache directory """
|
||||
return '/'.join([os.path.dirname(self.image.url), self.IKConfig.cache_dir])
|
||||
|
||||
def _spec_url(self, spec):
|
||||
return '/'.join([self._cache_url(), self._spec_filename(spec)])
|
||||
|
||||
def _spec_path(self, spec):
|
||||
return os.path.join(self._cache_dir(),
|
||||
self._spec_filename(spec))
|
||||
|
||||
def _spec_filename(self, spec):
|
||||
""" Returns a formatted filename for a specific ImageSpec """
|
||||
filename, extension = os.path.splitext(self._image_basename())
|
||||
return self.IKConfig.cache_filename_format % \
|
||||
{'filename': filename,
|
||||
'specname': spec.name,
|
||||
'extension': extension.lstrip('.')}
|
||||
|
||||
def _increment_view_count(self):
|
||||
""" Increment the view count If a field name is supplied in IKConfig """
|
||||
field_name = IKConfig.save_count_as
|
||||
if field_name is not None:
|
||||
new_count = getattr(self, field_name) + 1
|
||||
setattr(self, field_name, new_count)
|
||||
models.Model.save(self)
|
||||
|
||||
def _file_exists(self, spec):
|
||||
accessor = getattr(self, spec.name)
|
||||
return os.path.isfile(accessor.path)
|
||||
|
||||
def _cache_spec(self, spec):
|
||||
if self._file_exists(spec):
|
||||
return
|
||||
|
||||
# create cache directory if it does not exist
|
||||
if not os.path.isdir(self._cache_path()):
|
||||
os.makedirs(self._cache_path())
|
||||
|
||||
img = Image.open(self.image.path)
|
||||
|
||||
if img.size != spec.size and spec.size != (0, 0):
|
||||
resized = resize_image(img, spec)
|
||||
|
||||
output_filename = self._spec_filename(spec)
|
||||
|
||||
try:
|
||||
if img.format == 'JPEG':
|
||||
resized.save(output_filename, img.format, quality=int(spec.quality))
|
||||
else:
|
||||
try:
|
||||
im.save(im_filename)
|
||||
except KeyError:
|
||||
pass
|
||||
except IOError, e:
|
||||
if os.path.isfile(output_filename):
|
||||
os.unlink(output_filename)
|
||||
raise e
|
||||
|
||||
def _delete_spec(self, spec, remove_dirs=True):
|
||||
if not self._file_exists(spec):
|
||||
return
|
||||
accessor = getattr(self, spec.name)
|
||||
if os.path.isfile(accessor.path):
|
||||
os.remove(accessor.path)
|
||||
if remove_dirs:
|
||||
self._cleanup_cache_dirs
|
||||
|
||||
def _cleanup_cache_dirs(self):
|
||||
try:
|
||||
os.removedirs(self._cache_path())
|
||||
except:
|
||||
pass
|
||||
|
||||
def _clear_cache(self):
|
||||
cache = SpecCache()
|
||||
for photosize in cache.sizes.values():
|
||||
self._delete_spec(spec, False)
|
||||
self._cleanup_cache_dirs()
|
||||
|
||||
def _pre_cache(self):
|
||||
cache = SpecCache()
|
||||
for spec in cache.specs.values():
|
||||
if spec.cache_on_save:
|
||||
self._cache_spec(spec)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._get_pk_val():
|
||||
self._clear_cache()
|
||||
super(ImageModel, self).save(*args, **kwargs)
|
||||
self._pre_cache()
|
||||
|
||||
def delete(self):
|
||||
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
|
||||
self._clear_cache()
|
||||
super(ImageModel, self).delete()
|
||||
1
src/imagekit/views.py
Normal file
1
src/imagekit/views.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Create your views here.
|
||||
Loading…
Reference in a new issue