diff --git a/README.md b/README.md index aedef20..f38399e 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ primerp pryecto en python todo test persistir imagen - +no sobrevive al f5 por mas que se vea la data agregar desde menu o doble click ## Installation diff --git a/__init__.py b/__init__.py index a2b3e9f..4f3317c 100644 --- a/__init__.py +++ b/__init__.py @@ -11,7 +11,7 @@ from .nodes.primitive import CBoolean, CText, CTextML, CInteger, CFloat from .nodes.switch import CSwitchBooleanAny, CSwitchBooleanLatent, CSwitchBooleanConditioning, CSwitchBooleanImage, \ CSwitchBooleanString from .nodes.debugger import CConsoleAny -from .nodes.image import CImagePreviewAdvance, CImageLoadWithMetadata, CImageShowResolution +from .nodes.image import CImagePreviewFromImage, CImageLoadWithMetadata, CImageShowResolution, CImagePreviewFromMetadata from .nodes.list import CListAny, CListString from .nodes.pipe import CPipeToAny, CPipeFromAny from .nodes.utils import CUtilsCompareJsons, CUtilsStatSystem, CMetadataExtractor, CMetadataCompare @@ -39,7 +39,8 @@ NODE_CLASS_MAPPINGS = { CLASSES.CIMAGE_LOAD_METADATA_NAME.value: CImageLoadWithMetadata, CLASSES.CIMAGE_SHOW_RESOLUTION_NAME.value: CImageShowResolution, - CLASSES.CIMAGE_PREVIEW_ADVANCED_NAME.value: CImagePreviewAdvance, + CLASSES.CIMAGE_PREVIEW_IMAGE_NAME.value: CImagePreviewFromImage, + CLASSES.CIMAGE_PREVIEW_METADATA_NAME.value: CImagePreviewFromMetadata, CLASSES.CUTILS_METADATA_EXTRACTOR_NAME.value: CMetadataExtractor, CLASSES.CUTILS_METADATA_COMPARATOR_NAME.value: CMetadataCompare, @@ -70,7 +71,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { CLASSES.CIMAGE_LOAD_METADATA_NAME.value: CLASSES.CIMAGE_LOAD_METADATA_DESC.value, CLASSES.CIMAGE_SHOW_RESOLUTION_NAME.value: CLASSES.CIMAGE_SHOW_RESOLUTION_DESC.value, - CLASSES.CIMAGE_PREVIEW_ADVANCED_NAME.value: CLASSES.CIMAGE_PREVIEW_ADVANCED_DESC.value, + CLASSES.CIMAGE_PREVIEW_IMAGE_NAME.value: CLASSES.CIMAGE_PREVIEW_IMAGE_DESC.value, + CLASSES.CIMAGE_PREVIEW_METADATA_NAME.value: CLASSES.CIMAGE_PREVIEW_METADATA_DESC.value, CLASSES.CUTILS_METADATA_EXTRACTOR_NAME.value: CLASSES.CUTILS_METADATA_EXTRACTOR_DESC.value, CLASSES.CUTILS_METADATA_COMPARATOR_NAME.value: CLASSES.CUTILS_METADATA_COMPARATOR_DESC.value, diff --git a/nodes/_names.py b/nodes/_names.py index 5851d27..576e03e 100644 --- a/nodes/_names.py +++ b/nodes/_names.py @@ -46,8 +46,10 @@ class CLASSES(Enum): CIMAGE_LOAD_METADATA_DESC = "Load image with metadata [Crystools]" CIMAGE_SHOW_RESOLUTION_NAME = "Show resolution [Crystools]" CIMAGE_SHOW_RESOLUTION_DESC = "Show resolution [Crystools]" - CIMAGE_PREVIEW_ADVANCED_NAME = "Preview image advanced [Crystools]" - CIMAGE_PREVIEW_ADVANCED_DESC = "Preview image advanced (prompt) [Crystools]" + CIMAGE_PREVIEW_IMAGE_NAME = "Preview from image [Crystools]" + CIMAGE_PREVIEW_IMAGE_DESC = "Preview from image (show prompt) [Crystools]" + CIMAGE_PREVIEW_METADATA_NAME = "Preview from metadata [Crystools]" + CIMAGE_PREVIEW_METADATA_DESC = "Preview from metadata (show prompt) [Crystools]" CUTILS_METADATA_EXTRACTOR_NAME = "Metadata extractor [Crystools]" CUTILS_METADATA_EXTRACTOR_DESC = "Metadata extractor [Crystools]" diff --git a/nodes/image.py b/nodes/image.py index aa311a5..45c9de9 100644 --- a/nodes/image.py +++ b/nodes/image.py @@ -17,15 +17,14 @@ from ..core import CATEGORY, CONFIG, METADATA_RAW,TEXTS, setWidgetValues, logger sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy")) -class CImagePreviewAdvance(PreviewImage): +class CImagePreviewFromImage(PreviewImage): def __init__(self): - # self.output_dir = folder_paths.get_temp_directory() - # self.type = "temp" - # self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) - # self.compress_level = 1 + self.output_dir = folder_paths.get_temp_directory() + self.type = "temp" + self.prefix_append = "_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) + self.compress_level = 1 self.data_cached = None - # self.old_data = None - # self.old_text = None + self.data_cached_text = None @classmethod def INPUT_TYPES(cls): @@ -35,7 +34,6 @@ class CImagePreviewAdvance(PreviewImage): }, "optional": { "image": ("IMAGE",), - "metadata_raw": METADATA_RAW, }, "hidden": { "prompt": "PROMPT", @@ -48,82 +46,116 @@ class CImagePreviewAdvance(PreviewImage): RETURN_NAMES = ("Metadata RAW",) OUTPUT_NODE = True - FUNCTION = "preview" + FUNCTION = "execute" - def preview(self, image=None, metadata_raw=None, prompt=None, extra_pnginfo=None): - result = [] + def execute(self, image=None, prompt=None, extra_pnginfo=None): text = "" - images = [] + title = "" data = { - "result": [], + "result": [''], "ui": { - "text": [], + "text": [''], "images": [], } } - # TODO Refactor with unit tests + + if image is not None: + saved = self.save_images(image, "crystools/i", prompt, extra_pnginfo) + image = saved["ui"]["images"][0] + image_path = Path(self.output_dir).joinpath(image["subfolder"], image["filename"]) + + img, promptFromImage, metadata = buildMetadata(image_path) + + images = [image] + result = metadata + + data["result"] = [result] + data["ui"]["images"] = images + + title = "Source: Image link \n" + text += buildPreviewText(metadata, promptFromImage) + text += f"Current prompt (NO FROM IMAGE!):\n" + text += json.dumps(json.loads(json.loads(promptFromImage)), indent=CONFIG["indent"]) + + self.data_cached_text = text + self.data_cached = data + + elif image is None and self.data_cached is not None: + title = "Source: Image link - CACHED\n" + data = self.data_cached + text = self.data_cached_text + + else: + logger.debug("Source: Empty on CImagePreviewFromImage") + text = "Source: Empty" + + data['ui']['text'] = [title + text] + return data + + +class CImagePreviewFromMetadata(PreviewImage): + def __init__(self): + self.data_cached = None + self.data_cached_text = None + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # if it is required, in next node does not receive any value even the cache! + }, + "optional": { + "metadata_raw": METADATA_RAW, + }, + } + + CATEGORY = CATEGORY.MAIN.value + CATEGORY.IMAGE.value + RETURN_TYPES = ("METADATA_RAW",) + RETURN_NAMES = ("Metadata RAW",) + OUTPUT_NODE = True + + FUNCTION = "execute" + + def execute(self, metadata_raw=None): + text = "" + title = "" + data = { + "result": [''], + "ui": { + "text": [''], + "images": [], + } + } + + # TODO define snakecase? if metadata_raw is not None: - text += "Source: Metadata RAW\n" - text += json.dumps(metadata_raw, indent=CONFIG["indent"]) + promptFromImage = metadata_raw["prompt"] + title = "Source: Metadata RAW\n" + text += buildPreviewText(metadata_raw, promptFromImage) + text += f"Prompt from image:\n" + text += json.dumps(promptFromImage, indent=CONFIG["indent"]) images = self.resolveImage(metadata_raw["fileinfo"]["filename"]) result = metadata_raw - elif self.data_cached is not None: - text += "Source: Metadata RAW - CACHED\n" - text += json.dumps(self.data_cached, indent=CONFIG["indent"]) + data["result"] = [result] + data["ui"]["images"] = images + + self.data_cached_text = text + self.data_cached = data + + elif metadata_raw is None and self.data_cached is not None: + title = "Source: Metadata RAW - CACHED\n" + data = self.data_cached + text = self.data_cached_text - images = self.resolveImage(self.data_cached["fileinfo"]["filename"]) - result = self.data_cached else: - text += "Source: Empty" - - data["result"] = [result] - data["ui"]["text"] = [text] - data["ui"]["images"] = images - - self.data_cached = metadata_raw + logger.debug("Source: Empty on CImagePreviewFromMetadata") + text = "Source: Empty" + data["ui"]["text"] = [title + text] return data - # source = "" - # saved["ui"]["text"] = [source + text + json.dumps(prompt, indent=CONFIG["indent"])] - # saved["result"] = [metadataFromPng] - - # if image is None and self.old_data is None: - # text = "No image linked" - # return {"ui": {"text": [text]}, "result": [None]} - # - # if image is None and self.old_data is not None: - # prompt_to_return = self.old_data["prompt"] - # - # source = f"Source: Cache\n" - # return {"ui": {"text": [source + self.old_text + json.dumps(json.loads(prompt_to_return), indent=CONFIG["indent"])]}, "result": [self.old_data]} - - # saved = self.save_images(image, "crystools", prompt, extra_pnginfo) - # filename = saved["ui"]["images"][0]["filename"] - # image_path = os.path.join(self.output_dir, filename) - # img = Image.open(image_path) - # time = os.path.getmtime(image_path) - # time_human = datetime.datetime.fromtimestamp(time) - # text += f"File: {image_path}\n" - # text += f"Resolution: {img.width}x{img.height}\n" - # text += f"Date: {time_human}\n" - # text += f"Size: {get_size(image_path)}\n" - # text += f"Current prompt (NO FROM IMAGE!):\n" - - # metadataFromPng = {} - # - # if isinstance(img, PngImageFile): - # metadataFromPng = img.info - # - # source = f"Source: {source}\n" - # saved["ui"]["text"] = [source + text + json.dumps(prompt, indent=CONFIG["indent"])] - # saved["result"] = [metadataFromPng] - # - # self.old_data = metadataFromPng - # self.old_text = text - def resolveImage(self, filename=None): images = [] @@ -224,50 +256,8 @@ class CImageLoadWithMetadata: def execute(self, image): image_path = folder_paths.get_annotated_filepath(image) - if Path(image_path).is_file() is False: - raise Exception(TEXTS.FILE_NOT_FOUND.value) - img = Image.open(image_path) - - metadata = {} - prompt = {} - - metadata["fileinfo"] = { - "filename": Path(image_path).as_posix(), - "resolution": { - "x": img.width, - "y": img.height, - }, - "resolution_text": f"{img.width}x{img.height}", - "date": str(datetime.datetime.fromtimestamp(os.path.getmtime(image_path))), - "size": str(get_size(image_path)), - } - - # only for png files - if isinstance(img, PngImageFile): - metadataFromImg = img.info - - # for all metadataFromImg convert to string (but not for workflow and prompt!) - for k, v in metadataFromImg.items(): - # from ComfyUI - if k == "workflow": - try: - metadata["workflow"] = json.loads(metadataFromImg["workflow"]) - except Exception as e: - logger.warn(f"Error parsing metadataFromImg 'workflow': {e}") - - # from ComfyUI - elif k == "prompt": - try: - metadata["prompt"] = json.loads(metadataFromImg["prompt"]) - - # extract prompt from metadataFromImg - prompt = json.dumps(metadataFromImg["prompt"], indent=CONFIG["indent"]) - except Exception as e: - logger.warn(f"Error parsing metadataFromImg 'prompt': {e}") - - else: - metadata[str(k)] = str(v) + img, prompt, metadata = buildMetadata(image_path) img = ImageOps.exif_transpose(img) image = img.convert("RGB") @@ -295,3 +285,56 @@ class CImageLoadWithMetadata: return "Invalid image file: {}".format(image) return True + + +def buildMetadata(image_path): + if Path(image_path).is_file() is False: + raise Exception(TEXTS.FILE_NOT_FOUND.value) + + img = Image.open(image_path) + + metadata = {} + prompt = {} + + metadata["fileinfo"] = { + "filename": Path(image_path).as_posix(), + "resolution": f"{img.width}x{img.height}", + "date": str(datetime.datetime.fromtimestamp(os.path.getmtime(image_path))), + "size": str(get_size(image_path)), + } + + # only for png files + if isinstance(img, PngImageFile): + metadataFromImg = img.info + + # for all metadataFromImg convert to string (but not for workflow and prompt!) + for k, v in metadataFromImg.items(): + # from ComfyUI + if k == "workflow": + try: + metadata["workflow"] = json.loads(metadataFromImg["workflow"]) + except Exception as e: + logger.warn(f"Error parsing metadataFromImg 'workflow': {e}") + + # from ComfyUI + elif k == "prompt": + try: + metadata["prompt"] = json.loads(metadataFromImg["prompt"]) + + # extract prompt from metadataFromImg + prompt = json.dumps(metadataFromImg["prompt"], indent=CONFIG["indent"]) + except Exception as e: + logger.warn(f"Error parsing metadataFromImg 'prompt': {e}") + + else: + metadata[str(k)] = str(v) + + return img, prompt, metadata + + +def buildPreviewText(metadata, prompt): + text = f"File: {metadata['fileinfo']['filename']}\n" + text += f"Resolution: {metadata['fileinfo']['resolution']}\n" + text += f"Date: {metadata['fileinfo']['date']}\n" + text += f"Size: {metadata['fileinfo']['size']}\n" + return text diff --git a/nodes/utils.py b/nodes/utils.py index ad95670..84a3f8f 100644 --- a/nodes/utils.py +++ b/nodes/utils.py @@ -9,20 +9,20 @@ class CMetadataExtractor: def INPUT_TYPES(cls): return { "required": { - "metadata_raw": METADATA_RAW, }, "optional": { + "metadata_raw": METADATA_RAW, } } CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value RETURN_TYPES = ("JSON", "JSON", "JSON", "STRING", "STRING") RETURN_NAMES = ("prompt", "workflow", "file info", "raw to property", "raw to csv") - OUTPUT_NODE = True + # OUTPUT_NODE = True FUNCTION = "execute" - def execute(self, metadata_raw): + def execute(self, metadata_raw=None): prompt = {} workflow = {} fileinfo = {} diff --git a/web/js/image.js b/web/js/image.js index cd5ec5b..93ad3e7 100644 --- a/web/js/image.js +++ b/web/js/image.js @@ -13,8 +13,8 @@ app.registerExtension({ app.registerExtension({ name: 'Crystools.Image.PreviewImageAdvance', async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === 'Preview image advanced [Crystools]') { - displayContext(nodeType, app, 0, true); + if (nodeData.name === 'Preview from image [Crystools]' || nodeData.name === 'Preview from metadata [Crystools]') { + displayContext(nodeType, app, 0); } }, });