Last active
September 10, 2021 16:29
-
-
Save niccolomineo/f98ccebc08e47c961360f4aecb88ab74 to your computer and use it in GitHub Desktop.
(Django admin inline) PIL thumbnail generation w/ smart cropping
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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