Upload 3 files
Browse files
ComfyUI-Metadata-Saver/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .metadata_saver import SaveImageCustomMetadata
|
| 2 |
+
|
| 3 |
+
NODE_CLASS_MAPPINGS = {
|
| 4 |
+
"SaveImageCustomMetadata": SaveImageCustomMetadata
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 8 |
+
"SaveImageCustomMetadata": "Save Image (Custom Metadata)"
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
ComfyUI-Metadata-Saver/metadata_saver.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import Image, ExifTags
|
| 5 |
+
from PIL.PngImagePlugin import PngInfo
|
| 6 |
+
import folder_paths
|
| 7 |
+
|
| 8 |
+
class SaveImageCustomMetadata:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.output_dir = folder_paths.get_output_directory()
|
| 11 |
+
self.type = "output"
|
| 12 |
+
self.prefix_append = ""
|
| 13 |
+
self.compress_level = 4
|
| 14 |
+
|
| 15 |
+
@classmethod
|
| 16 |
+
def INPUT_TYPES(s):
|
| 17 |
+
return {
|
| 18 |
+
"required": {
|
| 19 |
+
"images": ("IMAGE", ),
|
| 20 |
+
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
| 21 |
+
"strip_workflow": ("BOOLEAN", {"default": True, "label": "Remove Original Workflow"}),
|
| 22 |
+
|
| 23 |
+
# PNG Text Info
|
| 24 |
+
"custom_author": ("STRING", {"default": "Me", "multiline": False}),
|
| 25 |
+
"copyright_info": ("STRING", {"default": "Copyright 2024", "multiline": False}),
|
| 26 |
+
|
| 27 |
+
# EXIF Data (Camera & GPS)
|
| 28 |
+
"camera_make": ("STRING", {"default": "Apple"}),
|
| 29 |
+
"camera_model": ("STRING", {"default": "iPhone 16 Pro Max"}),
|
| 30 |
+
|
| 31 |
+
# Default: Miami, FL (25.7617, -80.1918)
|
| 32 |
+
"latitude": ("FLOAT", {"default": 25.7617, "min": -90.0, "max": 90.0, "step": 0.0001}),
|
| 33 |
+
"longitude": ("FLOAT", {"default": -80.1918, "min": -180.0, "max": 180.0, "step": 0.0001}),
|
| 34 |
+
},
|
| 35 |
+
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
RETURN_TYPES = ()
|
| 39 |
+
FUNCTION = "save_images"
|
| 40 |
+
OUTPUT_NODE = True
|
| 41 |
+
CATEGORY = "image"
|
| 42 |
+
|
| 43 |
+
def _convert_to_degrees(self, value):
|
| 44 |
+
"""
|
| 45 |
+
Helper to convert decimal coordinates to EXIF (Degrees, Minutes, Seconds) format.
|
| 46 |
+
EXIF expects rational numbers (numerator, denominator).
|
| 47 |
+
"""
|
| 48 |
+
d = int(value)
|
| 49 |
+
m = int((value - d) * 60)
|
| 50 |
+
s = (value - d - m/60) * 3600.00
|
| 51 |
+
# precision of 100 for seconds
|
| 52 |
+
return ((d, 1), (m, 1), (int(s * 100), 100))
|
| 53 |
+
|
| 54 |
+
def save_images(self, images, filename_prefix="ComfyUI", strip_workflow=True,
|
| 55 |
+
custom_author="", copyright_info="",
|
| 56 |
+
camera_make="", camera_model="", latitude=0.0, longitude=0.0,
|
| 57 |
+
prompt=None, extra_pnginfo=None):
|
| 58 |
+
|
| 59 |
+
filename_prefix += self.prefix_append
|
| 60 |
+
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
| 61 |
+
|
| 62 |
+
results = list()
|
| 63 |
+
|
| 64 |
+
# EXIF Standard Tag IDs
|
| 65 |
+
TAG_MAKE = 271
|
| 66 |
+
TAG_MODEL = 272
|
| 67 |
+
TAG_SOFTWARE = 305
|
| 68 |
+
TAG_ARTIST = 315
|
| 69 |
+
TAG_COPYRIGHT = 33432
|
| 70 |
+
TAG_GPS_INFO = 34853
|
| 71 |
+
|
| 72 |
+
# GPS Sub-IFD Tags
|
| 73 |
+
GPS_LAT_REF = 1
|
| 74 |
+
GPS_LAT = 2
|
| 75 |
+
GPS_LONG_REF = 3
|
| 76 |
+
GPS_LONG = 4
|
| 77 |
+
|
| 78 |
+
for image in images:
|
| 79 |
+
i = 255. * image.cpu().numpy()
|
| 80 |
+
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
| 81 |
+
|
| 82 |
+
# --- 1. Handle PNG Text Metadata (Workflow, Author) ---
|
| 83 |
+
metadata = PngInfo()
|
| 84 |
+
if not strip_workflow:
|
| 85 |
+
if prompt is not None:
|
| 86 |
+
metadata.add_text("prompt", json.dumps(prompt))
|
| 87 |
+
if extra_pnginfo is not None:
|
| 88 |
+
for x in extra_pnginfo:
|
| 89 |
+
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
| 90 |
+
|
| 91 |
+
if custom_author:
|
| 92 |
+
metadata.add_text("Author", custom_author)
|
| 93 |
+
if copyright_info:
|
| 94 |
+
metadata.add_text("Copyright", copyright_info)
|
| 95 |
+
|
| 96 |
+
# --- 2. Handle EXIF Data (Camera & GPS) ---
|
| 97 |
+
exif_data = img.getexif()
|
| 98 |
+
|
| 99 |
+
# Set Camera Details
|
| 100 |
+
if camera_make:
|
| 101 |
+
exif_data[TAG_MAKE] = camera_make
|
| 102 |
+
if camera_model:
|
| 103 |
+
exif_data[TAG_MODEL] = camera_model
|
| 104 |
+
|
| 105 |
+
exif_data[TAG_SOFTWARE] = "ComfyUI Custom Node"
|
| 106 |
+
if custom_author:
|
| 107 |
+
exif_data[TAG_ARTIST] = custom_author
|
| 108 |
+
if copyright_info:
|
| 109 |
+
exif_data[TAG_COPYRIGHT] = copyright_info
|
| 110 |
+
|
| 111 |
+
# Set GPS Data
|
| 112 |
+
# We must access the GPS IFD (Sub-dictionary) specifically
|
| 113 |
+
gps_ifd = exif_data.get_ifd(TAG_GPS_INFO)
|
| 114 |
+
|
| 115 |
+
# Latitude
|
| 116 |
+
gps_ifd[GPS_LAT_REF] = 'N' if latitude >= 0 else 'S'
|
| 117 |
+
gps_ifd[GPS_LAT] = self._convert_to_degrees(abs(latitude))
|
| 118 |
+
|
| 119 |
+
# Longitude
|
| 120 |
+
gps_ifd[GPS_LONG_REF] = 'E' if longitude >= 0 else 'W'
|
| 121 |
+
gps_ifd[GPS_LONG] = self._convert_to_degrees(abs(longitude))
|
| 122 |
+
|
| 123 |
+
# Save the image with both PNG Info and EXIF binary data
|
| 124 |
+
file = f"{filename}_{counter:05}_.png"
|
| 125 |
+
img.save(
|
| 126 |
+
os.path.join(full_output_folder, file),
|
| 127 |
+
pnginfo=metadata,
|
| 128 |
+
exif=exif_data,
|
| 129 |
+
compress_level=self.compress_level
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
results.append({
|
| 133 |
+
"filename": file,
|
| 134 |
+
"subfolder": subfolder,
|
| 135 |
+
"type": self.type
|
| 136 |
+
})
|
| 137 |
+
counter += 1
|
| 138 |
+
|
| 139 |
+
return { "ui": { "images": results } }
|
ComfyUI-Metadata-Saver/📸 ComfyUI Custom Metadata Saver.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
📸 ComfyUI Custom Metadata Saver
|
| 2 |
+
This custom node allows you to save images while stripping the internal ComfyUI workflow data and injecting custom EXIF (Camera & GPS) and Copyright information.
|
| 3 |
+
📥 Installation
|
| 4 |
+
Navigate to your ComfyUI/custom_nodes/ folder.
|
| 5 |
+
Create a new folder named ComfyUI-Metadata-Saver.
|
| 6 |
+
Create two files inside: __init__.py and metadata_saver.py (paste the provided code).
|
| 7 |
+
Restart ComfyUI.
|
| 8 |
+
⚙️ How to Use
|
| 9 |
+
Add Node: Double-click the canvas and search for "Save Image (Custom Metadata)".
|
| 10 |
+
Connect: Link your final image output to this node (replace the standard "Save Image" node).
|
| 11 |
+
Configure Settings:
|
| 12 |
+
Setting Description
|
| 13 |
+
strip_workflow True: Removes the ComfyUI node graph/prompt (privacy mode).<br>False: Keeps the workflow hidden in the file.
|
| 14 |
+
filename_prefix Standard file naming (e.g., "MyRender").
|
| 15 |
+
custom_author Sets the "Artist/Author" tag.
|
| 16 |
+
camera_make e.g., Apple, Sony, Canon.
|
| 17 |
+
camera_model e.g., iPhone 16 Pro Max.
|
| 18 |
+
latitude / longitude Sets GPS location.
|
| 19 |
+
📍 Example Presets (Miami / iPhone)
|
| 20 |
+
To make your render look like a photo taken in Miami on a new iPhone:
|
| 21 |
+
Camera Make: Apple
|
| 22 |
+
Camera Model: iPhone 16 Pro Max
|
| 23 |
+
Latitude: 25.7617
|
| 24 |
+
Longitude: -80.1918
|
| 25 |
+
⚠️ Note: If you set strip_workflow to True, you will not be able to drag and drop the resulting image back into ComfyUI to load the workflow. The image will be "clean."
|