absfuyu 5.6.1__py3-none-any.whl → 6.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of absfuyu might be problematic. Click here for more details.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +2 -2
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +2 -2
- absfuyu/cli/config_group.py +2 -2
- absfuyu/cli/do_group.py +2 -2
- absfuyu/cli/game_group.py +20 -2
- absfuyu/cli/tool_group.py +68 -4
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +10 -6
- absfuyu/core/baseclass.py +104 -34
- absfuyu/core/baseclass2.py +43 -2
- absfuyu/core/decorator.py +2 -2
- absfuyu/core/docstring.py +4 -2
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +2 -2
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +188 -6
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +72 -4
- absfuyu/dxt/listext.py +495 -23
- absfuyu/dxt/strext.py +2 -2
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/audio/__init__.py +8 -0
- absfuyu/extra/audio/_util.py +57 -0
- absfuyu/extra/audio/convert.py +192 -0
- absfuyu/extra/audio/lossless.py +281 -0
- absfuyu/extra/beautiful.py +2 -2
- absfuyu/extra/da/__init__.py +39 -3
- absfuyu/extra/da/dadf.py +458 -29
- absfuyu/extra/da/dadf_base.py +2 -2
- absfuyu/extra/da/df_func.py +89 -5
- absfuyu/extra/da/mplt.py +2 -2
- absfuyu/extra/ggapi/__init__.py +8 -0
- absfuyu/extra/ggapi/gdrive.py +223 -0
- absfuyu/extra/ggapi/glicense.py +148 -0
- absfuyu/extra/ggapi/glicense_df.py +186 -0
- absfuyu/extra/ggapi/gsheet.py +88 -0
- absfuyu/extra/img/__init__.py +30 -0
- absfuyu/extra/img/converter.py +402 -0
- absfuyu/extra/img/dup_check.py +291 -0
- absfuyu/extra/pdf.py +4 -6
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +2 -20
- absfuyu/fun/rubik.py +2 -2
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/schulte.py +78 -0
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -2
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +2 -2
- absfuyu/general/content.py +2 -2
- absfuyu/general/human.py +2 -2
- absfuyu/general/resrel.py +213 -0
- absfuyu/general/shape.py +3 -8
- absfuyu/general/tax.py +344 -0
- absfuyu/logger.py +806 -59
- absfuyu/numbers/__init__.py +13 -0
- absfuyu/numbers/number_to_word.py +321 -0
- absfuyu/numbers/shorten_number.py +303 -0
- absfuyu/numbers/time_duration.py +217 -0
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/pkg_data/logo.py +1462 -0
- absfuyu/sort.py +4 -4
- absfuyu/tools/__init__.py +2 -2
- absfuyu/tools/checksum.py +119 -4
- absfuyu/tools/converter.py +2 -2
- absfuyu/tools/generator.py +24 -7
- absfuyu/tools/inspector.py +2 -2
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +2 -2
- absfuyu/tools/passwordlib.py +2 -2
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +213 -10
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +5 -8
- absfuyu/util/__init__.py +31 -2
- absfuyu/util/api.py +7 -4
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +2 -2
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +313 -4
- absfuyu/util/performance.py +2 -2
- absfuyu/util/shorten_number.py +206 -13
- absfuyu/util/text_table.py +2 -2
- absfuyu/util/zipped.py +2 -2
- absfuyu/version.py +22 -19
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/METADATA +37 -8
- absfuyu-6.1.3.dist-info/RECORD +105 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -21
- absfuyu-5.6.1.dist-info/RECORD +0 -79
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Google related
|
|
3
|
+
-----------------------
|
|
4
|
+
Google Sheet
|
|
5
|
+
|
|
6
|
+
Version: 6.1.2
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["GoogleSheet"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from absfuyu.extra.ggapi.gdrive import GoogleDriveClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Class
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
class GoogleSheet(GoogleDriveClient):
|
|
25
|
+
# Sheets — READ (NO DOWNLOAD)
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
def read_sheet(
|
|
28
|
+
self,
|
|
29
|
+
spreadsheet_id: str,
|
|
30
|
+
sheet_name: str = "Sheet1",
|
|
31
|
+
header: bool = True,
|
|
32
|
+
) -> list[list[Any]]:
|
|
33
|
+
"""
|
|
34
|
+
Read Google Sheet directly.
|
|
35
|
+
"""
|
|
36
|
+
result = (
|
|
37
|
+
self.sheets.spreadsheets()
|
|
38
|
+
.values()
|
|
39
|
+
.get(
|
|
40
|
+
spreadsheetId=spreadsheet_id,
|
|
41
|
+
range=sheet_name,
|
|
42
|
+
valueRenderOption="UNFORMATTED_VALUE",
|
|
43
|
+
)
|
|
44
|
+
.execute()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
values = result.get("values", [])
|
|
48
|
+
if values and header:
|
|
49
|
+
return values[1:]
|
|
50
|
+
|
|
51
|
+
return values
|
|
52
|
+
|
|
53
|
+
# Sheets — APPEND
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
def append_rows(
|
|
56
|
+
self,
|
|
57
|
+
spreadsheet_id: str,
|
|
58
|
+
range_: str,
|
|
59
|
+
rows: list[list],
|
|
60
|
+
) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Append rows to Google Sheet.
|
|
63
|
+
"""
|
|
64
|
+
self.sheets.spreadsheets().values().append(
|
|
65
|
+
spreadsheetId=spreadsheet_id,
|
|
66
|
+
range=range_,
|
|
67
|
+
valueInputOption="USER_ENTERED",
|
|
68
|
+
insertDataOption="INSERT_ROWS",
|
|
69
|
+
body={"values": rows},
|
|
70
|
+
).execute()
|
|
71
|
+
|
|
72
|
+
# Sheets — UPDATE
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
def update_range(
|
|
75
|
+
self,
|
|
76
|
+
spreadsheet_id: str,
|
|
77
|
+
range_: str,
|
|
78
|
+
values: list[list],
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Update an existing cell range.
|
|
82
|
+
"""
|
|
83
|
+
self.sheets.spreadsheets().values().update(
|
|
84
|
+
spreadsheetId=spreadsheet_id,
|
|
85
|
+
range=range_,
|
|
86
|
+
valueInputOption="USER_ENTERED",
|
|
87
|
+
body={"values": values},
|
|
88
|
+
).execute()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Image
|
|
3
|
+
--------------
|
|
4
|
+
Image related
|
|
5
|
+
|
|
6
|
+
Version: 6.1.2
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["ImgConverter"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
try:
|
|
18
|
+
from PIL import Image
|
|
19
|
+
except ImportError:
|
|
20
|
+
from subprocess import run
|
|
21
|
+
|
|
22
|
+
from absfuyu.config import ABSFUYU_CONFIG
|
|
23
|
+
|
|
24
|
+
if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
|
|
25
|
+
cmd = "python -m pip install -U absfuyu[pic]".split()
|
|
26
|
+
run(cmd)
|
|
27
|
+
else:
|
|
28
|
+
raise SystemExit("This feature is in absfuyu[pic] package") # noqa: B904
|
|
29
|
+
|
|
30
|
+
from absfuyu.extra.img.converter import ImgConverter
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Picture converter
|
|
3
|
+
--------------------------
|
|
4
|
+
Image converter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Version: 6.1.2
|
|
8
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Module level
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
__all__ = ["ImgConverter"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Library
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
import logging
|
|
19
|
+
import shutil
|
|
20
|
+
from functools import partial
|
|
21
|
+
from importlib.util import find_spec as check_for_package_installed
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Literal, Protocol
|
|
24
|
+
|
|
25
|
+
from absfuyu.core.dummy_func import tqdm as tqdm_base
|
|
26
|
+
from absfuyu.util.path import DirectorySelectMixin
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from PIL import Image
|
|
30
|
+
from PIL import features as pil_features
|
|
31
|
+
from PIL.ImageFile import ImageFile
|
|
32
|
+
except ImportError:
|
|
33
|
+
from subprocess import run
|
|
34
|
+
|
|
35
|
+
from absfuyu.config import ABSFUYU_CONFIG
|
|
36
|
+
|
|
37
|
+
if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
|
|
38
|
+
cmd = "python -m pip install -U absfuyu[pic]".split()
|
|
39
|
+
run(cmd)
|
|
40
|
+
else:
|
|
41
|
+
raise SystemExit("This feature is in absfuyu[pic] package") # noqa: B904
|
|
42
|
+
else:
|
|
43
|
+
from PIL import Image
|
|
44
|
+
from PIL import features as pil_features
|
|
45
|
+
from PIL.ImageFile import ImageFile
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from pillow_heif import register_heif_opener # type: ignore
|
|
49
|
+
except ImportError:
|
|
50
|
+
from absfuyu.core.dummy_func import dummy_function as register_heif_opener
|
|
51
|
+
register_heif_opener()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Setup
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
tqdm = partial(tqdm_base, unit_scale=True, dynamic_ncols=True)
|
|
57
|
+
SupportedImageFormat = Literal[".jpg", ".jpeg", ".png", ".webp"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Exporter/Converter
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
class SupportImageConverter(Protocol):
|
|
63
|
+
# Callable[[ImageFile, Path], None]
|
|
64
|
+
def __call__(self, image: ImageFile, path: Path, **params: Any) -> None: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _image_convert_default(image: ImageFile, path: Path, **params: Any) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Default convert image function
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
image : ImageFile
|
|
74
|
+
Image file
|
|
75
|
+
|
|
76
|
+
path : Path
|
|
77
|
+
Path to export
|
|
78
|
+
"""
|
|
79
|
+
image.save(path, **params)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _image_convert_webp(image: ImageFile, path: Path, **params: Any) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Convert image to .webp format (with custom settings)
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
image : ImageFile
|
|
89
|
+
Image file
|
|
90
|
+
|
|
91
|
+
path : Path
|
|
92
|
+
Path to export
|
|
93
|
+
"""
|
|
94
|
+
image.save(
|
|
95
|
+
path,
|
|
96
|
+
format="WEBP",
|
|
97
|
+
lossless=True,
|
|
98
|
+
quality=100,
|
|
99
|
+
alpha_quality=100,
|
|
100
|
+
method=4,
|
|
101
|
+
exact=False, # If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false.
|
|
102
|
+
**params,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _image_convert_png(image: ImageFile, path: Path, **params: Any) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Convert image to .png format (with custom settings)
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
image : ImageFile
|
|
113
|
+
Image file
|
|
114
|
+
|
|
115
|
+
path : Path
|
|
116
|
+
Path to export
|
|
117
|
+
"""
|
|
118
|
+
image.save(
|
|
119
|
+
path,
|
|
120
|
+
format="PNG",
|
|
121
|
+
# optimize=True,
|
|
122
|
+
compress_level=6,
|
|
123
|
+
**params,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _image_convert_jpg(image: ImageFile, path: Path, **params: Any) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Convert image to .jpg format (with custom settings)
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
image : ImageFile
|
|
134
|
+
Image file
|
|
135
|
+
|
|
136
|
+
path : Path
|
|
137
|
+
Path to export
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
if image.mode == "RGBA":
|
|
141
|
+
white_background = Image.new("RGB", image.size, (255, 255, 255))
|
|
142
|
+
white_background.paste(image, mask=image.getchannel("A"))
|
|
143
|
+
image = white_background
|
|
144
|
+
else:
|
|
145
|
+
image = image.convert("RGB")
|
|
146
|
+
|
|
147
|
+
image.save(
|
|
148
|
+
path,
|
|
149
|
+
format="JPEG",
|
|
150
|
+
optimize=True,
|
|
151
|
+
keep_rgb=True,
|
|
152
|
+
**params,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Class
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
class ImgConverter(DirectorySelectMixin):
|
|
159
|
+
_IMAGE_CONVERTER: dict[str, SupportImageConverter] = {
|
|
160
|
+
"default": _image_convert_default,
|
|
161
|
+
".webp": _image_convert_webp,
|
|
162
|
+
".png": _image_convert_png,
|
|
163
|
+
".jpg": _image_convert_jpg,
|
|
164
|
+
".jpeg": _image_convert_jpg,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
source_path: str | Path,
|
|
170
|
+
create_if_not_exist: bool = False,
|
|
171
|
+
backup_dir_name: str | None = None,
|
|
172
|
+
logger: logging.Logger | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
super().__init__(source_path, create_if_not_exist)
|
|
175
|
+
|
|
176
|
+
# Supported image extension
|
|
177
|
+
self._supported_image_format = [".png"]
|
|
178
|
+
self._register_img_format()
|
|
179
|
+
|
|
180
|
+
# Backup
|
|
181
|
+
if backup_dir_name is None:
|
|
182
|
+
backup_dir_name = "ZZZ_Backup"
|
|
183
|
+
self.backup_path = self.source_path.joinpath(backup_dir_name)
|
|
184
|
+
|
|
185
|
+
# Not available yet
|
|
186
|
+
self.logger = logger
|
|
187
|
+
|
|
188
|
+
# Extra format
|
|
189
|
+
@classmethod
|
|
190
|
+
def install_all_extension(cls) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Install all extra package to unlock all features
|
|
193
|
+
"""
|
|
194
|
+
extra = [
|
|
195
|
+
"pillow_heif", # heic support
|
|
196
|
+
"defusedxml", # xmp
|
|
197
|
+
"olefile", # FPX and MIC images
|
|
198
|
+
]
|
|
199
|
+
base = ["pip", "install", "-U"]
|
|
200
|
+
base.extend(extra)
|
|
201
|
+
|
|
202
|
+
import subprocess
|
|
203
|
+
|
|
204
|
+
subprocess.run(base)
|
|
205
|
+
|
|
206
|
+
def _register_img_format(self) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Try to register these format:
|
|
209
|
+
- ``.webp``
|
|
210
|
+
- ``.heif``, ``.heic`` (``pillow_heif`` package required)
|
|
211
|
+
"""
|
|
212
|
+
if pil_features.check("jpg"):
|
|
213
|
+
self._supported_image_format.extend([".jpg", ".jpeg"])
|
|
214
|
+
if pil_features.check("webp"):
|
|
215
|
+
self._supported_image_format.append(".webp")
|
|
216
|
+
|
|
217
|
+
if check_for_package_installed("pillow_heif", "pillow_heif") is not None:
|
|
218
|
+
self._supported_image_format.extend([".heic", ".heif"])
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def supported_image_format(self) -> list[str]:
|
|
222
|
+
"""
|
|
223
|
+
Supported image format
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
list[str]
|
|
228
|
+
Supported image format
|
|
229
|
+
"""
|
|
230
|
+
return self._supported_image_format
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def add_converter(cls, format_name: str, converter_func: SupportImageConverter) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Add image converter function to a format
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
format_name : str
|
|
240
|
+
Image format name
|
|
241
|
+
|
|
242
|
+
converter_func : SupportImageConverter
|
|
243
|
+
Converter function
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
--------
|
|
248
|
+
>>> ImgConverter.add_converter(".png", convert_to_png)
|
|
249
|
+
"""
|
|
250
|
+
cls._IMAGE_CONVERTER[format_name] = converter_func
|
|
251
|
+
|
|
252
|
+
# Support
|
|
253
|
+
def _make_suffix_selection(self, exclude_suffix: str) -> tuple[str, ...]:
|
|
254
|
+
"""
|
|
255
|
+
Make suffix selection (exclude the image with converted to suffix)
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
exclude_suffix : str
|
|
260
|
+
Converted to suffix
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
tuple[str, ...]
|
|
265
|
+
Suffix selection
|
|
266
|
+
"""
|
|
267
|
+
# out = []
|
|
268
|
+
# for x in self._supported_image_format:
|
|
269
|
+
# if x.lower() == exclude_suffix.lower():
|
|
270
|
+
# continue
|
|
271
|
+
# out.append(x.lower())
|
|
272
|
+
# out.append(x.upper())
|
|
273
|
+
out = (x for x in self._supported_image_format if x.lower() != exclude_suffix.lower())
|
|
274
|
+
return tuple(out)
|
|
275
|
+
|
|
276
|
+
def _make_backup(self, src_file: Path) -> None:
|
|
277
|
+
dest = self.backup_path.joinpath(src_file.name)
|
|
278
|
+
shutil.move(src_file, dest)
|
|
279
|
+
|
|
280
|
+
# Convert
|
|
281
|
+
def _image_convert_legacy(
|
|
282
|
+
self,
|
|
283
|
+
path: Path,
|
|
284
|
+
to_format: SupportedImageFormat | None = None,
|
|
285
|
+
lossless: bool = True,
|
|
286
|
+
compression_level: int | None = None,
|
|
287
|
+
) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Convert image to other format (settings are mostly for .webp format)
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
path : Path
|
|
294
|
+
Path to image
|
|
295
|
+
|
|
296
|
+
to_format : SupportedImageFormat | None, optional
|
|
297
|
+
New image format, by default None
|
|
298
|
+
|
|
299
|
+
lossless : bool, optional
|
|
300
|
+
Lossless compression, by default True
|
|
301
|
+
|
|
302
|
+
compression_level : int | None, optional
|
|
303
|
+
Compression level, by default None
|
|
304
|
+
"""
|
|
305
|
+
# Load image
|
|
306
|
+
new_suffix = path.suffix if to_format is None else to_format
|
|
307
|
+
image = Image.open(path)
|
|
308
|
+
|
|
309
|
+
# Extract metadata
|
|
310
|
+
# exif = image.info.get("exif")
|
|
311
|
+
# xmp = image.getxmp()
|
|
312
|
+
# icc_profile = image.info.get("icc_profile")
|
|
313
|
+
xmp = image.info.get("xmp")
|
|
314
|
+
exif = image.getexif()
|
|
315
|
+
icc_profile = image.info.get("icc_profile")
|
|
316
|
+
# print(image.info.keys())
|
|
317
|
+
|
|
318
|
+
# Save
|
|
319
|
+
image.save(
|
|
320
|
+
path.with_suffix(new_suffix),
|
|
321
|
+
format=new_suffix[1:].upper(),
|
|
322
|
+
lossless=lossless,
|
|
323
|
+
quality=100,
|
|
324
|
+
alpha_quality=100,
|
|
325
|
+
method=(4 if compression_level is None else compression_level),
|
|
326
|
+
exact=False, # If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false.
|
|
327
|
+
exif=exif,
|
|
328
|
+
icc_profile=icc_profile,
|
|
329
|
+
xmp=xmp,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _image_convert(
|
|
333
|
+
self,
|
|
334
|
+
path: Path,
|
|
335
|
+
to_format: str | None = None,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Convert image to other format
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
path : Path
|
|
343
|
+
Path to image
|
|
344
|
+
|
|
345
|
+
to_format : SupportedImageFormat | None, optional
|
|
346
|
+
New image format, by default None
|
|
347
|
+
"""
|
|
348
|
+
# Load image
|
|
349
|
+
new_suffix = path.suffix if to_format is None else to_format
|
|
350
|
+
image = Image.open(path)
|
|
351
|
+
|
|
352
|
+
# Extract metadata
|
|
353
|
+
save_kwargs = {}
|
|
354
|
+
if exif := image.getexif():
|
|
355
|
+
save_kwargs["exif"] = exif
|
|
356
|
+
if icc := image.info.get("icc_profile"):
|
|
357
|
+
save_kwargs["icc_profile"] = icc
|
|
358
|
+
if xmp := image.info.get("xmp"):
|
|
359
|
+
save_kwargs["xmp"] = xmp
|
|
360
|
+
|
|
361
|
+
# # Convert image mode
|
|
362
|
+
# if image.mode not in ("RGB", "RGBA", "L"):
|
|
363
|
+
# image = image.convert("RGBA")
|
|
364
|
+
|
|
365
|
+
# Save
|
|
366
|
+
convert_func = self._IMAGE_CONVERTER.get(new_suffix, _image_convert_default)
|
|
367
|
+
# self.logger.debug(f"Using {convert_func}")
|
|
368
|
+
convert_func(image, path.with_suffix(new_suffix), **save_kwargs)
|
|
369
|
+
|
|
370
|
+
def img_convert(self, to_format: SupportedImageFormat | str, backup: bool = True) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Convert images in directory to desire format
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
to_format : SupportedImageFormat
|
|
377
|
+
Format to convert
|
|
378
|
+
|
|
379
|
+
backup : bool
|
|
380
|
+
Move pictures to a backup folder
|
|
381
|
+
|
|
382
|
+
Raises
|
|
383
|
+
------
|
|
384
|
+
NotImplementedError
|
|
385
|
+
Not supported image format
|
|
386
|
+
"""
|
|
387
|
+
if to_format not in self._supported_image_format:
|
|
388
|
+
raise NotImplementedError("Format not supported")
|
|
389
|
+
|
|
390
|
+
imgs = self.select_all(*self._make_suffix_selection(to_format))
|
|
391
|
+
|
|
392
|
+
for x in tqdm(imgs, desc=f"Converting to {to_format}"):
|
|
393
|
+
try:
|
|
394
|
+
self._image_convert(x, to_format=to_format)
|
|
395
|
+
|
|
396
|
+
if backup:
|
|
397
|
+
self.backup_path.mkdir(parents=True, exist_ok=True)
|
|
398
|
+
self._make_backup(x)
|
|
399
|
+
except TypeError as err:
|
|
400
|
+
print(f" TYPE ERROR: {x} - {err}")
|
|
401
|
+
except Exception as err:
|
|
402
|
+
print(f" ERROR: {x} - {err}")
|