From 705bfe12e9f646fad724cfc57e9bdb9e100f92b6 Mon Sep 17 00:00:00 2001 From: kk8bit Date: Sun, 20 Oct 2024 00:33:07 +0800 Subject: [PATCH] v0.0.1 --- CHANGELOG.md | 12 +++ README.md | 24 ++++++ __init__.py | 3 + custom_save_image.py | 156 +++++++++++++++++++++++++++++++++++++ resources/AdobeRGB1998.icc | Bin 0 -> 560 bytes resources/sRGB Profile.icc | Bin 0 -> 3144 bytes 6 files changed, 195 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 __init__.py create mode 100644 custom_save_image.py create mode 100644 resources/AdobeRGB1998.icc create mode 100644 resources/sRGB Profile.icc diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e2f7218 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## [0.0.1] - 2024-10-19 +### Added +- 实现自定义保存图像节点。 +- 支持保存为 PNG 和 JPG 格式。 +- 添加 JPG 质量调整选项。 +- 支持嵌入作者和版权信息。 +- 支持选择 sRGB IEC61966-2.1 和 Adobe RGB (1998) 颜色配置文件。 +- 当选择 Adobe RGB 时,自动将图像转换为 Adobe RGB并保证色彩准确。 +- 支持保存元数据到 PNG 文件,包括 `prompt` 和 `extra_pnginfo`。 +- 自动生成唯一文件名。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f159d5e --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# KayTool + +这是一个为 ComfyUI 开发的自定义节点实用工具包,在未来我会陆续为它增加功能 + +## 当前功能: + +- 选择保存图片的格式(PNG 或 JPG) +- 选择是否保存元数据(工作流信息) +- 支持两种颜色配置文件(sRGB IEC61966-2.1 和 Adobe RGB (1998)) +- 当选择 Adobe RGB 时,自动将图像转换为 Adobe RGB并保证色彩准确 +- 自定义 JPG 图片质量(0-100) +- 可以嵌入作者和版权信息 +- 自动生成唯一文件名 +- 在默认的`output`文件夹下,自动创建`custom_save_images`文件夹,以保存所有的图片 + +## 一些说明 + +- 要保存”JPG“格式,必须将"metadata“设置为"False" +- 当选择保存"metadata“(工作流信息)时,将自动切换为"PNG"格式,并忽略"JPG"格式的设置 +- 如果你需要更小容量但不损失肉眼可见的画质,可以讲"JPG"图片质量设置为 40,更低的数值将肉眼可见色块 + +## 安装与使用 + +- 将本项目克隆到你的 `ComfyUI/custom_nodes`目录下,并确保将 sRGB Profile.icc 和AdobeRGB1998.icc 文件放在 resources 目录中 \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ea8d026 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .custom_save_image import register_custom_node as register_save_node + +NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_save_node() \ No newline at end of file diff --git a/custom_save_image.py b/custom_save_image.py new file mode 100644 index 0000000..4c79dae --- /dev/null +++ b/custom_save_image.py @@ -0,0 +1,156 @@ +import os +import json +import numpy as np +from PIL import Image +from PIL.PngImagePlugin import PngInfo +from PIL import ExifTags, TiffImagePlugin, ImageCms + +class CustomSaveImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "filename_prefix": ("STRING", {"default": "Custom_Save_Image"}), + "save_metadata": ("BOOLEAN", {"default": True}), + "color_profile": (["sRGB IEC61966-2.1", "Adobe RGB (1998)"], {"default": "sRGB IEC61966-2.1"}), # 将 color_profile 移到 save_metadata 下面 + "format": (["PNG", "JPG"], {"default": "PNG"}), + "jpg_quality": ("INT", {"default": 95, "min": 0, "max": 100}), + "author": ("STRING", {"default": ""}), + "copyright_info": ("STRING", {"default": ""}) + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "save_images" + + OUTPUT_NODE = True + + CATEGORY = "KayTool" + + def save_images(self, images, filename_prefix="Custom_Save_Image", save_metadata=True, format="PNG", jpg_quality=95, author="", copyright_info="", color_profile="sRGB IEC61966-2.1", prompt=None, extra_pnginfo=None): + output_dir = self.get_output_directory() + os.makedirs(output_dir, exist_ok=True) # 确保目录存在 + + results = [] # 存储图片保存路径以供UI预览使用 + + # 动态获取 sRGB 和 AdobeRGB 的 ICC profile 路径 + srgb_profile_path = os.path.join(os.path.dirname(__file__), 'resources', 'sRGB Profile.icc') + adobergb_profile_path = os.path.join(os.path.dirname(__file__), 'resources', 'AdobeRGB1998.icc') + + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + # 处理颜色配置文件 + if color_profile == "Adobe RGB (1998)": + img = self.convert_to_adobe_rgb(img, srgb_profile_path, adobergb_profile_path) + icc_profile_path = adobergb_profile_path + else: + icc_profile_path = srgb_profile_path + + # 加载选择的 ICC profile + icc_profile = self.load_icc_profile(icc_profile_path) + + # 如果 save_metadata 为 True,默认保存为 PNG + if save_metadata: + metadata = PngInfo() + + # 无论 save_metadata,始终嵌入作者和版权信息 + if author: + metadata.add_text("Author", author) + if copyright_info: + metadata.add_text("Copyright", copyright_info) + + # 嵌入额外元数据 + if prompt is not None: + metadata.add_text("prompt", json.dumps(prompt)) + if extra_pnginfo is not None: + for k, v in extra_pnginfo.items(): + metadata.add_text(k, json.dumps(v)) + + # 保存为 PNG 格式,嵌入所选颜色配置文件 + filename = f"{filename_prefix}_{self.get_unique_filename()}.png" + full_output_path = os.path.join(output_dir, filename) + img.save(full_output_path, pnginfo=metadata, icc_profile=icc_profile) + + # 如果 save_metadata 为 False,保存为指定的格式 + else: + if format == "PNG": + metadata = PngInfo() + + # 嵌入作者和版权信息 + if author: + metadata.add_text("Author", author) + if copyright_info: + metadata.add_text("Copyright", copyright_info) + + # 保存为 PNG 格式 + filename = f"{filename_prefix}_{self.get_unique_filename()}.png" + full_output_path = os.path.join(output_dir, filename) + img.save(full_output_path, pnginfo=metadata, icc_profile=icc_profile) + + elif format == "JPG": + exif_data = img.getexif() + + # 嵌入作者和版权信息 + if author: + exif_data[0x013B] = author # EXIF中的作者字段(Artist) + if copyright_info: + exif_data[0x8298] = copyright_info # EXIF中的版权字段(Copyright) + + exif_bytes = exif_data.tobytes() + + # 保存为 JPG 格式,附加 EXIF 和 ICC profile + filename = f"{filename_prefix}_{self.get_unique_filename()}.jpg" + full_output_path = os.path.join(output_dir, filename) + img.save(full_output_path, quality=jpg_quality, exif=exif_bytes, icc_profile=icc_profile) + + # 添加保存的图片信息用于UI预览 + results.append({ + "filename": filename, + "subfolder": "Custom_Save_Image", + "type": "output", + }) + + # 返回给UI的图片预览信息 + return {"ui": {"images": results}, "status": "Images saved successfully"} + + def convert_to_adobe_rgb(self, img, srgb_profile_path, adobergb_profile_path): + """ + 将图像从 sRGB 转换为 Adobe RGB + """ + srgb_profile = ImageCms.getOpenProfile(srgb_profile_path) + adobergb_profile = ImageCms.getOpenProfile(adobergb_profile_path) + img = ImageCms.profileToProfile(img, srgb_profile, adobergb_profile) + return img + + def load_icc_profile(self, path): + """ + 加载 ICC profile,如果加载失败,抛出异常 + """ + try: + with open(path, "rb") as f: + return f.read() + except FileNotFoundError: + raise Exception(f"ICC profile not found at: {path}") + + def get_unique_filename(self): + """ + 生成基于时间戳的唯一文件名 + """ + import time + return f"{int(time.time() * 1000)}" + + def get_output_directory(self): + """ + 返回输出目录 + """ + return os.path.join(os.getcwd(), "output", "Custom_Save_Image") + +# 这个函数用于注册节点 +def register_custom_node(): + NODE_CLASS_MAPPINGS = {"CustomSaveImage": CustomSaveImage} + NODE_DISPLAY_NAME_MAPPINGS = {"CustomSaveImage": "Custom Save Image"} + return NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS \ No newline at end of file diff --git a/resources/AdobeRGB1998.icc b/resources/AdobeRGB1998.icc new file mode 100644 index 0000000000000000000000000000000000000000..a79f576b5972afe74f2b1b4170b1b1ef5117078c GIT binary patch literal 560 zcmZQzU@~xYadKr6U|`72D=7+ccT$Lmj8b5~z`()4%^=KR%wV0ETwLH75a7cA1bO*+ zsUR8^e7nZL2okxh3o;8;jv$XKxuB>7sOk?88>OTcCo?cG8UWeZ>?CqdXUAa+S=1;}0o z=lp`oqRjM+5(Ogz0|N!el>DSrh2YBKlGNN{1<$23f`P$*DFXw` dAB31mF9X8@9R>#esR%JOLk0!`eFlboa{$AyP;USL literal 0 HcmV?d00001 diff --git a/resources/sRGB Profile.icc b/resources/sRGB Profile.icc new file mode 100644 index 0000000000000000000000000000000000000000..7f9d18d097d1bcccb32e6d5743ac4af593170b6f GIT binary patch literal 3144 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 L{PK7vZD!`*_k{EN literal 0 HcmV?d00001