You've already forked ImageCompressor
mirror of
https://github.com/Llloooggg/ImageCompressor.git
synced 2026-03-05 19:16:23 +03:00
Исправление обработки путей и хейшей после конвертации
This commit is contained in:
@@ -13,7 +13,6 @@ from pathlib import Path
|
|||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", category=UserWarning, module="PIL")
|
warnings.filterwarnings("ignore", category=UserWarning, module="PIL")
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -71,47 +70,48 @@ def inject_exif(jpeg_path, exif_bytes):
|
|||||||
logging.error(f"Ошибка при вставке EXIF в {jpeg_path}: {e}")
|
logging.error(f"Ошибка при вставке EXIF в {jpeg_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def convert_png_to_jpeg(path: str) -> bool:
|
def convert_png_to_jpeg(path: Path) -> Path | None:
|
||||||
try:
|
try:
|
||||||
|
temp_path = path.with_suffix(".jpg")
|
||||||
with Image.open(path) as img:
|
with Image.open(path) as img:
|
||||||
temp_path = path.with_suffix(".jpg")
|
|
||||||
img.convert("RGB").save(
|
img.convert("RGB").save(
|
||||||
temp_path, "JPEG", quality=85, optimize=True
|
temp_path, "JPEG", quality=85, optimize=True
|
||||||
)
|
)
|
||||||
|
if temp_path.stat().st_size < path.stat().st_size:
|
||||||
new_size = temp_path.stat().st_size
|
path.unlink()
|
||||||
if new_size < path.stat().st_size:
|
return temp_path
|
||||||
temp_path.replace(path)
|
else:
|
||||||
return True
|
temp_path.unlink()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Ошибка при конвертации PNG в JPEG для {path}: {e}")
|
logging.error(f"Ошибка при конвертации PNG в JPEG для {path}: {e}")
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def compress_with_external(path: str, ext: str) -> bool:
|
def compress_with_external(path: str, ext: str) -> tuple[bool, Path]:
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
original_size = path.stat().st_size
|
original_size = path.stat().st_size
|
||||||
tmp_path = path.with_name(path.name + ".compressed")
|
tmp_path = path.with_name(path.stem + ".compressed" + path.suffix)
|
||||||
target_size = TARGET_SIZE_MB
|
target_size = TARGET_SIZE_MB
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if ext == ".png":
|
if ext == ".png":
|
||||||
if not convert_png_to_jpeg(path):
|
new_path = convert_png_to_jpeg(path)
|
||||||
return False
|
if not new_path:
|
||||||
|
return False, path
|
||||||
|
path = new_path
|
||||||
ext = ".jpg"
|
ext = ".jpg"
|
||||||
|
|
||||||
if ext in [".jpg", ".jpeg"]:
|
if ext in [".jpg", ".jpeg"]:
|
||||||
exif_data = extract_exif(path)
|
exif_data = extract_exif(path)
|
||||||
|
|
||||||
tool = get_tool_path("cjpeg-static.exe")
|
tool = get_tool_path("cjpeg-static.exe")
|
||||||
quality = 85
|
quality = 85
|
||||||
while True:
|
while True:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
tool,
|
tool,
|
||||||
f"-quality",
|
"-quality",
|
||||||
str(quality),
|
str(quality),
|
||||||
f"-outfile",
|
"-outfile",
|
||||||
str(tmp_path),
|
str(tmp_path),
|
||||||
str(path),
|
str(path),
|
||||||
],
|
],
|
||||||
@@ -122,7 +122,6 @@ def compress_with_external(path: str, ext: str) -> bool:
|
|||||||
if os.path.getsize(tmp_path) <= target_size or quality < 50:
|
if os.path.getsize(tmp_path) <= target_size or quality < 50:
|
||||||
break
|
break
|
||||||
quality -= 5
|
quality -= 5
|
||||||
|
|
||||||
if tmp_path.exists() and exif_data:
|
if tmp_path.exists() and exif_data:
|
||||||
inject_exif(tmp_path, exif_data)
|
inject_exif(tmp_path, exif_data)
|
||||||
|
|
||||||
@@ -151,31 +150,33 @@ def compress_with_external(path: str, ext: str) -> bool:
|
|||||||
break
|
break
|
||||||
quality -= 5
|
quality -= 5
|
||||||
else:
|
else:
|
||||||
return False
|
return False, path
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return None
|
return None, path
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка внешнего сжатия {path}: {e}")
|
||||||
|
return False, path
|
||||||
|
|
||||||
if tmp_path.exists():
|
if tmp_path.exists():
|
||||||
new_size = os.path.getsize(tmp_path)
|
new_size = tmp_path.stat().st_size
|
||||||
if new_size < original_size:
|
if new_size < original_size:
|
||||||
tmp_path.replace(path)
|
tmp_path.replace(path)
|
||||||
return True
|
return True, path
|
||||||
else:
|
else:
|
||||||
tmp_path.unlink()
|
tmp_path.unlink()
|
||||||
return False
|
return False, path
|
||||||
|
|
||||||
|
|
||||||
def compress_with_pillow(path: str) -> bool:
|
def compress_with_pillow(path: str) -> tuple[bool, Path]:
|
||||||
original_size = os.path.getsize(path)
|
path = Path(path)
|
||||||
target_size = TARGET_SIZE_MB
|
original_size = path.stat().st_size
|
||||||
temp_path = Path(path).with_suffix(".pillowtmp")
|
temp_path = path.with_name(path.stem + ".pillowtmp" + path.suffix)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Image.open(path) as img:
|
with Image.open(path) as img:
|
||||||
img_format = img.format
|
img_format = img.format
|
||||||
exif = img.info.get("exif", None)
|
exif = img.info.get("exif", None)
|
||||||
quality = 85
|
quality = 85
|
||||||
|
|
||||||
while quality >= 50:
|
while quality >= 50:
|
||||||
img.save(
|
img.save(
|
||||||
temp_path,
|
temp_path,
|
||||||
@@ -184,73 +185,72 @@ def compress_with_pillow(path: str) -> bool:
|
|||||||
quality=quality,
|
quality=quality,
|
||||||
exif=exif,
|
exif=exif,
|
||||||
)
|
)
|
||||||
if temp_path.stat().st_size <= target_size:
|
if temp_path.stat().st_size <= TARGET_SIZE_MB:
|
||||||
break
|
break
|
||||||
quality -= 5
|
quality -= 5
|
||||||
|
|
||||||
if temp_path.exists() and temp_path.stat().st_size < original_size:
|
if temp_path.exists() and temp_path.stat().st_size < original_size:
|
||||||
temp_path.replace(path)
|
temp_path.replace(path)
|
||||||
return True
|
return True, path
|
||||||
elif temp_path.exists():
|
elif temp_path.exists():
|
||||||
temp_path.unlink()
|
temp_path.unlink()
|
||||||
return False
|
return False, path
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Ошибка Pillow для {path}: {e}")
|
logging.error(f"Ошибка Pillow для {path}: {e}")
|
||||||
return False
|
return False, path
|
||||||
|
|
||||||
|
|
||||||
def compress_image(path: str, fallback_to_pillow: bool = False):
|
def compress_image(path: str, fallback_to_pillow: bool = False):
|
||||||
global processed_count, skipped_count, total_saved_bytes
|
global processed_count, skipped_count, total_saved_bytes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
original_size = os.path.getsize(path)
|
path = Path(path)
|
||||||
|
original_size = path.stat().st_size
|
||||||
if original_size < MIN_SIZE:
|
if original_size < MIN_SIZE:
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
msg = f"Пропущено (уже малый): {path} ({original_size / 1024:.1f} KB)"
|
logging.info(
|
||||||
logging.info(msg)
|
f"Пропущено (уже малый): {path} ({original_size / 1024:.1f} KB)"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
h = file_hash(path)
|
h = file_hash(path)
|
||||||
filename = path.name
|
|
||||||
with db_lock:
|
with db_lock:
|
||||||
cursor.execute("SELECT 1 FROM processed WHERE hash = ?", (h,))
|
cursor.execute("SELECT 1 FROM processed WHERE hash = ?", (h,))
|
||||||
if cursor.fetchone():
|
if cursor.fetchone():
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
msg = f"Пропущено (уже сжато): {path} ({original_size / 1024:.1f} KB)"
|
logging.info(
|
||||||
logging.info(msg)
|
f"Пропущено (уже сжато): {path} ({original_size / 1024:.1f} KB)"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
ext = Path(path).suffix.lower()
|
ext = path.suffix.lower()
|
||||||
result = compress_with_external(path, ext)
|
result, path = compress_with_external(path, ext)
|
||||||
|
|
||||||
if result is None and fallback_to_pillow:
|
if result is None and fallback_to_pillow:
|
||||||
result = compress_with_pillow(path)
|
result, path = compress_with_pillow(path)
|
||||||
|
|
||||||
new_size = os.path.getsize(path)
|
new_size = path.stat().st_size
|
||||||
if result and new_size < original_size:
|
if result and new_size < original_size:
|
||||||
saved_bytes = original_size - new_size
|
saved_bytes = original_size - new_size
|
||||||
total_saved_bytes += saved_bytes
|
total_saved_bytes += saved_bytes
|
||||||
|
percent = (1 - new_size / original_size) * 100
|
||||||
saved_percent = (1 - new_size / original_size) * 100
|
logging.info(
|
||||||
msg = (
|
f"Сжато: {path} ({original_size / 1024:.1f} KB -> {new_size / 1024:.1f} KB, сохранено {percent:.2f}%)"
|
||||||
f"Сжато: {path} "
|
|
||||||
f"({original_size / 1024:.1f} KB -> {new_size / 1024:.1f} KB, "
|
|
||||||
f"сохранено {saved_percent:.2f}%)"
|
|
||||||
)
|
)
|
||||||
logging.info(msg)
|
|
||||||
else:
|
else:
|
||||||
msg = f"Пропущено (не меньше): {path} ({original_size / 1024:.1f} KB)"
|
logging.info(
|
||||||
logging.info(msg)
|
f"Пропущено (не меньше): {path} ({original_size / 1024:.1f} KB)"
|
||||||
|
)
|
||||||
|
|
||||||
|
h = file_hash(path)
|
||||||
with db_lock:
|
with db_lock:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO processed(hash, filename) VALUES(?, ?)",
|
"INSERT INTO processed(hash, filename) VALUES(?, ?)",
|
||||||
(h, filename),
|
(h, path.name),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
|
processed_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Ошибка обработки {path}: {e}")
|
logging.error(f"Ошибка обработки {path}: {e}")
|
||||||
|
|
||||||
@@ -286,10 +286,11 @@ def main():
|
|||||||
|
|
||||||
print("Проверка утилит...")
|
print("Проверка утилит...")
|
||||||
required_tools = ["cjpeg-static.exe", "cwebp.exe"]
|
required_tools = ["cjpeg-static.exe", "cwebp.exe"]
|
||||||
missing = []
|
missing = [
|
||||||
for tool in required_tools:
|
tool
|
||||||
if not os.path.exists(get_tool_path(tool)):
|
for tool in required_tools
|
||||||
missing.append(tool)
|
if not os.path.exists(get_tool_path(tool))
|
||||||
|
]
|
||||||
|
|
||||||
fallback = False
|
fallback = False
|
||||||
if missing:
|
if missing:
|
||||||
|
|||||||
Reference in New Issue
Block a user