Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49387f000 | ||
|
|
fd8388e9d8 | ||
|
|
1a39e8b0f7 | ||
|
|
29971dc90f | ||
|
|
c87b890b32 | ||
|
|
a7a2dffaf9 | ||
|
|
37ee3483e1 | ||
|
|
1fc32e0ad3 | ||
|
|
8e9b750840 |
@@ -19,12 +19,12 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
|
|||||||
<br><br>
|
<br><br>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Parameters / Usage
|
## Parameters / Reserved Keywords
|
||||||
|
|
||||||
| Attribute | Description |
|
| Attribute | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `filename_prefix` | Fixed string prefixed to file name. |
|
| `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. `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_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_prefix` | Fixed string prefixed to subfolders. |
|
||||||
| `foldername_keys` | Same rules as for `filename_keys`. Create subfolders by using `/` or `../` etc. |
|
| `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. |
|
| `delimiter` | **now a free field** Delimiter = any string you like. You can also use `/` to create subfolders. |
|
||||||
@@ -38,11 +38,13 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
|
|||||||
| `image_preview` | Turns the image preview on and off. |
|
| `image_preview` | Turns the image preview on and off. |
|
||||||
| `output_ext` | File extension: WEBP by default, AVIF, PNG, JXL, JPG, etc. |
|
| `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. |
|
| `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 |
|
| `named_keys` | Prefix each value by its key name. Example: prefix-seed=123456-width=1024-cfg=5.0-0001.avif |
|
||||||
|
|
||||||
|
- ANY other keyword that exist as a field name in a widget will be resolved.
|
||||||
- Unknown key names in `filename_keys` and `foldername_keys` are treated as fixed strings
|
- 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.
|
||||||
- if you enter `wrongNumber.attribute`, you will get `attribute` in your filename.
|
- 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)
|
- 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**
|
- 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
|
- use `ckpt_path` / `control_net_path` / `lora_path` to get the same subfolders as your models
|
||||||
@@ -110,6 +112,12 @@ There is a requirements.txt that will take care of that, but just in case:
|
|||||||
pip install numpy pillow pillow-avif-plugin pillow-jxl-plugin
|
pip install numpy pillow pillow-avif-plugin pillow-jxl-plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Tested on / up to:
|
||||||
|
* python 3.10.6
|
||||||
|
* pytorch 2.5.1+cu124
|
||||||
|
* ComfyUI v0.3.12-1-g7fc3ccdc | Released on '2025-01-16'
|
||||||
|
|
||||||
|
|
||||||
### Manual Download
|
### Manual Download
|
||||||
1. Open a terminal inside the 'custom_nodes' folder located in your ComfyUI installation dir
|
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 under ComfyUI\custom_nodes\
|
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\
|
||||||
@@ -204,6 +212,7 @@ Once I feel like I don't have time to work on it, I will gladly transfer ownersh
|
|||||||
|
|
||||||
TODO:
|
TODO:
|
||||||
|
|
||||||
|
- [ ] UI: add this node in the list when you drag the output slot out
|
||||||
- [ ] WebP: explore method values 0-6 and test filesize vs save time
|
- [ ] 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
|
- [ ] 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
|
- [ ] review accepted parameters at https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html to improve TIFF and other formats
|
||||||
@@ -213,7 +222,25 @@ TODO:
|
|||||||
- [ ] keep same counter if extension changes?
|
- [ ] keep same counter if extension changes?
|
||||||
- [ ] files will be out of order if prefixes change... that is expected, but is this what we want?
|
- [ ] files will be out of order if prefixes change... that is expected, but is this what we want?
|
||||||
- [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] 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
|
- [x] test import cv2 / OpenCV: https://github.com/python-pillow/Pillow/issues/5986 -> not faster then PIL
|
||||||
|
|
||||||
|
|
||||||
|
### release 2.88 💾
|
||||||
|
- updated pyproject.toml and README.md
|
||||||
|
|
||||||
|
### 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 💾
|
### release 2.83 💾
|
||||||
- quality is now also used for PNG compression: 0-90 translates to 0-9 levels of compression
|
- quality is now also used for PNG compression: 0-90 translates to 0-9 levels of compression
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+25
-5
@@ -1,15 +1,35 @@
|
|||||||
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
# updated in accordance to https://docs.comfy.org/registry/specifications
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "save-image-extended-comfyui"
|
name = "save-image-extended-comfyui"
|
||||||
|
version = "2.88"
|
||||||
description = "Save as JXL, AVIF, WebP, JPEG, JPEG2k, customize the folder, sub-folders, and filenames of your images!"
|
description = "Save as JXL, AVIF, WebP, JPEG, JPEG2k, customize the folder, sub-folders, and filenames of your images!"
|
||||||
version = "2.83"
|
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = ["piexif", "imagecodecs", "pillow", "pillow-avif-plugin", "pillow-jxl-plugin"]
|
requires-python = ">=3.9"
|
||||||
|
dependencies = [
|
||||||
|
"numpy",
|
||||||
|
"piexif",
|
||||||
|
"imagecodecs",
|
||||||
|
"pillow",
|
||||||
|
"pillow-avif-plugin",
|
||||||
|
"pillow-jxl-plugin",
|
||||||
|
"comfyui-frontend-package>=1.20.0"
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Operating System :: OS Independent" # Works on all operating systems
|
||||||
|
]
|
||||||
|
dynamic = ["dependencies"]
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
dependencies = {file = ["requirements.txt"]}
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Repository = "https://github.com/audioscavenger/save-image-extended-comfyui"
|
Repository = "https://github.com/audioscavenger/save-image-extended-comfyui"
|
||||||
# Used by Comfy Registry https://comfyregistry.org
|
Documentation = "https://github.com/audioscavenger/save-image-extended-comfyui"
|
||||||
|
"Bug Tracker" = "https://github.com/audioscavenger/save-image-extended-comfyui/issues"
|
||||||
|
|
||||||
[tool.comfy]
|
[tool.comfy]
|
||||||
PublisherId = "audioscavenger"
|
PublisherId = "audioscavenger"
|
||||||
DisplayName = "save-image-extended-comfyui"
|
DisplayName = "Save Image Extended"
|
||||||
Icon = ""
|
Icon = "💾"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
# imagecodecs # pillow-jxl-plugin is simpler
|
# imagecodecs # pillow-jxl-plugin is simpler
|
||||||
|
|
||||||
numpy
|
numpy
|
||||||
|
piexif
|
||||||
|
imagecodecs
|
||||||
pillow
|
pillow
|
||||||
pillow-avif-plugin
|
pillow-avif-plugin
|
||||||
pillow-jxl-plugin
|
pillow-jxl-plugin
|
||||||
|
|||||||
+126
-92
@@ -14,12 +14,13 @@ import numpy
|
|||||||
|
|
||||||
# import cv2 # not faster then PIL
|
# import cv2 # not faster then PIL
|
||||||
|
|
||||||
version = 2.83
|
version = 2.88
|
||||||
|
|
||||||
avif_supported = False
|
avif_supported = False
|
||||||
jxl_supported = False
|
jxl_supported = False
|
||||||
|
|
||||||
debug = False
|
debug = False
|
||||||
|
# debug = True
|
||||||
|
|
||||||
# Avif is included in requirements.txt
|
# Avif is included in requirements.txt
|
||||||
try:
|
try:
|
||||||
@@ -32,8 +33,9 @@ else:
|
|||||||
avif_supported = True
|
avif_supported = True
|
||||||
|
|
||||||
# Jxl requires jxlpy wheel to be compiled, and a valid MSVC environment, which is complex task
|
# 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:
|
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 jxlpy import JXLImagePlugin
|
||||||
# from imagecodecs import (jpegxl_encode, jpegxl_decode, jpegxl_check, jpegxl_version, JPEGXL)
|
# from imagecodecs import (jpegxl_encode, jpegxl_decode, jpegxl_check, jpegxl_version, JPEGXL)
|
||||||
import pillow_jxl
|
import pillow_jxl
|
||||||
@@ -55,6 +57,71 @@ original_locale = locale.setlocale(locale.LC_TIME, '')
|
|||||||
|
|
||||||
# class SaveImageExtended -------------------------------------------------------------------------------
|
# class SaveImageExtended -------------------------------------------------------------------------------
|
||||||
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 = ()
|
RETURN_TYPES = ()
|
||||||
FUNCTION = 'save_images'
|
FUNCTION = 'save_images'
|
||||||
OUTPUT_NODE = True
|
OUTPUT_NODE = True
|
||||||
@@ -113,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_digits = 4
|
||||||
counter_position = 'last'
|
counter_position = 'last'
|
||||||
counter_positions = ['last', 'first']
|
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
|
image_preview = True
|
||||||
modelExtensions = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
|
modelExtensions = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
|
||||||
output_ext = '.webp'
|
output_ext = '.webp'
|
||||||
@@ -134,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.output_dir = folder_paths.get_output_directory()
|
||||||
self.prefix_append = ''
|
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
|
# This class serves no purpose, it can only test 1 element. you always get all errors even if only one element is bad
|
||||||
# @classmethod
|
# @classmethod
|
||||||
@@ -237,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
|
# 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
|
counter = 1
|
||||||
if not os.path.exists(folder_path):
|
if not os.path.exists(folder_path):
|
||||||
print(f"SaveImageExtended {version} 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
|
return counter
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# grab all files with output_ext
|
||||||
files = [file for file in os.listdir(folder_path) if file.endswith(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)
|
extLen = len(output_ext)
|
||||||
|
# '.webp' = 5
|
||||||
|
|
||||||
if files:
|
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 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:
|
# issue/48 needs a special case for files, counter_position does not matter here, but the filename length does
|
||||||
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)]
|
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:
|
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:
|
if counters:
|
||||||
counter = max(counters) + 1
|
counter = max(counters) + 1
|
||||||
@@ -260,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:
|
except Exception as e:
|
||||||
print(f"SaveImageExtended {version} error: An error occurred while finding the latest counter: {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
|
return counter
|
||||||
|
|
||||||
|
|
||||||
@@ -675,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
|
# https://exiftool.org/TagNames/EXIF.html
|
||||||
# exif[0x9286] = "Prompt: " + json.dumps(metadata['prompt']) # UserComment
|
# exif[0x9286] = "Prompt: " + json.dumps(metadata['prompt']) # UserComment
|
||||||
exif[0x010f] = "Prompt: " + json.dumps(metadata['prompt']) # Make
|
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="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="jis") # type 1
|
||||||
@@ -735,6 +754,7 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
|
|||||||
kwargs["lossless"] = True
|
kwargs["lossless"] = True
|
||||||
else:
|
else:
|
||||||
kwargs["quality"] = quality
|
kwargs["quality"] = quality
|
||||||
|
kwargs["optimize"] = self.optimize_image
|
||||||
if output_ext in ['.j2k', '.jp2', '.jpc', '.jpf', '.jpx', '.j2c']:
|
if output_ext in ['.j2k', '.jp2', '.jpc', '.jpf', '.jpx', '.j2c']:
|
||||||
if save_metadata: kwargs["exif"] = self.genMetadataEXIF(img, prompt, extra_pnginfo)
|
if save_metadata: kwargs["exif"] = self.genMetadataEXIF(img, prompt, extra_pnginfo)
|
||||||
if quality < 100:
|
if quality < 100:
|
||||||
@@ -755,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
|
kwargs["optimize"] = self.optimize_image
|
||||||
elif output_ext in ['.png', '.gif']:
|
elif output_ext in ['.png', '.gif']:
|
||||||
if save_metadata: kwargs["pnginfo"] = self.genMetadataPng(img, prompt, extra_pnginfo)
|
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
|
# png/gif: no quality, rather we convert quality to compression level in the 0-9 range
|
||||||
old_min = 0
|
old_min = 0
|
||||||
old_max = 90
|
old_max = 90
|
||||||
@@ -813,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,
|
named_keys=named_keys,
|
||||||
):
|
):
|
||||||
|
|
||||||
# print(f"save_images filename_prefix = x{filename_prefix}x")
|
if debug:
|
||||||
# print(f"save_images filename_keys = x{filename_keys}x")
|
print(f"debug save_images filename_prefix = x{filename_prefix}x")
|
||||||
# print(f"save_images foldername_prefix = x{foldername_prefix}x")
|
print(f"debug save_images filename_keys = x{filename_keys}x")
|
||||||
# print(f"save_images foldername_keys = x{foldername_keys}x")
|
print(f"debug save_images foldername_prefix = x{foldername_prefix}x")
|
||||||
# print(f"save_images delimiter = x{delimiter}x")
|
print(f"debug save_images foldername_keys = x{foldername_keys}x")
|
||||||
# print(f"save_images save_job_data = x{save_job_data}x")
|
print(f"debug save_images delimiter = x{delimiter}x")
|
||||||
# print(f"save_images job_data_per_image = x{job_data_per_image}x")
|
print(f"debug save_images save_job_data = x{save_job_data}x")
|
||||||
# print(f"save_images output_ext = x{output_ext}x")
|
print(f"debug save_images job_data_per_image = x{job_data_per_image}x")
|
||||||
# print(f"save_images quality = x{quality}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
|
# bugfix: sometimes on load, quality == 0
|
||||||
if quality == 0: quality = self.quality
|
if quality == 0: quality = self.quality
|
||||||
|
|
||||||
# apply default values: we replicate the default save image box
|
# 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]
|
if delimiter: delimiter = delimiter[0]
|
||||||
|
|
||||||
filename_keys_to_extract = [item.strip() for item in filename_keys.split(',')]
|
filename_keys_to_extract = [item.strip() for item in filename_keys.split(',')]
|
||||||
@@ -851,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])
|
# 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 = os.path.join(full_output_folder, custom_foldername)
|
||||||
|
|
||||||
output_path = Path(os.path.join(self.output_dir, custom_foldername, custom_filename)).parent
|
# issue/48
|
||||||
filename = Path(os.path.join(self.output_dir, custom_foldername, custom_filename)).name
|
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_foldername= {custom_foldername}")
|
||||||
if debug: print(f"debug save_images: custom_filename= {custom_filename}")
|
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: output_path= {output_path}")
|
||||||
if debug: print(f"debug save_images: filename= {filename}")
|
if debug: print(f"debug save_images: filename= {filename}")
|
||||||
|
|
||||||
os.makedirs(output_path, exist_ok=True)
|
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)
|
counter = self.get_latest_counter(output_path, filename, counter_digits, counter_position, output_ext)
|
||||||
if debug: print(f"debug save_images: counter for {output_ext}: {counter}")
|
|
||||||
|
|
||||||
results = list()
|
results = list()
|
||||||
for image in images:
|
for image in images:
|
||||||
@@ -868,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))
|
img = Image.fromarray(numpy.clip(i, 0, 255).astype(numpy.uint8))
|
||||||
|
|
||||||
if counter_digits > 0:
|
if counter_digits > 0:
|
||||||
if counter_position == 'last':
|
# issue/48
|
||||||
image_name = f'{filename}{delimiter}{counter:0{counter_digits}}{output_ext}'
|
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:
|
else:
|
||||||
image_name = f'{counter:0{counter_digits}}{delimiter}{filename}{output_ext}'
|
image_name = f'{counter:0{counter_digits}}{output_ext}'
|
||||||
else:
|
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)
|
image_path = os.path.join(output_path, image_name)
|
||||||
self.writeImage(image_path, img, prompt, save_metadata, extra_pnginfo, quality)
|
self.writeImage(image_path, img, prompt, save_metadata, extra_pnginfo, quality)
|
||||||
|
|||||||
+55
-8
@@ -3,24 +3,28 @@ import { app } from "../../../scripts/app.js";
|
|||||||
// Adds context menu entries, code partly from pyssssscustom-scripts
|
// Adds context menu entries, code partly from pyssssscustom-scripts
|
||||||
|
|
||||||
function addMenuHandler(nodeType, cb) {
|
function addMenuHandler(nodeType, cb) {
|
||||||
|
// console.debug("SIE addMenuHandler nodeType:", nodeType);
|
||||||
|
// console.debug("SIE addMenuHandler cb:", cb);
|
||||||
const getOpts = nodeType.prototype.getExtraMenuOptions;
|
const getOpts = nodeType.prototype.getExtraMenuOptions;
|
||||||
|
|
||||||
|
// the below is never called, i suspect app.js has changed a lot
|
||||||
nodeType.prototype.getExtraMenuOptions = function () {
|
nodeType.prototype.getExtraMenuOptions = function () {
|
||||||
const r = getOpts.apply(this, arguments);
|
const r = getOpts.apply(this, arguments);
|
||||||
cb.apply(this, arguments);
|
cb.apply(this, arguments);
|
||||||
|
// console.debug("SIE addMenuHandler arguments:", arguments);
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function addNode(name, nextTo, options) {
|
function addNode(name, nextTo, options) {
|
||||||
console.log("name:", name);
|
// console.debug("SIE addNode name:", name);
|
||||||
console.log("nextTo:", nextTo);
|
// console.debug("SIE addNode nextTo:", nextTo);
|
||||||
options = { side: "left", select: true, shiftY: 0, shiftX: 0, ...(options || {}) };
|
options = { select: true, shiftY: 0, before: false, ...(options || {}) };
|
||||||
const node = LiteGraph.createNode(name);
|
const node = LiteGraph.createNode(name);
|
||||||
app.graph.add(node);
|
app.graph.add(node);
|
||||||
|
|
||||||
node.pos = [
|
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,
|
nextTo.pos[1] + options.shiftY,
|
||||||
];
|
];
|
||||||
if (options.select) {
|
if (options.select) {
|
||||||
@@ -29,12 +33,49 @@ function addNode(name, nextTo, options) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://docs.comfy.org/custom-nodes/js/javascript_overview
|
||||||
app.registerExtension({
|
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) {
|
async setup(app) {
|
||||||
|
// console.debug("SIEContextmenu app:", app);
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "SIE.helpPopup",
|
id: "SIE.helpPopup",
|
||||||
name: "🦛 SIE: Help popups",
|
name: "💾 SIE: Help popups",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
options: (value) => [
|
options: (value) => [
|
||||||
@@ -42,5 +83,11 @@ app.registerExtension({
|
|||||||
{ value: false, text: "Off", selected: value === false },
|
{ value: false, text: "Off", selected: value === false },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user