diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c14f1d5 --- /dev/null +++ b/__init__.py @@ -0,0 +1,13 @@ +from .svgnode import * + +NODE_CLASS_MAPPINGS = { + "ConvertRasterToVector": ConvertRasterToVector, + "SaveSVG": SaveSVG +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ConvertRasterToVector": "Raster to Vector (SVG)", + "SaveSVG": "Save SVG" +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/examples/SD3生成扁平风转SVG.json b/examples/SD3生成扁平风转SVG.json new file mode 100644 index 0000000..7714e37 --- /dev/null +++ b/examples/SD3生成扁平风转SVG.json @@ -0,0 +1,665 @@ +{ + "last_node_id": 48, + "last_link_id": 77, + "nodes": [ + { + "id": 26, + "type": "ModelSamplingSD3", + "pos": [ + 620, + 380 + ], + "size": { + "0": 220, + "1": 60 + }, + "flags": { + "collapsed": false + }, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 31, + "label": "model" + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 41 + ], + "shape": 3, + "slot_index": 0, + "label": "MODEL" + } + ], + "properties": { + "Node name for S&R": "ModelSamplingSD3" + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 33, + "type": "VAEDecode", + "pos": [ + 1120, + 380 + ], + "size": { + "0": 140, + "1": 60 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 38, + "label": "Latent" + }, + { + "name": "vae", + "type": "VAE", + "link": 39, + "label": "VAE" + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 46 + ], + "shape": 3, + "slot_index": 0, + "label": "图像" + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 31, + "type": "CLIPTextEncode", + "pos": [ + 440, + 760 + ], + "size": { + "0": 400, + "1": 200 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 49, + "label": "CLIP" + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 52 + ], + "shape": 3, + "slot_index": 0, + "label": "条件" + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "(worst quality:1.3), low quality, lowres, messy, ugly, disfigured, bad anatomy, draft, deformed hands, fused fingers, signature, text, multi views" + ] + }, + { + "id": 22, + "type": "SaveSVG", + "pos": [ + 2220, + 380 + ], + "size": [ + 220, + 100 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "svg_strings", + "type": "LIST", + "link": 25, + "label": "svg_strings" + } + ], + "properties": { + "Node name for S&R": "SaveSVG" + }, + "widgets_values": [ + "ComfyUI_SVG", + true, + "" + ] + }, + { + "id": 35, + "type": "CheckpointLoaderSimple", + "pos": [ + -20, + 380 + ], + "size": [ + 400, + 100 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 31 + ], + "shape": 3, + "slot_index": 0, + "label": "模型" + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 48, + 49 + ], + "shape": 3, + "slot_index": 1, + "label": "CLIP" + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 39 + ], + "shape": 3, + "slot_index": 2, + "label": "VAE" + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "SD3\\sd3_medium_incl_clips.safetensors" + ] + }, + { + "id": 38, + "type": "PreviewBridge", + "pos": [ + 1280, + 380 + ], + "size": [ + 320, + 300 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 46, + "label": "图像" + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 47 + ], + "shape": 3, + "label": "图像", + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3, + "label": "遮罩" + } + ], + "properties": { + "Node name for S&R": "PreviewBridge" + }, + "widgets_values": [ + "$38-0" + ] + }, + { + "id": 21, + "type": "ConvertRasterToVector", + "pos": [ + 1980, + 380 + ], + "size": [ + 220, + 300 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 76, + "label": "image" + } + ], + "outputs": [ + { + "name": "LIST", + "type": "LIST", + "links": [ + 25 + ], + "shape": 3, + "label": "LIST", + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ConvertRasterToVector" + }, + "widgets_values": [ + "color", + "stacked", + "spline", + 4, + 6, + 16, + 60, + 4, + 10, + 45, + 3 + ] + }, + { + "id": 19, + "type": "Image Rembg (Remove Background)", + "pos": [ + 1640, + 380 + ], + "size": [ + 320, + 260 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 47, + "label": "图像" + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": [ + 76, + 77 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Image Rembg (Remove Background)" + }, + "widgets_values": [ + true, + "isnet-anime", + false, + false, + false, + 240, + 10, + 10, + "none" + ] + }, + { + "id": 48, + "type": "SaveImage", + "pos": [ + 1640, + 680 + ], + "size": [ + 320, + 280 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 77, + "label": "图像" + } + ], + "properties": { + "Node name for S&R": "SaveImage" + }, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 32, + "type": "EmptySD3LatentImage", + "pos": [ + 160, + 520 + ], + "size": { + "0": 220, + "1": 106 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 44 + ], + "shape": 3, + "slot_index": 0, + "label": "LATENT" + } + ], + "properties": { + "Node name for S&R": "EmptySD3LatentImage" + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 24, + "type": "CLIPTextEncode", + "pos": [ + 440, + 500 + ], + "size": { + "0": 400, + "1": 220 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 48, + "label": "CLIP" + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 42 + ], + "shape": 3, + "slot_index": 0, + "label": "条件" + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "flat illustration, pair of shoes, peach color background, by Adrian Tomine" + ] + }, + { + "id": 36, + "type": "KSampler", + "pos": [ + 880, + 380 + ], + "size": [ + 220, + 260 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 41, + "label": "模型" + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 42, + "label": "正面条件" + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 52, + "label": "负面条件" + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 44, + "label": "Latent" + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 38 + ], + "shape": 3, + "slot_index": 0, + "label": "Latent" + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 763482007024129, + "fixed", + 28, + 4.5, + "dpmpp_2m", + "sgm_uniform", + 1 + ] + } + ], + "links": [ + [ + 25, + 21, + 0, + 22, + 0, + "LIST" + ], + [ + 31, + 35, + 0, + 26, + 0, + "MODEL" + ], + [ + 38, + 36, + 0, + 33, + 0, + "LATENT" + ], + [ + 39, + 35, + 2, + 33, + 1, + "VAE" + ], + [ + 41, + 26, + 0, + 36, + 0, + "MODEL" + ], + [ + 42, + 24, + 0, + 36, + 1, + "CONDITIONING" + ], + [ + 44, + 32, + 0, + 36, + 3, + "LATENT" + ], + [ + 46, + 33, + 0, + 38, + 0, + "IMAGE" + ], + [ + 47, + 38, + 0, + 19, + 0, + "IMAGE" + ], + [ + 48, + 35, + 1, + 24, + 0, + "CLIP" + ], + [ + 49, + 35, + 1, + 31, + 0, + "CLIP" + ], + [ + 52, + 31, + 0, + 36, + 2, + "CONDITIONING" + ], + [ + 76, + 19, + 0, + 21, + 0, + "IMAGE" + ], + [ + 77, + 19, + 0, + 48, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.6588450000000093, + "offset": [ + -311.4302663398564, + 25.544650420863906 + ] + }, + "groupNodes": {} + }, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/图片转SVG.json b/examples/图片转SVG.json new file mode 100644 index 0000000..d41562c --- /dev/null +++ b/examples/图片转SVG.json @@ -0,0 +1,255 @@ +{ + "last_node_id": 49, + "last_link_id": 80, + "nodes": [ + { + "id": 22, + "type": "SaveSVG", + "pos": [ + 2220, + 380 + ], + "size": [ + 220, + 100 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "svg_strings", + "type": "LIST", + "link": 25, + "label": "svg_strings" + } + ], + "properties": { + "Node name for S&R": "SaveSVG" + }, + "widgets_values": [ + "ComfyUI_SVG", + true, + "" + ] + }, + { + "id": 21, + "type": "ConvertRasterToVector", + "pos": [ + 1980, + 380 + ], + "size": [ + 220, + 300 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 80, + "label": "image" + } + ], + "outputs": [ + { + "name": "LIST", + "type": "LIST", + "links": [ + 25 + ], + "shape": 3, + "label": "LIST", + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ConvertRasterToVector" + }, + "widgets_values": [ + "color", + "stacked", + "spline", + 4, + 6, + 16, + 60, + 4, + 10, + 45, + 3 + ] + }, + { + "id": 48, + "type": "SaveImage", + "pos": [ + 1640, + 680 + ], + "size": [ + 320, + 280 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 77, + "label": "图像" + } + ], + "properties": { + "Node name for S&R": "SaveImage" + }, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 19, + "type": "Image Rembg (Remove Background)", + "pos": [ + 1640, + 380 + ], + "size": [ + 320, + 260 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 79, + "label": "图像", + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": [ + 77 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Image Rembg (Remove Background)" + }, + "widgets_values": [ + true, + "isnet-anime", + false, + false, + false, + 240, + 10, + 10, + "none" + ] + }, + { + "id": 49, + "type": "LoadImage", + "pos": [ + 1280, + 380 + ], + "size": [ + 320, + 320 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 79, + 80 + ], + "shape": 3, + "label": "图像", + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3, + "label": "遮罩" + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "cj.parklee_Retro_Ghibli_styleThe_magician_a_young_girl_pink_hai_34ed42e2-f958-45d9-bd57-46f34514b0cd.png", + "image" + ] + } + ], + "links": [ + [ + 25, + 21, + 0, + 22, + 0, + "LIST" + ], + [ + 77, + 19, + 0, + 48, + 0, + "IMAGE" + ], + [ + 79, + 49, + 0, + 19, + 0, + "IMAGE" + ], + [ + 80, + 49, + 0, + 21, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1.1671841070450175, + "offset": [ + -1031.970146842897, + -277.3179964416557 + ] + }, + "groupNodes": {} + }, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/透明底图片转SVG.json b/examples/透明底图片转SVG.json new file mode 100644 index 0000000..f9a4cb4 --- /dev/null +++ b/examples/透明底图片转SVG.json @@ -0,0 +1,223 @@ +{ + "last_node_id": 50, + "last_link_id": 84, + "nodes": [ + { + "id": 22, + "type": "SaveSVG", + "pos": [ + 2220, + 380 + ], + "size": { + "0": 220, + "1": 106 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "svg_strings", + "type": "LIST", + "link": 25, + "label": "svg_strings" + } + ], + "properties": { + "Node name for S&R": "SaveSVG" + }, + "widgets_values": [ + "ComfyUI_SVG", + true, + "" + ] + }, + { + "id": 21, + "type": "ConvertRasterToVector", + "pos": [ + 1980, + 380 + ], + "size": { + "0": 220, + "1": 300 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 84, + "label": "image" + } + ], + "outputs": [ + { + "name": "LIST", + "type": "LIST", + "links": [ + 25 + ], + "shape": 3, + "label": "LIST", + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ConvertRasterToVector" + }, + "widgets_values": [ + "color", + "stacked", + "spline", + 4, + 6, + 16, + 60, + 4, + 10, + 45, + 3 + ] + }, + { + "id": 50, + "type": "JoinImageWithAlpha", + "pos": [ + 1760, + 380 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 81, + "label": "图像" + }, + { + "name": "alpha", + "type": "MASK", + "link": 82, + "label": "透明遮罩" + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 84 + ], + "shape": 3, + "label": "图像", + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "JoinImageWithAlpha" + } + }, + { + "id": 49, + "type": "LoadImage", + "pos": [ + 1400, + 380 + ], + "size": { + "0": 320, + "1": 320 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 81 + ], + "shape": 3, + "label": "图像", + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [ + 82 + ], + "shape": 3, + "label": "遮罩", + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "ComfyUI_temp_czplp_00006_ (2).png", + "image" + ] + } + ], + "links": [ + [ + 25, + 21, + 0, + 22, + 0, + "LIST" + ], + [ + 81, + 49, + 0, + 50, + 0, + "IMAGE" + ], + [ + 82, + 49, + 1, + 50, + 1, + "MASK" + ], + [ + 84, + 50, + 0, + 21, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.8769226950000005, + "offset": [ + -1013.2097887128057, + -18.536754881162064 + ] + }, + "groupNodes": {} + }, + "version": 0.4 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9188dfe --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +numpy +Pillow +torch +vtracer diff --git a/svgnode.py b/svgnode.py new file mode 100644 index 0000000..08405a2 --- /dev/null +++ b/svgnode.py @@ -0,0 +1,141 @@ +import vtracer +import os +import time +import folder_paths +import numpy as np +from PIL import Image +from typing import List, Tuple +import torch + +def RGB2RGBA(image:Image, mask:Image) -> Image: + (R, G, B) = image.convert('RGB').split() + return Image.merge('RGBA', (R, G, B, mask.convert('L'))) + +def pil2tensor(image:Image) -> torch.Tensor: + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + +def tensor2pil(t_image: torch.Tensor) -> Image: + return Image.fromarray(np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +class ConvertRasterToVector: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "colormode": (["color", "binary"], {"default": "color"}), + "hierarchical": (["stacked", "cutout"], {"default": "stacked"}), + "mode": (["spline", "polygon", "none"], {"default": "spline"}), + "filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100}), + "color_precision": ("INT", {"default": 6, "min": 0, "max": 10}), + "layer_difference": ("INT", {"default": 16, "min": 0, "max": 256}), + "corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180}), + "length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0}), + "max_iterations": ("INT", {"default": 10, "min": 1, "max": 70}), + "splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180}), + "path_precision": ("INT", {"default": 3, "min": 0, "max": 10}), + } + } + + RETURN_TYPES = ("LIST",) + FUNCTION = "convert_to_svg" + + CATEGORY = "💎TOSVG" + + def convert_to_svg(self, image, colormode, hierarchical, mode, filter_speckle, color_precision, layer_difference, corner_threshold, length_threshold, max_iterations, splice_threshold, path_precision): + + svg_strings = [] + + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i) + + if _image.mode != 'RGBA': + alpha = Image.new('L', _image.size, 255) + _image.putalpha(alpha) + + + pixels = list(_image.getdata()) + + size = _image.size + + + svg_str = vtracer.convert_pixels_to_svg( + pixels, + size=size, + colormode=colormode, + hierarchical=hierarchical, + mode=mode, + filter_speckle=filter_speckle, + color_precision=color_precision, + layer_difference=layer_difference, + corner_threshold=corner_threshold, + length_threshold=length_threshold, + max_iterations=max_iterations, + splice_threshold=splice_threshold, + path_precision=path_precision + ) + + + svg_strings.append(svg_str) + + return (svg_strings,) + + + + + +class SaveSVG: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "svg_strings": ("LIST", {"forceInput": True}), + "filename_prefix": ("STRING", {"default": "ComfyUI_SVG"}), + }, + "optional": { + "append_timestamp": ("BOOLEAN", {"default": True}), + "custom_output_path": ("STRING", {"default": "", "multiline": False}), + } + } + + CATEGORY = "💎TOSVG" + DESCRIPTION = "Save SVG data to a file." + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "save_svg_file" + + def generate_unique_filename(self, prefix, timestamp=False): + if timestamp: + timestamp_str = time.strftime("%Y%m%d%H%M%S") + return f"{prefix}_{timestamp_str}.svg" + else: + return f"{prefix}.svg" + + def save_svg_file(self, svg_strings, filename_prefix="ComfyUI_SVG", append_timestamp=True, custom_output_path=""): + + output_path = custom_output_path if custom_output_path else self.output_dir + os.makedirs(output_path, exist_ok=True) + + ui_info_list = [] + + for index, svg_string in enumerate(svg_strings): + + unique_filename = self.generate_unique_filename(f"{filename_prefix}_{index}", append_timestamp) + final_filepath = os.path.join(output_path, unique_filename) + + + with open(final_filepath, "w") as svg_file: + svg_file.write(svg_string) + + + ui_info = {"ui": {"saved_svg": unique_filename, "path": final_filepath}} + ui_info_list.append(ui_info) + + return ui_info_list + +