From b662b720862d0af1684f05915830d4a29416f63c Mon Sep 17 00:00:00 2001 From: Jared Dueck Date: Wed, 6 Aug 2025 15:13:22 +0000 Subject: [PATCH] Update logic to add max dimensions to all images --- mealie/pkgs/img/minify.py | 124 +++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 34 deletions(-) diff --git a/mealie/pkgs/img/minify.py b/mealie/pkgs/img/minify.py index 471b632b8..b1764df8d 100644 --- a/mealie/pkgs/img/minify.py +++ b/mealie/pkgs/img/minify.py @@ -57,7 +57,7 @@ class ABCMinifier(ABC): ) @abstractmethod - def minify(self, image: Path, force=True): ... + def minify(self, image_path: Path, force=True): ... def purge(self, image: Path): if not self._purge: @@ -71,7 +71,11 @@ class ABCMinifier(ABC): class PillowMinifier(ABCMinifier): @staticmethod def _convert_image( - image_file: Path | None = None, image_format: ImageFormat, dest: Path | None = None, quality: int = 100, img: Image.Image | None = None + image_file: Path | None = None, + image_format: ImageFormat = WEBP, + dest: Path | None = None, + quality: int = 100, + img: Image.Image | None = None, ) -> Path: """ Converts an image to the specified format in-place. The original image is not @@ -91,78 +95,130 @@ class PillowMinifier(ABCMinifier): if image_file is None: raise ValueError("If dest is not provided, image_file must be.") dest = image_file.with_suffix(image_format.suffix) - + img.save(dest, image_format.format, quality=quality) - return dest + width, height = img.size + new_dest = dest.with_name(f"{dest.stem}_{width}x{height}{dest.suffix}") + + return new_dest @staticmethod - def to_jpg(image_file_path: Path | None = None, dest_path: Path | None = None, quality: int = 100, img: Image.Image | None = None) -> Path: - return PillowMinifier._convert_image(image_file_path, JPG, dest_path, quality, img) + def to_jpg( + image_file_path: Path | None = None, + dest: Path | None = None, + quality: int = 100, + img: Image.Image | None = None, + ) -> Path: + return PillowMinifier._convert_image(image_file_path, JPG, dest, quality, img) @staticmethod - def to_webp(image_file_path: Path | None = None, dest_path: Path | None = None, quality: int = 100, img: Image.Image | None = None) -> Path: + def to_webp( + image_file_path: Path | None = None, + dest_path: Path | None = None, + quality: int = 100, + img: Image.Image | None = None, + ) -> Path: return PillowMinifier._convert_image(image_file_path, WEBP, dest_path, quality, img) @staticmethod - def crop_center(pil_img: Image, crop_width=300, crop_height=300) -> Image.Image: - img_width, img_height = pil_img.size - return pil_img.crop( - ( - (img_width - crop_width) // 2, - (img_height - crop_height) // 2, - (img_width + crop_width) // 2, - (img_height + crop_height) // 2, - ) - ) + def crop_center(img: Image.Image, size=(300, 300), high_res: bool = True) -> Image.Image: + img = img.copy() + target_width, target_height = size - def minify(self, image_file: Path, force=True): - if not image_file.exists(): - raise FileNotFoundError(f"{image_file.name} does not exist") + # For retina displays, double the target size + if high_res: + target_width *= 2 + target_height *= 2 - org_dest = image_file.parent.joinpath("original.webp") - min_dest = image_file.parent.joinpath("min-original.webp") - tiny_dest = image_file.parent.joinpath("tiny-original.webp") + img_ratio = img.width / img.height + target_ratio = target_width / target_height + + # If original image smaller than target, do not upscale + if img.width < size[0] or img.height < size[1]: + return img + + # Resize first to fill area while preserving aspect ratio + if img_ratio > target_ratio: + # Wider than target + scale_height = target_height + scale_width = int(scale_height * img_ratio) + else: + # Taller than target + scale_width = target_width + scale_height = int(scale_width / img_ratio) + + img = img.resize((scale_width, scale_height), Image.LANCZOS) + + # Crop center of the resized image + left = (img.width - target_width) // 2 + top = (img.height - target_height) // 2 + right = left + target_width + bottom = top + target_height + + return img.crop((left, top, right, bottom)) + + def minify(self, image_path: Path, force=True): + if not image_path.exists(): + raise FileNotFoundError(f"{image_path.name} does not exist") + + org_dest = image_path.parent.joinpath("original.webp") + min_dest = image_path.parent.joinpath("min-original.webp") + tiny_dest = image_path.parent.joinpath("tiny-original.webp") if not force and min_dest.exists() and tiny_dest.exists() and org_dest.exists(): - self._logger.info(f"{image_file.name} already exists in all formats") + self._logger.info(f"{image_path.name} already exists in all formats") return success = False try: - with Image.open(image_file) as img: - + with Image.open(image_path) as img: + img = ImageOps.exif_transpose(img) if self._opts.original: + # Used as default if not force and org_dest.exists(): self._logger.info(f"{org_dest} already minified") else: - result_path = PillowMinifier.to_webp(dest_path=org_dest, quality=80, img=img.copy()) + result_path = PillowMinifier.to_webp( + dest_path=org_dest, quality=80, img=PillowMinifier.crop_center(img, size=(810, 400)) + ) self._logger.info(f"{result_path} created") success = True if self._opts.miniature: + # Used /g/home in desktop / tablet view + # Used g/home/cookbooks/ desktop view if not force and min_dest.exists(): self._logger.info(f"{min_dest} already minified") else: - mini = img.copy() - mini.thumbnail((1024, 1024), Image.LANCZOS) - result_path = PillowMinifier.to_webp(dest_path=min_dest, quality=70, img=mini) + result_path = PillowMinifier.to_webp( + dest_path=min_dest, + quality=70, + img=PillowMinifier.crop_center(img, size=(414, 200)), + ) self._logger.info(f"{result_path} created") success = True if self._opts.tiny: + # Used /g/home/ in mobile view + # Used /g/home/recipes/finder all views + # Used /household/mealplan/planner/edit (currently used) + # Used /g/home/recipes/timeline Desktop view only + # Used g/home/cookbooks/cookbook in mobile / tablet + if not force and tiny_dest.exists(): self._logger.info(f"{tiny_dest} already minified") else: - tiny_image = PillowMinifier.crop_center(ImageOps.exif_transpose(img.copy())) - result_path = PillowMinifier.to_webp(dest_path=tiny_dest, quality=70, img=tiny_image) + result_path = PillowMinifier.to_webp( + dest_path=tiny_dest, quality=70, img=PillowMinifier.crop_center(img, size=(300, 300)) + ) self._logger.info(f"{result_path} created") success = True except Exception as e: - self._logger.error(f"[ERROR] Failed to minify {image_file.name}. Error: {e}") + self._logger.error(f"[ERROR] Failed to minify {image_path.name}. Error: {e}") raise if self._purge and success: - self.purge(image_file) + self.purge(image_path)