mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-10 03:49:24 +00:00
auto compress product icons
This commit is contained in:
125
core/fields.py
Normal file
125
core/fields.py
Normal file
@ -0,0 +1,125 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import models
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from PIL import Image
|
||||
|
||||
from core.utils import resize_image_explicit
|
||||
|
||||
|
||||
class ResizedImageFieldFile(ImageFieldFile):
|
||||
def get_resized_dimensions(self, image: Image.Image) -> tuple[int, int]:
|
||||
"""Get the dimensions of the resized image.
|
||||
|
||||
If the width and height are given, they are used.
|
||||
If only one is given, the other is calculated to keep the same ratio.
|
||||
|
||||
Returns:
|
||||
Tuple of width and height
|
||||
"""
|
||||
width = self.field.width
|
||||
height = self.field.height
|
||||
if width is not None and height is not None:
|
||||
return self.field.width, self.field.height
|
||||
if width is None:
|
||||
width = int(image.width * height / image.height)
|
||||
elif height is None:
|
||||
height = int(image.height * width / image.width)
|
||||
return width, height
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Get the name of the resized image.
|
||||
|
||||
If the field has a force_format attribute,
|
||||
the extension of the file will be changed to match it.
|
||||
Otherwise, the name is left unchanged.
|
||||
|
||||
Raises:
|
||||
ValueError: If the image format is unknown
|
||||
"""
|
||||
if not self.field.force_format:
|
||||
return self.name
|
||||
formats = {val: key for key, val in Image.registered_extensions().items()}
|
||||
new_format = self.field.force_format
|
||||
if new_format in formats:
|
||||
extension = formats[new_format]
|
||||
else:
|
||||
raise ValueError(f"Unknown format {new_format}")
|
||||
return str(Path(self.file.name).with_suffix(f".{extension}"))
|
||||
|
||||
def save(self, name, content, save=True): # noqa FBT002
|
||||
content.file.seek(0)
|
||||
img = Image.open(content.file)
|
||||
width, height = self.get_resized_dimensions(img)
|
||||
img_format = self.field.force_format or img.format
|
||||
new_content = resize_image_explicit(img, (width, height), img_format)
|
||||
name = self.get_name()
|
||||
return super().save(name, new_content, save)
|
||||
|
||||
|
||||
class ResizedImageField(models.ImageField):
|
||||
"""A field that automatically resizes images to a given size.
|
||||
|
||||
This field is useful for profile pictures or product icons, for example.
|
||||
|
||||
The final size of the image is determined by the width and height parameters :
|
||||
|
||||
- If both are given, the image will be resized
|
||||
to fit in a rectangle of width x height
|
||||
- If only one is given, the other will be calculated to keep the same ratio
|
||||
|
||||
If the force_format parameter is given, the image will be converted to this format.
|
||||
|
||||
Examples:
|
||||
To resize an image with a height of 100px, without changing the ratio,
|
||||
and a format of WEBP :
|
||||
|
||||
```python
|
||||
class Product(models.Model):
|
||||
icon = ResizedImageField(height=100, force_format="WEBP")
|
||||
```
|
||||
|
||||
To explicitly resize an image to 100x100px (but possibly change the ratio) :
|
||||
|
||||
```python
|
||||
class Product(models.Model):
|
||||
icon = ResizedImageField(width=100, height=100)
|
||||
```
|
||||
|
||||
Raises:
|
||||
FieldError: If neither width nor height is given
|
||||
|
||||
Args:
|
||||
width: If given, the width of the resized image
|
||||
height: If given, the height of the resized image
|
||||
force_format: If given, the image will be converted to this format
|
||||
"""
|
||||
|
||||
attr_class = ResizedImageFieldFile
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
force_format: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
if width is None and height is None:
|
||||
raise FieldError(
|
||||
f"{self.__class__.__name__} requires "
|
||||
"width, height or both, but got neither"
|
||||
)
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.force_format = force_format
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.width is not None:
|
||||
kwargs["width"] = self.width
|
||||
if self.height is not None:
|
||||
kwargs["height"] = self.height
|
||||
kwargs["force_format"] = self.force_format
|
||||
return name, path, args, kwargs
|
@ -25,7 +25,7 @@ from django.core.files.base import ContentFile
|
||||
from django.http import HttpRequest
|
||||
from django.utils import timezone
|
||||
from PIL import ExifTags
|
||||
from PIL.Image import Resampling
|
||||
from PIL.Image import Image, Resampling
|
||||
|
||||
|
||||
def get_start_of_semester(today: Optional[date] = None) -> date:
|
||||
@ -82,18 +82,20 @@ def get_semester_code(d: Optional[date] = None) -> str:
|
||||
return "P" + str(start.year)[-2:]
|
||||
|
||||
|
||||
def scale_dimension(width, height, long_edge):
|
||||
ratio = long_edge / max(width, height)
|
||||
return int(width * ratio), int(height * ratio)
|
||||
|
||||
|
||||
def resize_image(im, edge, img_format):
|
||||
def resize_image(im: Image, edge: int, img_format: str):
|
||||
"""Resize an image to fit the given edge length and format."""
|
||||
(w, h) = im.size
|
||||
(width, height) = scale_dimension(w, h, long_edge=edge)
|
||||
ratio = edge / max(w, h)
|
||||
(width, height) = int(w * ratio), int(h * ratio)
|
||||
return resize_image_explicit(im, (width, height), img_format)
|
||||
|
||||
|
||||
def resize_image_explicit(im: Image, size: tuple[int, int], img_format: str):
|
||||
"""Resize an image to the given size and format."""
|
||||
img_format = img_format.upper()
|
||||
content = BytesIO()
|
||||
# use the lanczos filter for antialiasing and discard the alpha channel
|
||||
im = im.resize((width, height), Resampling.LANCZOS)
|
||||
im = im.resize((size[0], size[1]), Resampling.LANCZOS)
|
||||
if img_format == "JPEG":
|
||||
# converting an image with an alpha channel to jpeg would cause a crash
|
||||
im = im.convert("RGB")
|
||||
|
Reference in New Issue
Block a user