From 91706f5232c47ec008d89037237fb27f522760b4 Mon Sep 17 00:00:00 2001 From: anian Date: Tue, 28 Jul 2026 19:37:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=BE=E7=89=87=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++ README.md | 1 + image-compression.py | 169 ++++++++++++++++++++++--------------------- 3 files changed, 91 insertions(+), 84 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fcf0ad6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv/ +input/ +output/ +__pycache__/ + diff --git a/README.md b/README.md index 22bbeb9..b1f0f2c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ #### v260717补丁 1. 修改图片压缩文件名 +2. 完善图片压缩功能 #### v260717 1. 新增JS音乐播放器(基于Meting-API) diff --git a/image-compression.py b/image-compression.py index 85ac9d6..b4e5214 100644 --- a/image-compression.py +++ b/image-compression.py @@ -3,118 +3,119 @@ from PIL import Image import os import shutil -def get_file_kb(file_path: str) -> float: - """获取文件大小 KB""" - size_byte = os.path.getsize(file_path) - return size_byte / 1024 +SUPPORTED_OUTPUT_FORMATS = ("jpg", "jpeg", "png", "webp") -def compress_to_jpg_loop( - input_path: str, - output_path: str, - min_size_kb: float, - max_size_kb: float, - init_quality: int, - quality_step: int, - min_quality: int, - max_width: int, - max_height: int -): + +def get_file_kb(file_path: str) -> float: + return os.path.getsize(file_path) / 1024 + + +def _save_image(img: Image.Image, output_path: str, output_format: str, quality: int): + if output_format == "png": + img.save(output_path, format="PNG", optimize=True, compress_level=9) + elif output_format == "webp": + img.save(output_path, format="WEBP", quality=quality, method=6) + else: + img.save(output_path, format="JPEG", quality=quality, optimize=True) + + +def compress_to_format_loop(input_path, output_path, min_size_kb, max_size_kb, + init_quality, quality_step, min_quality, + max_width, max_height, output_format): try: + # None 表示保持输入图片格式。 + if output_format is None: + output_format = os.path.splitext(input_path)[1].lower().lstrip(".") + output_format = output_format.lower().lstrip(".") + if output_format not in SUPPORTED_OUTPUT_FORMATS: + raise ValueError(f"不支持的输出格式: {output_format}") + ori_size = get_file_kb(input_path) print(f"\n原图: {os.path.basename(input_path)} | 大小: {ori_size:.2f} KB") - # 小于阈值,直接复制原图,不压缩 - if ori_size < min_size_kb: - shutil.copy2(input_path, output_path) - print(f"✅ 小于{min_size_kb}KB,无需压缩,直接复制") + # INIT_QUALITY=None:不压缩、不缩放;目标格式不同则只做格式转换。 + # 小于阈值:同样不压缩、不缩放;目标格式不同则只做格式转换。 + if init_quality is None or ori_size < min_size_kb: + if os.path.splitext(input_path)[1].lower().lstrip(".") == output_format: + shutil.copy2(input_path, output_path) + print(f"✅ 小于{min_size_kb}KB,无需压缩,直接复制") + else: + img = Image.open(input_path) + img = _prepare_image(img, output_format) + _save_image(img, output_path, output_format, init_quality) + reason = "INIT_QUALITY=None" if init_quality is None else f"小于{min_size_kb}KB" + print(f"✅ {reason},不压缩,仅转换为 {output_format}") return - # 打开图片,处理透明通道 img = Image.open(input_path) - if img.mode in ("RGBA", "P"): - new_img = Image.new("RGB", img.size, (255, 255, 255)) - mask = img.split()[-1] if img.mode == "RGBA" else None - new_img.paste(img, mask=mask) - img = new_img - - # 等比例缩放 + img = _prepare_image(img, output_format) if max_width and max_height: img.thumbnail((max_width, max_height)) - current_quality = init_quality + # PNG 没有 quality 参数,直接用最高无损压缩保存。 + if output_format == "png": + _save_image(img, output_path, output_format, init_quality) + print(f"✅ PNG 已保存 | 输出大小: {get_file_kb(output_path):.2f} KB") + return + + quality = init_quality while True: - img.save(output_path, format="JPEG", quality=current_quality, optimize=True) + _save_image(img, output_path, output_format, quality) out_size = get_file_kb(output_path) - - if out_size <= max_size_kb or current_quality <= min_quality: - print(f"✅ 最终画质 {current_quality} | 输出大小: {out_size:.2f} KB") + if out_size <= max_size_kb or quality <= min_quality: + print(f"✅ 最终画质 {quality} | 输出大小: {out_size:.2f} KB") break - - current_quality -= quality_step - print(f"⚠️ {out_size:.2f}KB > {max_size_kb}KB,降低画质至 {current_quality} 重新压缩") - + quality = max(min_quality, quality - quality_step) + print(f"⚠️ {out_size:.2f}KB > {max_size_kb}KB,降低画质至 {quality} 重新压缩") except Exception as e: print(f"❌ 处理失败 {input_path}:{str(e)}") -def batch_compress_folder( - input_dir: str, - output_dir: str, - min_kb, - max_kb, - init_q, - q_step, - min_q, - max_w, - max_h -): +def _prepare_image(img, output_format): + has_alpha = "A" in img.getbands() or (img.mode == "P" and "transparency" in img.info) + if output_format in ("jpg", "jpeg"): + if has_alpha: + rgba = img.convert("RGBA") + result = Image.new("RGB", rgba.size, (255, 255, 255)) + result.paste(rgba, mask=rgba.getchannel("A")) + return result + return img.convert("RGB") + return img.convert("RGBA" if has_alpha else "RGB") + + +def batch_compress_folder(input_dir, output_dir, min_kb, max_kb, init_q, + q_step, min_q, max_w, max_h, output_format): os.makedirs(output_dir, exist_ok=True) support_ext = (".png", ".jpg", ".jpeg", ".webp", ".bmp") - for filename in os.listdir(input_dir): file_path = os.path.join(input_dir, filename) - if not filename.lower().endswith(support_ext): + if os.path.isdir(file_path) or not filename.lower().endswith(support_ext): continue - if os.path.isdir(file_path): - continue - name_no_ext = os.path.splitext(filename)[0] - out_file = os.path.join(output_dir, f"{name_no_ext}.jpg") - compress_to_jpg_loop( - input_path=file_path, - output_path=out_file, - min_size_kb=min_kb, - max_size_kb=max_kb, - init_quality=init_q, - quality_step=q_step, - min_quality=min_q, - max_width=max_w, - max_height=max_h - ) + suffix = output_format or os.path.splitext(filename)[1].lstrip(".") + out_file = os.path.join(output_dir, f"{name_no_ext}.{suffix}") + compress_to_format_loop(file_path, out_file, min_kb, max_kb, init_q, + q_step, min_q, max_w, max_h, output_format) if __name__ == "__main__": - # ==================== 批量配置区(仅需修改这里) ==================== + # 输入输出文件夹 INPUT_FOLDER = "./input" OUTPUT_FOLDER = "./output" - MIN_SIZE_KB = 200 # 小于该KB不压缩 - MAX_SIZE_KB = 800 # 超过该KB循环再压 - INIT_QUALITY = 75 # 初始画质 - QUALITY_STEP = 8 # 每次降低画质数值 - MIN_QUALITY = 20 # 最低画质底线 - LIMIT_W = 2560 # 不需要缩放填 None + # 首次压缩质量,不压缩填 None + INIT_QUALITY = 75 + # 过小不压缩,过大重复压缩 + MIN_SIZE_KB = 100 + MAX_SIZE_KB = 500 + # 重复压缩质量递减及最小值 + QUALITY_STEP = 8 + MIN_QUALITY = 20 + # 更换格式,不转换格式填 None + OUTPUT_FORMAT = "webp" + # 尺寸压缩,不缩放填 None + LIMIT_W = 2560 LIMIT_H = 1440 - # ================================================================== - # 仅批量执行,无任何单张代码 - batch_compress_folder( - input_dir=INPUT_FOLDER, - output_dir=OUTPUT_FOLDER, - min_kb=MIN_SIZE_KB, - max_kb=MAX_SIZE_KB, - init_q=INIT_QUALITY, - q_step=QUALITY_STEP, - min_q=MIN_QUALITY, - max_w=LIMIT_W, - max_h=LIMIT_H - ) + batch_compress_folder(INPUT_FOLDER, OUTPUT_FOLDER, MIN_SIZE_KB, MAX_SIZE_KB, + INIT_QUALITY, QUALITY_STEP, MIN_QUALITY, LIMIT_W, + LIMIT_H, OUTPUT_FORMAT)