Coverage for opt/mealie/lib/python3.12/site-packages/mealie/pkgs/img/minify.py: 30%

100 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-11-25 15:32 +0000

1from abc import ABC, abstractmethod 1a

2from dataclasses import dataclass 1a

3from logging import Logger 1a

4from pathlib import Path 1a

5 

6from PIL import Image, ImageOps 1a

7from pillow_heif import register_heif_opener 1a

8 

9register_heif_opener() 1a

10 

11 

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

18 

19 

20JPG = ImageFormat(".jpg", "JPEG", ["RGB"]) 1a

21WEBP = ImageFormat(".webp", "WEBP", ["RGB", "RGBA"]) 1a

22IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".avif"} 1a

23 

24 

25def get_format(image: Path) -> str: 1a

26 img = Image.open(image) 

27 return img.format 

28 

29 

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}" 

39 

40 

41@dataclass 1a

42class MinifierOptions: 1a

43 original: bool = True 1a

44 miniature: bool = True 1a

45 tiny: bool = True 1a

46 

47 

48class ABCMinifier(ABC): 1a

49 def __init__(self, purge=False, opts: MinifierOptions | None = None, logger: Logger | None = None): 1a

50 self._purge = purge 

51 self._opts = opts or MinifierOptions() 

52 self._logger = logger or Logger("Minifier") 

53 

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 ) 

58 

59 @abstractmethod 1a

60 def minify(self, image: Path, force=True): ... 60 ↛ exitline 60 didn't return from function 'minify' because 1a

61 

62 def purge(self, image: Path): 1a

63 if not self._purge: 

64 return 

65 

66 for file in image.parent.glob("*.*"): 

67 if file.suffix != WEBP.suffix: 

68 file.unlink() 

69 

70 

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 """ 

80 

81 img = Image.open(image_file) 

82 img = ImageOps.exif_transpose(img) 

83 if img.mode not in image_format.modes: 

84 img = img.convert(image_format.modes[0]) 

85 

86 dest = dest or image_file.with_suffix(image_format.suffix) 

87 img.save(dest, image_format.format, quality=quality) 

88 

89 return dest 

90 

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) 

94 

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) 

98 

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 ) 

110 

111 def minify(self, image_file: Path, force=True): 1a

112 if not image_file.exists(): 

113 raise FileNotFoundError(f"{image_file.name} does not exist") 

114 

115 org_dest = image_file.parent.joinpath("original.webp") 

116 min_dest = image_file.parent.joinpath("min-original.webp") 

117 tiny_dest = image_file.parent.joinpath("tiny-original.webp") 

118 

119 if not force and min_dest.exists() and tiny_dest.exists() and org_dest.exists(): 

120 self._logger.info(f"{image_file.name} already minified") 

121 return 

122 

123 success = False 

124 

125 if self._opts.original: 

126 if not force and org_dest.exists(): 

127 self._logger.info(f"{image_file.name} already minified") 

128 else: 

129 PillowMinifier.to_webp(image_file, org_dest, quality=70) 

130 success = True 

131 

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 

139 

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 

150 

151 if self._purge and success: 

152 self.purge(image_file)