Skip to content

Instantly share code, notes, and snippets.

@t0saki
Created September 8, 2025 03:01
Show Gist options
  • Select an option

  • Save t0saki/e1a8aa79ba0e98d724b4e2fa8b4005ec to your computer and use it in GitHub Desktop.

Select an option

Save t0saki/e1a8aa79ba0e98d724b4e2fa8b4005ec to your computer and use it in GitHub Desktop.
Image Codec Comparison
# -*- coding: utf-8 -*-
import subprocess
import os
import sys
from PIL import Image, ImageDraw, ImageFont
# 尝试导入pillow-heif库,用于直接读取HEIC/AVIF格式
try:
import pillow_heif
# 注册HEIF/AVIF格式的解码器
pillow_heif.register_heif_opener()
except ImportError:
print("警告: pillow-heif 未安装。脚本将无法直接打开HEIC/AVIF作为输入文件。")
print("请运行: pip install pillow-heif")
# --- 可配置参数 ---
# 定义输出格式和对应的ImageMagick质量参数
FORMATS_CONFIG = {
'JPG': {'ext': 'jpg', 'quality': '13'},
'WebP': {'ext': 'webp', 'quality': '9'},
'HEIF': {'ext': 'heic', 'quality': '27'},
'AVIF': {'ext': 'avif', 'quality': '29'}
}
# 定义要裁剪和放大的局部区域 (左, 上, 右, 下)
CROP_BOX = None
# CROP_BOX = (500, 300, 756, 556) # 示例:手动指定一个 256x256 的区域
# 对比图中的间距设置
PADDING = 20
# [修改] 移除了全局的FONT定义,因为它将在函数内根据图片大小动态加载。
# --- 脚本核心功能 ---
def check_imagemagick():
"""检查ImageMagick是否安装并可用"""
try:
result = subprocess.run(
['magick', '-version'], capture_output=True, text=True, check=True, encoding='utf-8')
print("ImageMagick 已安装并找到。")
if 'heic' not in result.stdout:
print("警告: 您的 ImageMagick 可能不支持 HEIF (heic) 格式转换。")
if 'avif' not in result.stdout:
print("警告: 您的 ImageMagick 可能不支持 AVIF 格式转换。")
return True
except (subprocess.CalledProcessError, FileNotFoundError):
print("错误: ImageMagick 未安装或未在系统路径中。")
print("请访问 https://imagemagick.org/script/download.php 下载并安装。")
return False
def convert_image(source_path, temp_dir):
"""使用ImageMagick将源图片转换为多种格式"""
converted_files = {}
base_name = os.path.splitext(os.path.basename(source_path))[0]
print("\n--- 开始转换图片 ---")
for fmt, config in FORMATS_CONFIG.items():
ext = config['ext']
quality = config['quality']
output_path = os.path.join(temp_dir, f"{base_name}_{quality}.{ext}")
print(f"正在转换到 {fmt} (quality: {quality})...")
command = [
'magick',
source_path,
'-resize', '3200x2400>', # 限制最大尺寸为3200x2400
'-quality', str(quality),
output_path
]
try:
subprocess.run(command, check=True, capture_output=True, text=True)
file_size = os.path.getsize(output_path) / 1024
converted_files[fmt] = {'path': output_path, 'size': file_size}
print(f" -> 成功: {output_path} ({file_size:.1f} KB)")
except subprocess.CalledProcessError as e:
print(f" !! 转换到 {fmt} 失败。")
print(f" 错误信息: {e.stderr}")
converted_files[fmt] = None
return converted_files
def create_comparison_image(original_path, converted_files, output_path):
"""创建并排对比图,并在每个图的右下角放置一个放大的局部视图"""
print("\n--- 开始生成对比图 ---")
try:
original_img = Image.open(original_path).convert('RGBA').resize((3200, 2400), Image.Resampling.LANCZOS)
except FileNotFoundError:
print(f"错误: 找不到原始图片 {original_path}")
return
images = {'Original HEIF': original_img}
for fmt, data in converted_files.items():
if data:
try:
images[fmt] = Image.open(data['path']).convert('RGBA')
except FileNotFoundError:
print(f"警告: 找不到转换后的文件 {data['path']},将跳过。")
# --- 准备布局和尺寸 ---
img_width, img_height = original_img.size
num_images = len(images)
# --- [新增] 动态计算字体、边框和标题区域大小 ---
# 根据图片高度计算一个合适的字体大小,最小为32px
font_size = max(32, int(img_height * 0.06))
try:
# font_name = "arial.ttf"
font_name = "/Library/Fonts/Arial.ttf" if os.name == 'posix' else "arial.ttf"
font = ImageFont.truetype(font_name, font_size)
except IOError as e:
print(f"警告: '{font_name}' 字体未找到,将使用Pillow的默认字体,文字可能非常小: {e}")
font = ImageFont.load_default(size=font_size)
# 根据字体大小动态计算顶部标题区的高度(约2.5行文字的高度)
header_height = int(font_size * 2.5)
# 根据图片宽度计算红色预览框的边框粗细
crop_box_outline_width = max(4, int(img_width / 200))
# 根据主边框粗细计算放大图的边框粗细
overlay_border_outer = max(2, int(crop_box_outline_width / 8))
overlay_border_inner = max(4, int(crop_box_outline_width / 4))
# --- [新增] 动态计算结束 ---
# --- 定义裁剪区域 ---
global CROP_BOX
if CROP_BOX is None:
crop_w, crop_h = 256, 256
center_x, center_y = img_width / 2, img_height / 2
left = max(0, center_x - crop_w / 2)
top = max(0, center_y - crop_h / 2)
right = min(img_width, center_x + crop_w / 2)
bottom = min(img_height, center_y + crop_h / 2)
CROP_BOX = (int(left), int(top), int(right), int(bottom))
# --- 定义右下角放大预览图的尺寸 ---
crop_w = CROP_BOX[2] - CROP_BOX[0]
crop_h = CROP_BOX[3] - CROP_BOX[1]
aspect_ratio = crop_h / crop_w if crop_w > 0 else 1
overlay_w = int(img_width * 0.45)
overlay_h = int(overlay_w * aspect_ratio)
# --- 计算画布总尺寸 ---
# [修改] 使用动态计算出的 header_height
total_width = (img_width * num_images) + (PADDING * (num_images + 1))
total_height = PADDING + header_height + img_height + PADDING
canvas = Image.new('RGBA', (total_width, total_height), 'white')
draw = ImageDraw.Draw(canvas)
# --- 绘制图片、标签和放大预览 ---
current_x = PADDING
for i, (label, img) in enumerate(images.items()):
# 步骤1: 粘贴图片
# [修改] 使用动态计算出的 header_height
canvas.paste(img, (current_x, PADDING + header_height))
# 步骤2: 准备并绘制标题文字
if label == 'Original HEIF':
size_kb = os.path.getsize(original_path) / 1024
text = f"Original HEIF\n({size_kb:.1f} KB)"
else:
size_kb = converted_files[label]['size']
text = f"{label} (Q:{FORMATS_CONFIG[label]['quality']})\n({size_kb:.1f} KB)"
# [修改] 使用动态加载的 font 对象
text_bbox = draw.textbbox((0, 0), text, font=font)
text_w = text_bbox[2] - text_bbox[0]
draw.text(
(current_x + (img_width - text_w) / 2, PADDING),
text, fill='black', font=font
)
# 步骤3: 在主图上绘制红色的裁剪框
# [修改] 使用动态计算出的 header_height 和边框宽度
box_on_canvas = (
CROP_BOX[0] + current_x,
CROP_BOX[1] + PADDING + header_height,
CROP_BOX[2] + current_x,
CROP_BOX[3] + PADDING + header_height
)
draw.rectangle(box_on_canvas, outline="red",
width=crop_box_outline_width)
# 步骤4: 创建并粘贴右下角的放大预览图
cropped_img = img.crop(CROP_BOX)
zoomed_overlay = cropped_img.resize(
(overlay_w, overlay_h), Image.Resampling.NEAREST)
overlay_margin = 5
# [修改] 使用动态计算出的 header_height
overlay_x = (current_x + img_width) - overlay_w - overlay_margin
overlay_y = (PADDING + header_height + img_height) - \
overlay_h - overlay_margin
canvas.paste(zoomed_overlay, (overlay_x, overlay_y))
# [修改] 使用动态计算出的边框宽度
draw.rectangle(
(overlay_x, overlay_y, overlay_x + overlay_w, overlay_y + overlay_h),
outline="white", width=overlay_border_inner
)
draw.rectangle(
(overlay_x - 2, overlay_y - 2, overlay_x +
overlay_w + 2, overlay_y + overlay_h + 2),
outline="black", width=overlay_border_outer
)
current_x += img_width + PADDING
# 步骤5: 保存最终结果
final_output_path = output_path
if canvas.mode == 'RGBA':
if not final_output_path.lower().endswith('.png'):
final_output_path = os.path.splitext(output_path)[0] + ".png"
canvas.save(final_output_path, 'PNG')
else:
if not final_output_path.lower().endswith(('.jpg', '.jpeg')):
final_output_path = os.path.splitext(output_path)[0] + ".jpg"
canvas.convert('RGB').save(final_output_path, 'JPEG', quality=95)
print(f"\n对比图已生成: {final_output_path}")
def main():
"""主函数,处理命令行参数和程序流程"""
if not check_imagemagick():
sys.exit(1)
if len(sys.argv) < 2:
print(f"用法: python {os.path.basename(__file__)} <图片路径> [输出路径]")
sys.exit(1)
source_path = sys.argv[1]
if not os.path.exists(source_path):
print(f"错误: 输入文件不存在 '{source_path}'")
sys.exit(1)
base_name = os.path.splitext(os.path.basename(source_path))[0]
if len(sys.argv) > 2:
output_path = sys.argv[2]
else:
output_path = f"{base_name}_comparison.png"
temp_dir = "temp_converted_images"
os.makedirs(temp_dir, exist_ok=True)
try:
converted_files = convert_image(source_path, temp_dir)
create_comparison_image(source_path, converted_files, output_path)
finally:
print("\n正在清理临时文件...")
try:
for root, dirs, files in os.walk(temp_dir):
for file in files:
os.remove(os.path.join(root, file))
os.rmdir(temp_dir)
print("清理完成。")
except OSError as e:
print(f"清理临时文件时出错: {e}")
if __name__ == '__main__':
main()
https://i.tsk.im/file/ahY2BjFW.heic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment