- 新增 Grok Image 节点及客户端 - 新增 save_image_format 节点 - 新增前端 JS 扩展:画笔工具、点阵网格、侧边栏隐藏、资源切换、重命名等 - 重构 nano-banana 节点,移除 pro 版本 - 移除 multi_res_preview 节点 - 新增 http_error 工具模块 - 各客户端和节点优化改进 Co-Authored-By: Claude Opus 4.6 <[email protected]>
189 lines
5.7 KiB
Python
189 lines
5.7 KiB
Python
"""
|
||
图像元数据去除节点
|
||
|
||
提供批量去除已有图片中元数据的功能
|
||
"""
|
||
|
||
import os
|
||
|
||
from PIL import Image
|
||
from PIL.PngImagePlugin import PngInfo
|
||
|
||
# 支持的图片格式
|
||
SUPPORTED_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif'}
|
||
|
||
|
||
def _save_image_clean(image: Image.Image, path: str, fmt: str = None, quality: int = 95) -> None:
|
||
"""
|
||
保存图像,不包含任何元数据
|
||
|
||
通过提取纯像素数据并重建全新的 Image 对象,确保没有任何元数据残留。
|
||
|
||
Args:
|
||
image: PIL Image 对象
|
||
path: 保存路径
|
||
fmt: 图像格式(PNG/JPEG/WEBP),为 None 时根据扩展名推断
|
||
quality: JPEG/WEBP 质量(1-100)
|
||
"""
|
||
# 确保 RGB 模式
|
||
if image.mode != 'RGB':
|
||
image = image.convert('RGB')
|
||
|
||
# 提取纯像素数据,重建全新的 Image 对象
|
||
# 使用 tobytes() + frombytes() 确保只保留像素数据,彻底断开与原图像的关联
|
||
pixel_data = image.tobytes()
|
||
clean = Image.frombytes('RGB', image.size, pixel_data)
|
||
|
||
# 显式清空 info 字典,确保不会有任何残留元数据
|
||
clean.info = {}
|
||
|
||
# 推断格式
|
||
if fmt is None:
|
||
ext = os.path.splitext(path)[1].lower()
|
||
format_map = {
|
||
'.png': 'PNG',
|
||
'.jpg': 'JPEG',
|
||
'.jpeg': 'JPEG',
|
||
'.webp': 'WEBP',
|
||
'.bmp': 'BMP',
|
||
'.tiff': 'TIFF',
|
||
'.tif': 'TIFF',
|
||
}
|
||
fmt = format_map.get(ext, 'PNG')
|
||
|
||
# 构建保存参数(确保不写入任何元数据)
|
||
save_kwargs = {}
|
||
if fmt == 'PNG':
|
||
save_kwargs['pnginfo'] = PngInfo() # 空的 PngInfo,不包含任何文本块
|
||
elif fmt == 'JPEG':
|
||
save_kwargs['quality'] = quality
|
||
# 不传 exif 参数,自然不会写入 EXIF 数据
|
||
elif fmt == 'WEBP':
|
||
save_kwargs['quality'] = quality
|
||
save_kwargs['exif'] = b"" # 显式清空 EXIF
|
||
|
||
clean.save(path, format=fmt, **save_kwargs)
|
||
|
||
|
||
# ============================================================================
|
||
# 批量去除元数据
|
||
# ============================================================================
|
||
|
||
class BatchCleanMetadata:
|
||
"""
|
||
批量去除文件夹中图片元数据的节点
|
||
|
||
功能:
|
||
- 指定文件夹路径,批量处理其中所有图片
|
||
- 去除 EXIF、PNG tEXt 块、ComfyUI 工作流等所有元数据
|
||
- 支持保存到原目录(添加 _nometa 后缀)或覆盖原文件
|
||
- 支持 PNG/JPG/JPEG/WEBP/BMP/TIFF 格式
|
||
|
||
使用场景:
|
||
- 已经保存了一批含有 AI 元数据的图片,需要批量清理
|
||
- 批量处理指定文件夹中的所有图片
|
||
"""
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
"""
|
||
定义输入参数
|
||
|
||
Returns:
|
||
输入参数配置字典
|
||
"""
|
||
return {
|
||
"required": {
|
||
"文件夹路径": ("STRING", {"default": ""}),
|
||
"覆盖原文件": ("BOOLEAN", {"default": False}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING",)
|
||
RETURN_NAMES = ("处理结果",)
|
||
OUTPUT_NODE = True
|
||
FUNCTION = "batch_clean"
|
||
CATEGORY = "image"
|
||
|
||
DESCRIPTION = (
|
||
"批量去除文件夹中图片的元数据。\n"
|
||
"支持 PNG/JPG/JPEG/WEBP/BMP/TIFF 格式。\n"
|
||
"默认在原文件名后添加 _nometa 后缀保存,也可选择覆盖原文件。"
|
||
)
|
||
|
||
def batch_clean(
|
||
self,
|
||
文件夹路径: str,
|
||
覆盖原文件: bool = False,
|
||
) -> tuple:
|
||
"""
|
||
批量去除文件夹中图片的元数据
|
||
|
||
Args:
|
||
文件夹路径: 待处理图片所在的文件夹路径
|
||
覆盖原文件: 是否覆盖原文件(False 则添加 _nometa 后缀)
|
||
|
||
Returns:
|
||
处理结果字符串
|
||
|
||
Raises:
|
||
ValueError: 文件夹路径无效
|
||
"""
|
||
if not 文件夹路径 or not 文件夹路径.strip():
|
||
raise ValueError("请输入文件夹路径")
|
||
|
||
folder = 文件夹路径.strip()
|
||
|
||
if not os.path.isdir(folder):
|
||
raise ValueError(f"文件夹路径无效或不存在: {folder}")
|
||
|
||
# 扫描支持的图片文件
|
||
files = []
|
||
for f in sorted(os.listdir(folder)):
|
||
ext = os.path.splitext(f)[1].lower()
|
||
if ext in SUPPORTED_EXTENSIONS:
|
||
files.append(f)
|
||
|
||
if not files:
|
||
msg = f"文件夹中未找到支持的图片文件 ({', '.join(SUPPORTED_EXTENSIONS)})"
|
||
print(f"批量去除元数据: {msg}")
|
||
return (msg,)
|
||
|
||
print(f"批量去除元数据: 找到 {len(files)} 张图片,开始处理...")
|
||
|
||
success_count = 0
|
||
fail_count = 0
|
||
|
||
for f in files:
|
||
try:
|
||
src_path = os.path.join(folder, f)
|
||
img = Image.open(src_path)
|
||
|
||
if 覆盖原文件:
|
||
dst_path = src_path
|
||
else:
|
||
name, ext = os.path.splitext(f)
|
||
dst_path = os.path.join(folder, f"{name}_nometa{ext}")
|
||
|
||
_save_image_clean(img, dst_path)
|
||
success_count += 1
|
||
|
||
except Exception as e:
|
||
print(f"批量去除元数据: 处理 {f} 失败 - {str(e)}")
|
||
fail_count += 1
|
||
|
||
# 构建结果消息
|
||
if fail_count > 0:
|
||
msg = f"处理完成: 成功 {success_count} 张, 失败 {fail_count} 张"
|
||
else:
|
||
msg = f"处理完成: 全部 {success_count} 张成功"
|
||
|
||
if not 覆盖原文件:
|
||
msg += " (已添加 _nometa 后缀)"
|
||
else:
|
||
msg += " (已覆盖原文件)"
|
||
|
||
print(f"批量去除元数据: {msg}")
|
||
|
||
return (msg,)
|