You've already forked ImageCompressor
mirror of
https://github.com/Llloooggg/ImageCompressor.git
synced 2026-03-06 03:26:23 +03:00
Исправление сравнения обжатия
This commit is contained in:
@@ -11,41 +11,41 @@ import warnings
|
|||||||
from PIL import Image, ImageFile
|
from PIL import Image, ImageFile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
# Константы
|
# --- Константы ---
|
||||||
TARGET_SIZE = 2 * 1024 * 1024
|
TARGET_SIZE = 2 * 1024 * 1024 # 2MB
|
||||||
MIN_SIZE = TARGET_SIZE
|
MIN_SIZE = TARGET_SIZE
|
||||||
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"
|
||||||
|
|
||||||
# Настройки Pillow
|
# --- Настройки Pillow ---
|
||||||
Image.MAX_IMAGE_PIXELS = None
|
Image.MAX_IMAGE_PIXELS = None
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
warnings.filterwarnings("ignore", category=UserWarning, module="PIL")
|
warnings.filterwarnings("ignore", category=UserWarning, module="PIL")
|
||||||
warnings.simplefilter("ignore", Image.DecompressionBombWarning)
|
warnings.simplefilter("ignore", Image.DecompressionBombWarning)
|
||||||
|
|
||||||
# Логирование
|
# --- Логирование ---
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename="image_compressor.log",
|
filename="image_compressor.log",
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Глобальные счётчики
|
# --- Глобальные переменные ---
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
total_saved_bytes = 0
|
total_saved_bytes = 0
|
||||||
db_lock = threading.Lock()
|
db_lock = threading.Lock()
|
||||||
|
|
||||||
# Инициализация БД
|
# --- База данных ---
|
||||||
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS processed_images (hash TEXT PRIMARY KEY, filename TEXT)"
|
"CREATE TABLE IF NOT EXISTS processed_images (hash TEXT PRIMARY KEY, filename TEXT, reduced BOOLEAN)"
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
# --- Утилиты ---
|
# --- Утилиты ---
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def inject_exif(path: Path, exif):
|
|||||||
# --- Сжатие ---
|
# --- Сжатие ---
|
||||||
|
|
||||||
|
|
||||||
def convert_png_to_jpeg(path: Path) -> Path | None:
|
def convert_png_to_jpeg(path: Path) -> Optional[Path]:
|
||||||
temp_path = path.with_suffix(".jpg")
|
temp_path = path.with_suffix(".jpg")
|
||||||
try:
|
try:
|
||||||
with Image.open(path) as img:
|
with Image.open(path) as img:
|
||||||
@@ -100,7 +100,7 @@ def convert_png_to_jpeg(path: Path) -> Path | None:
|
|||||||
|
|
||||||
def compress_with_external(
|
def compress_with_external(
|
||||||
path: Path, ext: str
|
path: Path, ext: str
|
||||||
) -> tuple[bool, Path] | tuple[None, Path]:
|
) -> Tuple[Optional[bool], Path]:
|
||||||
original_size = path.stat().st_size
|
original_size = path.stat().st_size
|
||||||
tmp_path = path.with_name(path.stem + ".compressed" + path.suffix)
|
tmp_path = path.with_name(path.stem + ".compressed" + path.suffix)
|
||||||
exif = extract_exif(path)
|
exif = extract_exif(path)
|
||||||
@@ -112,15 +112,12 @@ def compress_with_external(
|
|||||||
return False, path
|
return False, path
|
||||||
path = converted
|
path = converted
|
||||||
ext = ".jpg"
|
ext = ".jpg"
|
||||||
|
original_size = path.stat().st_size
|
||||||
tool = None
|
|
||||||
args = []
|
|
||||||
quality = 85
|
|
||||||
|
|
||||||
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 = [
|
||||||
tool,
|
str(tool),
|
||||||
"-quality",
|
"-quality",
|
||||||
"",
|
"",
|
||||||
"-outfile",
|
"-outfile",
|
||||||
@@ -130,7 +127,7 @@ def compress_with_external(
|
|||||||
elif ext == ".webp":
|
elif ext == ".webp":
|
||||||
tool = get_tool_path("cwebp.exe")
|
tool = get_tool_path("cwebp.exe")
|
||||||
args_base = [
|
args_base = [
|
||||||
tool,
|
str(tool),
|
||||||
str(path),
|
str(path),
|
||||||
"-o",
|
"-o",
|
||||||
str(tmp_path),
|
str(tmp_path),
|
||||||
@@ -144,20 +141,17 @@ def compress_with_external(
|
|||||||
else:
|
else:
|
||||||
return False, path
|
return False, path
|
||||||
|
|
||||||
|
quality = 85
|
||||||
while quality >= 50:
|
while quality >= 50:
|
||||||
args = args_base.copy()
|
args = args_base.copy()
|
||||||
if ext == ".webp":
|
|
||||||
args[args.index("")] = str(quality)
|
args[args.index("")] = str(quality)
|
||||||
else:
|
|
||||||
args[args.index("")] = str(quality)
|
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
args,
|
args,
|
||||||
check=True,
|
check=True,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
if tmp_path.stat().st_size <= TARGET_SIZE or quality <= 50:
|
if tmp_path.stat().st_size <= TARGET_SIZE:
|
||||||
break
|
break
|
||||||
quality -= 5
|
quality -= 5
|
||||||
|
|
||||||
@@ -177,7 +171,7 @@ def compress_with_external(
|
|||||||
return False, path
|
return False, path
|
||||||
|
|
||||||
|
|
||||||
def compress_with_pillow(path: Path) -> tuple[bool, Path]:
|
def compress_with_pillow(path: Path) -> Tuple[bool, Path]:
|
||||||
original_size = path.stat().st_size
|
original_size = path.stat().st_size
|
||||||
temp_path = path.with_name(path.stem + ".pillowtmp" + path.suffix)
|
temp_path = path.with_name(path.stem + ".pillowtmp" + path.suffix)
|
||||||
|
|
||||||
@@ -211,11 +205,14 @@ def compress_image(path: Path, use_fallback: bool = False):
|
|||||||
global processed_count, skipped_count, total_saved_bytes
|
global processed_count, skipped_count, total_saved_bytes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if path.stat().st_size < MIN_SIZE:
|
if not path.exists() or path.stat().st_size < MIN_SIZE:
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
logging.info(f"Пропущено (малый размер): {path}")
|
logging.info(
|
||||||
|
f"Пропущено (малый размер или не найден): {path} ({path.stat().st_size // 1024}KB)"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
original_size = path.stat().st_size
|
||||||
h = file_hash(path)
|
h = file_hash(path)
|
||||||
with db_lock:
|
with db_lock:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -223,7 +220,9 @@ def compress_image(path: Path, use_fallback: bool = False):
|
|||||||
)
|
)
|
||||||
if cursor.fetchone():
|
if cursor.fetchone():
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
logging.info(f"Пропущено (уже обработано): {path}")
|
logging.info(
|
||||||
|
f"Пропущено (уже обработано): {path} ({original_size // 1024}KB)"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
ext = path.suffix.lower()
|
ext = path.suffix.lower()
|
||||||
@@ -232,10 +231,14 @@ def compress_image(path: Path, use_fallback: bool = False):
|
|||||||
if result is None and use_fallback:
|
if result is None and use_fallback:
|
||||||
result, final_path = compress_with_pillow(path)
|
result, final_path = compress_with_pillow(path)
|
||||||
|
|
||||||
new_size = final_path.stat().st_size
|
if not final_path.exists():
|
||||||
original_size = path.stat().st_size
|
logging.warning(f"Файл не найден после сжатия: {final_path}")
|
||||||
|
return
|
||||||
|
|
||||||
if result and new_size < original_size:
|
new_size = final_path.stat().st_size
|
||||||
|
|
||||||
|
if result:
|
||||||
|
if new_size < original_size:
|
||||||
saved = original_size - new_size
|
saved = original_size - new_size
|
||||||
total_saved_bytes += saved
|
total_saved_bytes += saved
|
||||||
percent = (1 - new_size / original_size) * 100
|
percent = (1 - new_size / original_size) * 100
|
||||||
@@ -243,13 +246,15 @@ def compress_image(path: Path, use_fallback: bool = False):
|
|||||||
f"Сжато: {path} ({original_size//1024}KB -> {new_size//1024}KB, -{percent:.2f}%)"
|
f"Сжато: {path} ({original_size//1024}KB -> {new_size//1024}KB, -{percent:.2f}%)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.info(f"Пропущено (не уменьшилось): {path}")
|
logging.info(
|
||||||
|
f"Пропущено (не уменьшилось): {path} ({new_size // 1024}KB)"
|
||||||
|
)
|
||||||
|
|
||||||
h = file_hash(final_path)
|
h = file_hash(final_path)
|
||||||
with db_lock:
|
with db_lock:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO processed_images(hash, filename) VALUES(?, ?)",
|
"INSERT INTO processed_images(hash, filename, reduced) VALUES(?, ?, ?)",
|
||||||
(h, final_path.name),
|
(h, final_path.name, new_size < original_size),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -269,6 +274,23 @@ def find_images(root: Path):
|
|||||||
yield Path(dirpath) / name
|
yield Path(dirpath) / name
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_and_copy_files(input_dir: Path, output_dir: Path) -> list[Path]:
|
||||||
|
if input_dir.resolve() == output_dir.resolve():
|
||||||
|
return list(find_images(input_dir))
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
copied = []
|
||||||
|
|
||||||
|
for image in find_images(input_dir):
|
||||||
|
rel_path = image.relative_to(input_dir)
|
||||||
|
dest = output_dir / rel_path
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest.write_bytes(image.read_bytes())
|
||||||
|
copied.append(dest)
|
||||||
|
|
||||||
|
return copied
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Сжатие изображений до заданного размера"
|
description="Сжатие изображений до заданного размера"
|
||||||
@@ -276,9 +298,19 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--input", help="Папка со входными изображениями", default=os.getcwd()
|
"--input", help="Папка со входными изображениями", default=os.getcwd()
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output", help="Папка для сжатых изображений", default=None
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
input_dir = Path(args.input)
|
input_dir = Path(args.input).resolve()
|
||||||
|
output_dir = Path(args.output).resolve() if args.output else input_dir
|
||||||
|
|
||||||
|
print(f"Входная папка: {input_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"]
|
||||||
@@ -293,7 +325,7 @@ def main():
|
|||||||
return
|
return
|
||||||
use_fallback = True
|
use_fallback = True
|
||||||
|
|
||||||
files = list(find_images(input_dir))
|
files = prepare_and_copy_files(input_dir, output_dir)
|
||||||
print(f"Найдено {len(files)} изображений.")
|
print(f"Найдено {len(files)} изображений.")
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||||
|
|||||||
Reference in New Issue
Block a user