Compare commits

...
2 Commits
Author SHA1 Message Date
audioscavenger 906d2b9669 release 2.50 JXL
modified:   README.md
	modified:   assets/save-image-extended-comfyui-example.png
	modified:   save_image_extended.py
2024-05-23 09:20:58 -07:00
audioscavenger 53b9270e12 release 2.46
- bug discovered with *rgthree's Ksampler Config*: using `steps_total` as an input to a Ksampler, will issue the index of the output, instead of the steps value ("\[nodeNum, 0]" instead of steps value). FIX: use `steps_total` instead of `steps`!
- uncommented `__all__` in init.py
- potential bugfix in splitKey, `len(splitKey) = 2` to identify actual "node.widget" format
	modified:   README.md
	modified:   save_image_extended.py
2024-05-21 07:29:10 -07:00
3 changed files with 70 additions and 32 deletions
+27 -18
View File
@@ -1,5 +1,5 @@
# 💾 Save Image Extended for ComfyUI
**AVIF** and **WebP** support!
**AVIF** / **WebP** / **JXL** support!
<p align="center">
<img src="assets/save-image-extended-comfyui-example.png" />
@@ -7,8 +7,9 @@
* Customize the folder, sub-folders, and filenames of your images!
* Save data about the generated job (sampler, prompts, models) as entries in a `json` (text) file, in each folder.
* Use the values of ANY node's widget, by simply adding its badge number in the form _id.widget_name_:
* Oh btw... also saves your output as **WebP** / **JPEG** / **AVIP** ... And yes the prompt is included :) ComfyUI can load it but a PR approval is needed to fix a bug on their side.
* Use the values of ANY node's widget, by simply adding its badge number in the form _id.widget_name_
* Oh btw... also saves your output as **AVIF** / **WebP** / **JPEG** / **JXL** ... And yes the prompt is included :)
* ComfyUI can only load PNG and WebP atm
<br>
@@ -33,10 +34,15 @@ There is a requirements.txt that will take care of that, but just in case:
pip install piexif pillow pillow-avif-plugin
```
For Jpeg XL / jxl it's more complicated. You need to install and compile the wheel jxlpy, and therefore, need a valid and functional MSVC installation.
```
pip install jxlpy
```
### Manual Download
1. Open a terminal inside the 'custom_nodes' folder located in your ComfyUI installation dir
2. Use the `git clone` command to clone the [save-image-extended-comfyui](https://github.com/audioscavenger/save-image-extended-comfyui) repo.
2. Use the `git clone` command to clone the [save-image-extended-comfyui](https://github.com/audioscavenger/save-image-extended-comfyui) repo under ComfyUI\custom_nodes\
```
git clone https://github.com/audioscavenger/save-image-extended-comfyui
```
@@ -59,7 +65,7 @@ git clone https://github.com/audioscavenger/save-image-extended-comfyui
| `image_preview` | Turns the image preview on and off. |
| `output_ext` | File extension: PNG by default, or WEBP (coming soon). |
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 custom strings: if you enter `wrongNumber.attribute`, you will get `attribute` in your filename.
## Node inputs
@@ -82,13 +88,14 @@ Quality and compression settings:
* AVIF quality is fixed at 60.
* WebP quality is fixed at 75.
* JPEG quality is fixed at 91.
* JXL quality is fixed at 91.
* PNG is maxed compressed (9)
Start a discussion or a poll if you want to change output quality, and add another attribute in the node. There's so many already, some complain about the node being too tall.
#
About extensions WebP AVIF JPEG: ComfyUI cannot load it atm... Feel free to ask ComfyUI team to add support for AVIF/WebP/jpeg!
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!
The prompt is included under the **EXIF** tag `UserComment` (IFD0 / 0x9286) as defined [here](https://exiftool.org/TagNames/EXIF.html).
It is saved in this form: `UserComment = {"prompt": {"1": {"inputs": {...}}}}`.
@@ -102,15 +109,15 @@ You can retrieve the prompt manually with [exiftool](https://exiftool.org/), her
Incompatible with *extended-saveimage-comfyui* - This node can be safely discarded, as it only offers WebP output. My node already adds JPEG and WebP.
#
You asked for it... Now you can select which node to get the widget values from! Formerly, this custom node would simply return the last value found: useles if you have many Ksamplers...
Make sure you enable the badge IDs to benefit from this:
You asked for it... Now you can select which node to get the widget values from! Formerly, this custom node would simply return the last value found: useless if you have multiple same nodes...
To see node numbers in the UI, **enable the badge IDs**:
<br>
<p align="center">
<img src="assets/ComfyUI-enable-badge-ids.png" />
</p>
#
jobs.json sample:
jobs.json sample: always generated and appended, not sure what it can be used for.
<br>
<p align="center">
<img src="assets/save-image-extended-comfyui-jobs-example.png" />
@@ -121,12 +128,11 @@ jobs.json sample:
## RoadMap
I won't promise anything, just like @thedyze did not promise anything when they released this custom node.
Then disappeared for good 3 months later. That's fine, I do that too somtimes :)
However, I do provide a way to contact me, and will accept PR and collabs.
I won't promise you the moon, but since I use this node myself, I will maintain it as much as I can. I do provide a way to contact me, and will accept PR and collabs.
Once I feel like I don't have time to work on it, I will gladly transfer ownership or let collabs maintain it.
TODO:
- [ ] ComfyRoll CR XY Save Grid Image: it offers jpeg webp tif - check how it embeds prompt and see if that works better with Comfy!
- [ ] offer quality setting in the node?
- [ ] remove save_job_to_json? thisis pretty much useless actually, since it only saves the last value found for each node.
@@ -136,9 +142,12 @@ Once I feel like I don't have time to work on it, I will gladly transfer ownersh
- [ ] offer to place the counter anywhere, as a key in filename_keys
- [ ] files can get out of order if prefixes change... that is expected, but is this what we want? another reason to have the counter place anywhere we want
### release 2.50 💾
- added JXL support
### release 2.46 💾
- bug discovered with *rgthree's Ksampler Config*: using `steps_total` as an input to a Ksampler, will issue the index of the output, instead of the steps value ("\[nodeNum, 0]" instead of steps value). FIX: use `steps_total` instead of `steps`!
- uncommented \_\_all\_\_ in init.py
- uncommented `__all__` in init.py
- potential bugfix in splitKey, `len(splitKey) = 2` to identify actual "node.widget" format
### release 2.45 💾
@@ -147,10 +156,10 @@ Once I feel like I don't have time to work on it, I will gladly transfer ownersh
### release 2.44
- so many bugfixes
- complete rework of generate_custom_name to handle ALL the possible scenarios
- [x] bugfix: when using /name in foldername_keys, Comfy thinks you want to save outside the output folder
- [x] bugfix: when using `/name` in foldername_keys, Comfy thinks you want to save outside the output folder
### release 2.43
- [x] support for AVIF
- [x] support for **AVIF**
- [x] added requirements.txt
### release 2.42
@@ -161,8 +170,8 @@ Once I feel like I don't have time to work on it, I will gladly transfer ownersh
- [x] WebP is indeed loaded properly like a PNG, if you apply the patch above to `pnginfo.js and `app.js`
### release 2.4
- [x] integrate webp
- [x] integrate jpg
- [x] integrate **webp**
- [x] integrate **jpeg**
### release 2.3
- [x] for each keys, we return only the last value found in the prompt. Not the last Ksampler. Impossible to know which one is the last. Therefore, simply use this syntax: number.widget_name
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

