This commit is contained in:
sjh00
2025-06-03 20:12:36 +08:00
parent bdb14227e0
commit 1198e67a0a
2 changed files with 110 additions and 94 deletions
+10 -12
View File
@@ -20,10 +20,9 @@
该节点可以保存图像并保留原始图像的元数据:
- 支持保存为原始格式或指定格式(avif、webp、jpg、png)
- 支持保存为原始格式或指定格式(avif、webp、jpg、png、tiff)
- 保留原始图像的DPI信息
- 保留原始图像的EXIF数据(对支持EXIF的格式)
- 支持批量图像保存
## 安装
@@ -49,15 +48,15 @@ git clone https://github.com/sjh00/ComfyUI-LoadImageWithInfo.git
### SaveImageWithInfo 节点
1. 在ComfyUI工作流中,找到`image`分类下的`Save Image With Info`节点
2. 将要保存的图像连接到节点的`images`输入
2. 将要保存的图像连接到节点的`image`输入
3. 设置以下参数:
- `filename`:保存的文件名(不含扩展名)
- `format`:保存格式(original、avif、webp、jpg、png)
- `format`:保存格式(original、avif、webp、jpg、png、tiff)
- `original_format`:原始格式(当format设为original时使用)
- `quality`:保存质量(1-100,默认100,png格式换算为0-9压缩率)
- `quality`:保存质量(1-100,AVIF默认60,其他默认90,png/tiff格式不受影响)
- `dpi`:图像DPI值
- `exif`:EXIF元数据(JSON格式字符串)
4. 节点将保存图像并返回保存后的图像和文件名
4. 节点将保存图像
## 示例输出
@@ -100,9 +99,9 @@ LoadImageWithInfo:
- exif -> SaveImageWithInfo.exif
SaveImageWithInfo:
- images <- LoadImageWithInfo.image
- image <- LoadImageWithInfo.image
- filename <- LoadImageWithInfo.filename
- format -> 选择保存格式(original/avif/webp/jpg/png)
- format -> 选择保存格式(original/avif/webp/jpg/png/tiff)
- original_format <- LoadImageWithInfo.format
- quality -> 设置保存质量(1-100)
- dpi <- LoadImageWithInfo.dpi
@@ -121,14 +120,13 @@ SaveImageWithInfo:
### SaveImageWithInfo 节点
- 并非所有图像格式都支持EXIF数据,目前只有JPG和TIFF格式支持保存EXIF数据
- 并非所有图像格式都支持EXIF数据,目前只有JPG格式支持保存EXIF数据
- 如果选择的保存格式不支持EXIF,EXIF数据将被忽略
- 当保存多张图像时(批处理),除第一张图像外,其他图像的文件名会自动添加索引号
- 如果指定的格式无法保存,将自动回退到PNG格式
- quality参数对不同格式的影响:
- JPG/JPEG:直接影响图像质量(1-100,值越高质量越好)
- PNG:影响压缩级别(quality值会被映射到PNG的compress_level,quality值越高质量越好)
- WEBP/AVIF:影响图像质量(1-100,值越高质量越好)
- PNG/TIFF:不受影响
- WEBP/AVIF:影响图像质量(1-100,值越高质量越好,AVIF 60 is same quality as WebP 90)
- 其他格式:如果格式支持quality参数则使用,否则忽略
## 许可证
+100 -82
View File
@@ -1,6 +1,8 @@
import hashlib
import os
import json
from PIL import Image, ImageOps, ImageSequence, ExifTags
from PIL.PngImagePlugin import PngInfo
import pillow_avif
import numpy as np
import torch
@@ -11,8 +13,8 @@ class LoadImageWithInfo:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = folder_paths.filter_files_content_types(files, ["image"])
img_exts = [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".PNG", ".JPG", ".JPEG", ".WEBP", ".BMP", ".avif", ".AVIF", ".tif", ".tiff", ".TIF", ".TIFF"]
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and os.path.splitext(f)[1] in img_exts]
return {"required":
{"image": (sorted(files), {"image_upload": True})},
}
@@ -39,7 +41,7 @@ class LoadImageWithInfo:
try:
dpi = img.info.get('dpi', (96, 96))[0]
except:
dpi = 96
dpi = 0
# 获取EXIF信息
exif_data = {}
@@ -130,24 +132,33 @@ class SaveImageWithInfo:
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"image": ("IMAGE",),
"filename": ("STRING", {"default": "image"}),
"format": (["original", "avif", "webp", "jpg", "png"], {"default": "original"}),
"format": (["original", "avif", "webp", "jpg", "png", "tiff"], {"default": "original"}),
"original_format": ("STRING", {"default": "png"}),
"quality": ("INT", {"default": 100, "min": 1, "max": 100}),
"quality": ("INT", {"default": 90, "min": 1, "max": 100, "step": 1, "display": "silder", 'tooltip': "Quality for JPEG/WebP/AVIF formats; Quality is relative to each format. \n* Example: AVIF 60 is same quality as WebP 90. \n* PNG compression is fixed at 4 and not affected by this. PNG compression times skyrocket above level 4 for zero benefits on filesize."}),
"dpi": ("INT", {"default": 96}),
"exif": ("STRING", {"default": "{}"}),
'image_preview': ('BOOLEAN', {'default': True, 'tooltip': "Turns the image preview on and off"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
CATEGORY = "image"
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("images", "filename")
RETURN_TYPES = ()
FUNCTION = "save_image"
OUTPUT_NODE = True
def save_image(self, images, filename, format, original_format, quality, dpi, exif, prompt=None, extra_pnginfo=None):
CATEGORY = "image"
DESCRIPTION = "Saves the input image with to your ComfyUI output directory."
type = 'output'
quality = 90
quality_avif = 60
# optimize_image only works for jpeg, png and TIFF, with like just 2% reduction in size; not used for PNG as it forces a level 9 compression.
optimize_image = True
def save_image(self, image, filename, format, original_format, quality, dpi, exif, prompt=None, extra_pnginfo=None):
# 确定保存格式
save_format = original_format if format == "original" else format
@@ -167,7 +178,6 @@ class SaveImageWithInfo:
try:
if exif and exif != "{}":
if isinstance(exif, str):
import json
exif_data = json.loads(exif)
else:
exif_data = exif
@@ -177,80 +187,88 @@ class SaveImageWithInfo:
exif_data = {}
# 保存图像
results = []
for i, image in enumerate(images):
img = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
# 设置DPI
if dpi > 0:
img.info['dpi'] = (dpi, dpi)
# 构建单个图像的文件名
if i > 0:
current_filename = f"{filename}_{i}.{save_format}"
current_path = os.path.join(output_dir, current_filename)
else:
current_filename = full_filename
current_path = full_path
# 根据格式保存图像
if save_format.lower() == 'png':
# PNG的compress_level范围是0-9,将quality(1-100)映射到compress_level(0-9)
# 注意:对于PNG,较低的compress_level意味着较低的压缩率和较高的质量
compress_level = max(0, min(9, 9 - int(quality / 11)))
img.save(current_path, format='PNG', compress_level=compress_level, pnginfo=None)
elif save_format.lower() in ['jpg', 'jpeg']:
# 添加EXIF数据
if exif_data:
exif_bytes = img.getexif()
for k, v in exif_data.items():
try:
# 尝试找到EXIF标签的数字ID
tag_id = None
for tag, tag_name in ExifTags.TAGS.items():
if tag_name == k:
tag_id = tag
break
if tag_id:
exif_bytes[tag_id] = v
else:
# 如果找不到标签ID,尝试直接使用键名
exif_bytes[k] = v
except:
pass
img.save(current_path, format='JPEG', quality=quality, exif=exif_bytes)
img.save(current_path, format='JPEG', quality=quality)
img = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
# 设置DPI
if dpi > 0:
img.info['dpi'] = (dpi, dpi)
kwargs = dict()
# 根据格式保存图像
if save_format.lower() == 'png':
kwargs['compress_level'] = 4
kwargs["pnginfo"] = self.genMetadataPng(prompt, extra_pnginfo)
else:
kwargs["optimize"] = self.optimize_image
if save_format.lower() == 'avif':
if quality == 100:
kwargs["lossless"] = True
else:
if quality == 0 or quality > self.quality_avif:
quality = self.quality_avif
kwargs['quality'] = quality
elif save_format.lower() == 'webp':
img.save(current_path, format='WEBP', quality=quality)
elif save_format.lower() == 'avif':
img.save(current_path, format='AVIF', quality=quality)
else:
# 尝试使用原始格式保存
try:
# 尝试使用quality参数,如果格式不支持则忽略
try:
img.save(current_path, format=save_format.upper(), quality=quality)
except TypeError:
img.save(current_path, format=save_format.upper())
except:
# 如果失败,默认保存为PNG
current_filename = f"{filename}_{i if i > 0 else ''}.png"
current_path = os.path.join(output_dir, current_filename)
compress_level = max(0, min(9, 9 - int(quality / 11)))
img.save(current_path, format='PNG', compress_level=compress_level)
results.append({
'filename': current_filename,
'path': current_path
})
if quality == 100:
kwargs["lossless"] = True
else:
if quality == 0:
quality = self.quality
kwargs['quality'] = quality
elif save_format.lower() != 'tiff':
if quality == 0:
quality = self.quality
kwargs['quality'] = quality
if save_format.lower() in ['jpg', 'jpeg']:
# 添加EXIF数据
if exif_data:
exif_bytes = img.getexif()
for k, v in exif_data.items():
try:
# 尝试找到EXIF标签的数字ID
tag_id = None
for tag, tag_name in ExifTags.TAGS.items():
if tag_name == k:
tag_id = tag
break
if tag_id:
exif_bytes[tag_id] = v
else:
# 如果找不到标签ID,尝试直接使用键名
exif_bytes[k] = v
except:
pass
kwargs['exif'] = exif_bytes
kwargs["subsampling"] = 0
else:
# 默认保存为PNG
kwargs['compress_level'] = 4
kwargs["pnginfo"] = self.genMetadataPng(prompt, extra_pnginfo)
full_filename = os.path.splitext(full_filename)[0] + '.png'
full_path = os.path.splitext(full_path)[0] + '.png'
# 添加到ComfyUI的保存图像列表中
for result in results:
folder_paths.add_to_output_list(result['path'])
img.save(full_path, **kwargs)
return (images, full_filename)
results = [{
'filename': full_filename,
'path': full_path,
'type': self.type
}]
return { "ui": { "images": results } }
def genMetadataPng(self, prompt, extra_pnginfo=None):
metadata = PngInfo()
if prompt is not None:
metadata.add_text('prompt', json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
return metadata
# 注册节点
NODE_CLASS_MAPPINGS = {