Source code for services.posts.compression

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()