mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-02 11:26:08 +00:00
refactor Picture.generate_thumbnails
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user