Created
September 8, 2025 03:01
-
-
Save t0saki/e1a8aa79ba0e98d724b4e2fa8b4005ec to your computer and use it in GitHub Desktop.
Image Codec Comparison
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # -*- 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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