Skip to content

Instantly share code, notes, and snippets.

@niccolomineo
Last active September 10, 2021 16:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save niccolomineo/f98ccebc08e47c961360f4aecb88ab74 to your computer and use it in GitHub Desktop.
Save niccolomineo/f98ccebc08e47c961360f4aecb88ab74 to your computer and use it in GitHub Desktop.
(Django admin inline) PIL thumbnail generation w/ smart cropping
# Requirements:
# - a model with `file`and `thumbnail` fields.
# - the smartcrop module for Python https://github.com/smartcrop/smartcrop.py
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
"""Set MyModel Admin."""
formset = MyModelFormset
def generate_media_thumbnail(self, obj):
"""Generate media thumbnail and return sizes."""
def get_alpha_composited_image(image):
"""Return image with converted transparency layer if alpha detected."""
if image.mode == "RGBA":
background = Image.new("RGBA", image.size, (255, 255, 255))
image = Image.alpha_composite(background, image)
image = image.convert("RGB")
return image
def get_thumbnail(image):
"""Return smart-cropped image as thumbnail."""
image = get_alpha_composited_image(image.copy())
best_crop = SmartCrop().crop(image, THUMBNAIL_SIZE[0], THUMBNAIL_SIZE[1])[
"top_crop"
]
left = best_crop["x"]
top = best_crop["y"]
right = best_crop["width"] + best_crop["x"]
bottom = best_crop["height"] + best_crop["y"]
return image.crop((left, top, right, bottom)).resize(
(THUMBNAIL_SIZE[0], THUMBNAIL_SIZE[1])
)
image = Image.open(obj.media.file.file)
thumbnail = get_thumbnail(image)
image_file = BytesIO()
thumbnail.save(image_file, image.format)
obj.thumbnail.save(obj.media.name, File(image_file), save=False)
return {
"original": {"width": image.size[0], "height": image.size[1]},
"thumbnail": {"width": obj.thumbnail.width, "height": obj.thumbnail.height},
}
def save_formset(self, request, form, formset, change):
"""Save inline formset."""
objs = formset.save(commit=False)
for obj in formset.deleted_objects:
obj.delete()
for obj in objs:
if isinstance(obj, MyModel):
sizes = self.generate_media_thumbnail(obj)
formset.save_m2m()
THUMBNAIL_SIZE = (200, 200)
ALLOWED_IMAGE_FORMATS = (".jpg", ".jpeg", ".png")
class MyModelFormset(forms.models.BaseInlineFormSet):
"""Define formset."""
def file_extension_allowed(self, file_path):
"""Allow JPG, JPEG and PNG image extensions only."""
return file_path.endswith(ALLOWED_IMAGE_FORMATS)
def image_larger_than_thumbnail(self, image_data):
"""Validate image dimensions."""
image_size = get_image_dimensions(image_data)
return image_size > (
THUMBNAIL_SIZE[0],
THUMBNAIL_SIZE[1],
)
def clean(self, exclude=None):
"""Validate fields."""
for form in self.forms:
media_field_data = form.cleaned_data.get("media", None)
if media_field_data:
if not self.file_extension_allowed(media_field_data.name):
raise ValidationError(
f'Sono supportati {", ".join(list(ALLOWED_IMAGE_FORMATS))}'
)
if not self.image_larger_than_thumbnail(media_field_data):
raise ValidationError(
f"L'immagine inserita non può essere più piccola di "
f"{THUMBNAIL_SIZE[0]}x"
f"{THUMBNAIL_SIZE[1]} px"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment