|
|
|
@@ -2,7 +2,7 @@ import os
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
import numpy as np
|
|
|
|
|
import numpy
|
|
|
|
|
import locale
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from pathlib import Path
|
|
|
|
@@ -11,7 +11,9 @@ import pprint
|
|
|
|
|
import piexif
|
|
|
|
|
import piexif.helper
|
|
|
|
|
|
|
|
|
|
version = 2.74
|
|
|
|
|
# import cv2 # not paster then PIL
|
|
|
|
|
|
|
|
|
|
version = 2.80
|
|
|
|
|
|
|
|
|
|
avif_supported = False
|
|
|
|
|
jxl_supported = False
|
|
|
|
@@ -56,19 +58,20 @@ class SaveImageExtended:
|
|
|
|
|
CATEGORY = 'image'
|
|
|
|
|
DESCRIPTION = """
|
|
|
|
|
## Advice
|
|
|
|
|
It is strongly advised to enable Badge numbers in the Manager: _#ID Nickname_
|
|
|
|
|
You must enable Badge numbers in the Manager if you have duplicate nodes: _#ID Nickname_
|
|
|
|
|
|
|
|
|
|
### Default behavior
|
|
|
|
|
- Workflows with multiple of the same node:
|
|
|
|
|
- if you don't specify the node number in your keys like _13.sampler_name_,
|
|
|
|
|
- then the highest node number value will be returned
|
|
|
|
|
- if your checkpoint/controlnet is in subfolders like SDXL/whatnot, `ckpt_name` will **not** have subfolders
|
|
|
|
|
- to get the subfolders from controlnets and checkpoints, use _ckpt_path_ and _control_net_path_
|
|
|
|
|
- get name from CheckPoint/LorA/ControlNet: use _ckpt_name_ / _control_net_name_ / _lora_name_
|
|
|
|
|
- get subfolders from CheckPoint/LorA/ControlNet: use use _ckpt_path_ / _control_net_path_ / _lora_path_
|
|
|
|
|
|
|
|
|
|
### subfolders
|
|
|
|
|
- you can use `/` as the separator, or use keys such as `/name` or `name/` as a folder separator
|
|
|
|
|
- you can use `/` or `./` or even `../` anywhere in your keys
|
|
|
|
|
- there can be subfolders in _filename\_keys_ as well, altough it will mess the order of your keys
|
|
|
|
|
- you can use `/` or `./` or even `../`
|
|
|
|
|
- you can prepend `/` to your key such as `/sampler_name` or `/12.sampler_name`
|
|
|
|
|
- you can use subfolders in _filename\_keys_ as well
|
|
|
|
|
- trailing `/` will be removed
|
|
|
|
|
|
|
|
|
|
### Datetime formats
|
|
|
|
|
See [man datetime](https://www.man7.org/linux/man-pages/man1/date.1.html) for all possible values
|
|
|
|
@@ -78,13 +81,14 @@ See [man datetime](https://www.man7.org/linux/man-pages/man1/date.1.html) for al
|
|
|
|
|
|
|
|
|
|
### 💾 Prompt Embedded
|
|
|
|
|
Prompt and Workflows are embedded in every files except BMP.
|
|
|
|
|
They are saved in Exif tags _Make_ [0x010f] and _ImageDescription_ [0x010e].
|
|
|
|
|
ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
Prompt and Workflows are saved in Exif tags _Make_ [0x010f] and _ImageDescription_ [0x010e].
|
|
|
|
|
ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dropped when they implemented audio files in Summer 2024.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
type = 'output'
|
|
|
|
|
|
|
|
|
|
# BUG: PIL.Image doesn't respect compress_level value and always output max 9 compressed images when optimize_image = True
|
|
|
|
|
png_compress_level = 9
|
|
|
|
|
avif_quality = 60
|
|
|
|
|
webp_quality = 90
|
|
|
|
@@ -93,8 +97,8 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
j2k_quality = 90
|
|
|
|
|
# tiff_quality has no impact unless you start meddling with the compressions algorithms
|
|
|
|
|
tiff_quality = 90
|
|
|
|
|
# optimize_image only works for jpeg, with like just 2% reduction in size
|
|
|
|
|
optimize_image = True
|
|
|
|
|
# optimize_image only works for jpeg, png anf TIFF, with like just 2% reduction in size
|
|
|
|
|
optimize_image = False
|
|
|
|
|
|
|
|
|
|
filename_prefix = 'ComfyUI'
|
|
|
|
|
filename_keys = 'sampler_name, cfg, steps, %F %H-%M-%S'
|
|
|
|
@@ -110,10 +114,12 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
counter_positions = ['last', 'first']
|
|
|
|
|
one_counter_per_folder = True
|
|
|
|
|
image_preview = True
|
|
|
|
|
extToRemove = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
|
|
|
|
|
modelExtensions = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
|
|
|
|
|
output_ext = '.webp'
|
|
|
|
|
output_exts = ['.webp', '.png', '.jpg', '.jpeg', '.j2k', '.jp2', '.gif', '.tiff', '.bmp']
|
|
|
|
|
# quality is a lossy compression unused by PNG/tiff/gif
|
|
|
|
|
quality = 90
|
|
|
|
|
named_keys = False
|
|
|
|
|
|
|
|
|
|
print(f"\033[92m[💾 save_image_extended]\033[0m version: {version}\033[0m")
|
|
|
|
|
if jxl_supported:
|
|
|
|
@@ -165,7 +171,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
'save_metadata': ('BOOLEAN', {'default': self.save_metadata}),
|
|
|
|
|
'counter_digits': ('INT', {
|
|
|
|
|
"default": self.counter_digits,
|
|
|
|
|
"min": 1,
|
|
|
|
|
"min": 0,
|
|
|
|
|
"max": 8,
|
|
|
|
|
"step": 1,
|
|
|
|
|
"display": "silder"
|
|
|
|
@@ -181,6 +187,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
"step": 1,
|
|
|
|
|
"display": "silder"
|
|
|
|
|
}),
|
|
|
|
|
'named_keys': ('BOOLEAN', {'default': self.named_keys}),
|
|
|
|
|
},
|
|
|
|
|
'optional': {
|
|
|
|
|
'positive_text_opt': ('STRING', {'forceInput': True}),
|
|
|
|
@@ -218,6 +225,14 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
return str(subfolder_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ██████ ██████ ██ ██ ███ ██ ████████ ███████ ██████
|
|
|
|
|
# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██
|
|
|
|
|
# ██ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██████
|
|
|
|
|
# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ██████ ██████ ██████ ██ ████ ██ ███████ ██ ██
|
|
|
|
|
|
|
|
|
|
# Get current counter number from file names
|
|
|
|
|
def get_latest_counter(self, one_counter_per_folder, folder_path, filename_prefix, counter_digits=counter_digits, counter_position=counter_position, output_ext=output_ext):
|
|
|
|
|
counter = 1
|
|
|
|
@@ -251,20 +266,32 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
for key, value in prompt.items():
|
|
|
|
|
if key in keys_to_find:
|
|
|
|
|
# print(f"debug find_keys_recursively: found key={key}")
|
|
|
|
|
if key == 'ckpt_name' and 'ckpt_path' in keys_to_find:
|
|
|
|
|
if key in ['ckpt_path','ckpt_name']:
|
|
|
|
|
value_path = Path(value)
|
|
|
|
|
found_values['ckpt_path'] = self.cleanup_fileName(str(value_path.parent))
|
|
|
|
|
if 'ckpt_path' in keys_to_find:
|
|
|
|
|
found_values['ckpt_path'] = self.cleanup_fileName(str(value_path.parent))
|
|
|
|
|
elif 'ckpt_name' in keys_to_find:
|
|
|
|
|
found_values['ckpt_name'] = self.cleanup_fileName(str(value_path.name))
|
|
|
|
|
# print(f"debug find_keys_recursively: ckpt_name={value_path.name} ckpt_path={str(value_path.parent)}")
|
|
|
|
|
elif key == 'control_net_name' and 'control_net_path' in keys_to_find:
|
|
|
|
|
elif key in ['control_net_path','control_net_name']:
|
|
|
|
|
value_path = Path(value)
|
|
|
|
|
found_values['control_net_path'] = self.cleanup_fileName(str(value_path.parent))
|
|
|
|
|
if 'control_net_path' in keys_to_find:
|
|
|
|
|
found_values['control_net_path'] = self.cleanup_fileName(str(value_path.parent))
|
|
|
|
|
elif 'control_net_name' in keys_to_find:
|
|
|
|
|
found_values['control_net_name'] = self.cleanup_fileName(str(value_path.name))
|
|
|
|
|
elif key in ['lora_path','lora_name']:
|
|
|
|
|
value_path = Path(value)
|
|
|
|
|
if 'lora_path' in keys_to_find:
|
|
|
|
|
found_values['lora_path'] = self.cleanup_fileName(str(value_path.parent))
|
|
|
|
|
elif 'lora_name' in keys_to_find:
|
|
|
|
|
found_values['lora_name'] = self.cleanup_fileName(str(value_path.name))
|
|
|
|
|
else:
|
|
|
|
|
found_values[key] = self.cleanup_fileName(value)
|
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
|
self.find_keys_recursively(value, keys_to_find, found_values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cleanup_fileName(self, file='', extToRemove=extToRemove):
|
|
|
|
|
def cleanup_fileName(self, file='', extToRemove=modelExtensions):
|
|
|
|
|
if isinstance(file, str):
|
|
|
|
|
# takes care of all the possible safetensor extensions under the sun
|
|
|
|
|
# cannot do that... maybe the user want a string.string fixed value to use, that does not end with extToRemove
|
|
|
|
@@ -302,17 +329,30 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
return found_values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ███ ██ █████ ███ ███ ███████ ██ ██████ ██████ ██████
|
|
|
|
|
# ████ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ██ ██ ██ ███████ ██ ████ ██ █████ ██ ██ ██ ██ ██ ██████
|
|
|
|
|
# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ██ ████ ██ ██ ██ ██ ███████ ███████ ██████ ██████ ██
|
|
|
|
|
|
|
|
|
|
# String Type Example isdecimal() isdigit() isnumeric()
|
|
|
|
|
# --------------------------- --------- ----------- --------- -----------
|
|
|
|
|
# Base 10 Numbers '0123' True True True
|
|
|
|
|
# Fractions and Superscripts '⅔','2²' False True True
|
|
|
|
|
# Roman Numerals 'ↁ' False False True
|
|
|
|
|
# --------------------------- --------- ----------- --------- -----------
|
|
|
|
|
def generate_custom_name(self, keys_to_extract, prefix, delimiter, prompt, timestamp=datetime.now()):
|
|
|
|
|
if '%' in prefix:
|
|
|
|
|
custom_name = timestamp.strftime(prefix)
|
|
|
|
|
else:
|
|
|
|
|
custom_name = prefix
|
|
|
|
|
# def generate_custom_name: (self, keys_to_extract, prefix, delimiter, prompt, resolution, timestamp=datetime.now(), named_keys=False):
|
|
|
|
|
def generate_custom_name(self, keys_to_extract, prefix, delimiter, prompt, resolution, timestamp=datetime.now(), named_keys=False):
|
|
|
|
|
custom_name = []
|
|
|
|
|
|
|
|
|
|
# only filename has prefix
|
|
|
|
|
if prefix:
|
|
|
|
|
if '%' in prefix:
|
|
|
|
|
custom_name.append(timestamp.strftime(prefix))
|
|
|
|
|
else:
|
|
|
|
|
custom_name.append(prefix)
|
|
|
|
|
|
|
|
|
|
if prompt is not None and keys_to_extract != ['']:
|
|
|
|
|
found_values = {}
|
|
|
|
@@ -334,16 +374,32 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
value = None
|
|
|
|
|
node, nodeKey = None, None
|
|
|
|
|
|
|
|
|
|
# check if this is a subfolder: starts with ./ or /, can also end with /
|
|
|
|
|
# we also exclude datetime formats like "%A %d. %B %Y"
|
|
|
|
|
if os.sep in key and not '%' in key:
|
|
|
|
|
# key is a subfolder: ./subfolder or ../subfolder or /subfolder
|
|
|
|
|
# datetime format
|
|
|
|
|
if '%' in key:
|
|
|
|
|
value = timestamp.strftime(key)
|
|
|
|
|
|
|
|
|
|
# check if there is an os.sep involved, cleanup the key
|
|
|
|
|
if '/' in key:
|
|
|
|
|
# key is a subfolder: ./key or ../key or /key ==> last / is removed
|
|
|
|
|
values = re.split('/+', key)
|
|
|
|
|
# we will strip the last / in all cases
|
|
|
|
|
if values[0] in ['','.','..']:
|
|
|
|
|
custom_name.append(values[0]+'/')
|
|
|
|
|
key = values[1]
|
|
|
|
|
# was it just a folder separator?
|
|
|
|
|
if not key: continue
|
|
|
|
|
else:
|
|
|
|
|
key = values[0]
|
|
|
|
|
|
|
|
|
|
# fixed string:
|
|
|
|
|
if (key.startswith("'") and key.endswith("'")) or (key.startswith('"') and key.endswith('"')):
|
|
|
|
|
value = key
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# is it num.key ?
|
|
|
|
|
if value is None:
|
|
|
|
|
splitKey = key.split('.')
|
|
|
|
|
# we also exclude cases like "..string" or "sting.string.x" etc
|
|
|
|
|
# we also exclude datetime formats like "%A %d. %B %Y"
|
|
|
|
|
if len(splitKey) == 2 and not '%' in key:
|
|
|
|
|
if len(splitKey) == 2:
|
|
|
|
|
# key has the form string.string
|
|
|
|
|
if '' not in splitKey:
|
|
|
|
|
# key has the form string.string
|
|
|
|
@@ -357,6 +413,8 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
self.find_keys_recursively(prompt[node], ['ckpt_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'control_net_path':
|
|
|
|
|
self.find_keys_recursively(prompt[node], ['control_net_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'lora_path':
|
|
|
|
|
self.find_keys_recursively(prompt[node], ['lora_name', nodeKey], found_values)
|
|
|
|
|
else:
|
|
|
|
|
self.find_keys_recursively(prompt[node], [nodeKey], found_values)
|
|
|
|
|
else:
|
|
|
|
@@ -366,6 +424,8 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
self.find_keys_recursively(prompt, ['ckpt_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'control_net_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['control_net_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'lora_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['lora_name', nodeKey], found_values)
|
|
|
|
|
else:
|
|
|
|
|
self.find_keys_recursively(prompt, [nodeKey], found_values)
|
|
|
|
|
else:
|
|
|
|
@@ -374,79 +434,83 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
else:
|
|
|
|
|
# key is in the form ".string" or "string." or "." - we won't clean that up and keep as is, maybe it's a separator
|
|
|
|
|
value = key
|
|
|
|
|
|
|
|
|
|
# key is not a datetime, folder, has no dot, or multiple dots, could be a valid key to find, could be a fixed string - keep as is
|
|
|
|
|
else:
|
|
|
|
|
# could be a datetime format
|
|
|
|
|
if '%' in key:
|
|
|
|
|
value = timestamp.strftime(key)
|
|
|
|
|
# nodeKey is not a datetime, folder, has no dot, or multiple dots, could be a valid key to find, could be a fixed string - keep as is
|
|
|
|
|
nodeKey = key
|
|
|
|
|
if nodeKey == 'ckpt_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['ckpt_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'control_net_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['control_net_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'lora_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['lora_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'resolution':
|
|
|
|
|
value = resolution
|
|
|
|
|
else:
|
|
|
|
|
nodeKey = key
|
|
|
|
|
if nodeKey == 'ckpt_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['ckpt_name', nodeKey], found_values)
|
|
|
|
|
elif nodeKey == 'control_net_path':
|
|
|
|
|
self.find_keys_recursively(prompt, ['control_net_name', nodeKey], found_values)
|
|
|
|
|
else:
|
|
|
|
|
self.find_keys_recursively(prompt, [nodeKey], found_values)
|
|
|
|
|
self.find_keys_recursively(prompt, [nodeKey], found_values)
|
|
|
|
|
# is key num.widget_name
|
|
|
|
|
# is key subfolder
|
|
|
|
|
|
|
|
|
|
# value was None
|
|
|
|
|
|
|
|
|
|
# at this point we have a nodeKey, or a value, or both
|
|
|
|
|
# print(f"debug generate_custom_name: ----nodeKey: {nodeKey}")
|
|
|
|
|
# print(f"debug generate_custom_name: ----value: {value}")
|
|
|
|
|
# print(f"debug generate_custom_name: ----found_values: {found_values}")
|
|
|
|
|
if value is None:
|
|
|
|
|
if nodeKey is not None:
|
|
|
|
|
if nodeKey in found_values: value = found_values[nodeKey]
|
|
|
|
|
if value is None:
|
|
|
|
|
value = nodeKey
|
|
|
|
|
if nodeKey in found_values:
|
|
|
|
|
if named_keys:
|
|
|
|
|
value = f"{nodeKey}={found_values[nodeKey]}"
|
|
|
|
|
else:
|
|
|
|
|
value = found_values[nodeKey]
|
|
|
|
|
if value is None: value = nodeKey
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# at this point, value is not None anymore
|
|
|
|
|
# now we analyze each value found and format them accordingly:
|
|
|
|
|
# print(f"debug generate_custom_name: ----value: {value}")
|
|
|
|
|
delim = delimiter
|
|
|
|
|
|
|
|
|
|
# now we build the custom_name:
|
|
|
|
|
if isinstance(value, str):
|
|
|
|
|
# prefix and keys can very well be subfolders ending or starting with a /
|
|
|
|
|
if custom_name.endswith(os.sep):
|
|
|
|
|
# for subfolders, do not start filename with a delimiter...
|
|
|
|
|
delim = ''
|
|
|
|
|
# for subfolders in keys, do not clean the filename...
|
|
|
|
|
# print(f"debug generate_custom_name: ---------: / in value? {nodeKey}={value}")
|
|
|
|
|
|
|
|
|
|
# Now process the custom cases ckpt_path and control_net_path; maybe we should do that to `image` as well?
|
|
|
|
|
# The recursive function creates ckpt_path and ckpt_name already; we just need to eliminate path if == '.'
|
|
|
|
|
if nodeKey in ['ckpt_path', 'control_net_path', 'lora_path']:
|
|
|
|
|
# smth_path is most likely placed before smth_name, therefore value cannot be resolved during the previous check
|
|
|
|
|
value = found_values[nodeKey]
|
|
|
|
|
else:
|
|
|
|
|
# for subfolders in keys, do not clean the filename...
|
|
|
|
|
# print(f"debug generate_custom_name: ---------: {os.sep} in value? {nodeKey}={value}")
|
|
|
|
|
if os.sep in value and not value.endswith(os.sep):
|
|
|
|
|
# print(f"debug generate_custom_name: ---------: folder")
|
|
|
|
|
delim= ''
|
|
|
|
|
|
|
|
|
|
# now process the custom cases ckpt_path and control_net_path; maybe we should do that to `image` as well?
|
|
|
|
|
# the recursive function creates ckpt_path and ckpt_name already; we just need to eliminate path if == '.'
|
|
|
|
|
if nodeKey in ['ckpt_path', 'control_net_path']:
|
|
|
|
|
# smth_path is most likely placed before smth_name, therefore value cannot be resolved during the previous check
|
|
|
|
|
value = found_values[nodeKey]
|
|
|
|
|
# and indeed we bypass anything that does not have a subfolder
|
|
|
|
|
if value == '.':
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
# terminate the subfolder with os.sep so it creates an actual subfolder.
|
|
|
|
|
# Because find_keys_recursively will only return the path part: 'path' or 'path/subpath'
|
|
|
|
|
# Therefore, enclosing this value with os.sep ensure we get a subfolder
|
|
|
|
|
value = os.sep + value + os.sep
|
|
|
|
|
else:
|
|
|
|
|
value = self.cleanup_fileName(value)
|
|
|
|
|
|
|
|
|
|
# ".string" case
|
|
|
|
|
if value.startswith('.'):
|
|
|
|
|
delim = ''
|
|
|
|
|
value = self.cleanup_fileName(value)
|
|
|
|
|
elif isinstance(value, float):
|
|
|
|
|
# value = round(float(value), 1) # too much rounding
|
|
|
|
|
value = float(f'{value:.10g}')
|
|
|
|
|
|
|
|
|
|
if isinstance(value, float):
|
|
|
|
|
value = round(float(value), 1)
|
|
|
|
|
|
|
|
|
|
custom_name += f"{delim}{value}"
|
|
|
|
|
custom_name.append(str(value))
|
|
|
|
|
# print(f"debug generate_custom_name: ------custom_name: {custom_name}")
|
|
|
|
|
# for each key
|
|
|
|
|
return custom_name.strip(delimiter).strip('.').strip(os.sep).strip(delimiter)
|
|
|
|
|
|
|
|
|
|
# remove empty values
|
|
|
|
|
custom_name = list(filter(None, custom_name))
|
|
|
|
|
# join
|
|
|
|
|
stringName = delimiter.join(custom_name).replace('/'+delimiter, '/').replace(delimiter+'/', '/').replace(delimiter+'.', '.')
|
|
|
|
|
# clean
|
|
|
|
|
stringName = stringName.strip(delimiter).strip('/').strip(delimiter).strip('.')
|
|
|
|
|
|
|
|
|
|
print(f"debug generate_custom_name: ------custom_name: {custom_name}")
|
|
|
|
|
print(f"debug generate_custom_name: ------stringName: {stringName}")
|
|
|
|
|
return re.sub(r'[*?:"<>|]','',stringName).replace('/', os.sep)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ██ ███████ ██████ ███ ██
|
|
|
|
|
# ██ ██ ██ ██ ████ ██
|
|
|
|
|
# ██ ███████ ██ ██ ██ ██ ██
|
|
|
|
|
# ██ ██ ██ ██ ██ ██ ██ ██
|
|
|
|
|
# █████ ███████ ██████ ██ ████
|
|
|
|
|
|
|
|
|
|
def save_job_to_json(self, save_job_data, prompt, filename_prefix, positive_text_opt, negative_text_opt, job_custom_text, resolution, output_path, filename, timestamp=datetime.now()):
|
|
|
|
|
prompt_keys_to_save = {}
|
|
|
|
|
if 'basic' in save_job_data:
|
|
|
|
@@ -541,7 +605,15 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
json.dump(existing_data, f, indent=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_metadata_png(self, img, prompt, extra_pnginfo=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ██████ ███ ██ ██████
|
|
|
|
|
# ██ ██ ████ ██ ██
|
|
|
|
|
# ██████ ██ ██ ██ ██ ███
|
|
|
|
|
# ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ██ ██ ████ ██████
|
|
|
|
|
|
|
|
|
|
def genMetadataPng(self, img, prompt, extra_pnginfo=None):
|
|
|
|
|
metadata = PngInfo()
|
|
|
|
|
if prompt is not None:
|
|
|
|
|
metadata.add_text('prompt', json.dumps(prompt))
|
|
|
|
@@ -551,8 +623,13 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
|
|
|
|
|
return metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_metadata_exif(self, img, prompt, extra_pnginfo=None):
|
|
|
|
|
# ███████ ██ ██ ██ ███████
|
|
|
|
|
# ██ ██ ██ ██ ██
|
|
|
|
|
# █████ ███ ██ █████
|
|
|
|
|
# ██ ██ ██ ██ ██
|
|
|
|
|
# ███████ ██ ██ ██ ██
|
|
|
|
|
|
|
|
|
|
def genMetadataEXIF(self, img, prompt, extra_pnginfo=None):
|
|
|
|
|
metadata = {}
|
|
|
|
|
if prompt is not None:
|
|
|
|
|
metadata["prompt"] = prompt
|
|
|
|
@@ -577,9 +654,10 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
# exif[ExifTags.Base.UserComment] = dump
|
|
|
|
|
|
|
|
|
|
## It seems better to separate the two
|
|
|
|
|
# 0x010e: ImageDescription
|
|
|
|
|
# 0x010f: Make
|
|
|
|
|
# 0x9286: UserComment
|
|
|
|
|
# 0x010d: DocumentName Parameters (SD)
|
|
|
|
|
# 0x010e: ImageDescription Workflow
|
|
|
|
|
# 0x010f: Make Prompt
|
|
|
|
|
# 0x9286: UserComment cancelled
|
|
|
|
|
# both prompt and workflow must be in IFD close together of that can cause problems for the parseIFD function on import
|
|
|
|
|
# https://exiftool.org/TagNames/EXIF.html
|
|
|
|
|
# exif[0x9286] = "Prompt: " + json.dumps(metadata['prompt']) # UserComment
|
|
|
|
@@ -618,10 +696,16 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
# exif_dat = piexif.dump(exif_dict)
|
|
|
|
|
|
|
|
|
|
return exif_dat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_image(self, image_path, img, prompt, save_metadata=save_metadata, extra_pnginfo=None, quality=quality):
|
|
|
|
|
# print(f"debug save_images: image_path={image_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ██ ██ ██████ ██ ████████ ███████
|
|
|
|
|
# ██ ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ██ █ ██ ██████ ██ ██ █████
|
|
|
|
|
# ██ ███ ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ███ ███ ██ ██ ██ ██ ███████
|
|
|
|
|
|
|
|
|
|
def writeImage(self, image_path, img, prompt, save_metadata=save_metadata, extra_pnginfo=None, quality=quality):
|
|
|
|
|
# print(f"debug writeImage: image_path={image_path}")
|
|
|
|
|
if quality == 0:
|
|
|
|
|
quality = self.quality
|
|
|
|
|
# output_ext = os.path.splitext(os.path.basename(image_path))[1]
|
|
|
|
@@ -633,13 +717,13 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
|
|
|
|
|
|
|
|
|
if output_ext in ['.avif', '.webp', '.jxl']:
|
|
|
|
|
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
|
|
|
|
|
if save_metadata: kwargs["exif"] = self.genMetadataEXIF(img, prompt, extra_pnginfo)
|
|
|
|
|
if quality == 100:
|
|
|
|
|
kwargs["lossless"] = True
|
|
|
|
|
else:
|
|
|
|
|
kwargs["quality"] = quality
|
|
|
|
|
if output_ext in ['.j2k', '.jp2', '.jpc', '.jpf', '.jpx', '.j2c']:
|
|
|
|
|
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
|
|
|
|
|
if save_metadata: kwargs["exif"] = self.genMetadataEXIF(img, prompt, extra_pnginfo)
|
|
|
|
|
if quality < 100:
|
|
|
|
|
kwargs["irreversible"] = True
|
|
|
|
|
# there is no such thing as compression level in JPEG2000. Read https://comprimato.com/blog/2017/06/22/bitrate-control-quality-layers-jpeg2000/
|
|
|
|
@@ -648,15 +732,17 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
else:
|
|
|
|
|
kwargs["quality"] = quality
|
|
|
|
|
elif output_ext in ['.jpg', '.jpeg']:
|
|
|
|
|
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
|
|
|
|
|
if save_metadata: kwargs["exif"] = self.genMetadataEXIF(img, prompt, extra_pnginfo)
|
|
|
|
|
# https://stackoverflow.com/questions/19303621/why-is-the-quality-of-jpeg-images-produced-by-pil-so-poor
|
|
|
|
|
kwargs["subsampling"] = 0
|
|
|
|
|
kwargs["quality"] = quality
|
|
|
|
|
kwargs["optimize"] = self.optimize_image
|
|
|
|
|
elif output_ext in ['.tiff']:
|
|
|
|
|
# tiff: no quality
|
|
|
|
|
kwargs["optimize"] = self.optimize_image
|
|
|
|
|
elif output_ext in ['.png', '.gif']:
|
|
|
|
|
# png/gif: no quality
|
|
|
|
|
if save_metadata: kwargs["pnginfo"] = self.get_metadata_png(img, prompt, extra_pnginfo)
|
|
|
|
|
if save_metadata: kwargs["pnginfo"] = self.genMetadataPng(img, prompt, extra_pnginfo)
|
|
|
|
|
kwargs["compress_level"] = self.png_compress_level
|
|
|
|
|
kwargs["optimize"] = self.optimize_image
|
|
|
|
|
# elif output_ext in ['.bmp']:
|
|
|
|
@@ -665,9 +751,20 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
# img.save(image_path, pnginfo=metadata, compress_level=self.png_compress_level)
|
|
|
|
|
img.save(image_path, **kwargs)
|
|
|
|
|
|
|
|
|
|
# class SaveImageExtended -------------------------------------------------------------------------------
|
|
|
|
|
# Is saving image with OpenCV really faster then PIL? https://github.com/python-pillow/Pillow/issues/5986
|
|
|
|
|
# I found that it does not matter for anything smaller then 8k*8k, which Comfy cannot produce anyways.
|
|
|
|
|
# compression_level = [cv2.IMWRITE_PNG_COMPRESSION, self.png_compress_level]
|
|
|
|
|
# image_array = numpy.array(img)
|
|
|
|
|
# image_array = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
|
|
|
|
|
# cv2.imwrite(image_path.replace('000','cv2'), image_array)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ███████ █████ ██ ██ ███████
|
|
|
|
|
# ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ███████ ███████ ██ ██ █████
|
|
|
|
|
# ██ ██ ██ ██ ██ ██
|
|
|
|
|
# ███████ ██ ██ ████ ███████
|
|
|
|
|
|
|
|
|
|
# node will never return None values, except for optional input. Impossible.
|
|
|
|
|
def save_images(self,
|
|
|
|
|
images,
|
|
|
|
@@ -690,6 +787,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
extra_pnginfo=None,
|
|
|
|
|
prompt=None,
|
|
|
|
|
quality=quality,
|
|
|
|
|
named_keys=named_keys,
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
# print(f"save_images filename_prefix = x{filename_prefix}x")
|
|
|
|
@@ -715,15 +813,15 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
################################## UNCOMMENT HERE TO SEE THE ENTIRE PROMPT
|
|
|
|
|
# pprint.pprint(prompt)
|
|
|
|
|
##########################################################################
|
|
|
|
|
timestamp = datetime.now()
|
|
|
|
|
custom_foldername = self.generate_custom_name(foldername_keys_to_extract, foldername_prefix, delimiter, prompt, timestamp)
|
|
|
|
|
custom_filename = self.generate_custom_name(filename_keys_to_extract, filename_prefix, delimiter, prompt, timestamp)
|
|
|
|
|
|
|
|
|
|
# Get set resolution value
|
|
|
|
|
# Get set resolution value - that's a secret keyword
|
|
|
|
|
i = 255. * images[0].cpu().numpy()
|
|
|
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
|
|
|
img = Image.fromarray(numpy.clip(i, 0, 255).astype(numpy.uint8))
|
|
|
|
|
resolution = f'{img.width}x{img.height}'
|
|
|
|
|
|
|
|
|
|
timestamp = datetime.now()
|
|
|
|
|
custom_foldername = self.generate_custom_name(foldername_keys_to_extract, foldername_prefix, delimiter, prompt, resolution, timestamp, named_keys)
|
|
|
|
|
custom_filename = self.generate_custom_name(filename_keys_to_extract, filename_prefix, delimiter, prompt, resolution, timestamp, named_keys)
|
|
|
|
|
|
|
|
|
|
# Create folders, count images, save images
|
|
|
|
|
try:
|
|
|
|
|
# folder_paths.get_save_image_path() is kind of magic, I don't like that. Outpout is wrong anyway, when custom_filename contains folders
|
|
|
|
@@ -744,15 +842,18 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
results = list()
|
|
|
|
|
for image in images:
|
|
|
|
|
i = 255. * image.cpu().numpy()
|
|
|
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
|
|
|
img = Image.fromarray(numpy.clip(i, 0, 255).astype(numpy.uint8))
|
|
|
|
|
|
|
|
|
|
if counter_position == 'last':
|
|
|
|
|
image_name = f'{filename}{delimiter}{counter:0{counter_digits}}{output_ext}'
|
|
|
|
|
if counter_digits > 0:
|
|
|
|
|
if counter_position == 'last':
|
|
|
|
|
image_name = f'{filename}{delimiter}{counter:0{counter_digits}}{output_ext}'
|
|
|
|
|
else:
|
|
|
|
|
image_name = f'{counter:0{counter_digits}}{delimiter}{filename}{output_ext}'
|
|
|
|
|
else:
|
|
|
|
|
image_name = f'{counter:0{counter_digits}}{delimiter}{filename}{output_ext}'
|
|
|
|
|
image_name = f'{filename}{output_ext}'
|
|
|
|
|
|
|
|
|
|
image_path = os.path.join(output_path, image_name)
|
|
|
|
|
self.save_image(image_path, img, prompt, save_metadata, extra_pnginfo, quality)
|
|
|
|
|
self.writeImage(image_path, img, prompt, save_metadata, extra_pnginfo, quality)
|
|
|
|
|
|
|
|
|
|
if save_job_data != 'disabled' and job_data_per_image:
|
|
|
|
|
self.save_job_to_json(save_job_data, prompt, filename_prefix, positive_text_opt, negative_text_opt, job_custom_text, resolution, output_path, f'{image_name.removesuffix(output_ext)}.json', timestamp)
|
|
|
|
@@ -771,6 +872,8 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
|
|
|
|
results = list()
|
|
|
|
|
return { 'ui': { 'images': results } }
|
|
|
|
|
|
|
|
|
|
# class SaveImageExtended -------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
|
|
|
'SaveImageExtended': SaveImageExtended,
|
|
|
|
@@ -778,7 +881,7 @@ NODE_CLASS_MAPPINGS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
|
|
|
'SaveImageExtended': '💾 Save Image Extended',
|
|
|
|
|
'SaveImageExtended': f'💾 Save Image Extended {version}',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|