12 KiB
12 KiB
Documentation
- Class name: WAS_Image_Save
- Category: WAS Suite/IO
- Output node: True
- Repo Ref: https://github.com/WASasquatch/was-node-suite-comfyui
WAS_Image_Save节点负责将图片保存到指定的输出目录。它处理各种文件格式,并提供命名和组织保存图片的选项,确保了图像输出管理的流程化。
Input types
Required
- images
- 图片参数是必需的,它定义了需要保存的输入图片。它在节点的执行中起着关键作用,因为它直接影响输出。
- Comfy dtype: IMAGE
- Python dtype: List[PIL.Image.Image]
Optional
- output_path
- output_path参数指定了保存图片的目录。它在确定文件系统内保存文件的最终位置时非常重要。
- Comfy dtype: STRING
- Python dtype: str
- filename_prefix
- filename_prefix用于为保存的图片创建一致的命名约定,这对于组织和识别文件非常有益。
- Comfy dtype: STRING
- Python dtype: str
- filename_delimiter
- filename_delimiter是一个字符,用于在保存的图像文件名中分隔前缀和数字标识符。
- Comfy dtype: STRING
- Python dtype: str
- filename_number_padding
- filename_number_padding参数确定文件名中用于数字标识符的位数,确保文件命名方案的一致性。
- Comfy dtype: INT
- Python dtype: int
- extension
- extension参数指示图片将被保存的文件格式,允许产生不同类型的图片文件。
- Comfy dtype: COMBO['png', 'jpg', 'jpeg', 'gif', 'tiff', 'webp', 'bmp']
- Python dtype: str
- quality
- quality参数用于设置保存图片的压缩级别,这可能会影响文件大小和图片质量之间的平衡。
- Comfy dtype: INT
- Python dtype: int
- lossless_webp
- lossless_webp参数指定是否应使用无损压缩保存WEBP图片,这保留了更多的图像数据,但代价是文件大小更大。
- Comfy dtype: COMBO[false, true]
- Python dtype: bool
- overwrite_mode
- overwrite_mode参数确定节点如何处理同名的现有文件,可以覆盖它们,或者使用文件名前缀作为新文件名的一部分。
- Comfy dtype: COMBO[false, prefix_as_filename]
- Python dtype: str
- show_history
- show_history参数控制是否显示保存图片的历史记录,这对于查看过去的输出非常有用。
- Comfy dtype: COMBO[false, true]
- Python dtype: bool
- show_history_by_prefix
- 当show_history_by_prefix参数为true时,它将显示的图片历史记录过滤为仅显示具有相同文件名前缀的图片,增强了组织性。
- Comfy dtype: COMBO[true, false]
- Python dtype: bool
- embed_workflow
- embed_workflow参数指示是否应在图像文件中包含工作流元数据,提供有关图像创建过程的额外上下文和信息。
- Comfy dtype: COMBO[true, false]
- Python dtype: bool
- show_previews
- show_previews参数决定保存后是否显示图片预览,提供了一种快速查看操作结果的方法。
- Comfy dtype: COMBO[true, false]
- Python dtype: bool
Output types
- ui
- ui参数包含保存图片后显示的用户界面元素,包括预览和历史记录(如果启用了这些选项)。
- Comfy dtype: Dict[str, Any]
- Python dtype: Dict[str, Union[List[Dict[str, Union[str, pathlib.Path]]], List[]]]
Usage tips
- Infra type: CPU
Source code
class WAS_Image_Save:
def __init__(self):
self.output_dir = comfy_paths.output_directory
self.type = 'output'
@classmethod
def INPUT_TYPES(cls):
return {'required': {'images': ('IMAGE',), 'output_path': ('STRING', {'default': '[time(%Y-%m-%d)]', 'multiline': False}), 'filename_prefix': ('STRING', {'default': 'ComfyUI'}), 'filename_delimiter': ('STRING', {'default': '_'}), 'filename_number_padding': ('INT', {'default': 4, 'min': 1, 'max': 9, 'step': 1}), 'filename_number_start': (['false', 'true'],), 'extension': (['png', 'jpg', 'jpeg', 'gif', 'tiff', 'webp', 'bmp'],), 'quality': ('INT', {'default': 100, 'min': 1, 'max': 100, 'step': 1}), 'lossless_webp': (['false', 'true'],), 'overwrite_mode': (['false', 'prefix_as_filename'],), 'show_history': (['false', 'true'],), 'show_history_by_prefix': (['true', 'false'],), 'embed_workflow': (['true', 'false'],), 'show_previews': (['true', 'false'],)}, 'hidden': {'prompt': 'PROMPT', 'extra_pnginfo': 'EXTRA_PNGINFO'}}
RETURN_TYPES = ()
FUNCTION = 'was_save_images'
OUTPUT_NODE = True
CATEGORY = 'WAS Suite/IO'
def was_save_images(self, images, output_path='', filename_prefix='ComfyUI', filename_delimiter='_', extension='png', quality=100, lossless_webp='false', prompt=None, extra_pnginfo=None, overwrite_mode='false', filename_number_padding=4, filename_number_start='false', show_history='false', show_history_by_prefix='true', embed_workflow='true', show_previews='true'):
delimiter = filename_delimiter
number_padding = filename_number_padding
lossless_webp = lossless_webp == 'true'
tokens = TextTokens()
original_output = self.output_dir
filename_prefix = tokens.parseTokens(filename_prefix)
if output_path in [None, '', 'none', '.']:
output_path = self.output_dir
else:
output_path = tokens.parseTokens(output_path)
if not os.path.isabs(output_path):
output_path = os.path.join(self.output_dir, output_path)
base_output = os.path.basename(output_path)
if output_path.endswith('ComfyUI/output') or output_path.endswith('ComfyUI\\output'):
base_output = ''
if output_path.strip() != '':
if not os.path.isabs(output_path):
output_path = os.path.join(comfy_paths.output_directory, output_path)
if not os.path.exists(output_path.strip()):
cstr(f"The path `{output_path.strip()}` specified doesn't exist! Creating directory.").warning.print()
os.makedirs(output_path, exist_ok=True)
if filename_number_start == 'true':
pattern = f'(\\d+){re.escape(delimiter)}{re.escape(filename_prefix)}'
else:
pattern = f'{re.escape(filename_prefix)}{re.escape(delimiter)}(\\d+)'
existing_counters = [int(re.search(pattern, filename).group(1)) for filename in os.listdir(output_path) if re.match(pattern, os.path.basename(filename))]
existing_counters.sort(reverse=True)
if existing_counters:
counter = existing_counters[0] + 1
else:
counter = 1
if existing_counters:
counter = existing_counters[0] + 1
else:
counter = 1
file_extension = '.' + extension
if file_extension not in ALLOWED_EXT:
cstr(f"The extension `{extension}` is not valid. The valid formats are: {', '.join(sorted(ALLOWED_EXT))}").error.print()
file_extension = 'png'
results = list()
for image in images:
i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
if extension == 'webp':
img_exif = img.getexif()
workflow_metadata = ''
prompt_str = ''
if prompt is not None:
prompt_str = json.dumps(prompt)
img_exif[271] = 'Prompt:' + prompt_str
if extra_pnginfo is not None:
for x in extra_pnginfo:
workflow_metadata += json.dumps(extra_pnginfo[x])
img_exif[270] = 'Workflow:' + workflow_metadata
exif_data = img_exif.tobytes()
else:
metadata = PngInfo()
if embed_workflow == 'true':
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]))
exif_data = metadata
if overwrite_mode == 'prefix_as_filename':
file = f'{filename_prefix}{file_extension}'
else:
if filename_number_start == 'true':
file = f'{counter:0{number_padding}}{delimiter}{filename_prefix}{file_extension}'
else:
file = f'{filename_prefix}{delimiter}{counter:0{number_padding}}{file_extension}'
if os.path.exists(os.path.join(output_path, file)):
counter += 1
try:
output_file = os.path.abspath(os.path.join(output_path, file))
if extension in ['jpg', 'jpeg']:
img.save(output_file, quality=quality, optimize=True)
elif extension == 'webp':
img.save(output_file, quality=quality, lossless=lossless_webp, exif=exif_data)
elif extension == 'png':
img.save(output_file, pnginfo=exif_data, optimize=True)
elif extension == 'bmp':
img.save(output_file)
elif extension == 'tiff':
img.save(output_file, quality=quality, optimize=True)
else:
img.save(output_file, pnginfo=exif_data, optimize=True)
cstr(f'Image file saved to: {output_file}').msg.print()
if show_history != 'true' and show_previews == 'true':
subfolder = self.get_subfolder_path(output_file, original_output)
results.append({'filename': file, 'subfolder': subfolder, 'type': self.type})
update_history_output_images(output_file)
except OSError as e:
cstr(f'Unable to save file to: {output_file}').error.print()
print(e)
except Exception as e:
cstr('Unable to save file due to the to the following error:').error.print()
print(e)
if overwrite_mode == 'false':
counter += 1
filtered_paths = []
if show_history == 'true' and show_previews == 'true':
HDB = WASDatabase(WAS_HISTORY_DATABASE)
conf = getSuiteConfig()
if HDB.catExists('History') and HDB.keyExists('History', 'Output_Images'):
history_paths = HDB.get('History', 'Output_Images')
else:
history_paths = None
if history_paths:
for image_path in history_paths:
image_subdir = self.get_subfolder_path(image_path, self.output_dir)
current_subdir = self.get_subfolder_path(output_file, self.output_dir)
if not os.path.exists(image_path):
continue
if show_history_by_prefix == 'true' and image_subdir != current_subdir:
continue
if show_history_by_prefix == 'true' and (not os.path.basename(image_path).startswith(filename_prefix)):
continue
filtered_paths.append(image_path)
if conf.__contains__('history_display_limit'):
filtered_paths = filtered_paths[-conf['history_display_limit']:]
filtered_paths.reverse()
if filtered_paths:
for image_path in filtered_paths:
subfolder = self.get_subfolder_path(image_path, self.output_dir)
image_data = {'filename': os.path.basename(image_path), 'subfolder': subfolder, 'type': self.type}
results.append(image_data)
if show_previews == 'true':
return {'ui': {'images': results}}
else:
return {'ui': {'images': []}}
def get_subfolder_path(self, image_path, output_path):
output_parts = output_path.strip(os.sep).split(os.sep)
image_parts = image_path.strip(os.sep).split(os.sep)
common_parts = os.path.commonprefix([output_parts, image_parts])
subfolder_parts = image_parts[len(common_parts):]
subfolder_path = os.sep.join(subfolder_parts[:-1])
return subfolder_path