Compare commits

...
9 Commits
Author SHA1 Message Date
audioscavenger fd8388e9d8 release 2.87 right-click
- finally! right-click any node with IMAGE output adds "Add SaveImageExtended" option
    - upgraded purify.min.js
    	modified:   README.md
    	modified:   pyproject.toml
    	modified:   save_image_extended.py
    	modified:   web/js/contextmenu.js
    	modified:   web/js/purify.min.js
	modified:   pyproject.toml
	modified:   save_image_extended.py
2025-09-01 18:36:17 -07:00
audioscavenger 1a39e8b0f7 release 2.87 right-click
- finally! right-click any node with IMAGE output adds "Add SaveImageExtended" option
- upgraded purify.min.js
	modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
	modified:   web/js/contextmenu.js
	modified:   web/js/purify.min.js
2025-09-01 18:19:40 -07:00
audioscavenger 29971dc90f release 2.86 bugfix in counter_digits which does not accept 0 to disable counter
modified:   README.md
    	modified:   pyproject.toml
    	modified:   save_image_extended.py
2025-08-02 10:26:40 -07:00
audioscavenger c87b890b32 release 2.86 bugfix in counter_digits which does not accept 0 to disable counter
modified:   README.md
    	modified:   pyproject.toml
    	modified:   save_image_extended.py
	modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
2025-08-02 10:18:18 -07:00
audioscavenger a7a2dffaf9 release 2.85 fix issue/51
modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
2025-06-30 16:52:09 -07:00
Eric 37ee3483e1 Merge pull request #51 from zhouyc98/main
fix: #31 #49 API mode KeyError
2025-06-30 16:48:23 -07:00
audioscavenger 1fc32e0ad3 release 2.84 issue-48
- issue/48: allow no prefix and no filename_keys at all to generate 0001.jpg files
- one_counter_per_folder is depracated as it was in fact, not implemented at all. Cannot remove it or all previous saves will crash the widget
	modified:   README.md
	modified:   pyproject.toml
	modified:   save_image_extended.py
2025-06-01 19:01:14 -07:00
周育丞 8e9b750840 fix: #31 #49 API mode KeyError 2025-04-10 17:09:30 +08:00
audioscavenger 54b30b9c1a release 2.83 fixed PNG save time by fixing compression to 4 + cleanups, updated wiki
modified:   README.md
        	modified:   pyproject.toml
        	modified:   save_image_extended.py
    	modified:   requirements.txt
    	modified:   save_image_extended.py
	modified:   README.md
	modified:   requirements.txt
	modified:   save_image_extended.py
