You've already forked ImageCompressor
mirror of
https://github.com/Llloooggg/ImageCompressor.git
synced 2026-03-06 03:26:23 +03:00
Исправлена перезапись существующего файла при конвертации png
This commit is contained in:
@@ -14,7 +14,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
TARGET_SIZE = 2 * 1024 * 1024
|
TARGET_SIZE = 2 * 1024 * 1024
|
||||||
MAX_SIZE = 2 * 1024 * 1024
|
MIN_SIZE = 2 * 1024 * 1024
|
||||||
MAX_WORKERS = min(32, (multiprocessing.cpu_count() or 1) * 5)
|
MAX_WORKERS = min(32, (multiprocessing.cpu_count() or 1) * 5)
|
||||||
DB_PATH = "image_compressor.db"
|
DB_PATH = "image_compressor.db"
|
||||||
|
|
||||||
@@ -54,17 +54,10 @@ def get_tool_path(name: str) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def get_folder_size(path: Path) -> int:
|
def get_folder_size(path: Path) -> int:
|
||||||
excluded = {
|
|
||||||
"image_compressor.exe",
|
|
||||||
"image_compressor.py",
|
|
||||||
"image_compressor.db",
|
|
||||||
"image_compressor.db-journal",
|
|
||||||
"image_compressor.log",
|
|
||||||
}
|
|
||||||
total_size = 0
|
total_size = 0
|
||||||
for dirpath, dirnames, filenames in os.walk(path):
|
for dirpath, _, filenames in os.walk(path):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
if filename in excluded:
|
if filename.startswith("image_compressor"):
|
||||||
continue
|
continue
|
||||||
file_path = Path(dirpath) / filename
|
file_path = Path(dirpath) / filename
|
||||||
total_size += file_path.stat().st_size
|
total_size += file_path.stat().st_size
|
||||||
@@ -83,27 +76,51 @@ def extract_exif(path: Path):
|
|||||||
try:
|
try:
|
||||||
with Image.open(path) as img:
|
with Image.open(path) as img:
|
||||||
return img.info.get("exif")
|
return img.info.get("exif")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logging.warning(
|
||||||
|
f"Не удалось извлечь EXIF из {path} {path.stat().st_size // 1024} KB): {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def inject_exif(path: Path, exif):
|
def inject_exif(path: Path, exif):
|
||||||
try:
|
try:
|
||||||
with Image.open(path) as img:
|
with Image.open(path) as img:
|
||||||
img.convert("RGB").save(path, "JPEG", exif=exif)
|
fmt = img.format
|
||||||
|
if fmt == "JPEG" and img.mode in ("L", "RGB"):
|
||||||
|
mode = img.mode
|
||||||
|
elif fmt == "WEBP" and img.mode in ("RGBA", "LA"):
|
||||||
|
mode = img.mode
|
||||||
|
else:
|
||||||
|
mode = "RGB"
|
||||||
|
img_converted = img.convert(mode)
|
||||||
|
img_converted.save(path, format=fmt, exif=exif)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Не удалось вставить EXIF в {path}: {e}")
|
logging.warning(
|
||||||
|
f"Не удалось вставить EXIF в {path} {path.stat().st_size // 1024} KB): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_png_to_jpeg(path: Path) -> Optional[Path]:
|
def convert_png_to_jpeg(path: Path) -> Optional[Path]:
|
||||||
tmp_path = path.with_suffix(".jpg")
|
base_name = path.stem
|
||||||
|
parent = path.parent
|
||||||
|
suffix = ".jpg"
|
||||||
|
|
||||||
|
new_name = f"{base_name}{suffix}"
|
||||||
|
counter = 1
|
||||||
|
while (parent / new_name).exists():
|
||||||
|
new_name = f"{base_name} ({counter}){suffix}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
tmp_path = parent / new_name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Image.open(path) as img:
|
with Image.open(path) as img:
|
||||||
img.convert("RGB").save(tmp_path, "JPEG")
|
img.convert("RGB").save(tmp_path, "JPEG")
|
||||||
path.unlink()
|
path.unlink()
|
||||||
return tmp_path
|
return tmp_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.warning(
|
||||||
f"Ошибка при конвертации PNG в JPEG: {path} ({path.stat().st_size // 1024} KB): {e}"
|
f"Ошибка при конвертации PNG в JPEG: {path} ({path.stat().st_size // 1024} KB): {e}"
|
||||||
)
|
)
|
||||||
if tmp_path.exists():
|
if tmp_path.exists():
|
||||||
@@ -123,9 +140,15 @@ def compress_with_external(
|
|||||||
converted = convert_png_to_jpeg(path)
|
converted = convert_png_to_jpeg(path)
|
||||||
if not converted:
|
if not converted:
|
||||||
return False, path
|
return False, path
|
||||||
|
conerted_size = converted.stat().st_size
|
||||||
|
logging.warning(
|
||||||
|
f"Сконвертирован PNG в JPEG: {path} ({original_size // 1024} KB) -> {converted} ({conerted_size // 1024} KB)"
|
||||||
|
)
|
||||||
|
if conerted_size < TARGET_SIZE:
|
||||||
|
return True, converted
|
||||||
path = converted
|
path = converted
|
||||||
ext = ".jpg"
|
ext = ".jpg"
|
||||||
original_size = path.stat().st_size
|
original_size = conerted_size
|
||||||
if ext in [".jpg", ".jpeg"]:
|
if ext in [".jpg", ".jpeg"]:
|
||||||
tool = get_tool_path("cjpeg-static.exe")
|
tool = get_tool_path("cjpeg-static.exe")
|
||||||
args_base = [
|
args_base = [
|
||||||
@@ -151,6 +174,9 @@ def compress_with_external(
|
|||||||
"all",
|
"all",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
logging.warning(
|
||||||
|
f"Неподдерживаемый формат {path} ({original_size // 1024} KB)"
|
||||||
|
)
|
||||||
return False, path
|
return False, path
|
||||||
|
|
||||||
quality = 85
|
quality = 85
|
||||||
@@ -168,7 +194,7 @@ def compress_with_external(
|
|||||||
quality -= 5
|
quality -= 5
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.warning(
|
||||||
f"Ошибка при сжатии внешней утилитой {path} ({original_size // 1024} KB): {e}"
|
f"Ошибка при сжатии внешней утилитой {path} ({original_size // 1024} KB): {e}"
|
||||||
)
|
)
|
||||||
if tmp_path.exists():
|
if tmp_path.exists():
|
||||||
@@ -176,14 +202,13 @@ def compress_with_external(
|
|||||||
return False, path
|
return False, path
|
||||||
|
|
||||||
if tmp_path.exists():
|
if tmp_path.exists():
|
||||||
new_size = tmp_path.stat().st_size
|
if tmp_path.stat().st_size < original_size:
|
||||||
if new_size < original_size:
|
|
||||||
if exif:
|
if exif:
|
||||||
inject_exif(tmp_path, exif)
|
inject_exif(tmp_path, exif)
|
||||||
tmp_path.replace(path)
|
tmp_path.replace(path)
|
||||||
return True, path
|
return True, path
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.warning(
|
||||||
f"Не удалось сжать внешней утилитой (не уменьшилось): {path} ({original_size // 1024} KB)"
|
f"Не удалось сжать внешней утилитой (не уменьшилось): {path} ({original_size // 1024} KB)"
|
||||||
)
|
)
|
||||||
tmp_path.unlink()
|
tmp_path.unlink()
|
||||||
@@ -213,7 +238,7 @@ def compress_with_pillow(path: Path) -> Tuple[bool, Path]:
|
|||||||
quality -= 5
|
quality -= 5
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.warning(
|
||||||
f"Ошибка при сжатии Pillow {path} ({original_size // 1024} KB): {e}"
|
f"Ошибка при сжатии Pillow {path} ({original_size // 1024} KB): {e}"
|
||||||
)
|
)
|
||||||
if tmp_path.exists():
|
if tmp_path.exists():
|
||||||
@@ -225,7 +250,7 @@ def compress_with_pillow(path: Path) -> Tuple[bool, Path]:
|
|||||||
inject_exif(tmp_path, exif)
|
inject_exif(tmp_path, exif)
|
||||||
tmp_path.replace(path)
|
tmp_path.replace(path)
|
||||||
return True, path
|
return True, path
|
||||||
logging.error(
|
logging.warning(
|
||||||
f"Не удалось сжать Pillow (не уменьшилось): {path} ({original_size // 1024} KB)"
|
f"Не удалось сжать Pillow (не уменьшилось): {path} ({original_size // 1024} KB)"
|
||||||
)
|
)
|
||||||
tmp_path.unlink()
|
tmp_path.unlink()
|
||||||
@@ -242,7 +267,7 @@ def compress_image(path: Path):
|
|||||||
|
|
||||||
h = file_hash(path)
|
h = file_hash(path)
|
||||||
|
|
||||||
if original_size < MAX_SIZE:
|
if original_size < MIN_SIZE:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Пропущено (малый размер): {path} ({original_size // 1024} KB)"
|
f"Пропущено (малый размер): {path} ({original_size // 1024} KB)"
|
||||||
)
|
)
|
||||||
@@ -268,7 +293,7 @@ def compress_image(path: Path):
|
|||||||
existing_paths.add(file_path_str)
|
existing_paths.add(file_path_str)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE processed_images SET filename = ? WHERE hash = ?",
|
"UPDATE processed_images SET filename = ? WHERE hash = ?",
|
||||||
("|".join(sorted(existing_paths)), h),
|
("|".join(existing_paths), h),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logging.info(
|
logging.info(
|
||||||
@@ -321,13 +346,17 @@ def compress_image(path: Path):
|
|||||||
processed_count += 1
|
processed_count += 1
|
||||||
total_saved_bytes += saved
|
total_saved_bytes += saved
|
||||||
else:
|
else:
|
||||||
logging.error(f"Не удалось сжать: {path}")
|
logging.error(
|
||||||
|
f"Не удалось сжать: {path} ({original_size // 1024} KB)"
|
||||||
|
)
|
||||||
processed_hashes.add(h)
|
processed_hashes.add(h)
|
||||||
error_count += 1
|
error_count += 1
|
||||||
total_images_new_size += original_size
|
total_images_new_size += original_size
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Ошибка при обработке {path}: {e}")
|
logging.error(
|
||||||
|
f"Ошибка при обработке {path} ({original_size // 1024} KB): {e}"
|
||||||
|
)
|
||||||
error_count += 1
|
error_count += 1
|
||||||
total_images_new_size += original_size
|
total_images_new_size += original_size
|
||||||
|
|
||||||
@@ -374,9 +403,6 @@ def main():
|
|||||||
|
|
||||||
print(f"Входная папка: {input_dir}")
|
print(f"Входная папка: {input_dir}")
|
||||||
print(f"Выходная папка: {output_dir}")
|
print(f"Выходная папка: {output_dir}")
|
||||||
if input("Начать обработку? [y/n]: ").strip().lower() != "y":
|
|
||||||
print("Отменено.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Проверка необходимых инструментов...")
|
print("Проверка необходимых инструментов...")
|
||||||
required = ["cjpeg-static.exe", "cwebp.exe"]
|
required = ["cjpeg-static.exe", "cwebp.exe"]
|
||||||
@@ -392,6 +418,11 @@ def main():
|
|||||||
files = prepare_and_copy_files(input_dir, output_dir)
|
files = prepare_and_copy_files(input_dir, output_dir)
|
||||||
total_files = len(files)
|
total_files = len(files)
|
||||||
print(f"Найдено {total_files} изображений.")
|
print(f"Найдено {total_files} изображений.")
|
||||||
|
logging.info(f"Найдено {total_files} изображений.")
|
||||||
|
|
||||||
|
if input("Начать обработку? [y/n]: ").strip().lower() != "y":
|
||||||
|
return
|
||||||
|
|
||||||
logging.info(f"Начато. Найдено {total_files} изображений.")
|
logging.info(f"Начато. Найдено {total_files} изображений.")
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||||
|
|||||||
Reference in New Issue
Block a user