133 lines
3.7 KiB
Python
Executable File
133 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Сжатие только скриншотов в Report/images/ (не PlantUML PNG).
|
||
|
||
JPEG: quality=28 (ImageMagick).
|
||
PNG-скриншоты (Gradle, ручная схема): pngquant quality 25–40.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# Имена из IMAGES_REGISTRY: Скриншот UI, Gradle, Ручная схема
|
||
SCREENSHOT_STEMS = {
|
||
"fig_05_local_vaults",
|
||
"fig_06_encrypt_dialog",
|
||
"fig_07_open_close_dialog",
|
||
"fig_08_rename_delete_dialog",
|
||
"fig_09_remote_vaults",
|
||
"fig_10_yandex_oauth",
|
||
"fig_12_tasks_screen",
|
||
"fig_13_tasks_notification",
|
||
"fig_33_storage_secrets_2fa",
|
||
"fig_34_2fa_single_token",
|
||
"fig_24_domain_class_manual",
|
||
"fig_27_gradle_domain_test",
|
||
"fig_28_gradle_usecases_test",
|
||
"fig_29_gradle_ui_test",
|
||
"fig_30_gradle_test_summary",
|
||
"fig_31_gradle_connected_test",
|
||
}
|
||
|
||
JPEG_QUALITY = "28"
|
||
PNGQUANT_QUALITY = "25-40"
|
||
|
||
|
||
def stem(path: Path) -> str:
|
||
return path.stem
|
||
|
||
|
||
def is_screenshot(path: Path) -> bool:
|
||
if path.suffix.lower() == ".jpg":
|
||
return stem(path) in SCREENSHOT_STEMS or path.name.startswith("fig_")
|
||
if path.suffix.lower() == ".png":
|
||
return stem(path) in SCREENSHOT_STEMS
|
||
return False
|
||
|
||
|
||
def size_kb(path: Path) -> float:
|
||
return path.stat().st_size / 1024
|
||
|
||
|
||
def compress_jpeg(path: Path, magick: str) -> None:
|
||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||
subprocess.run(
|
||
[magick, str(path), "-strip", "-quality", JPEG_QUALITY, str(tmp)],
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
tmp.replace(path)
|
||
|
||
|
||
def compress_png(path: Path, pngquant: str) -> bool:
|
||
tmp = path.with_suffix(".pngquant.tmp.png")
|
||
for quality in (PNGQUANT_QUALITY, "15-55", "10-80"):
|
||
try:
|
||
subprocess.run(
|
||
[
|
||
pngquant,
|
||
f"--quality={quality}",
|
||
"--force",
|
||
"--output",
|
||
str(tmp),
|
||
str(path),
|
||
],
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
tmp.replace(path)
|
||
return True
|
||
except subprocess.CalledProcessError:
|
||
if tmp.exists():
|
||
tmp.unlink()
|
||
print(f" WARN: pngquant skip {path.name}", file=sys.stderr)
|
||
return False
|
||
|
||
|
||
def main() -> int:
|
||
report = Path(__file__).resolve().parents[1]
|
||
images = report / "images"
|
||
magick = shutil.which("magick") or shutil.which("convert")
|
||
pngquant = shutil.which("pngquant")
|
||
if not magick:
|
||
print("ERROR: ImageMagick (magick/convert) not found", file=sys.stderr)
|
||
return 1
|
||
if not pngquant:
|
||
print("ERROR: pngquant not found", file=sys.stderr)
|
||
return 1
|
||
|
||
targets = sorted(
|
||
p for p in images.iterdir() if p.is_file() and is_screenshot(p)
|
||
)
|
||
if not targets:
|
||
print("No screenshots to compress", file=sys.stderr)
|
||
return 1
|
||
|
||
total_before = 0.0
|
||
total_after = 0.0
|
||
for path in targets:
|
||
before = size_kb(path)
|
||
total_before += before
|
||
if path.suffix.lower() in {".jpg", ".jpeg"}:
|
||
compress_jpeg(path, magick)
|
||
else:
|
||
compress_png(path, pngquant)
|
||
after = size_kb(path)
|
||
total_after += after
|
||
pct = (1 - after / before) * 100 if before else 0
|
||
print(f" {path.name}: {before:.0f} KiB → {after:.0f} KiB (−{pct:.0f}%)")
|
||
|
||
saved = (1 - total_after / total_before) * 100 if total_before else 0
|
||
print(
|
||
f"OK: {len(targets)} screenshots, "
|
||
f"{total_before:.0f} KiB → {total_after:.0f} KiB (−{saved:.0f}%)"
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|