2025-01-05 12:27:21 -07:00
11 changed files with 249 additions and 141 deletions
+58 -34
View File
@@ -19,12 +19,12 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
<br><br>
</p>
## Parameters / Usage
## Parameters / Reserved Keywords
| Attribute | Description |
| --- | --- |
| `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`. |
| `filename_prefix` | **Static 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. `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. |
@@ -38,16 +38,27 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
| `image_preview` | Turns the image preview on and off. |
| `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. |
| `resolution` | Saved image reolution in format `000x000` (pixels).
| `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 fixed strings
- if you enter `wrongNumber.attribute`, you will get `attribute` in your filename.
- ANY other keyword that exist as a field name in a widget will be resolved.
- That is why I do not list all common keywords like `ckpt_name` etc, as they are virtually infinite and exist only within your workflow.
- Unknown key names not found in any widget's fields are treated as fixed strings
- if you enter `wrongNumber.attribute`, you will also get `attribute` as a fixed string.
- 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
### FAQ
* How do I format the date/timestamps?
* see [unix datetime formats](https://www.man7.org/linux/man-pages/man1/date.1.html)
* How do I reference the prompt (aka CLIP Text Encode) in the filename (given it's not too long)?
* Like this: use `#num.text` ![CLIP_save_prompt](assets/save-image-extended-comfyui-CLIP_save_prompt.png)
* How do I show the nodes ID badges?
* legacy: ![ComfyUI-enable-badge-ids-legacy](assets/ComfyUI-enable-badge-ids-legacy.png)
* modern: ![ComfyUI-enable-badge-ids-legacy](assets/ComfyUI-enable-badge-ids-modern.png)
### Examples
- prefix = `PREFIX`
- filename = `cfg, sampler_name, /steps, %F %H-%M-%S`
@@ -72,9 +83,9 @@ Optional:
- `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
## How To Date/Time conversion in file/folder names:
Converts [unix datetime formats](https://www.man7.org/linux/man-pages/man1/date.1.html):
Converts [unix datetime formats](https://www.man7.org/linux/man-pages/man1/date.1.html) following POSIX format. Examples:
| Unix datetime | Example | Comment |
| --- | --- | --- |
@@ -91,22 +102,21 @@ Converts [unix datetime formats](https://www.man7.org/linux/man-pages/man1/date.
### Requirements:
There is a requirements.txt that will take care of that, but just in case:
- python 10.6
- piexif
- imagecodecs
- python 10.6+
- numpy
- pillow
- pillow-avif-plugin
- pillow-jxl-plugin
```
pip install piexif pillow pillow-avif-plugin
pip install numpy pillow pillow-avif-plugin pillow-jxl-plugin
```
For Jpeg XL / jxl it's more complicated. You cannot compile the wheel jxlpy on Windows. Therefore, we use an alternative: [imagecodecs](https://pypi.org/project/imagecodecs/)
### Tested on / up to:
* python 3.10.6
* pytorch 2.5.1+cu124
* ComfyUI v0.3.12-1-g7fc3ccdc | Released on '2025-01-16'
```
pip install -U imagecodecs
```
### Manual Download
1. Open a terminal inside the 'custom_nodes' folder located in your ComfyUI installation dir
@@ -115,14 +125,17 @@ pip install -U imagecodecs
git clone https://github.com/audioscavenger/save-image-extended-comfyui
```
## Known Bugs
* ComfyUI cannot load lossless WebP atm. Feel free to try and fix `pnginfo.js`.
* Pillow cannot save Exif data in JPEG2000, nor can it compress it in any way. Who the heck is using JPEG2000 in 2024 anyway?
* Incompatible with *extended-saveimage-comfyui* - This node can be safely discarded, as it only offers WebP output. My node adds all major and latest image formats.
* WebP cannot handle images larger than 12,000 x 12,0000 pixels and that's by design. Ask a Google developer to fix the cwebp library.
* JXL (pillow-jxl-plugin) cannot even save images 12,000 x 12,0000 pixels and will crash Comfy completely.
* [Load Image //Inspire](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) will keep loaded images as base64 inside the prompt... exploding your output filesize. JPEG has a metadata size limit and will likely fail with error `EXIF data is too long`.
## Miscelaneous
#
[JPEG XL is a heated debate on chromium forum](https://issues.chromium.org/issues/40168998#comment85) and if true indeed that Google is working on WebP2, jxl is unlikely to take off any day soon. Proponents arguably declare with no proof, that jxl is better and faster than the current best codec: AVIF. But again, without support from the industry, it's going nowhere.
I tested with compression 90 and it's good, with a caveat. The compression offered by pillow is 3x lower then Image Magick for the same level. No idea why.
#
Pillow cannot save Exif data in JPEG2000, nor can it compress it in any way. Who the heck is using JPEG2000 in 2024 anyway?
#
Disclaimer: Does not check for illegal characters entered in file or folder names. May not be compatible with every other custom node, depending on changes in the `prompt` object.
@@ -176,18 +189,10 @@ You can retrieve the prompt manually with [exiftool](https://exiftool.org/), her
- `exiftool -Parameters -UserComment -ImageDescription image.{jpg|jpeg|webp|avif|jxl}`
#
ComfyUI cannot load lossless WebP atm. Feel free to try and fix `pnginfo.js`
[JPEG XL is a heated debate on chromium forum](https://issues.chromium.org/issues/40168998#comment85) and if true, that Google is working on WebP2, JXL is unlikely to take off any day soon. Proponents arguably declare with no proof, that jxl is better and faster than the current best codec: AVIF. But again, without support from the industry, it's going nowhere.
#
Incompatible with *extended-saveimage-comfyui* - This node can be safely discarded, as it only offers WebP output. My node already adds JPEG and WebP.
I tested with compression 90 and it's good compared to WebP, with a caveat. The compression offered by pillow is 3x lower then Image Magick for the same level. No idea why.
#
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:
@@ -207,16 +212,35 @@ Once I feel like I don't have time to work on it, I will gladly transfer ownersh
TODO:
- [ ] WebP: explore method values 0-6 and test filesize vs save time
- [ ] WebP: quality has an impact on both lossy and lossless, more testing needed
- [ ] review accepted parameters at https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html to improve TIFF and other formats
- [ ] 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?
- [x] PIL.Image doesn ot respect compress_level value and always output max 9 compressed images.
- [x] PIL.Image doesn't respect compress_level value and always output max 9 compressed images -> when optimize_image = True! So we turn that off for PNG
- [x] TODO: test import cv2 / OpenCV: https://github.com/python-pillow/Pillow/issues/5986 -> not faster then PIL
### release 2.87 💾
- finally! right-click any node with IMAGE output adds "Add SaveImageExtended" option
- upgraded purify.min.js
### release 2.86 💾
- bugfix in counter_digits which does not accept 0 to disable counter
### release 2.85 💾
- issue/51: #31 #49 API mode KeyError - fix by adding empty {} to the metadata for the API call
### release 2.84 💾
- issue/48: allow no prefix and no filename_keys at all to generate 0001.jpg files
- one_counter_per_folder is depracated as it was in fact, not implemented at all. Cannot remove it or all previous saves will crash the widget
### release 2.83 💾
- quality is now also used for PNG compression: 0-90 translates to 0-9 levels of compression
- requirements.txt and import cleanups, updated wiki
### release 2.82 💾
- bugfix: removed extra debug lines

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+2 -2
View File
@@ -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.83"
version = "2.87"
license = { file = "LICENSE" }
dependencies = ["piexif", "imagecodecs", "pillow", "pillow-avif-plugin", "pillow-jxl-plugin"]
@@ -12,4 +12,4 @@ Repository = "https://github.com/audioscavenger/save-image-extended-comfyui"
[tool.comfy]
PublisherId = "audioscavenger"
DisplayName = "save-image-extended-comfyui"
Icon = ""
Icon = "💾"
+3 -2
View File
@@ -1,7 +1,8 @@
# piexif
# piexif # for testing
# jpegxl support: imagecodecs will bring libjxl 0.9.0
imagecodecs
# imagecodecs # pillow-jxl-plugin is simpler
numpy
pillow
pillow-avif-plugin
pillow-jxl-plugin
+128 -93
View File
@@ -2,23 +2,25 @@ import os
import re
import sys
import json
import numpy
import locale
from datetime import datetime
from pathlib import Path
import folder_paths
import pprint
import numpy
# import piexif
# import piexif.helper
# import cv2 # not faster then PIL
version = 2.83
version = 2.87
avif_supported = False
jxl_supported = False
debug = False
# debug = True
# Avif is included in requirements.txt
try:
@@ -31,8 +33,9 @@ else:
avif_supported = True
# Jxl requires jxlpy wheel to be compiled, and a valid MSVC environment, which is complex task
# Therefore we use pillow_jxl which makes more sense
try:
# jxlpy is in early stages of development. None one has ever compiled it on Windows AFAIK
# jxlpy is in early stages of development. No one has ever compiled it on Windows in 2025 AFAIK, please prove me wrong
# from jxlpy import JXLImagePlugin
# from imagecodecs import (jpegxl_encode, jpegxl_decode, jpegxl_check, jpegxl_version, JPEGXL)
import pillow_jxl
@@ -54,6 +57,71 @@ original_locale = locale.setlocale(locale.LC_TIME, '')
# class SaveImageExtended -------------------------------------------------------------------------------
class SaveImageExtended:
"""
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.
Returns: `dict`:
- Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required`
- Value input_fields (`dict`): Contains input fields config:
* Key field_name (`string`): Name of a entry-point method's argument
* Value field_config (`tuple`):
+ First value is a string indicate the type of field or a list for selection.
+ Secound value is a config for type "INT", "STRING" or "FLOAT".
"""
@classmethod
def INPUT_TYPES(self):
# this is checked by ComfyUI/execution.py: validate_inputs(prompt, item, validated):
# must also define VALIDATE_INPUTS(self) so we can fill in missing inputs when new inputs are added by new versions
return {
'required': {
'images': ('IMAGE', ),
'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, '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": 0,
"max": 8,
"step": 1,
"display": "silder",
'tooltip': "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': (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': "deprecated but I cannot remove it of all your saved prompts will break"}),
'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": 0,
"max": 100,
"step": 1,
"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, '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'},
}
RETURN_TYPES = ()
FUNCTION = 'save_images'
OUTPUT_NODE = True
@@ -112,7 +180,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
counter_digits = 4
counter_position = 'last'
counter_positions = ['last', 'first']
one_counter_per_folder = True
one_counter_per_folder = True # deprecated but cannot remove it or all values loaded after it will be shifted
image_preview = True
modelExtensions = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
output_ext = '.webp'
@@ -133,70 +201,6 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
self.output_dir = folder_paths.get_output_directory()
self.prefix_append = ''
"""
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.
Returns: `dict`:
- Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required`
- Value input_fields (`dict`): Contains input fields config:
* Key field_name (`string`): Name of a entry-point method's argument
* Value field_config (`tuple`):
+ First value is a string indicate the type of field or a list for selection.
+ Secound value is a config for type "INT", "STRING" or "FLOAT".
"""
@classmethod
def INPUT_TYPES(self):
# this is checked by ComfyUI/execution.py: validate_inputs(prompt, item, validated):
# must also define VALIDATE_INPUTS(self) so we can fill in missing inputs when new inputs are added by new versions
return {
'required': {
'images': ('IMAGE', ),
'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, '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": 0,
"max": 8,
"step": 1,
"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, '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": 0,
"max": 100,
"step": 1,
"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, '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'},
}
# This class serves no purpose, it can only test 1 element. you always get all errors even if only one element is bad
# @classmethod
@@ -236,22 +240,37 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
# ██████ ██████ ██████ ██ ████ ██ ███████ ██ ██
# 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):
def get_latest_counter(self, folder_path, filename, counter_digits=counter_digits, counter_position=counter_position, output_ext=output_ext):
counter = 1
if not os.path.exists(folder_path):
print(f"SaveImageExtended {version} error: Folder {folder_path} does not exist, starting counter at 1.")
return counter
try:
# grab all files with output_ext
files = [file for file in os.listdir(folder_path) if file.endswith(output_ext)]
# ['0001.webp', '0003-a.webp', 'AnythingV5_inkBase-0001.webp', 'AnythingV5_inkBase-0002.webp']
extLen = len(output_ext)
# '.webp' = 5
if files:
# get default counter position if passed value is somehow not in the allowed values list
if counter_position not in self.counter_positions: counter_position = self.counter_position
if counter_position == 'last':
# BUG: this works only if extension is 3 letters like png, this will break with webp and avif:
counters = [int(file[-(extLen + counter_digits):-extLen]) if file[-(extLen + counter_digits):-extLen].isdecimal() else 0 for file in files if one_counter_per_folder or file.startswith(filename_prefix)]
# issue/48 needs a special case for files, counter_position does not matter here, but the filename length does
if not filename:
counters = [int(file[:counter_digits]) if (file[:counter_digits].isdecimal() and len(file)==(counter_digits+extLen)) else 0 for file in files]
# [1, 0, 0, 0]
else:
counters = [int(file[:counter_digits]) if file[:counter_digits].isdecimal() else 0 for file in files if one_counter_per_folder or file[counter_digits +1:].startswith(filename_prefix)]
if counter_position == 'last':
counters = [int(file[-(extLen + counter_digits):-extLen]) if file[-(extLen + counter_digits):-extLen].isdecimal() else 0 for file in files if file.startswith(filename)]
# [1, 0, 1, 2]
else:
# file[:counter_digits] = '0001'
# file[counter_digits +1:] = 'webp'
counters = [int(file[:counter_digits]) if file[:counter_digits].isdecimal() else 0 for file in files if file[counter_digits +1:].startswith(filename)]
# [1, 3, 0, 0]
if counters:
counter = max(counters) + 1
@@ -259,6 +278,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
except Exception as e:
print(f"SaveImageExtended {version} error: An error occurred while finding the latest counter: {e}")
if debug: print(f"debug get_latest_counter: counter={counter}")
return counter
@@ -674,7 +694,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
# https://exiftool.org/TagNames/EXIF.html
# exif[0x9286] = "Prompt: " + json.dumps(metadata['prompt']) # UserComment
exif[0x010f] = "Prompt: " + json.dumps(metadata['prompt']) # Make
exif[0x010e] = "Workflow: " + json.dumps(metadata['workflow']) # ImageDescription
exif[0x010e] = "Workflow: " + json.dumps(metadata.get('workflow', {})) # ImageDescription
# 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
@@ -734,6 +754,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
kwargs["lossless"] = True
else:
kwargs["quality"] = quality
kwargs["optimize"] = self.optimize_image
if output_ext in ['.j2k', '.jp2', '.jpc', '.jpf', '.jpx', '.j2c']:
if save_metadata: kwargs["exif"] = self.genMetadataEXIF(img, prompt, extra_pnginfo)
if quality < 100:
@@ -754,7 +775,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
kwargs["optimize"] = self.optimize_image
elif output_ext in ['.png', '.gif']:
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
@@ -812,21 +833,23 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
named_keys=named_keys,
):
# print(f"save_images filename_prefix = x{filename_prefix}x")
# print(f"save_images filename_keys = x{filename_keys}x")
# print(f"save_images foldername_prefix = x{foldername_prefix}x")
# print(f"save_images foldername_keys = x{foldername_keys}x")
# print(f"save_images delimiter = x{delimiter}x")
# print(f"save_images save_job_data = x{save_job_data}x")
# print(f"save_images job_data_per_image = x{job_data_per_image}x")
# print(f"save_images output_ext = x{output_ext}x")
# print(f"save_images quality = x{quality}x")
if debug:
print(f"debug save_images filename_prefix = x{filename_prefix}x")
print(f"debug save_images filename_keys = x{filename_keys}x")
print(f"debug save_images foldername_prefix = x{foldername_prefix}x")
print(f"debug save_images foldername_keys = x{foldername_keys}x")
print(f"debug save_images delimiter = x{delimiter}x")
print(f"debug save_images save_job_data = x{save_job_data}x")
print(f"debug save_images job_data_per_image = x{job_data_per_image}x")
print(f"debug save_images output_ext = x{output_ext}x")
print(f"debug save_images quality = x{quality}x")
# bugfix: sometimes on load, quality == 0
if quality == 0: quality = self.quality
# apply default values: we replicate the default save image box
if not filename_prefix and not filename_keys: filename_prefix=self.filename_prefix
# Nope: as per issues/48 request, we could want 0001.jpg etc with no name
# if not filename_prefix and not filename_keys: filename_prefix=self.filename_prefix
if delimiter: delimiter = delimiter[0]
filename_keys_to_extract = [item.strip() for item in filename_keys.split(',')]
@@ -850,16 +873,20 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
# full_output_folder, filename, _, _, custom_filename = folder_paths.get_save_image_path(custom_filename, self.output_dir, images[0].shape[1], images[0].shape[0])
# output_path = os.path.join(full_output_folder, custom_foldername)
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
# issue/48
if custom_filename:
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
else:
output_path = Path(os.path.join(self.output_dir, custom_foldername))
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)
if debug: print(f"debug save_images: counter for {output_ext}: {counter}")
counter = self.get_latest_counter(output_path, filename, counter_digits, counter_position, output_ext)
results = list()
for image in images:
@@ -867,12 +894,20 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
img = Image.fromarray(numpy.clip(i, 0, 255).astype(numpy.uint8))
if counter_digits > 0:
if counter_position == 'last':
image_name = f'{filename}{delimiter}{counter:0{counter_digits}}{output_ext}'
# issue/48
if filename:
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'{counter:0{counter_digits}}{output_ext}'
else:
image_name = f'{filename}{output_ext}'
# issue/48
if filename:
image_name = f'{filename}{output_ext}'
else:
image_name = f'{filename_prefix}{output_ext}'
image_path = os.path.join(output_path, image_name)
self.writeImage(image_path, img, prompt, save_metadata, extra_pnginfo, quality)
+55 -8
View File
@@ -3,24 +3,28 @@ import { app } from "../../../scripts/app.js";
// Adds context menu entries, code partly from pyssssscustom-scripts
function addMenuHandler(nodeType, cb) {
// console.debug("SIE addMenuHandler nodeType:", nodeType);
// console.debug("SIE addMenuHandler cb:", cb);
const getOpts = nodeType.prototype.getExtraMenuOptions;
// the below is never called, i suspect app.js has changed a lot
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments);
cb.apply(this, arguments);
// console.debug("SIE addMenuHandler arguments:", arguments);
return r;
};
}
function addNode(name, nextTo, options) {
console.log("name:", name);
console.log("nextTo:", nextTo);
options = { side: "left", select: true, shiftY: 0, shiftX: 0, ...(options || {}) };
// console.debug("SIE addNode name:", name);
// console.debug("SIE addNode nextTo:", nextTo);
options = { select: true, shiftY: 0, before: false, ...(options || {}) };
const node = LiteGraph.createNode(name);
app.graph.add(node);
node.pos = [
options.side === "left" ? nextTo.pos[0] - (node.size[0] + options.offset): nextTo.pos[0] + nextTo.size[0] + options.offset,
options.before ? nextTo.pos[0] - node.size[0] - 30 : nextTo.pos[0] + nextTo.size[0] + 30,
nextTo.pos[1] + options.shiftY,
];
if (options.select) {
@@ -29,12 +33,49 @@ function addNode(name, nextTo, options) {
return node;
}
// https://docs.comfy.org/custom-nodes/js/javascript_overview
app.registerExtension({
name: "SIEContextmenu",
name: "SIE.QuickNodes",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (
// this could go forever if we want to list all nodes that can output IMAGE
// nodeData.name === "VAEDecode" ||
// nodeData.name === "LoadImage" ||
// fortunately, output is an array where "IMAGE" is always the first item.
nodeData.output[0] == "IMAGE"
) {
// console.debug("SIE.QuickNodes VAEDecode nodeType:", nodeType);
// console.debug("SIE.QuickNodes VAEDecode nodeData:", nodeData);
// console.debug("SIE.QuickNodes VAEDecode app:", app);
// https://docs.comfy.org/custom-nodes/js/javascript_examples#node-menu
// Code below adds SaveImageExtended option on right-click menu for VAEDecode
addMenuHandler(nodeType, function (_, options) {
options.unshift(
{
content: "💾 Add SaveImageExtended",
callback: () => {
// const saveNode = addNode("SaveImageExtended", this, { before: true });
const saveNode = addNode("SaveImageExtended", this);
this.connect(0, saveNode, 0);
},
},
);
});
}
},
},
// https://docs.comfy.org/custom-nodes/walkthrough#write-a-client-extension
// I forgot why I added this one, probably not needed after all
{
name: "SIE.Contextmenu",
async setup(app) {
// console.debug("SIEContextmenu app:", app);
app.ui.settings.addSetting({
id: "SIE.helpPopup",
name: "🦛 SIE: Help popups",
name: "💾 SIE: Help popups",
defaultValue: true,
type: "boolean",
options: (value) => [
@@ -42,5 +83,11 @@ app.registerExtension({
{ value: false, text: "Off", selected: value === false },
],
});
}
});
+2 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long