refactor Picture.generate_thumbnails

This commit is contained in:
imperosol
2026-05-01 18:00:22 +02:00
parent 060dde78e7
commit 441a016025
4 changed files with 40 additions and 54 deletions
+1 -2
View File
@@ -622,8 +622,7 @@ class Command(BaseCommand):
) )
pict.file.name = p.name pict.file.name = p.name
pict.full_clean() pict.full_clean()
pict.generate_thumbnails() pict.generate_thumbnails(save=True)
pict.save()
img_skia = Picture.objects.get(name="skia.jpg") img_skia = Picture.objects.get(name="skia.jpg")
img_sli = Picture.objects.get(name="sli.jpg") img_sli = Picture.objects.get(name="sli.jpg")
-17
View File
@@ -25,7 +25,6 @@ from django.core.files.base import ContentFile
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.timezone import localdate from django.utils.timezone import localdate
from PIL import ExifTags
from PIL.Image import Image, Resampling from PIL.Image import Image, Resampling
RED_PIXEL_PNG: Final[bytes] = ( RED_PIXEL_PNG: Final[bytes] = (
@@ -178,22 +177,6 @@ def resize_image_explicit(
return ContentFile(content.getvalue()) return ContentFile(content.getvalue())
def exif_auto_rotate(image):
for orientation in ExifTags.TAGS:
if ExifTags.TAGS[orientation] == "Orientation":
break
exif = dict(image._getexif().items())
if exif[orientation] == 3:
image = image.rotate(180, expand=True)
elif exif[orientation] == 6:
image = image.rotate(270, expand=True)
elif exif[orientation] == 8:
image = image.rotate(90, expand=True)
return image
def get_client_ip(request: HttpRequest) -> str | None: def get_client_ip(request: HttpRequest) -> str | None:
headers = ( headers = (
"X_FORWARDED_FOR", # Common header for proxies "X_FORWARDED_FOR", # Common header for proxies
+1 -2
View File
@@ -126,9 +126,8 @@ class PicturesController(ControllerBase):
if self_moderate: if self_moderate:
new.moderator = user new.moderator = user
try: try:
new.generate_thumbnails()
new.full_clean() new.full_clean()
new.save() new.generate_thumbnails(save=True)
except ValidationError as e: except ValidationError as e:
return self.create_response({"detail": dict(e)}, status_code=409) return self.create_response({"detail": dict(e)}, status_code=409)
+38 -33
View File
@@ -15,7 +15,6 @@
from __future__ import annotations from __future__ import annotations
import contextlib
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import ClassVar, Self from typing import ClassVar, Self
@@ -30,7 +29,7 @@ from django.utils.translation import gettext_lazy as _
from PIL import Image from PIL import Image
from core.models import Notification, SithFile, User from core.models import Notification, SithFile, User
from core.utils import exif_auto_rotate, resize_image from core.utils import resize_image
class SasFile(SithFile): class SasFile(SithFile):
@@ -123,45 +122,51 @@ class Picture(SasFile):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("sas:picture", kwargs={"picture_id": self.id}) return reverse("sas:picture", kwargs={"picture_id": self.id})
def generate_thumbnails(self): def generate_thumbnails(
im = Image.open(BytesIO(self.file.read())) self, *, img: Image.Image | None = None, save: bool = False
with contextlib.suppress(Exception): ):
im = exif_auto_rotate(im) """Generate the thumbnail and the compressed version of this picture.
Args:
img: if given, this will be used to generate
all three images (file, compressed, thumbnail).
Else, `self.file` will be used
save: if True, save the instance in database.
"""
img = img or Image.open(self.file)
extension = self.mime_type.split("/")[-1]
previous_files = [
f.name for f in (self.file, self.thumbnail, self.compressed) if f
]
# convert the compressed image and the thumbnail into webp # convert the compressed image and the thumbnail into webp
# The original image keeps its original type, because it's not # The original image keeps its original type, because it's not
# meant to be shown on the website, but rather to keep the real image # meant to be shown on the website, but rather to keep the real image
# for less frequent cases (like downloading the pictures of an user) # for less frequent cases (like downloading the pictures of a user)
extension = self.mime_type.split("/")[-1]
# the HD version of the image doesn't need to be optimized, because : # the HD version of the image doesn't need to be optimized, because :
# - it isn't frequently queried # - it isn't frequently queried
# - optimizing large images takes a lot time, which greatly hinders the UX # - optimizing large images takes a lot of time, which greatly hinders the UX
# - photographers usually already optimize their images # - photographers usually already optimize their images
file = resize_image(im, max(im.size), extension, optimize=False)
thumb = resize_image(im, 200, "webp")
compressed = resize_image(im, 1200, "webp")
new_extension_name = str(Path(self.name).with_suffix(".webp")) new_extension_name = str(Path(self.name).with_suffix(".webp"))
self.file = file file = resize_image(img, max(img.size), extension, optimize=False)
self.file.name = self.name self.file.save(self.name, file, save=False)
self.thumbnail = thumb thumbnail = resize_image(img, 200, "webp")
self.thumbnail.name = new_extension_name self.thumbnail.save(new_extension_name, thumbnail, save=False)
self.compressed = compressed compressed = resize_image(img, 1200, "webp")
self.compressed.name = new_extension_name self.compressed.save(new_extension_name, compressed, save=save)
# once the new images have been saved, delete the previous ones.
# The deletion of old files is done after, so that if anything goes
# during the whole process, no data will be lost.
for filename in previous_files:
self.file.storage.delete(filename)
def rotate(self, degree): def rotate(self, degree: int | float):
for attr in ["file", "compressed", "thumbnail"]: """Rotate this picture and update its thumbnails accordingly.
name = self.__getattribute__(attr).name
with open(settings.MEDIA_ROOT / name, "r+b") as file: Args:
if file: degree: the rotation angle, in degree, counter-clockwise
im = Image.open(BytesIO(file.read())) """
file.seek(0) img = Image.open(self.file).rotate(degree)
im = im.rotate(degree, expand=True) self.generate_thumbnails(img=img, save=True)
im.save(
fp=file,
format=self.mime_type.split("/")[-1].upper(),
quality=90,
optimize=True,
progressive=True,
)
def get_next(self): def get_next(self):
if self.is_moderated: if self.is_moderated: