Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54e9183ecd | ||
|
|
669e7a9d03 | ||
|
|
ab2410ba95 |
@@ -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" />
|
||||
@@ -89,33 +89,36 @@ 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>
|
||||
|
||||
#
|
||||
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}`
|
||||
|
||||
#
|
||||
Incompatible with *extended-saveimage-comfyui* - This node can be safely discarded, as it only offers WebP output. My node already adds JPEG and WebP.
|
||||
@@ -154,6 +157,17 @@ 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.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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
+63
-35
@@ -44,21 +44,24 @@ original_locale = locale.setlocale(locale.LC_TIME, '')
|
||||
|
||||
# class SaveImageExtended -------------------------------------------------------------------------------
|
||||
class SaveImageExtended:
|
||||
version = 2.51
|
||||
version = 2.61
|
||||
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 = '_'
|
||||
foldername_keys = 'ckpt_name'
|
||||
delimiter = '-'
|
||||
save_job_data = 'basic, models, sampler, prompt'
|
||||
job_data_per_image = False
|
||||
job_custom_text = ''
|
||||
@@ -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
|
||||
|
||||
@@ -458,9 +475,14 @@ 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
|
||||
exif[0x9286] = "Prompt: " + json.dumps(metadata['prompt']) # UserComment
|
||||
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 +518,40 @@ 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()
|
||||
|
||||
|
||||
# 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 +580,7 @@ class SaveImageExtended:
|
||||
positive_text_opt=None,
|
||||
extra_pnginfo=None,
|
||||
prompt=None,
|
||||
quality=75,
|
||||
):
|
||||
|
||||
# print(f"filename_prefix = x{filename_prefix}x")
|
||||
@@ -605,7 +633,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)
|
||||
|
||||
Reference in New Issue
Block a user