import os
from io import BytesIO
from django.conf import settings
from django.core.files import File
from PIL import Image, ImageFilter
from apps.posts.constants import StatusPostChoice
from apps.posts.models import PostMedia
from services.posts.notifications import PostNotificationService
[docs]
class ImageCompressionService:
[docs]
def __init__(self, media_id):
self.media_id = media_id
self.max_width = settings.IMAGE_MAX_RESOLUTION_WIDTH
self.max_height = settings.IMAGE_MAX_RESOLUTION_HEIGHT
self.min_width = settings.IMAGE_MIN_RESOLUTION_WIDTH
self.min_height = settings.IMAGE_MIN_RESOLUTION_HEIGHT
self.media = self._get_media()
self.original_image = self._open_original_image()
self.aspect_ratio = self._get_aspect_ratio()
[docs]
@classmethod
def compress(cls, media_id):
compressor = cls(media_id)
if compressor._is_image():
compressor._generate_preview()
compressor._process_image()
compressor._save_compressed_image()
def _get_media(self):
return PostMedia.objects.get(pk=self.media_id)
def _open_original_image(self):
if settings.USE_BUCKET:
path = self.media.original
else:
path = self.media.original.path
return Image.open(path)
def _get_aspect_ratio(self):
return self.original_image.width / self.original_image.height
def _is_image(self):
# Adjust according to your ContentTypeChoices
return self.media.type == "image"
def _exceeds_limit(self):
return self.original_image.width > self.max_width or self.original_image.height > self.max_height
def _calculate_new_dimensions(self):
if self.original_image.width / self.max_width > self.original_image.height / self.max_height:
new_width = self.max_width
new_height = int(new_width / self.aspect_ratio)
else:
new_height = self.max_height
new_width = int(new_height * self.aspect_ratio)
return new_width, new_height
def _resize_image(self, width, height):
return self.original_image.resize((width, height))
def _calculate_background_dimensions(self, width):
if width < self.max_width:
bg_width = self.max_width
bg_height = int(bg_width / self.aspect_ratio)
else:
bg_height = self.max_height
bg_width = int(bg_height * self.aspect_ratio)
return bg_width, bg_height
def _create_blurred_background(self, bg_width, bg_height):
return self.original_image.resize((bg_width, bg_height)).filter(ImageFilter.GaussianBlur(15))
def _calculate_center_position(self, width, height):
x = (self.max_width - width) // 2
y = (self.max_height - height) // 2
return x, y
def _process_image(self):
if self._exceeds_limit():
new_width, new_height = self._calculate_new_dimensions()
self.original_image = self._resize_image(new_width, new_height)
bg_width, bg_height = self._calculate_background_dimensions(new_width)
bg_img = self._create_blurred_background(bg_width, bg_height)
background = Image.new("RGB", (self.max_width, self.max_height))
bg_x, bg_y = self._calculate_center_position(bg_width, bg_height)
background.paste(bg_img, (bg_x, bg_y))
img_x, img_y = self._calculate_center_position(new_width, new_height)
background.paste(self.original_image, (img_x, img_y))
self.original_image = background
def _save_compressed_image(self):
output_io_stream = BytesIO()
self.original_image.save(output_io_stream, format="WEBP", quality=80)
output_io_stream.seek(0)
webp_filename = self.media.original.name.split("/")[-1].rsplit(".", 1)[0] + ".webp"
django_file = File(output_io_stream, name=webp_filename)
self.media.formatted_path.save(webp_filename, django_file)
self.media.save()
self._check_on_publish()
def _check_on_publish(self):
post = self.media.post
is_media_processed = self._get_processing_status()
if is_media_processed and post.status == StatusPostChoice.READY_FOR_PUBLISH:
PostNotificationService.publish(post)
elif is_media_processed and post.status == StatusPostChoice.PROCEED:
post.status = StatusPostChoice.READY_FOR_PUBLISH
post.save()
def _generate_preview(self):
target_aspect_ratio = self.min_width / self.min_height
if self.aspect_ratio > target_aspect_ratio:
# Base on width
new_width = self.min_width
new_height = int(new_width / self.aspect_ratio)
else:
# Base on height
new_height = self.min_height
new_width = int(new_height * self.aspect_ratio)
resized_image = self.original_image.resize((new_width, new_height))
# If after resizing, the image is smaller than target dimensions,
# resize based on the larger dimension and crop to target size
if resized_image.width < self.min_width or resized_image.height < self.min_height:
if new_width < self.min_width:
new_width = self.min_width
new_height = int(new_width / self.aspect_ratio)
else:
new_height = self.min_height
new_width = int(new_height * self.aspect_ratio)
resized_image = self.original_image.resize((new_width, new_height))
# Crop to target dimensions
left = (resized_image.width - self.min_width) / 2
top = (resized_image.height - self.min_height) / 2
right = (resized_image.width + self.min_width) / 2
bottom = (resized_image.height + self.min_height) / 2
cropped_image = resized_image.crop((left, top, right, bottom))
# Save preview to a separate field
preview_stream = BytesIO()
cropped_image.save(preview_stream, format="WEBP", quality=80)
preview_stream.seek(0)
file_name, _ = os.path.splitext(os.path.basename(self.media.original.name))
preview_filename = file_name + ".webp"
django_preview_file = File(preview_stream, name=preview_filename)
self.media.preview_path.save(preview_filename, django_preview_file)
self.media.save()
def _expand_until_minimum_size(self):
expanded_image = self.original_image
while expanded_image.width < self.min_width or expanded_image.height < self.min_height:
expanded_image = expanded_image.resize((expanded_image.width * 2, expanded_image.height * 2))
return expanded_image
def _get_processing_status(self):
post = self.media.post
return not post.linked_media.filter(formatted_path__isnull=True).exists()