Coverage for opt/mealie/lib/python3.12/site-packages/mealie/pkgs/img/minify.py: 43%
100 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:48 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:48 +0000
1from abc import ABC, abstractmethod 1a
2from dataclasses import dataclass 1a
3from logging import Logger 1a
4from pathlib import Path 1a
6from PIL import Image, ImageOps 1a
7from pillow_heif import register_heif_opener 1a
9register_heif_opener() 1a
12@dataclass 1a
13class ImageFormat: 1a
14 suffix: str 1a
15 format: str 1a
16 modes: list[str] 1a
17 """If the image is not in the correct mode, it will be converted to the first mode in the list""" 1a
20JPG = ImageFormat(".jpg", "JPEG", ["RGB"]) 1a
21WEBP = ImageFormat(".webp", "WEBP", ["RGB", "RGBA"]) 1a
22IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".avif"} 1a
25def get_format(image: Path) -> str: 1a
26 img = Image.open(image)
27 return img.format
30def sizeof_fmt(file_path: Path, decimal_places=2): 1a
31 if not file_path.exists():
32 return "(File Not Found)"
33 size: int | float = file_path.stat().st_size
34 for unit in ["B", "kB", "MB", "GB", "TB", "PB"]:
35 if size < 1024 or unit == "PiB":
36 break
37 size /= 1024
38 return f"{size:.{decimal_places}f} {unit}"
41@dataclass 1a
42class MinifierOptions: 1a
43 original: bool = True 1a
44 miniature: bool = True 1a
45 tiny: bool = True 1a
48class ABCMinifier(ABC): 1a
49 def __init__(self, purge=False, opts: MinifierOptions | None = None, logger: Logger | None = None): 1a
50 self._purge = purge 1bc
51 self._opts = opts or MinifierOptions() 1bc
52 self._logger = logger or Logger("Minifier") 1bc
54 def get_image_sizes(self, org_img: Path, min_img: Path, tiny_img: Path): 1a
55 self._logger.info(
56 f"{org_img.name} Minified: {sizeof_fmt(org_img)} -> {sizeof_fmt(min_img)} -> {sizeof_fmt(tiny_img)}"
57 )
59 @abstractmethod 1a
60 def minify(self, image: Path, force=True): ... 60 ↛ exitline 60 didn't return from function 'minify' because 1a
62 def purge(self, image: Path): 1a
63 if not self._purge:
64 return
66 for file in image.parent.glob("*.*"):
67 if file.suffix != WEBP.suffix:
68 file.unlink()
71class PillowMinifier(ABCMinifier): 1a
72 @staticmethod 1a
73 def _convert_image( 1a
74 image_file: Path, image_format: ImageFormat, dest: Path | None = None, quality: int = 100
75 ) -> Path:
76 """
77 Converts an image to the specified format in-place. The original image is not
78 removed. By default, the quality is set to 100.
79 """
81 img = Image.open(image_file) 1bc
82 img = ImageOps.exif_transpose(img)
83 if img.mode not in image_format.modes:
84 img = img.convert(image_format.modes[0])
86 dest = dest or image_file.with_suffix(image_format.suffix)
87 img.save(dest, image_format.format, quality=quality)
89 return dest
91 @staticmethod 1a
92 def to_jpg(image_file: Path, dest: Path | None = None, quality: int = 100) -> Path: 1a
93 return PillowMinifier._convert_image(image_file, JPG, dest, quality)
95 @staticmethod 1a
96 def to_webp(image_file: Path, dest: Path | None = None, quality: int = 100) -> Path: 1a
97 return PillowMinifier._convert_image(image_file, WEBP, dest, quality) 1bc
99 @staticmethod 1a
100 def crop_center(pil_img: Image, crop_width=300, crop_height=300): 1a
101 img_width, img_height = pil_img.size
102 return pil_img.crop(
103 (
104 (img_width - crop_width) // 2,
105 (img_height - crop_height) // 2,
106 (img_width + crop_width) // 2,
107 (img_height + crop_height) // 2,
108 )
109 )
111 def minify(self, image_file: Path, force=True): 1a
112 if not image_file.exists(): 112 ↛ 113line 112 didn't jump to line 113 because the condition on line 112 was never true1bc
113 raise FileNotFoundError(f"{image_file.name} does not exist")
115 org_dest = image_file.parent.joinpath("original.webp") 1bc
116 min_dest = image_file.parent.joinpath("min-original.webp") 1bc
117 tiny_dest = image_file.parent.joinpath("tiny-original.webp") 1bc
119 if not force and min_dest.exists() and tiny_dest.exists() and org_dest.exists(): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true1bc
120 self._logger.info(f"{image_file.name} already minified")
121 return
123 success = False 1bc
125 if self._opts.original: 125 ↛ 132line 125 didn't jump to line 132 because the condition on line 125 was always true1bc
126 if not force and org_dest.exists(): 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true1bc
127 self._logger.info(f"{image_file.name} already minified")
128 else:
129 PillowMinifier.to_webp(image_file, org_dest, quality=70) 1bc
130 success = True
132 if self._opts.miniature:
133 if not force and min_dest.exists():
134 self._logger.info(f"{image_file.name} already minified")
135 else:
136 PillowMinifier.to_webp(image_file, min_dest, quality=70)
137 self._logger.info(f"{image_file.name} minified")
138 success = True
140 if self._opts.tiny:
141 if not force and tiny_dest.exists():
142 self._logger.info(f"{image_file.name} already minified")
143 else:
144 img = Image.open(image_file)
145 img = ImageOps.exif_transpose(img)
146 tiny_image = PillowMinifier.crop_center(img)
147 tiny_image.save(tiny_dest, WEBP.format, quality=70)
148 self._logger.info("Tiny image saved")
149 success = True
151 if self._purge and success:
152 self.purge(image_file)