Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61aed6e7e1 | ||
|
|
8aa85b1c15 | ||
|
|
a2b9b4ed72 | ||
|
|
f7c06ba306 | ||
|
|
d1d0ddab43 | ||
|
|
4b1d081a2e | ||
|
|
449d3dab03 | ||
|
|
8255b57e9c | ||
|
|
ef8870d03e | ||
|
|
66b3877c11 | ||
|
|
43d6b8b622 | ||
|
|
8a82a5784a | ||
|
|
0ca2a77ea7 | ||
|
|
6ae161115c | ||
|
|
a75e901318 |
@@ -23,39 +23,54 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
|
||||
|
||||
| Attribute | Description |
|
||||
| --- | --- |
|
||||
| `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 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. |
|
||||
| `job_custom_text` | Custom string to save along with the job data. Right click the node and convert to input to connect with another node. |
|
||||
| `filename_prefix` | Fixed string prefixed to file name. |
|
||||
| `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` | Fixed string prefixed to subfolders. |
|
||||
| `foldername_keys` | Same rules as for `filename_keys`. Create subfolders by using `/` or `../` etc. |
|
||||
| `delimiter` | **now a free field** Delimiter = any string you like. You can also use `/` to create subfolders. |
|
||||
| `save_job_data` | Saves information about each job as entries in a `jobs.json` text file, inside the generated subfolder. Multiple options for its content: `prompt`, `basic data`, `sampler settings`, `loaded models`. |
|
||||
| `job_data_per_image` | Saves individual job data file per image. |
|
||||
| `job_custom_text` | Custom string to save along with the job data. |
|
||||
| `save_metadata` | Saves metadata into the image. |
|
||||
| `counter_digits` | Number of digits used for the image counter. `3` = image_001.png. Will adjust the counter if files are deleted. Looks for the highest number in the folder, does not fill gaps. |
|
||||
| `counter_position` | Image counter first or last in the filename. |
|
||||
| `one_counter_per_folder` | Toggles the counter. Either one counter per folder, or resets when a parameter/prompt changes. |
|
||||
| `counter_digits` | Number of digits used for the image counter. `3` = image_001.png, based on highest number in the subfolder, ignores gaps. **Can be disabled** when == 0. |
|
||||
| `counter_position` | Image counter postition: image_001.png or 001_image.png. |
|
||||
| `one_counter_per_folder` | Toggles one counter per subfolder, 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 |
|
||||
| `output_ext` | File extension: WEBP by default, AVIF, PNG, JXL, JPG, etc. |
|
||||
| `quality` | Quality for JPEG/JXL/WebP/AVIF/J2K formats; Quality is relative to each format. Example: AVIF 60 is same quality as WebP 90. PNG compression is fixed at 4 and not affected by this. PNG compression times skyrocket above level 4 for zero benefits on filesize. |
|
||||
| `named_keys` | Prefix each value by its key name. Example: prefix-seed=123456-width=1024-cfg=5.0-0001.avif |
|
||||
|
||||
|
||||
- Unknown key names in `filename_keys` and `foldername_keys` are treated as custom strings
|
||||
- Unknown key names in `filename_keys` and `foldername_keys` are treated as fixed strings
|
||||
- if you enter `wrongNumber.attribute`, you will get `attribute` in your filename.
|
||||
- Datetime UNIX format is now included! `%Y-%m-%d` or `%F` etc
|
||||
- default it output only the **name** for `ckpt_name` and `control_net_name`
|
||||
- use `ckpt_path` or `control_net_path` in case you have subfolders for those and want to use them as subfolders
|
||||
- Datetime UNIX format is now included! `%Y-%m-%d` or `%F` etc: see [unix datetime formats](https://www.man7.org/linux/man-pages/man1/date.1.html)
|
||||
- default it output only the **name** for `ckpt_name` / `control_net_name` / `lora_name` not the **path**
|
||||
- use `ckpt_path` / `control_net_path` / `lora_path` to get the same subfolders as your models
|
||||
- using `.custom_string` will prevent appending delimiter, the dot will be the delimiter
|
||||
|
||||
### Examples
|
||||
- prefix = `PREFIX`
|
||||
- filename = `cfg, sampler_name, /steps, %F %H-%M-%S`
|
||||
- foldername = `ckpt_name/, .cfg`
|
||||
- > `v1-5-pruned.cfg / PREFIX-8.0-euler / 20-2024-12-18 23-17-17-0001.png`
|
||||
|
||||
- prefix = `PREFIX`
|
||||
- filename = `cfg, sampler_name, steps, %F %H-%M-%S`
|
||||
- foldername = `ckpt_name, /, cfg`
|
||||
- > `v1-5-pruned / 8.0 / PREFIX-cfg-8.0-euler-20-2024-12-18 23-21-41-0001.png`
|
||||
|
||||
- prefix = `PREFIX`
|
||||
- filename = `"cfg", cfg`
|
||||
- foldername = `ckpt_name, /, cfg`
|
||||
- > `v1-5-pruned / 8.0 / PREFIX-cfg-8.0-0002.png`
|
||||
|
||||
## Node inputs
|
||||
|
||||
- `images` - The generated images.
|
||||
|
||||
Optional:
|
||||
- `positive_text_opt` - Optional string input for when using custom nodes for positive prompt text.
|
||||
- `negative_text_opt` - Optional string input for when using custom nodes for negative prompt text.
|
||||
- `positive_text_opt` - Optional string saved as `positive_text_opt` in job.json when `save_job_data`=True.
|
||||
- `negative_text_opt` - Optional string saved as `negative_text_opt` in job.json when `save_job_data`=True.
|
||||
|
||||
## Automatic date/time conversion in file/folder names
|
||||
|
||||
@@ -114,7 +129,7 @@ Disclaimer: Does not check for illegal characters entered in file or folder name
|
||||
Tested and working with default samplers, Efficiency nodes, UltimateSDUpscale, ComfyRoll, composer, NegiTools, and 45 other nodes.
|
||||
|
||||
#
|
||||
Quality and compression settings: default is 90, 100 will activate **lossless** for AVIF and WEBP only.
|
||||
Quality and compression settings: min = 1, default is **90**, max = 100 will activate **lossless** for AVIF and WEBP only.
|
||||
|
||||
Quick comparison of size per extension, for the same 512x512 picture, with similar visual quality:
|
||||
| Ext | Compression | Maker | Size | Compression |
|
||||
@@ -130,6 +145,20 @@ Quick comparison of size per extension, for the same 512x512 picture, with simil
|
||||
| avif | 60 | PIL | 47353 | 89% |
|
||||
| avif | 60 | Imagick | 33691 | 92% |
|
||||
|
||||
#
|
||||
PNG compression 0-9 is fixed at level 4 for the following reason: ***zero** compression benefits above level 4*, while saving time skyrockets. Test results below for a 2560x1440 image on an Intel i7-9700:
|
||||
| Compression | time | Size |
|
||||
| --- | --- | --- |
|
||||
| 0 | 0.5s | 10.6 MB |
|
||||
| 1 | 0.6s | 4.6 MB |
|
||||
| 2 | 0.6s | 4.5 MB |
|
||||
| 3 | 0.6s | 4.4 MB |
|
||||
| **4** | **0.7s** | **4.0 MB** |
|
||||
| 5 | 1.1s | 4.0 MB |
|
||||
| 6 | 1.9s | 4.0 MB |
|
||||
| 7 | 2.6s | 4.0 MB |
|
||||
| 9 | 10.5s | 3.9 MB |
|
||||
|
||||
|
||||
#
|
||||
About extensions WebP AVIF JPEG JXL: ComfyUI can only load PNG and WebP atm... Feel free to ask ComfyUI team to add support for AVIF/jpeg/JXL!
|
||||
@@ -161,7 +190,7 @@ To see node numbers in the UI, **enable the badge IDs**:
|
||||
</p>
|
||||
|
||||
#
|
||||
jobs.json sample: always generated and appended, not sure what it can be used for.
|
||||
jobs.json sample:
|
||||
<br>
|
||||
<p align="center">
|
||||
<img src="assets/save-image-extended-comfyui-jobs-example.png" />
|
||||
@@ -183,6 +212,34 @@ TODO:
|
||||
- [ ] 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?
|
||||
- [x] PIL.Image doesn ot respect compress_level value and always output max 9 compressed images.
|
||||
- [x] TODO: test import cv2 / OpenCV: https://github.com/python-pillow/Pillow/issues/5986 -> not faster then PIL
|
||||
|
||||
### release 2.83 💾
|
||||
- quality is now also used for PNG compression: 0-90 translates to 0-9 levels of compression
|
||||
|
||||
### release 2.82 💾
|
||||
- bugfix: removed extra debug lines
|
||||
- improvement: now can extract CLIP text
|
||||
- improvement: added tooltips
|
||||
|
||||
### release 2.81 💾
|
||||
- fix for pythongosssss/ComfyUI-Custom-Scripts Checkpoint Loader bug and other nodes that store values in a dict
|
||||
|
||||
### release 2.80 💾
|
||||
- tested OpenCV vs PIL for saving images: same speed
|
||||
- various typos fixed
|
||||
- counter can now be disabled: 0 digits
|
||||
|
||||
### release 2.79 💾
|
||||
- enhancement 28-request-handle-lora-names-like-checkpoint-names
|
||||
|
||||
### release 2.78 💾
|
||||
- fix 40-subdirectory-creation
|
||||
|
||||
### release 2.77 💾
|
||||
- fix in ckpt_path: will only build subfolders if ckpt_name is also used: https://github.com/audioscavenger/save-image-extended-comfyui/pull/38
|
||||
- fix for too much rounding of floats (did not affect actual value in prompt saved inside the image)
|
||||
|
||||
### release 2.76 💾
|
||||
- brought back resolution
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[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.76"
|
||||
version = "2.83"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["piexif", "imagecodecs", "pillow", "pillow-avif-plugin", "pillow-jxl-plugin"]
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
piexif
|
||||
# piexif
|
||||
# jpegxl support: imagecodecs will bring libjxl 0.9.0
|
||||
imagecodecs
|
||||
|
||||
|
||||
+247
-138
@@ -2,20 +2,24 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import numpy as np
|
||||
import numpy
|
||||
import locale
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import folder_paths
|
||||
import pprint
|
||||
import piexif
|
||||
import piexif.helper
|
||||
# import piexif
|
||||
# import piexif.helper
|
||||
|
||||
version = 2.76
|
||||
# import cv2 # not faster then PIL
|
||||
|
||||
version = 2.83
|
||||
|
||||
avif_supported = False
|
||||
jxl_supported = False
|
||||
|
||||
debug = False
|
||||
|
||||
# Avif is included in requirements.txt
|
||||
try:
|
||||
import pillow_avif
|
||||
@@ -56,19 +60,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,14 +83,13 @@ 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'
|
||||
|
||||
png_compress_level = 9
|
||||
avif_quality = 60
|
||||
webp_quality = 90
|
||||
jpeg_quality = 90
|
||||
@@ -93,7 +97,7 @@ 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 only works for jpeg, png anf TIFF, with like just 2% reduction in size; not used for PNG as it forces a level 9 compression.
|
||||
optimize_image = True
|
||||
|
||||
filename_prefix = 'ComfyUI'
|
||||
@@ -110,9 +114,10 @@ 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 but also translated to integers 0-9 for PNG compression level
|
||||
quality = 90
|
||||
named_keys = False
|
||||
|
||||
@@ -149,44 +154,46 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
return {
|
||||
'required': {
|
||||
'images': ('IMAGE', ),
|
||||
'filename_prefix': ('STRING', {'default': self.filename_prefix, 'multiline': False}),
|
||||
'filename_keys': ('STRING', {'default': self.filename_keys, 'multiline': True}),
|
||||
'foldername_prefix': ('STRING', {'default': self.foldername_prefix, 'multiline': False}),
|
||||
'foldername_keys': ('STRING', {'default': self.foldername_keys, 'multiline': True}),
|
||||
'delimiter': ('STRING', {'default': self.delimiter, 'multiline': False}),
|
||||
'filename_prefix': ('STRING', {'default': self.filename_prefix, 'multiline': False, 'tooltip': "Fixed string prefixed to file name"}),
|
||||
'filename_keys': ('STRING', {'default': self.filename_keys, 'multiline': True, 'tooltip': "Comma separated string with sampler parameters to add to filename. \n* Example: `sampler_name, scheduler, cfg, denoise` Added to filename in written order. \n* Example: also accepts `vae_name` `model_name` (upscale model), `ckpt_name` (checkpoint). \n* `resolution` also works. \n\n* ANY parameter name of any node will work. The same applies to `foldername_keys`"}),
|
||||
'foldername_prefix': ('STRING', {'default': self.foldername_prefix, 'multiline': False, 'tooltip': "Fixed string prefixed to subfolders"}),
|
||||
'foldername_keys': ('STRING', {'default': self.foldername_keys, 'multiline': True, 'tooltip': "Same rules as for `filename_keys`. Create subfolders by using `/` or `../` etc"}),
|
||||
'delimiter': ('STRING', {'default': self.delimiter, 'multiline': False, 'tooltip': "Any string you like. You can also use `/` to create subfolders"}),
|
||||
'save_job_data': ([
|
||||
'disabled',
|
||||
'prompt',
|
||||
'basic, prompt',
|
||||
'basic, sampler, prompt',
|
||||
'basic, models, sampler, prompt'
|
||||
], {'default': self.save_job_data}),
|
||||
'job_data_per_image': ('BOOLEAN', {"default": self.job_data_per_image}),
|
||||
'job_custom_text': ('STRING', {'default': self.job_custom_text, 'multiline': False}),
|
||||
'save_metadata': ('BOOLEAN', {'default': self.save_metadata}),
|
||||
], {'default': self.save_job_data, 'tooltip': "Saves information about each job as entries in a `jobs.json` text file, under the generated subfolder. \nMultiple options for its content: `prompt`, `basic data`, `sampler settings`, `loaded models`"}),
|
||||
'job_data_per_image': ('BOOLEAN', {"default": self.job_data_per_image, 'tooltip': "Saves individual job data file per image"}),
|
||||
'job_custom_text': ('STRING', {'default': self.job_custom_text, 'multiline': False, 'tooltip': "Custom string to save along with the job data"}),
|
||||
'save_metadata': ('BOOLEAN', {'default': self.save_metadata, 'tooltip': "Saves metadata into the image"}),
|
||||
'counter_digits': ('INT', {
|
||||
"default": self.counter_digits,
|
||||
"min": 1,
|
||||
"min": 0,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"display": "silder"
|
||||
"display": "silder",
|
||||
'tooltip': "umber of digits used for the image counter. `3` = image_001.png, based on highest number in the subfolder, ignores gaps. **Can be disabled** when == 0"
|
||||
}),
|
||||
'counter_position': (self.counter_positions, {'default': self.counter_position}),
|
||||
'one_counter_per_folder': ('BOOLEAN', {'default': self.one_counter_per_folder}),
|
||||
'image_preview': ('BOOLEAN', {'default': self.image_preview}),
|
||||
'output_ext': (self.output_exts, {'default': self.output_ext}),
|
||||
'counter_position': (self.counter_positions, {'default': self.counter_position, 'tooltip': "Image counter postition: image_001.png or 001_image.png"}),
|
||||
'one_counter_per_folder': ('BOOLEAN', {'default': self.one_counter_per_folder, 'tooltip': "Toggles one counter per subfolder, or resets when a parameter/prompt changes"}),
|
||||
'image_preview': ('BOOLEAN', {'default': self.image_preview, 'tooltip': "Turns the image preview on and off"}),
|
||||
'output_ext': (self.output_exts, {'default': self.output_ext, 'tooltip': "File extension: WEBP by default, AVIF, PNG, JXL, JPG, etc"}),
|
||||
'quality': ('INT', {
|
||||
"default": self.quality,
|
||||
"min": 1,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"display": "silder"
|
||||
}),
|
||||
'named_keys': ('BOOLEAN', {'default': self.named_keys}),
|
||||
"display": "silder",
|
||||
'tooltip': "Quality for JPEG/JXL/WebP/AVIF/J2K formats; Quality is relative to each format. \n* Example: AVIF 60 is same quality as WebP 90. \n* PNG compression is fixed at 4 and not affected by this. PNG compression times skyrocket above level 4 for zero benefits on filesize."
|
||||
}),
|
||||
'named_keys': ('BOOLEAN', {'default': self.named_keys, 'tooltip': "Prefix each value by its key name. Example: prefix-seed=123456-width=1024-cfg=5.0-0001.avif"}),
|
||||
},
|
||||
'optional': {
|
||||
'positive_text_opt': ('STRING', {'forceInput': True}),
|
||||
'negative_text_opt': ('STRING', {'forceInput': True}),
|
||||
'positive_text_opt': ('STRING', {'forceInput': True, 'tooltip': "Optional string saved as `positive_text_opt` in job.json when `save_job_data`=True"}),
|
||||
'negative_text_opt': ('STRING', {'forceInput': True, 'tooltip': "Optional string saved as `negative_text_opt` in job.json when `save_job_data`=True"}),
|
||||
},
|
||||
'hidden': {'prompt': 'PROMPT', 'extra_pnginfo': 'EXTRA_PNGINFO'},
|
||||
}
|
||||
@@ -220,6 +227,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
|
||||
@@ -249,30 +264,44 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
|
||||
# find_keys_recursively is a self-updating recursive method, that will update the dict found_values
|
||||
def find_keys_recursively(self, prompt={}, keys_to_find=[], found_values={}):
|
||||
# print(f"debug find_keys_recursively: keys_to_find={keys_to_find} found_values={found_values}")
|
||||
if debug: print(f"debug find_keys_recursively: keys_to_find={keys_to_find} found_values={found_values}")
|
||||
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':
|
||||
if debug: print(f"debug find_keys_recursively: found key={key}")
|
||||
if debug: print(f"debug find_keys_recursively: value={value}")
|
||||
# pythongosssss/ComfyUI-Custom-Scripts stores the value as a dict: value={'content': 'v1-5-pruned-emaonly.safetensors', 'image': 'checkpoints/v1-5-pruned-emaonly.jpg'}
|
||||
if isinstance(value, dict):
|
||||
if 'content' in value:
|
||||
value = value['content']
|
||||
else:
|
||||
value = ''
|
||||
|
||||
if key in ['ckpt_path','ckpt_name']:
|
||||
value_path = Path(value)
|
||||
if 'ckpt_path' in keys_to_find:
|
||||
found_values['ckpt_path'] = self.cleanup_fileName(str(value_path.parent))
|
||||
else:
|
||||
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':
|
||||
if debug: print(f"debug find_keys_recursively: ckpt_name={value_path.name} ckpt_path={str(value_path.parent)}")
|
||||
elif key in ['control_net_path','control_net_name']:
|
||||
value_path = Path(value)
|
||||
if 'control_net_path' in keys_to_find:
|
||||
found_values['control_net_path'] = self.cleanup_fileName(str(value_path.parent))
|
||||
else:
|
||||
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
|
||||
@@ -285,7 +314,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
def find_parameter_values(self, target_keys, prompt={}, found_values={}):
|
||||
loras_string = ''
|
||||
for key, value in prompt.items():
|
||||
# print(f"debug find_parameter_values: key={key} value={value}")
|
||||
if debug: print(f"debug find_parameter_values: key={key} value={value}")
|
||||
if 'loras' in target_keys:
|
||||
# Match both formats: lora_xx and lora_name_x
|
||||
if re.match(r'lora(_name)?(_\d+)?', key):
|
||||
@@ -310,23 +339,36 @@ 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, 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):
|
||||
if '%' in prefix:
|
||||
custom_name = timestamp.strftime(prefix)
|
||||
else:
|
||||
custom_name = prefix
|
||||
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 = {}
|
||||
# print(f"debug generate_custom_name: --prefix: {prefix}")
|
||||
# print(f"debug generate_custom_name: --keys_to_extract: {keys_to_extract}")
|
||||
# print(f"debug generate_custom_name: --prompt:")
|
||||
if debug: print(f"debug generate_custom_name: --prefix: {prefix}")
|
||||
if debug: print(f"debug generate_custom_name: --keys_to_extract: {keys_to_extract}")
|
||||
if debug: print(f"debug generate_custom_name: --prompt:")
|
||||
|
||||
# now separating numbered keys from non-numbered keys:
|
||||
# 37.ckpt_name = i want the ckpt_name from node #37
|
||||
@@ -342,16 +384,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
|
||||
@@ -359,12 +417,14 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
# key has the form num.widget_name like 123.widget_name, we will then look for widget_name value in node #123
|
||||
node, nodeKey = splitKey[0], splitKey[1]
|
||||
if node in prompt:
|
||||
# print(f"debug generate_custom_name: --node.nodeKey = {node}.{nodeKey}")
|
||||
if debug: print(f"debug generate_custom_name: --node.nodeKey = {node}.{nodeKey}")
|
||||
# splitKey[0] = #node number found in prompt, we will recurse only in that node:
|
||||
if nodeKey == 'ckpt_path':
|
||||
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:
|
||||
@@ -374,6 +434,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:
|
||||
@@ -382,28 +444,27 @@ 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)
|
||||
elif nodeKey == 'resolution':
|
||||
value = resolution
|
||||
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 debug: print(f"debug generate_custom_name: ----nodeKey: {nodeKey}")
|
||||
if debug: print(f"debug generate_custom_name: ----value: {value}")
|
||||
if debug: print(f"debug generate_custom_name: ----found_values: {found_values}")
|
||||
if value is None:
|
||||
if nodeKey is not None:
|
||||
if nodeKey in found_values:
|
||||
@@ -416,50 +477,51 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
|
||||
# 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
|
||||
if debug: print(f"debug generate_custom_name: ----value: {value}")
|
||||
|
||||
# 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...
|
||||
if debug: 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= ''
|
||||
value = self.cleanup_fileName(value)
|
||||
elif isinstance(value, float):
|
||||
# value = round(float(value), 1) # too much rounding
|
||||
value = float(f'{value:.10g}')
|
||||
|
||||
# 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 = ''
|
||||
|
||||
if isinstance(value, float):
|
||||
value = round(float(value), 1)
|
||||
|
||||
custom_name += f"{delim}{value}"
|
||||
# print(f"debug generate_custom_name: ------custom_name: {custom_name}")
|
||||
custom_name.append(str(value))
|
||||
if debug: 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))
|
||||
# strip each item
|
||||
custom_name = list(map(str.strip, custom_name))
|
||||
# join
|
||||
stringName = delimiter.join(custom_name).replace('/'+delimiter, '/').replace(delimiter+'/', '/').replace(delimiter+'.', '.')
|
||||
# clean and remove line feeds
|
||||
stringName = re.sub('\s+',' ',stringName).strip(delimiter).strip('/').strip(delimiter).strip('.')
|
||||
|
||||
if debug: print(f"debug generate_custom_name: ------custom_name: {custom_name}")
|
||||
if debug: 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 = {}
|
||||
@@ -555,7 +617,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))
|
||||
@@ -565,8 +635,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
|
||||
@@ -635,8 +710,14 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
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):
|
||||
if debug: 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]
|
||||
@@ -648,13 +729,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/
|
||||
@@ -663,26 +744,49 @@ 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)
|
||||
kwargs["compress_level"] = self.png_compress_level
|
||||
kwargs["optimize"] = self.optimize_image
|
||||
if save_metadata: kwargs["pnginfo"] = self.genMetadataPng(img, prompt, extra_pnginfo)
|
||||
|
||||
# png/gif: no quality, rather we convert quality to compression level in the 0-9 range
|
||||
old_min = 0
|
||||
old_max = 90
|
||||
new_min = 0
|
||||
new_max = 9
|
||||
if quality >= 91: quality = 90
|
||||
png_compress_level = round( ( (quality - old_min) / (old_max - old_min) ) * (new_max - new_min) + new_min )
|
||||
|
||||
kwargs["compress_level"] = png_compress_level
|
||||
# BUG: PIL will compress at level 9 when PNG optimize_image = True
|
||||
# kwargs["optimize"] = self.optimize_image
|
||||
# elif output_ext in ['.bmp']:
|
||||
# nothing to add
|
||||
|
||||
# img.save(image_path, pnginfo=metadata, compress_level=self.png_compress_level)
|
||||
# BUG: PIL.Image doesn't respect compress_level value and always output max 9 compressed images when optimize_image = True
|
||||
# img.save(image_path, pnginfo=metadata, compress_level=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, 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,
|
||||
@@ -733,7 +837,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
##########################################################################
|
||||
# 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()
|
||||
@@ -748,27 +852,30 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR still not merged.
|
||||
|
||||
output_path = Path(os.path.join(self.output_dir, custom_foldername, custom_filename)).parent
|
||||
filename = Path(os.path.join(self.output_dir, custom_foldername, custom_filename)).name
|
||||
# print(f"debug save_images: custom_foldername= {custom_foldername}")
|
||||
# print(f"debug save_images: custom_filename= {custom_filename}")
|
||||
# print(f"debug save_images: output_path= {output_path}")
|
||||
# print(f"debug save_images: filename= {filename}")
|
||||
if debug: print(f"debug save_images: custom_foldername= {custom_foldername}")
|
||||
if debug: print(f"debug save_images: custom_filename= {custom_filename}")
|
||||
if debug: print(f"debug save_images: output_path= {output_path}")
|
||||
if debug: print(f"debug save_images: filename= {filename}")
|
||||
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
counter = self.get_latest_counter(one_counter_per_folder, output_path, filename, counter_digits, counter_position, output_ext)
|
||||
# print(f"debug save_images: counter for {output_ext}: {counter}")
|
||||
if debug: print(f"debug save_images: counter for {output_ext}: {counter}")
|
||||
|
||||
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)
|
||||
@@ -787,6 +894,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,
|
||||
@@ -794,7 +903,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
'SaveImageExtended': '💾 Save Image Extended',
|
||||
'SaveImageExtended': f'💾 Save Image Extended {version}',
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user