Compare commits

...
9 Commits
Author SHA1 Message Date
Eric 01b16c7056 Merge pull request #38 from rea-ducks/main
Fixing ckpt_name includes subfolder path, issue #36
2024-12-01 12:35:43 -07:00
Reah Ducks 13e7ccf849 Fixing ckpt_name includes subfolder path, issue #36 in upstream 2024-11-01 16:30:23 +01:00
Eric 3ca7d02f3d Merge pull request #26 from rainlizard/patch-1
Update README.md for correct usage of ./ in foldername_keys
2024-09-06 19:37:03 -07:00
rainlizard e32bb26b51 Update README.md for correct usage of ./ 2024-09-07 12:03:33 +10:00
Eric ff4e4f9ba8 Merge pull request #17 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-09-06 17:26:18 -07:00
snomiao 75c8fdd886 chore(licence-update): Update PyProject Toml - License 2024-07-31 18:03:26 +00:00
audioscavenger bf91779271 release 2.76 resolution
modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
2024-06-27 11:28:01 -07:00
audioscavenger 189bb9997c release 2.75 named_keys
modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
	modified:   web/js/shared_utils.js
2024-06-17 21:57:10 -07:00
audioscavenger b0d5dd8aa6 relese 2.74 help_popup
modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
2024-06-06 11:41:09 -07:00
4 changed files with 89 additions and 34 deletions
+13 -1
View File
@@ -26,7 +26,7 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
| `filename_prefix` | String prefix added to files. |
| `filename_keys` | Comma separated string with sampler parameters to add to filename. E.g: `sampler_name, scheduler, cfg, denoise` Added to filename in written order. `resolution` also works. `vae_name` `model_name` (upscale model), `ckpt_name` (checkpoint) are others that should work. Here you can try any parameter name of any node. As long as the parameter has the same variable name defined in the `prompt` object they should work. The same applies to `foldername_keys`. |
| `foldername_prefix` | String prefix added to folders. |
| `foldername_keys` | Comma separated string with _sampler_ parameters to add to foldername. Add more subfolders by prepending "./" to the key name. |
| `foldername_keys` | Comma separated string with _sampler_ parameters to add to foldername. Add more subfolders by writing a "./" separated with commas. For example: `sampler_name, ./, ckpt_name` |
| `delimiter` | **now a free field** Delimiter = 1 character, can be anything your file system supports. Windows users should still use "/" for subfolders. |
| `save_job_data` | If enabled, saves information about each job as entries in a `jobs.json` text file, inside the generated folder. Mulitple options for saving `prompt`, `basic data`, `sampler settings`, `loaded models`. |
| `job_data_per_image` | When enabled, saves individual job data files for each image. |
@@ -37,6 +37,8 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
| `one_counter_per_folder` | Toggles the counter. Either one counter per folder, or resets when a parameter/prompt changes. |
| `image_preview` | Turns the image preview on and off. |
| `output_ext` | File extension: PNG by default, or WEBP (coming soon). |
| `quality` | Quality for JPEG/JXL/WebP/AVIF/J2K formats; default 90 (AVIF only needs 60 for same results). |
| `named_keys` | Prefix each value by its key name: prefix-seed=123456-width=1024-etc-0001.avif |
- Unknown key names in `filename_keys` and `foldername_keys` are treated as custom strings
@@ -176,11 +178,21 @@ Once I feel like I don't have time to work on it, I will gladly transfer ownersh
TODO:
- [ ] accept multiple images as input
- [ ] improve get_latest_counter: restarts when user renames files after the counter part.
- [ ] offer to place the counter anywhere, as a key in filename_keys
- [ ] keep same counter if extension changes?
- [ ] files will be out of order if prefixes change... that is expected, but is this what we want?
### release 2.76 💾
- brought back resolution
### release 2.75 💾
- Prefix each value by its key name: prefix-seed=123456-width=1024-etc-0001.avif
### release 2.74 💾
- updated help popup and minor cosmetic fixes
### release 2.73 💾
- added ckpt_path and control_net_path in case you have subfolders for those: default it output only the name
- complete fix of save_images() which allows for use of subfolders in filename_keys
+2 -2
View File
@@ -1,8 +1,8 @@
[project]
name = "save-image-extended-comfyui"
description = "Save as JXL, AVIF, WebP, JPEG, JPEG2k, customize the folder, sub-folders, and filenames of your images!"
version = "2.73"
license = "LICENCE"
version = "2.76"
license = { file = "LICENSE" }
dependencies = ["piexif", "imagecodecs", "pillow", "pillow-avif-plugin", "pillow-jxl-plugin"]
[project.urls]
+64 -31
View File
@@ -11,7 +11,7 @@ import pprint
import piexif
import piexif.helper
version = 2.73
version = 2.76
avif_supported = False
jxl_supported = False
@@ -20,10 +20,10 @@ jxl_supported = False
try:
import pillow_avif
except:
print(f"\033[92m[save_image_extended]\033[0m AVIF is not supported. To add it: pip install pillow pillow-avif-plugin\033[0m")
print(f"\033[92m[💾 save_image_extended]\033[0m AVIF is not supported. To add it: pip install pillow pillow-avif-plugin\033[0m")
pass
else:
print(f"\033[92m[save_image_extended] AVIF is supported! Woohoo!\033[0m")
print(f"\033[92m[💾 save_image_extended] AVIF is supported! Woohoo!\033[0m")
avif_supported = True
# Jxl requires jxlpy wheel to be compiled, and a valid MSVC environment, which is complex task
@@ -33,11 +33,11 @@ try:
# from imagecodecs import (jpegxl_encode, jpegxl_decode, jpegxl_check, jpegxl_version, JPEGXL)
import pillow_jxl
except:
print(f"\033[92m[save_image_extended]\033[0m JXL is not supported. To add it: pip install jxlpy\033[0m")
print(f"\033[92m[save_image_extended]\033[0m You will need a valid MSVC env to build the wheel\033[0m")
print(f"\033[92m[💾 save_image_extended]\033[0m JXL is not supported. To add it: pip install jxlpy\033[0m")
print(f"\033[92m[💾 save_image_extended]\033[0m You will need a valid MSVC env to build the wheel\033[0m")
pass
else:
print(f"\033[92m[save_image_extended] JPEGXL is supported! YeePee!\033[0m")
print(f"\033[92m[💾 save_image_extended] JPEGXL is supported! YeePee!\033[0m")
jxl_supported = True
# PIL must be loaded after pillow plugins
@@ -55,15 +55,32 @@ class SaveImageExtended:
OUTPUT_NODE = True
CATEGORY = 'image'
DESCRIPTION = """
### subfolders:
- you can use / or ./ or even ../ to start from parent
- you can also use / as the separator
- if your widget name has subfolders like SDXL/whatnot, you will have subfolders too
### Sample datetime formats: see [man datetime](https://www.man7.org/linux/man-pages/man1/date.1.html)
## Advice
It is strongly advised to enable Badge numbers in the Manager: _#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_
### 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
### Datetime formats
See [man datetime](https://www.man7.org/linux/man-pages/man1/date.1.html) for all possible values
- %F = %Y-%m-%d = 2024-05-22
- %H-%M-%S = 09-13-58
- %D = 05/22/24 (subfolders)
### 💾 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.
"""
type = 'output'
@@ -97,8 +114,9 @@ class SaveImageExtended:
output_ext = '.webp'
output_exts = ['.webp', '.png', '.jpg', '.jpeg', '.j2k', '.jp2', '.gif', '.tiff', '.bmp']
quality = 90
named_keys = False
print(f"\033[92m[save_image_extended]\033[0m version: {version}\033[0m")
print(f"\033[92m[💾 save_image_extended]\033[0m version: {version}\033[0m")
if jxl_supported:
output_exts.insert(0, '.jxl')
# if pillow_avif not in sys.modules:
@@ -111,7 +129,7 @@ class SaveImageExtended:
self.prefix_append = ''
"""
Return a dictionary which contains config for all input fields.
INPUT_TYPES Return a dictionary which contains config for all input fields.
Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT".
Input types "INT", "STRING" or "FLOAT" are special values for fields on the node.
The type can be a list for selection.
@@ -164,6 +182,7 @@ class SaveImageExtended:
"step": 1,
"display": "silder"
}),
'named_keys': ('BOOLEAN', {'default': self.named_keys}),
},
'optional': {
'positive_text_opt': ('STRING', {'forceInput': True}),
@@ -234,13 +253,19 @@ class SaveImageExtended:
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 == '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))
else:
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 == '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))
else:
found_values['control_net_name'] = self.cleanup_fileName(str(value_path.name))
else:
found_values[key] = self.cleanup_fileName(value)
elif isinstance(value, dict):
@@ -291,7 +316,7 @@ class SaveImageExtended:
# 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()):
def generate_custom_name(self, keys_to_extract, prefix, delimiter, prompt, resolution, timestamp=datetime.now(), named_keys=False):
if '%' in prefix:
custom_name = timestamp.strftime(prefix)
else:
@@ -368,6 +393,8 @@ class SaveImageExtended:
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 == 'resolution':
value = resolution
else:
self.find_keys_recursively(prompt, [nodeKey], found_values)
# is key num.widget_name
@@ -379,9 +406,13 @@ class SaveImageExtended:
# 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:
@@ -401,8 +432,8 @@ class SaveImageExtended:
# 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 == '.'
# 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]
@@ -560,9 +591,10 @@ class SaveImageExtended:
# 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
@@ -673,6 +705,7 @@ class SaveImageExtended:
extra_pnginfo=None,
prompt=None,
quality=quality,
named_keys=named_keys,
):
# print(f"save_images filename_prefix = x{filename_prefix}x")
@@ -698,15 +731,15 @@ class SaveImageExtended:
################################## 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))
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
+10
View File
@@ -61,6 +61,7 @@ export function getResolver(timeout = 5000) {
}, timeout);
return resolver;
}
export function wait(ms = 16, value) {
if (ms === 16) {
return new Promise((resolve) => {
@@ -73,14 +74,17 @@ export function wait(ms = 16, value) {
}, ms);
});
}
function dec2hex(dec) {
return dec.toString(16).padStart(2, "0");
}
export function generateId(length) {
const arr = new Uint8Array(length / 2);
crypto.getRandomValues(arr);
return Array.from(arr, dec2hex).join("");
}
export function getObjectValue(obj, objKey, def) {
if (!obj || !objKey)
return def;
@@ -92,6 +96,7 @@ export function getObjectValue(obj, objKey, def) {
}
return found;
}
export function setObjectValue(obj, objKey, value, createMissingObjects = true) {
if (!obj || !objKey)
return obj;
@@ -114,14 +119,17 @@ export function setObjectValue(obj, objKey, value, createMissingObjects = true)
}
return obj;
}
export function moveArrayItem(arr, itemOrFrom, to) {
const from = typeof itemOrFrom === "number" ? itemOrFrom : arr.indexOf(itemOrFrom);
arr.splice(to, 0, arr.splice(from, 1)[0]);
}
export function removeArrayItem(arr, itemOrIndex) {
const index = typeof itemOrIndex === "number" ? itemOrIndex : arr.indexOf(itemOrIndex);
arr.splice(index, 1);
}
export function injectCss(href) {
if (document.querySelector(`link[href^="${href}"]`)) {
return Promise.resolve();
@@ -139,6 +147,7 @@ export function injectCss(href) {
document.head.appendChild(link);
});
}
export function injectJs(href) {
if (document.querySelector(`script[src^="${href}"]`)) {
return Promise.resolve();
@@ -155,3 +164,4 @@ export function injectJs(href) {
document.head.appendChild(script);
});
}