+43 -14
View File
@@ -11,10 +11,28 @@ import pprint
import piexif
import piexif.helper
avif_supported = False
jxl_supported = False
# Avif is included in requirements.txt
try:
import pillow_avif
except ModuleNotFoundError:
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")
pass
else:
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
try:
from jxlpy import JXLImagePlugin
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")
pass
else:
jxl_supported = True
# PIL must be loaded after pillow_avif
from PIL import Image, ExifTags
@@ -26,13 +44,15 @@ original_locale = locale.setlocale(locale.LC_TIME, '')
# class SaveImageExtended -------------------------------------------------------------------------------
class SaveImageExtended:
version = 2.46
version = 2.50
type = 'output'
png_compress_level = 9
lossless = False
avif_quality = 60
webp_quality = 75
jpeg_quality = 91
jxl_quality = 91
filename_prefix = 'ComfyUI'
filename_keys = 'sampler_name, scheduler, cfg, steps'
@@ -49,15 +69,15 @@ class SaveImageExtended:
one_counter_per_folder = True
image_preview = True
extToRemove = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
output_ext = '.webp'
output_exts = ['.webp', '.png', '.jpg']
print(f"\033[92m[save_image_extended]\033[0m version: {version}\033[0m")
if pillow_avif not in sys.modules:
output_ext = '.avif'
output_exts = ['.avif', '.png', '.webp', '.jpg']
print(f"\033[92m[save_image_extended] AVIF is supported! Woohoo!\033[0m\n")
else:
output_ext = '.png'
output_exts = ['.png', '.webp', '.jpg']
if jxl_supported:
output_exts.insert(0, '.jxl')
# if pillow_avif not in sys.modules:
if avif_supported:
output_exts.insert(0, '.avif')
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@@ -136,7 +156,7 @@ class SaveImageExtended:
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
if not os.path.exists(folder_path):
print(f"SaveImageExtended error: Folder {folder_path} does not exist, starting counter at 1.")
print(f"SaveImageExtended {version} error: Folder {folder_path} does not exist, starting counter at 1.")
return counter
try:
@@ -154,7 +174,7 @@ class SaveImageExtended:
counter = max(counters) + 1
except Exception as e:
print(f"SaveImageExtended error: An error occurred while finding the latest counter: {e}")
print(f"SaveImageExtended {version} error: An error occurred while finding the latest counter: {e}")
return counter
@@ -388,7 +408,7 @@ class SaveImageExtended:
with open(json_file_path, 'r') as f:
existing_data = json.load(f)
except json.JSONDecodeError:
print(f"SaveImageExtended error: The file {json_file_path} is empty or malformed. Initializing with empty data.")
print(f"SaveImageExtended {version} error: The file {json_file_path} is empty or malformed. Initializing with empty data.")
existing_data = {}
timestamp = datetime.now().strftime('%c')
@@ -433,6 +453,9 @@ class SaveImageExtended:
dump = json.dumps(metadata)
# print(f"dump={dump}") {"prompt": { .. }, "workflow": { .. }}
exif[ExifTags.Base.UserComment] = json.dumps(metadata)
# This should work the same:
# exif[0x9286] = json.dumps(metadata)
# exif[ExifTags.Base.UserComment] = piexif.helper.UserComment.dump(json.dumps(metadata), encoding="unicode") # type 4
# exif[ExifTags.Base.UserComment] = piexif.helper.UserComment.dump(json.dumps(metadata), encoding="jis") # type 1
# exif[ExifTags.Base.UserComment] = piexif.helper.UserComment.dump(json.dumps(metadata), encoding="ascii") # type 1
@@ -477,16 +500,22 @@ class SaveImageExtended:
match output_ext:
case '.avif':
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["lossless"] = self.lossless
kwargs["quality"] = self.avif_quality
case '.webp':
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["lossless"] = False
kwargs["lossless"] = self.lossless
kwargs["quality"] = self.webp_quality
if self.webp_quality == 100: kwargs["lossless"] = True
case '.jpg':
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = self.jpeg_quality
case '.jxl':
kwargs["lossless"] = self.lossless
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = self.jxl_quality
case _:
# png: no quality
if save_metadata: kwargs["pnginfo"] = self.get_metadata_png(img, prompt, extra_pnginfo)
kwargs["compress_level"] = self.png_compress_level
# png
@@ -582,7 +611,7 @@ class SaveImageExtended:
self.save_job_to_json(save_job_data, prompt, filename_prefix, positive_text_opt, negative_text_opt, job_custom_text, resolution, output_path, 'jobs.json')
except OSError as e:
print(f"SaveImageExtended error: An error occurred while creating the subfolder or saving the image: {e}")
print(f"SaveImageExtended {version} error: An error occurred while creating the subfolder or saving the image: {e}")
else:
if not image_preview:
results = list()