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