Files
CavinHuang-comfyui-nodes-docs/docs/Image Save.md
2024-05-26 20:35:16 +08:00

12 KiB

Documentation

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