Compare commits

...
6 Commits
Author SHA1 Message Date
audioscavenger 85967a9a4b release 2.62 IFD-270-271
modified:   README.md
    	modified:   save_image_extended.py
	modified:   README.md
	modified:   save_image_extended.py
2024-05-27 17:11:48 -07:00
audioscavenger bfabb38bea release 2.62 IFD-270-271
modified:   README.md
	modified:   save_image_extended.py
2024-05-27 09:11:20 -07:00
audioscavenger 2a2e38397a release 2.61 quality
modified:   README.md
    	modified:   save_image_extended.py
	modified:   README.md
2024-05-23 20:42:30 -07:00
audioscavenger 54e9183ecd release 2.61 quality
modified:   README.md
	modified:   save_image_extended.py
2024-05-23 14:47:05 -07:00
audioscavenger 669e7a9d03 release 2.60 jpeg gif tiff bmp
modified:   README.md
	modified:   save_image_extended.py
2024-05-23 12:59:37 -07:00
audioscavenger ab2410ba95 release 2.51 datetime
modified:   README.md
    	modified:   save_image_extended.py
	modified:   README.md
	modified:   save_image_extended.py
2024-05-23 11:53:33 -07:00
4 changed files with 135 additions and 80 deletions
+65 -44
View File
@@ -1,5 +1,5 @@
# 💾 Save Image Extended for ComfyUI
**AVIF** / **WebP** / **JXL** support!
Supports those extensions: **AVIF WebP JXL jpg jpeg png gif tiff bmp**
<p align="center">
<img src="assets/save-image-extended-comfyui-example.png" />
@@ -8,45 +8,15 @@
* Customize the folder, sub-folders, and filenames of your images!
* Save data about the generated job (sampler, prompts, models) as entries in a `json` (text) file, in each folder.
* Use the values of ANY node's widget, by simply adding its badge number in the form _id.widget_name_
* Oh btw... also saves your output as **AVIF** / **WebP** / **JPEG** / **JXL** ... And yes the prompt is included :)
* ComfyUI can only load PNG and WebP atm
* Promt saved in jpeg and AVIF, but ComfyUI can only load PNG and WebP atm
<br>
<p align="center">
<img src="assets/save-image-extended-comfyui-named_nodes_widgets-example.png" />
<br><br>
Happy saving!
</p>
*Reboot by AudioscavengeR since 2024-05-05, original idea from [@thedyze](https://github.com/thedyze/save-image-extended-comfyui)*
## Installation
### Requirements:
There is a requirements.txt that will take care of that, but just in case:
- python 10.6
- piexif
- pillow
- pillow-avif-plugin
```
pip install piexif pillow pillow-avif-plugin
```
For Jpeg XL / jxl it's more complicated. You need to install and compile the wheel jxlpy, and therefore, need a valid and functional MSVC installation.
```
pip install jxlpy
```
### Manual Download
1. Open a terminal inside the 'custom_nodes' folder located in your ComfyUI installation dir
2. Use the `git clone` command to clone the [save-image-extended-comfyui](https://github.com/audioscavenger/save-image-extended-comfyui) repo under ComfyUI\custom_nodes\
```
git clone https://github.com/audioscavenger/save-image-extended-comfyui
```
## Parameters / Usage
| Attribute | Description |
@@ -89,33 +59,66 @@ Converts [unix datetime formats](https://www.man7.org/linux/man-pages/man1/date.
| %F %H-%M-%S | 2024-05-22 09-13-58 | |
| %Y/%V | 2024/21 | year subfolder / ISO week number subfolder |
<p align="center">
<img src="assets/save-image-extended-comfyui-example-2.png" />
</p>
## Installation
### Requirements:
There is a requirements.txt that will take care of that, but just in case:
- python 10.6
- piexif
- pillow
- pillow-avif-plugin
```
pip install piexif pillow pillow-avif-plugin
```
For Jpeg XL / jxl it's more complicated. You need to install and compile the wheel jxlpy, and therefore, need a valid and functional MSVC installation.
```
pip install jxlpy
```
### Manual Download
1. Open a terminal inside the 'custom_nodes' folder located in your ComfyUI installation dir
2. Use the `git clone` command to clone the [save-image-extended-comfyui](https://github.com/audioscavenger/save-image-extended-comfyui) repo under ComfyUI\custom_nodes\
```
git clone https://github.com/audioscavenger/save-image-extended-comfyui
```
## Miscelaneous
#
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:
Quality and compression settings: default is 75, 100 will activate **lossless** for AVIF and WEBP only.
* AVIF quality is fixed at 60.
* WebP quality is fixed at 75.
* JPEG quality is fixed at 91.
* JXL quality is fixed at 91.
* PNG is maxed compressed (9)
Start a discussion or a poll if you want to change output quality, and add another attribute in the node. There's so many already, some complain about the node being too tall.
Quick comparison of size per extension, for the same picture, AVIF quality=60, WebP quality=75, jpeg quality=91 (and yes, AVIF still looks better then WebP):
<p align="center">
<img src="assets/save-image-extended-sizes-comparison.png" />
</p>
#
About extensions WebP AVIF JPEG JXL: ComfyUI can only load PNG and WebP atm... Feel free to ask ComfyUI team to add support for AVIF/jpeg/JXL!
The prompt is included under the **EXIF** tag `UserComment` (IFD0 / 0x9286) as defined [here](https://exiftool.org/TagNames/EXIF.html).
It is saved in this form: `UserComment = {"prompt": {"1": {"inputs": {...}}}}`.
The metadata Are included under the **EXIF** tags IFD0 as defined [here](https://exiftool.org/TagNames/EXIF.html)
| Data | EXIF | Name | String looks like |
| --- | --- | --- | --- |
| prompt | 0x9286 | UserComment | Prompt: {"5" ... } |
| workflow | 0x010e | ImageDescription | Workflow: {"5" ... } |
You can retrieve the prompt manually with [exiftool](https://exiftool.org/), here are some example commands:
- `exiftool -Parameters -Prompt -Workflow file.png`
- `exiftool -Parameters -UserComment -Prompt -Workflow file.{jpg|webp|avif}`
- `exiftool -Parameters -Prompt -Workflow image.png`
- `exiftool -Parameters -UserComment -ImageDescription image.{jpg|jpeg|webp|avif|jxl}`
#
ComfyUI cannot load lossless WebP atm. Feel free to try and fix `pnginfo.js`
#
Incompatible with *extended-saveimage-comfyui* - This node can be safely discarded, as it only offers WebP output. My node already adds JPEG and WebP.
@@ -139,6 +142,7 @@ jobs.json sample: always generated and appended, not sure what it can be used fo
## RoadMap
*Reboot by AudioscavengeR since 2024-05-05, original idea from [@thedyze](https://github.com/thedyze/save-image-extended-comfyui)*
I won't promise you the moon, but since I use this node myself, I will maintain it as much as I can. I do provide a way to contact me, and will accept PR and collabs.
Once I feel like I don't have time to work on it, I will gladly transfer ownership or let collabs maintain it.
@@ -154,6 +158,21 @@ TODO:
- [ ] offer to place the counter anywhere, as a key in filename_keys
- [ ] files can get out of order if prefixes change... that is expected, but is this what we want? another reason to have the counter place anywhere we want
### release 2.62 💾
- prompt and workflow are saved in IFD 270 and 271 for better load compatibility
- disabled jobs.json by default, this thingy seems useless to me
### release 2.61 💾
- added quality input
### release 2.60 💾
- added extensions jpeg, gif, tiff, bmp
- added image_optimization (only for jpeg)
- now saves prompt and workflow separately into 0x9286/UserComment and 0x010e/ImageDescription
### release 2.51 💾
- added unix datetime formats
### release 2.50 💾
- added JXL support
- added 'BOOLEAN' support
@@ -206,3 +225,5 @@ TODO:
### release 2.0
- [x] Reboot on 2024-05-05
## :ribbon: Licence
[GPL 3.0](https://choosealicense.com/licenses/gpl-3.0/)
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

+70 -36
View File
@@ -44,22 +44,25 @@ original_locale = locale.setlocale(locale.LC_TIME, '')
# class SaveImageExtended -------------------------------------------------------------------------------
class SaveImageExtended:
version = 2.51
version = 2.62
type = 'output'
png_compress_level = 9
lossless = False
avif_quality = 60
webp_quality = 75
jpeg_quality = 91
jxl_quality = 91
# tiff_quality has no impact unless you start meddling with the compressions algorithms
tiff_quality = 91
# optimize_image only works for jpeg, with like just 2% reduction in size
optimize_image = True
filename_prefix = 'ComfyUI'
filename_keys = 'sampler_name, scheduler, cfg, steps'
filename_keys = 'sampler_name, cfg, steps, %F %H-%M-%S'
foldername_prefix = ''
foldername_keys = 'ckpt_name, ./subfolder'
delimiter = '_'
save_job_data = 'basic, models, sampler, prompt'
foldername_keys = 'ckpt_name'
delimiter = '-'
save_job_data = 'disabled'
job_data_per_image = False
job_custom_text = ''
save_metadata = True
@@ -70,7 +73,8 @@ class SaveImageExtended:
image_preview = True
extToRemove = ['.safetensors', '.ckpt', '.pt', '.bin', '.pth']
output_ext = '.webp'
output_exts = ['.webp', '.png', '.jpg']
output_exts = ['.webp', '.png', '.jpg', '.jpeg', '.gif', '.tiff', '.bmp']
quality = 75
print(f"\033[92m[save_image_extended]\033[0m version: {version}\033[0m")
if jxl_supported:
@@ -129,6 +133,13 @@ class SaveImageExtended:
'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}),
'quality': ('INT', {
"default": self.quality,
"min": 1,
"max": 100,
"step": 1,
"display": "silder"
}),
},
'optional': {
'positive_text_opt': ('STRING', {'forceInput': True}),
@@ -233,7 +244,11 @@ class SaveImageExtended:
# Roman Numerals 'ↁ' False False True
# --------------------------- --------- ----------- --------- -----------
def generate_custom_name(self, keys_to_extract, prefix, delimiter, prompt, timestamp=datetime.now()):
custom_name = prefix
if '%' in prefix:
custom_name = timestamp.strftime(prefix)
else:
custom_name = prefix
if prompt is not None and keys_to_extract != ['']:
found_values = {}
# print(f"debug generate_custom_name: --prefix: {prefix}")
@@ -248,6 +263,8 @@ class SaveImageExtended:
# 'inputs': {'cfg': 1.6,
# 'denoise': 1.0, ...
for key in keys_to_extract:
if not key: continue
value = None
node, nodeKey = None, None
@@ -447,7 +464,8 @@ class SaveImageExtended:
## For Comfy to load an image, it need a PNG tag at the root: Prompt={prompt}
## For AVIF WebP Jpeg, it's only Exif that's available... and UserComment is the best choice.
## This method gives wrong results, as [ExifTool] will issue Warning: Invalid EXIF text encoding for UserComment
## This method gives good results, as long as you save the prompt/workflow in 2 separate Exif tags
## Otherwise, [ExifTool] will issue Warning: Invalid EXIF text encoding for UserComment
# entryOffset 10
# tag 37510
# type 2
@@ -458,9 +476,17 @@ class SaveImageExtended:
exif = img.getexif()
dump = json.dumps(metadata)
# print(f"dump={dump}") {"prompt": { .. }, "workflow": { .. }}
exif[ExifTags.Base.UserComment] = json.dumps(metadata)
# This should work the same:
# exif[0x9286] = json.dumps(metadata)
# exif[ExifTags.Base.UserComment] = dump
## It seems better to separate the two
# 0x010e: ImageDescription
# 0x010f: Make
# 0x9286: UserComment
# both prompt and workflow must be in IFD close together of that can cause problems for the parseIFD function on import
# 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[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
@@ -496,35 +522,42 @@ class SaveImageExtended:
return exif_dat
def save_image(self, image_path, img, prompt, save_metadata=save_metadata, extra_pnginfo=None):
def save_image(self, image_path, img, prompt, save_metadata=save_metadata, extra_pnginfo=None, quality=75):
# print(f"debug save_images: image_path={image_path}")
output_ext = os.path.splitext(os.path.basename(image_path))[1]
metadata = None
kwargs = dict()
# TODO: see if convert_hdr_to_8bit=False make a change
# match is python 3.10+
match output_ext:
case '.avif':
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["lossless"] = self.lossless
kwargs["quality"] = self.avif_quality
case '.webp':
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["lossless"] = self.lossless
kwargs["quality"] = self.webp_quality
if self.webp_quality == 100: kwargs["lossless"] = True
case '.jpg':
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = self.jpeg_quality
case '.jxl':
kwargs["lossless"] = self.lossless
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = self.jxl_quality
case _:
# png: no quality
if save_metadata: kwargs["pnginfo"] = self.get_metadata_png(img, prompt, extra_pnginfo)
kwargs["compress_level"] = self.png_compress_level
# png
if output_ext in ['.avif']:
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = quality
if quality == 100: kwargs["lossless"] = True
elif output_ext in ['.webp']:
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = quality
if quality == 100: kwargs["lossless"] = True
elif output_ext in ['.jpg', '.jpeg']:
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = quality
kwargs["optimize"] = self.optimize_image
elif output_ext in ['.jxl']:
if save_metadata: kwargs["exif"] = self.get_metadata_exif(img, prompt, extra_pnginfo)
kwargs["quality"] = quality
if quality == 100: kwargs["lossless"] = True
elif output_ext in ['.tiff']:
# tiff: i suspect no quality either
kwargs["quality"] = quality
kwargs["optimize"] = self.optimize_image
elif output_ext in ['.png', '.gif']:
# png/gif: no quality
if save_metadata: kwargs["pnginfo"] = self.get_metadata_png(img, prompt, extra_pnginfo)
kwargs["compress_level"] = self.png_compress_level
kwargs["optimize"] = self.optimize_image
# elif output_ext in ['.bmp']:
# nothing to add
# img.save(image_path, pnginfo=metadata, compress_level=self.png_compress_level)
img.save(image_path, **kwargs)
@@ -553,6 +586,7 @@ class SaveImageExtended:
positive_text_opt=None,
extra_pnginfo=None,
prompt=None,
quality=75,
):
# print(f"filename_prefix = x{filename_prefix}x")
@@ -605,7 +639,7 @@ class SaveImageExtended:
file = f'{counter:0{counter_digits}}{delimiter}{filename}{output_ext}'
image_path = os.path.join(output_path, file)
self.save_image(image_path, img, prompt, save_metadata, extra_pnginfo)
self.save_image(image_path, img, prompt, save_metadata, extra_pnginfo, quality)
if save_job_data != 'disabled' and job_data_per_image:
self.save_job_to_json(save_job_data, prompt, filename_prefix, positive_text_opt, negative_text_opt, job_custom_text, resolution, output_path, f'{file.removesuffix(output_ext)}.json', timestamp)