From 12044d0afc4b5f194d05ef52a057a026dd09133e Mon Sep 17 00:00:00 2001 From: bigcat88 Date: Sat, 26 Jul 2025 21:01:02 +0300 Subject: [PATCH] converted SaveSVGNode to V3 --- comfy_api/latest/_io.py | 19 +++++++- comfy_api_nodes/nodes_recraft.py | 8 ++-- comfy_extras/v3/nodes_images.py | 74 ++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index ce7dbb434..d03e3c588 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -6,6 +6,7 @@ from abc import ABC, abstractmethod from collections import Counter from dataclasses import asdict, dataclass from enum import Enum +from io import BytesIO from typing import Any, Callable, Literal, TypedDict, TypeVar # used for type hinting @@ -27,7 +28,6 @@ from comfy_api.internal import (_ComfyNodeInternal, _NodeOutputInternal, classpr from comfy_api.latest._resources import Resources, ResourcesLocal from comfy_execution.graph import ExecutionBlocker -# from comfy_extras.nodes_images import SVG as SVG_ # NOTE: needs to be moved before can be imported due to circular reference class FolderType(str, Enum): input = "input" @@ -605,9 +605,24 @@ class Audio(ComfyTypeIO): class Video(ComfyTypeIO): Type = VideoInput + @comfytype(io_type="SVG") class SVG(ComfyTypeIO): - Type = Any # TODO: SVG class is defined in comfy_extras/nodes_images.py, causing circular reference; should be moved to somewhere else before referenced directly in v3 + class Data: + """Stores SVG representations via a list of BytesIO objects.""" + + def __init__(self, data: list[BytesIO]): + self.data = data + + @staticmethod + def combine_all(svgs: list['SVG.Data']) -> 'SVG.Data': + all_svgs_list: list[BytesIO] = [] + for svg_item in svgs: + all_svgs_list.extend(svg_item.data) + return SVG.Data(all_svgs_list) + + Type = Data + @comfytype(io_type="LORA_MODEL") class LoraModel(ComfyTypeIO): diff --git a/comfy_api_nodes/nodes_recraft.py b/comfy_api_nodes/nodes_recraft.py index e369c4b7e..5ce8ac30b 100644 --- a/comfy_api_nodes/nodes_recraft.py +++ b/comfy_api_nodes/nodes_recraft.py @@ -2,7 +2,6 @@ from __future__ import annotations from inspect import cleandoc from typing import Optional from comfy.utils import ProgressBar -from comfy_extras.nodes_images import SVG # Added from comfy.comfy_types.node_typing import IO from comfy_api_nodes.apis.recraft_api import ( RecraftImageGenerationRequest, @@ -35,6 +34,7 @@ from server import PromptServer import torch from io import BytesIO from PIL import UnidentifiedImageError +from comfy_api.latest import io def handle_recraft_file_request( @@ -833,7 +833,7 @@ class RecraftTextToVectorNode: ) svg_data.append(download_url_to_bytesio(data.url, timeout=1024)) - return (SVG(svg_data),) + return (io.SVG.Data(svg_data),) class RecraftVectorizeImageNode: @@ -875,10 +875,10 @@ class RecraftVectorizeImageNode: path="/proxy/recraft/images/vectorize", auth_kwargs=kwargs, ) - svgs.append(SVG(sub_bytes)) + svgs.append(io.SVG.Data(sub_bytes)) pbar.update(1) - return (SVG.combine_all(svgs), ) + return (io.SVG.combine_all(svgs), ) class RecraftReplaceBackgroundNode: diff --git a/comfy_extras/v3/nodes_images.py b/comfy_extras/v3/nodes_images.py index db528dd3c..c5b21bbcc 100644 --- a/comfy_extras/v3/nodes_images.py +++ b/comfy_extras/v3/nodes_images.py @@ -1,5 +1,7 @@ import hashlib +import json import os +import re import numpy as np import torch @@ -708,6 +710,77 @@ class SaveImage(io.ComfyNode): ) +class SaveSVGNode(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="SaveSVGNode_V3", + display_name="Save SVG _V3", + description="Save SVG files on disk.", + category="image/save", + inputs=[ + io.SVG.Input("svg"), + io.String.Input( + "filename_prefix", + default="svg/ComfyUI", + tooltip="The prefix for the file to save. This may include formatting information such as " + "%date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes.", + ), + ], + hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], + is_output_node=True, + ) + + @classmethod + def execute(cls, svg: io.SVG.Data, filename_prefix="svg/ComfyUI") -> io.NodeOutput: + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( + filename_prefix, folder_paths.get_output_directory() + ) + results = [] + + # Prepare metadata JSON + metadata_dict = {} + if cls.hidden.prompt is not None: + metadata_dict["prompt"] = cls.hidden.prompt + if cls.hidden.extra_pnginfo is not None: + metadata_dict.update(cls.hidden.extra_pnginfo) + + # Convert metadata to JSON string + metadata_json = json.dumps(metadata_dict, indent=2) if metadata_dict else None + for batch_number, svg_bytes in enumerate(svg.data): + filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) + file = f"{filename_with_batch_num}_{counter:05}_.svg" + + # Read SVG content + svg_bytes.seek(0) + svg_content = svg_bytes.read().decode('utf-8') + + # Inject metadata if available + if metadata_json: + # Create metadata element with CDATA section + metadata_element = f""" + + + """ + # Insert metadata after opening svg tag using regex with a replacement function + def replacement(match): + # match.group(1) contains the captured tag + return match.group(1) + '\n' + metadata_element + + # Apply the substitution + svg_content = re.sub(r'(]*>)', replacement, svg_content, flags=re.UNICODE) + + # Write the modified SVG to file + with open(os.path.join(full_output_folder, file), 'wb') as svg_file: + svg_file.write(svg_content.encode('utf-8')) + + results.append(ui.SavedResult(file, subfolder, io.FolderType.output)) + counter += 1 + return io.NodeOutput(ui={"images": results}) + + NODES_LIST: list[type[io.ComfyNode]] = [ GetImageSize, ImageAddNoise, @@ -724,4 +797,5 @@ NODES_LIST: list[type[io.ComfyNode]] = [ SaveAnimatedPNG, SaveAnimatedWEBP, SaveImage, + SaveSVGNode, ]