Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
  • 10 commits
  • 9 files changed
  • 0 commit comments
  • 1 contributor
View
30 cropduster/admin.py
@@ -4,26 +4,27 @@
class SizeInline(admin.TabularInline):
model = Size
- prepopulated_fields = {"slug" : ('name',)}
+ prepopulated_fields = {"slug" : ("name",)}
fieldsets = (
(None, {
- 'fields': (
- 'name',
- 'slug',
- 'width',
- 'height',
- 'auto_size',
- 'size_set',
- 'aspect_ratio',
+ "fields": (
+ "name",
+ "slug",
+ "width",
+ "height",
+ "auto_size",
+ "size_set",
+ "aspect_ratio",
+ "create_on_request",
)
}),
)
- readonly_fields = ('aspect_ratio',)
+ readonly_fields = ("aspect_ratio",)
class SizeSetAdmin(admin.ModelAdmin):
- prepopulated_fields = {"slug" : ('name',)}
+ prepopulated_fields = {"slug" : ("name",)}
inlines = [
SizeInline,
@@ -32,7 +33,12 @@ class SizeSetAdmin(admin.ModelAdmin):
class Media:
js = (
- settings.STANDARD_ADMIN_MEDIA_PREFIX + 'cropduster/js/size_set.js',
+ settings.STANDARD_ADMIN_MEDIA_PREFIX + "cropduster/js/size_set.js",
)
+ css = {
+ "all": (
+ settings.STANDARD_ADMIN_MEDIA_PREFIX + "cropduster/css/size_set.css",
+ )
+ }
admin.site.register(SizeSet, SizeSetAdmin)
View
89 cropduster/models.py
@@ -26,18 +26,25 @@ class CachingMixin(object):
class SizeSet(CachingMixin, models.Model):
objects = CachingManager()
+
name = models.CharField(max_length=255, db_index=True)
+
slug = models.SlugField(max_length=50, null=False,)
def __unicode__(self):
return u"%s" % self.name
- def get_size_by_ratio(self):
+ def get_size_by_ratio(self, created=False):
""" Shorthand to get all the unique ratios for display in the admin,
rather than show every possible thumbnail
"""
size_query = Size.objects.all().filter(size_set__id=self.id)
+
+ # Whether to only get the image sizes that have been created
+ if created:
+ size_query = size_query.filter(create_on_request=False)
+
size_query.query.group_by = ["aspect_ratio"]
try:
return size_query
@@ -78,15 +85,17 @@ class Size(CachingMixin, models.Model):
aspect_ratio = models.FloatField(default=1)
+ create_on_request = models.BooleanField("Crop on request", default=False)
+
def clean(self):
if not (self.width or self.height):
- raise ValidationError("Crop size requires either a width, a height, or both")
- elif GENERATION_CHOICES[self.auto_size][1] != "Auto-Size" and not (self.width and self.height):
+ raise ValidationError("Size requires either a width, a height, or both")
+ elif GENERATION_CHOICES[self.auto_size][1] == "Auto-Crop" and not (self.width and self.height):
"""
Raise a validation error if one of the sizes is not set for cropping.
Auto-size is the only one that can take a missing size.
"""
- raise ValidationError("Cropping requires both sizes be valid")
+ raise ValidationError("Auto-crop requires both sizes be specified")
def save(self, *args, **kwargs):
self.aspect_ratio = utils.aspect_ratio(self.width, self.height)
@@ -129,15 +138,23 @@ def save(self, *args, **kwargs):
super(Crop, self).save(*args, **kwargs)
if self.size:
- # get all the sizes with the same aspect ratio as this crop/size
+ # get all the manually cropped sizes with the same aspect ratio as this crop/size
sizes = Size.objects.all().filter(
aspect_ratio=self.size.aspect_ratio,
size_set=self.size.size_set,
- ).filter(auto_size=0).order_by("-width")
+ auto_size=0,
+ create_on_request=False
+ ).order_by("-width")
if sizes:
# create the cropped image
- cropped_image = utils.create_cropped_image(self.image.image.path, self.crop_x, self.crop_y, self.crop_w, self.crop_h)
+ cropped_image = utils.create_cropped_image(
+ self.image.image.path,
+ self.crop_x,
+ self.crop_y,
+ self.crop_w,
+ self.crop_h
+ )
# loop through the other sizes of the same aspect ratio, and create those crops
for size in sizes:
@@ -147,40 +164,66 @@ def save(self, *args, **kwargs):
if not os.path.exists(self.image.folder_path):
os.makedirs(self.image.folder_path)
- thumbnail.save(self.image.thumbnail_path(size), **IMAGE_SAVE_PARAMS)
+ thumbnail.save(self.image.thumbnail_path(size.slug), **IMAGE_SAVE_PARAMS)
class Image(CachingMixin, models.Model):
objects = CachingManager()
+
image = models.ImageField(
upload_to=settings.CROPDUSTER_UPLOAD_PATH + "%Y/%m/%d",
max_length=255,
db_index=True
)
+
size_set = models.ForeignKey(
SizeSet,
)
+
attribution = models.CharField(max_length=255, blank=True, null=True)
+
caption = models.CharField(max_length=255, blank=True, null=True)
def save(self, *args, **kwargs):
super(Image, self).save(*args, **kwargs)
-
- for size in self.size_set.size_set.all().exclude(auto_size=0):
- auto_crop = True if size.auto_size == 1 else False
-
- if self.image.width > size.width and self.image.height > size.height:
- thumbnail = utils.rescale(pil.open(self.image.path), size.width, size.height, crop=auto_crop)
- else:
- thumbnail = pil.open(self.image.path)
-
- if not os.path.exists(self.folder_path):
- os.makedirs(self.folder_path)
- thumbnail.save(self.thumbnail_path(size), **IMAGE_SAVE_PARAMS)
-
+
+ # get all the auto sized thumbnails and create them
+ sizes = self.size_set.size_set.all().filter(
+ auto_size__in=[1,2],
+ create_on_request=False
+ )
+ for size in sizes:
+ self.create_individual_thumbnail(size)
+
+ def create_individual_thumbnail(self, size):
+
+ auto_crop = True if size.auto_size == 1 else False
+
+ if size.auto_size == 0:
+ try:
+ crop = Crop.objects.get(size=size, image=self)
+ cropped_image = utils.create_cropped_image(
+ self.image.path,
+ crop.crop_x,
+ crop.crop_y,
+ crop.crop_w,
+ crop.crop_h
+ )
+ except Crop.DoesNotExist:
+ # auto-crop if no crop is defined
+ cropped_image = pil.open(self.image.path)
+ auto_crop = True
+ else:
+ cropped_image = pil.open(self.image.path)
+ thumbnail = utils.rescale(cropped_image, size.width, size.height, crop=auto_crop)
+
+ if not os.path.exists(self.folder_path):
+ os.makedirs(self.folder_path)
+
+ thumbnail.save(self.thumbnail_path(size.slug), **IMAGE_SAVE_PARAMS)
class Meta:
db_table = "cropduster_image"
@@ -198,10 +241,10 @@ def folder_path(self):
file_root, extension = os.path.splitext(file)
return u"%s" % os.path.join(file_path, file_root)
- def thumbnail_path(self, size):
+ def thumbnail_path(self, size_slug):
file_path, file = os.path.split(self.image.path)
file_root, extension = os.path.splitext(file)
- return u"%s" % os.path.join(file_path, file_root, size.slug) + extension
+ return u"%s" % os.path.join(file_path, file_root, size_slug) + extension
@property
def folder_url(self):
View
7 cropduster/static/admin/cropduster/css/size_set.css
@@ -0,0 +1,7 @@
+.height, .height input, .width, .width input {
+ width:50px;
+}
+
+.group .name, .group .name input, .group .slug, .group .slug input {
+ width:200px;
+}
View
2  cropduster/templates/admin/inline.html
@@ -53,7 +53,7 @@
<div class="cropduster_thumbs">
{% if image %}
- {% for size in image.size_set.get_size_by_ratio() %}
+ {% for size in image.size_set.get_size_by_ratio(created=True) %}
<img src="{{ image.thumbnail_url(size.slug) }}" />
{% endfor %}
{% endif %}
View
18 cropduster/templates/admin/upload.html
@@ -95,6 +95,7 @@ <h1 id="step-header">Upload, Crop, and Generate Thumbnails {% if min_w or min_h
$(document).ready(function(){
+ {% if min_w and min_h %}
$("#cropbox img").Jcrop({
"setSelect": [
scale_down({{ crop_x }}),
@@ -106,6 +107,23 @@ <h1 id="step-header">Upload, Crop, and Generate Thumbnails {% if min_w or min_h
"aspectRatio": {{ aspect_ratio }},
"onChange": updateCrop
});
+ {% endif %}
+
+ {% if min_w and not min_h %}
+ $("#cropbox img").Jcrop({
+ "minSize":[scale_down({{ min_w }}), 0],
+ "onChange": updateCrop
+ });
+ {% endif %}
+
+ {% if min_h and not min_w %}
+ $("#cropbox img").Jcrop({
+ "minSize":[0, scale_down({{ min_h }})],
+ "onChange": updateCrop
+ });
+
+ {% endif %}
+
});
}(django.jQuery));
View
65 cropduster/templatetags/images.py
@@ -3,45 +3,68 @@
register = template.Library()
from django.conf import settings
from cropduster.models import Size
+from os.path import exists
+CROPDUSTER_CROP_ONLOAD = getattr(settings, "CROPDUSTER_CROP_ONLOAD", True)
+CROPDUSTER_KITTY_MODE = getattr(settings, "CROPDUSTER_KITTY_MODE", False)
-image_sizes = Size.objects.all()
-image_size_map = {}
-for size in image_sizes:
- image_size_map[(size.size_set_id, size.slug)] = size
+
+# preload a map of image sizes so it doesn"t make a DB call for each templatetag use
+IMAGE_SIZE_MAP = {}
+for size in Size.objects.all():
+ IMAGE_SIZE_MAP[(size.size_set_id, size.slug)] = size
@register.object
-def get_image(image, size_name="large", template_name="image.html", width=None, height=None, **kwargs):
+def get_image(image, size_name=None, template_name="image.html", **kwargs):
+ """ Templatetag to get the HTML for an image from a cropduster image object """
if image:
+ if CROPDUSTER_CROP_ONLOAD:
+ # If set, will check for thumbnail existence
+ # if not there, will create the thumb based on predefiend crop/size settings
+
+ thumb_path = image.thumbnail_path(size_name)
+ if not exists(thumb_path) and exists(image.image.path):
+ try:
+ size = image.size_set.size_set.get(slug=size_name)
+ except Size.DoesNotExist:
+ return ""
+ image.create_individual_thumbnail(size)
+
image_url = image.thumbnail_url(size_name)
- if image_url is None or image_url == "":
+
+ if not image_url:
return ""
try:
- image_size = image_size_map[(image.size_set_id,size_name)]
+ image_size = IMAGE_SIZE_MAP[(image.size_set_id, size_name)]
except KeyError:
return ""
- kwargs["image_url"] = image_url
- kwargs["width"] = width or image_size.width or ""
- kwargs["height"] = height or image_size.height or ""
+ # Set all the args that get passed to the template
+
+ kwargs["image_url"] = image_url
+
+ kwargs["width"] = image_size.width if hasattr(image_size, "width") else ""
+
+ kwargs["height"] = image_size.height if hasattr(image_size, "height") else ""
+
+
+ if CROPDUSTER_KITTY_MODE:
+ kwargs["image_url"] = "http://placekitten.com/%s/%s" % (kwargs["width"], kwargs["height"])
-
- if hasattr(settings, "CROPDUSTER_KITTY_MODE") and settings.CROPDUSTER_KITTY_MODE:
- kwargs["image_url"] = "http://placekitten.com/{0}/{1}".format(kwargs["width"], kwargs["height"])
-
kwargs["size_name"] = size_name
+
kwargs["attribution"] = image.attribution
- kwargs["alt"] = kwargs["alt"] if "alt" in kwargs else image.caption
- kwargs["title"] = kwargs["title"] if "title" in kwargs else kwargs["alt"]
-
-
+
+ if hasattr(image, "caption"): kwargs["alt"] = image.caption
+
+ if "title" not in kwargs: kwargs["title"] = kwargs["alt"]
- tpl = get_template("templatetags/" + template_name)
- ctx = template.Context(kwargs)
- return tpl.render(ctx)
+ tmpl = get_template("templatetags/" + template_name)
+ context = template.Context(kwargs)
+ return tmpl.render(context)
else:
return ""
View
57 cropduster/utils.py
@@ -1,24 +1,23 @@
from PIL import Image
from decimal import Decimal
-def aspect_ratio(w, h):
- if not h or not w:
+def aspect_ratio(width, height):
+ if not height or not width:
return 1
else:
- return Decimal(str(round(float(w)/float(h), 2)))
+ return Decimal(str(round(float(width)/float(height), 2)))
-
-def rescale(img, w=0, h=0, crop=True, **kwargs):
+def rescale(img, width=0, height=0, auto_crop=True, **kwargs):
"""Rescale the given image, optionally cropping it to make sure the result image has the specified width and height."""
- if w <= 0:
- w = float(img.size[0] * h) /float(img.size[1])
- if h <= 0:
- h = float(img.size[1] * w) /float(img.size[0])
+ if width <= 0:
+ width = float(img.size[0] * height) /float(img.size[1])
+ if height <= 0:
+ height = float(img.size[1] * width) /float(img.size[0])
- max_width = w
- max_height = h
+ max_width = width
+ max_height = height
src_width, src_height = img.size
@@ -28,7 +27,8 @@ def rescale(img, w=0, h=0, crop=True, **kwargs):
dst_ratio = float(dst_width) / float(dst_height)
- if crop:
+ if auto_crop:
+
if dst_ratio < src_ratio:
crop_height = src_height
crop_width = crop_height * dst_ratio
@@ -39,36 +39,35 @@ def rescale(img, w=0, h=0, crop=True, **kwargs):
crop_height = crop_width / dst_ratio
x_offset = 0
y_offset = float(src_height - crop_height) / 3
-
-
- img = img.crop((
- int(x_offset),
- int(y_offset),
- int(x_offset+crop_width),
- int(y_offset+crop_height)
- ))
+
+ img = img.crop((
+ int(x_offset),
+ int(y_offset),
+ int(x_offset + crop_width),
+ int(y_offset + crop_height)
+ ))
# if not cropping, don't squish, use w/h as max values to resize on
else:
- if (src_ratio * w) > h:
- dst_width = src_ratio * h
- dst_height = h
+ if (src_ratio * width) > height:
+ dst_width = src_ratio * height
+ dst_height = height
else:
- dst_width = w
- dst_height = w/src_ratio
-
- img = img.resize((int(dst_width), int(dst_height)), Image.ANTIALIAS)
+ dst_width = width
+ dst_height = width/src_ratio
+
+ img = img.resize((int(dst_width), int(dst_height)), Image.ANTIALIAS)
return img
-def create_cropped_image(path=None, x=0, y=0, w=0, h=0):
+def create_cropped_image(path=None, x=0, y=0, width=0, height=0):
if path is None:
raise ValueError("A path must be specified")
img = Image.open(path)
img.copy()
img.load()
- img = img.crop((x, y, x + w, y + h))
+ img = img.crop((x, y, x + width, y + height))
img.load()
return img
View
2  cropduster/views.py
@@ -219,7 +219,7 @@ def upload(request):
# No more cropping to be done, close out
else :
- image_thumbs = [image.thumbnail_url(size.slug) for size in image.size_set.get_size_by_ratio()]
+ image_thumbs = [image.thumbnail_url(size.slug) for size in image.size_set.get_size_by_ratio(created=True)]
context = {
"image": image,
View
6 cropduster/widgets.py
@@ -27,8 +27,8 @@ def render(self, name, value, attrs=None):
image = None
- t = loader.get_template(self.template)
- c = Context({
+ template = loader.get_template(self.template)
+ context = Context({
"image": image,
"size_set": self.size_set,
"static_url": settings.STATIC_URL,
@@ -36,4 +36,4 @@ def render(self, name, value, attrs=None):
"input": input,
"attrs": attrs,
})
- return t.render(c)
+ return template.render(context)

No commit comments for this range

Something went wrong with that request. Please try again.