Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87b890b32 | ||
|
|
a7a2dffaf9 | ||
|
|
37ee3483e1 | ||
|
|
1fc32e0ad3 | ||
|
|
8e9b750840 | ||
|
|
54b30b9c1a | ||
|
|
61aed6e7e1 | ||
|
|
8aa85b1c15 | ||
|
|
a2b9b4ed72 |
@@ -23,22 +23,22 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
|
||||
|
||||
| Attribute | Description |
|
||||
| --- | --- |
|
||||
| `filename_prefix` | String prefix added to files. |
|
||||
| `filename_prefix` | Fixed string prefixed to file name. |
|
||||
| `filename_keys` | Comma separated string with sampler parameters to add to filename. E.g: `sampler_name, scheduler, cfg, denoise` Added to filename in written order. `resolution` also works. `vae_name` `model_name` (upscale model), `ckpt_name` (checkpoint) are others that should work. Here you can try any parameter name of any node. As long as the parameter has the same variable name defined in the `prompt` object, they should work. The same applies to `foldername_keys`. |
|
||||
| `foldername_prefix` | String prefix for subfolders. |
|
||||
| `foldername_keys` | Comma separated string with _sampler_ parameters to add to foldername. Add more subfolders by writing a "./" separated with commas. For example: `sampler_name, ./, ckpt_name` |
|
||||
| `delimiter` | **now a free field** Delimiter = 1 character, can be anything your file system supports. Windows users should still use "/" for subfolders. |
|
||||
| `save_job_data` | If enabled, saves information about each job as entries in a `jobs.json` text file, inside the generated folder. Mulitple options for saving `prompt`, `basic data`, `sampler settings`, `loaded models`. |
|
||||
| `job_data_per_image` | When enabled, saves individual job data files for each image. |
|
||||
| `job_custom_text` | Custom string to save along with the job data. Right click the node and convert to input to connect with another node. |
|
||||
| `foldername_prefix` | Fixed string prefixed to subfolders. |
|
||||
| `foldername_keys` | Same rules as for `filename_keys`. Create subfolders by using `/` or `../` etc. |
|
||||
| `delimiter` | **now a free field** Delimiter = any string you like. You can also use `/` to create subfolders. |
|
||||
| `save_job_data` | Saves information about each job as entries in a `jobs.json` text file, inside the generated subfolder. Multiple options for its content: `prompt`, `basic data`, `sampler settings`, `loaded models`. |
|
||||
| `job_data_per_image` | Saves individual job data file per image. |
|
||||
| `job_custom_text` | Custom string to save along with the job data. |
|
||||
| `save_metadata` | Saves metadata into the image. |
|
||||
| `counter_digits` | Number of digits used for the image counter. `3` = image_001.png. Will adjust the counter if files are deleted. Looks for the highest number in the folder, does not fill gaps. **Can be disabled** when == 0 |
|
||||
| `counter_position` | Image counter first or last in the filename. |
|
||||
| `one_counter_per_folder` | Toggles the counter. Either one counter per folder, or resets when a parameter/prompt changes. |
|
||||
| `counter_digits` | Number of digits used for the image counter. `3` = image_001.png, based on highest number in the subfolder, ignores gaps. **Can be disabled** when == 0. |
|
||||
| `counter_position` | Image counter postition: image_001.png or 001_image.png. |
|
||||
| `one_counter_per_folder` | Toggles one counter per subfolder, or resets when a parameter/prompt changes. |
|
||||
| `image_preview` | Turns the image preview on and off. |
|
||||
| `output_ext` | File extension: PNG by default, or WEBP (coming soon). |
|
||||
| `quality` | Quality for JPEG/JXL/WebP/AVIF/J2K formats; default 90 (AVIF only needs 60 for same results). |
|
||||
| `named_keys` | Prefix each value by its key name: prefix-seed=123456-width=1024-etc-0001.avif |
|
||||
| `output_ext` | File extension: WEBP by default, AVIF, PNG, JXL, JPG, etc. |
|
||||
| `quality` | Quality for JPEG/JXL/WebP/AVIF/J2K formats; Quality is relative to each format. Example: AVIF 60 is same quality as WebP 90. PNG compression is fixed at 4 and not affected by this. PNG compression times skyrocket above level 4 for zero benefits on filesize. |
|
||||
| `named_keys` | Prefix each value by its key name. Example: prefix-seed=123456-width=1024-cfg=5.0-0001.avif |
|
||||
|
||||
|
||||
- Unknown key names in `filename_keys` and `foldername_keys` are treated as fixed strings
|
||||
@@ -48,6 +48,15 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
|
||||
- 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` 
|
||||
* How do I show the nodes ID badges?
|
||||
* legacy: 
|
||||
* modern: 
|
||||
|
||||
### Examples
|
||||
- prefix = `PREFIX`
|
||||
- filename = `cfg, sampler_name, /steps, %F %H-%M-%S`
|
||||
@@ -69,12 +78,12 @@ Supports those extensions: **JXL AVIF WebP jpg jpeg j2k jp2 png gif tiff bmp**
|
||||
- `images` - The generated images.
|
||||
|
||||
Optional:
|
||||
- `positive_text_opt` - Optional string input for when using custom nodes for positive prompt text.
|
||||
- `negative_text_opt` - Optional string input for when using custom nodes for negative prompt text.
|
||||
- `positive_text_opt` - Optional string saved as `positive_text_opt` in job.json when `save_job_data`=True.
|
||||
- `negative_text_opt` - Optional string saved as `negative_text_opt` in job.json when `save_job_data`=True.
|
||||
|
||||
## Automatic date/time conversion in file/folder names
|
||||
## 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,21 +100,14 @@ 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
|
||||
```
|
||||
|
||||
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/)
|
||||
|
||||
```
|
||||
pip install -U imagecodecs
|
||||
pip install numpy pillow pillow-avif-plugin pillow-jxl-plugin
|
||||
```
|
||||
|
||||
### Manual Download
|
||||
@@ -115,21 +117,24 @@ 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.
|
||||
Tested and working with default samplers, Efficiency nodes, UltimateSDUpscale, ComfyRoll, composer, NegiTools, and 45 other nodes.
|
||||
|
||||
#
|
||||
Quality and compression settings: default is 90, 100 will activate **lossless** for AVIF and WEBP only.
|
||||
Quality and compression settings: min = 1, default is **90**, max = 100 will activate **lossless** for AVIF and WEBP only.
|
||||
|
||||
Quick comparison of size per extension, for the same 512x512 picture, with similar visual quality:
|
||||
| Ext | Compression | Maker | Size | Compression |
|
||||
@@ -145,6 +150,20 @@ Quick comparison of size per extension, for the same 512x512 picture, with simil
|
||||
| avif | 60 | PIL | 47353 | 89% |
|
||||
| avif | 60 | Imagick | 33691 | 92% |
|
||||
|
||||
#
|
||||
PNG compression 0-9 is fixed at level 4 for the following reason: ***zero** compression benefits above level 4*, while saving time skyrockets. Test results below for a 2560x1440 image on an Intel i7-9700:
|
||||
| Compression | time | Size |
|
||||
| --- | --- | --- |
|
||||
| 0 | 0.5s | 10.6 MB |
|
||||
| 1 | 0.6s | 4.6 MB |
|
||||
| 2 | 0.6s | 4.5 MB |
|
||||
| 3 | 0.6s | 4.4 MB |
|
||||
| **4** | **0.7s** | **4.0 MB** |
|
||||
| 5 | 1.1s | 4.0 MB |
|
||||
| 6 | 1.9s | 4.0 MB |
|
||||
| 7 | 2.6s | 4.0 MB |
|
||||
| 9 | 10.5s | 3.9 MB |
|
||||
|
||||
|
||||
#
|
||||
About extensions WebP AVIF JPEG JXL: ComfyUI can only load PNG and WebP atm... Feel free to ask ComfyUI team to add support for AVIF/jpeg/JXL!
|
||||
@@ -162,18 +181,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:
|
||||
@@ -193,17 +204,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.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
|
||||
- improvement: now can extract CLIP text
|
||||
- improvement: added tooltips
|
||||
|
||||
### release 2.81 💾
|
||||
- fix for pythongosssss/ComfyUI-Custom-Scripts Checkpoint Loader bug and other nodes that store values in a dict
|
||||
|
||||
|
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: 11 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "save-image-extended-comfyui"
|
||||
description = "Save as JXL, AVIF, WebP, JPEG, JPEG2k, customize the folder, sub-folders, and filenames of your images!"
|
||||
version = "2.82"
|
||||
version = "2.86"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["piexif", "imagecodecs", "pillow", "pillow-avif-plugin", "pillow-jxl-plugin"]
|
||||
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+103
-60
@@ -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 piexif
|
||||
import piexif.helper
|
||||
import numpy
|
||||
# import piexif
|
||||
# import piexif.helper
|
||||
|
||||
# import cv2 # not paster then PIL
|
||||
# import cv2 # not faster then PIL
|
||||
|
||||
version = 2.82
|
||||
version = 2.86
|
||||
|
||||
avif_supported = False
|
||||
jxl_supported = False
|
||||
|
||||
debug = False
|
||||
# debug = True
|
||||
|
||||
# Avif is included in requirements.txt
|
||||
try:
|
||||
@@ -90,8 +92,6 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
|
||||
|
||||
type = 'output'
|
||||
|
||||
# BUG: PIL.Image doesn't respect compress_level value and always output max 9 compressed images when optimize_image = True
|
||||
png_compress_level = 9
|
||||
avif_quality = 60
|
||||
webp_quality = 90
|
||||
jpeg_quality = 90
|
||||
@@ -99,8 +99,8 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
|
||||
j2k_quality = 90
|
||||
# tiff_quality has no impact unless you start meddling with the compressions algorithms
|
||||
tiff_quality = 90
|
||||
# optimize_image only works for jpeg, png anf TIFF, with like just 2% reduction in size
|
||||
optimize_image = False
|
||||
# optimize_image only works for jpeg, png anf TIFF, with like just 2% reduction in size; not used for PNG as it forces a level 9 compression.
|
||||
optimize_image = True
|
||||
|
||||
filename_prefix = 'ComfyUI'
|
||||
filename_keys = 'sampler_name, cfg, steps, %F %H-%M-%S'
|
||||
@@ -114,12 +114,12 @@ 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'
|
||||
output_exts = ['.webp', '.png', '.jpg', '.jpeg', '.j2k', '.jp2', '.gif', '.tiff', '.bmp']
|
||||
# quality is a lossy compression unused by PNG/tiff/gif
|
||||
# quality is a lossy compression unused by PNG/tiff/gif but also translated to integers 0-9 for PNG compression level
|
||||
quality = 90
|
||||
named_keys = False
|
||||
|
||||
@@ -156,44 +156,46 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
|
||||
return {
|
||||
'required': {
|
||||
'images': ('IMAGE', ),
|
||||
'filename_prefix': ('STRING', {'default': self.filename_prefix, 'multiline': False}),
|
||||
'filename_keys': ('STRING', {'default': self.filename_keys, 'multiline': True}),
|
||||
'foldername_prefix': ('STRING', {'default': self.foldername_prefix, 'multiline': False}),
|
||||
'foldername_keys': ('STRING', {'default': self.foldername_keys, 'multiline': True}),
|
||||
'delimiter': ('STRING', {'default': self.delimiter, 'multiline': False}),
|
||||
'filename_prefix': ('STRING', {'default': self.filename_prefix, 'multiline': False, 'tooltip': "Fixed string prefixed to file name"}),
|
||||
'filename_keys': ('STRING', {'default': self.filename_keys, 'multiline': True, 'tooltip': "Comma separated string with sampler parameters to add to filename. \n* Example: `sampler_name, scheduler, cfg, denoise` Added to filename in written order. \n* Example: also accepts `vae_name` `model_name` (upscale model), `ckpt_name` (checkpoint). \n* `resolution` also works. \n\n* ANY parameter name of any node will work. The same applies to `foldername_keys`"}),
|
||||
'foldername_prefix': ('STRING', {'default': self.foldername_prefix, 'multiline': False, 'tooltip': "Fixed string prefixed to subfolders"}),
|
||||
'foldername_keys': ('STRING', {'default': self.foldername_keys, 'multiline': True, 'tooltip': "Same rules as for `filename_keys`. Create subfolders by using `/` or `../` etc"}),
|
||||
'delimiter': ('STRING', {'default': self.delimiter, 'multiline': False, 'tooltip': "Any string you like. You can also use `/` to create subfolders"}),
|
||||
'save_job_data': ([
|
||||
'disabled',
|
||||
'prompt',
|
||||
'basic, prompt',
|
||||
'basic, sampler, prompt',
|
||||
'basic, models, sampler, prompt'
|
||||
], {'default': self.save_job_data}),
|
||||
'job_data_per_image': ('BOOLEAN', {"default": self.job_data_per_image}),
|
||||
'job_custom_text': ('STRING', {'default': self.job_custom_text, 'multiline': False}),
|
||||
'save_metadata': ('BOOLEAN', {'default': self.save_metadata}),
|
||||
], {'default': self.save_job_data, 'tooltip': "Saves information about each job as entries in a `jobs.json` text file, under the generated subfolder. \nMultiple options for its content: `prompt`, `basic data`, `sampler settings`, `loaded models`"}),
|
||||
'job_data_per_image': ('BOOLEAN', {"default": self.job_data_per_image, 'tooltip': "Saves individual job data file per image"}),
|
||||
'job_custom_text': ('STRING', {'default': self.job_custom_text, 'multiline': False, 'tooltip': "Custom string to save along with the job data"}),
|
||||
'save_metadata': ('BOOLEAN', {'default': self.save_metadata, 'tooltip': "Saves metadata into the image"}),
|
||||
'counter_digits': ('INT', {
|
||||
"default": self.counter_digits,
|
||||
"min": 0,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"display": "silder"
|
||||
"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}),
|
||||
'one_counter_per_folder': ('BOOLEAN', {'default': self.one_counter_per_folder}),
|
||||
'image_preview': ('BOOLEAN', {'default': self.image_preview}),
|
||||
'output_ext': (self.output_exts, {'default': self.output_ext}),
|
||||
'counter_position': (self.counter_positions, {'default': self.counter_position, 'tooltip': "Image counter postition: image_001.png or 001_image.png"}),
|
||||
'one_counter_per_folder': ('BOOLEAN', {'default': self.one_counter_per_folder, 'tooltip': "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": 1,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"display": "silder"
|
||||
}),
|
||||
'named_keys': ('BOOLEAN', {'default': self.named_keys}),
|
||||
"display": "silder",
|
||||
'tooltip': "Quality for JPEG/JXL/WebP/AVIF/J2K formats; Quality is relative to each format. \n* Example: AVIF 60 is same quality as WebP 90. \n* PNG compression is fixed at 4 and not affected by this. PNG compression times skyrocket above level 4 for zero benefits on filesize."
|
||||
}),
|
||||
'named_keys': ('BOOLEAN', {'default': self.named_keys, 'tooltip': "Prefix each value by its key name. Example: prefix-seed=123456-width=1024-cfg=5.0-0001.avif"}),
|
||||
},
|
||||
'optional': {
|
||||
'positive_text_opt': ('STRING', {'forceInput': True}),
|
||||
'negative_text_opt': ('STRING', {'forceInput': True}),
|
||||
'positive_text_opt': ('STRING', {'forceInput': True, 'tooltip': "Optional string saved as `positive_text_opt` in job.json when `save_job_data`=True"}),
|
||||
'negative_text_opt': ('STRING', {'forceInput': True, 'tooltip': "Optional string saved as `negative_text_opt` in job.json when `save_job_data`=True"}),
|
||||
},
|
||||
'hidden': {'prompt': 'PROMPT', 'extra_pnginfo': 'EXTRA_PNGINFO'},
|
||||
}
|
||||
@@ -236,22 +238,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 +276,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 +692,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 +752,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:
|
||||
@@ -753,19 +772,29 @@ ComfyUI can only load PNG and WebP at the moment, AVIF is a PR that was sadly dr
|
||||
# tiff: no quality
|
||||
kwargs["optimize"] = self.optimize_image
|
||||
elif output_ext in ['.png', '.gif']:
|
||||
# png/gif: no quality
|
||||
if save_metadata: kwargs["pnginfo"] = self.genMetadataPng(img, prompt, extra_pnginfo)
|
||||
kwargs["compress_level"] = self.png_compress_level
|
||||
kwargs["optimize"] = self.optimize_image
|
||||
|
||||
# png/gif: no quality, rather we convert quality to compression level in the 0-9 range
|
||||
old_min = 0
|
||||
old_max = 90
|
||||
new_min = 0
|
||||
new_max = 9
|
||||
if quality >= 91: quality = 90
|
||||
png_compress_level = round( ( (quality - old_min) / (old_max - old_min) ) * (new_max - new_min) + new_min )
|
||||
|
||||
kwargs["compress_level"] = png_compress_level
|
||||
# BUG: PIL will compress at level 9 when PNG optimize_image = True
|
||||
# kwargs["optimize"] = self.optimize_image
|
||||
# elif output_ext in ['.bmp']:
|
||||
# nothing to add
|
||||
|
||||
# img.save(image_path, pnginfo=metadata, compress_level=self.png_compress_level)
|
||||
# BUG: PIL.Image doesn't respect compress_level value and always output max 9 compressed images when optimize_image = True
|
||||
# img.save(image_path, pnginfo=metadata, compress_level=png_compress_level)
|
||||
img.save(image_path, **kwargs)
|
||||
|
||||
# Is saving image with OpenCV really faster then PIL? https://github.com/python-pillow/Pillow/issues/5986
|
||||
# I found that it does not matter for anything smaller then 8k*8k, which Comfy cannot produce anyways.
|
||||
# compression_level = [cv2.IMWRITE_PNG_COMPRESSION, self.png_compress_level]
|
||||
# compression_level = [cv2.IMWRITE_PNG_COMPRESSION, png_compress_level]
|
||||
# image_array = numpy.array(img)
|
||||
# image_array = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
|
||||
# cv2.imwrite(image_path.replace('000','cv2'), image_array)
|
||||
@@ -802,21 +831,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(',')]
|
||||
@@ -840,16 +871,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:
|
||||
@@ -857,12 +892,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)
|
||||
|
||||
Reference in New Issue
Block a user