├── .github └── workflows │ └── publish.yml ├── LICENSE ├── README.md ├── __init__.py ├── __pycache__ ├── __init__.cpython-311.pyc ├── __init__.cpython-312.pyc ├── svgnode.cpython-311.pyc └── svgnode.cpython-312.pyc ├── examples ├── 20241012_111028_265.png └── Workflow.json ├── pyproject.toml ├── requirements.txt └── svgnode.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - "pyproject.toml" 10 | 11 | permissions: 12 | issues: write 13 | 14 | jobs: 15 | publish-node: 16 | name: Publish Custom Node to registry 17 | runs-on: ubuntu-latest 18 | if: ${{ github.repository_owner == 'Yanick112' }} 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | - name: Publish Custom Node 23 | uses: Comfy-Org/publish-node-action@v1 24 | with: 25 | ## Add your own personal access token to your Github Repository secrets and reference it here. 26 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yanick112 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-ToSVG 2 | 3 | Huge thanks to visioncortex for this amazing thing! Original repository: https://github.com/visioncortex/vtracer 4 | 5 | ![截图_20240613204507](examples/20241012_111028_265.png) 6 | 7 | ## VTracer ComfyUI Non-Official Implementation 8 | 9 | Welcome to the unofficial implementation of the ComfyUI for VTracer. This project converts raster images into SVG format using the VTracer library. It's a handy tool for designers and developers who need to work with vector graphics programmatically. 10 | 11 | ### Installation 12 | 13 | 1. Navigate to your `/ComfyUI/custom_nodes/` folder. 14 | 2. Run the following command to clone the repository: 15 | 16 | ```shell 17 | git clone https://github.com/Yanick112/ComfyUI-ToSVG/ 18 | ``` 19 | 20 | 4. Navigate to your `ComfyUI-ToSVG` folder. 21 | 22 | - For Portable/venv: 23 | - Run the following command: 24 | ```shell 25 | path/to/ComfUI/python_embeded/python.exe -s -m pip install -r requirements.txt 26 | ``` 27 | - With system Python: 28 | - Run the following command: 29 | ```shell 30 | pip install -r requirements.txt 31 | ``` 32 | 33 | Enjoy setting up your ComfyUI-ToSVG tool! If you encounter any issues or need further help, feel free to reach out. 34 | 35 | ### Partial Parameter Description 36 | 37 | - Filter Speckle (Cleaner) 38 | - Color Precision (More accurate) 39 | - Gradient Step (Less layers) 40 | - Corner Threshold (Smoother) 41 | - Segment Length (More coarse) 42 | - Splice Threshold (Less accurate) 43 | 44 | ### Features 45 | 46 | - Converts images to RGBA format if necessary 47 | - Support batch conversion 48 | 49 | - node `ConvertRasterToVector` to handle the conversion of raster images to SVG format with various parameters for customization. 50 | - node `SaveSVG` to save the resulting SVG data into files. 51 | 52 | ### What's next? 53 | 54 | - [x] Add SVG preview node 55 | - [x] Color and BW mode split 56 | 57 | --- 58 | 59 | Enjoy converting your raster images to SVG with this handy tool! If you have any questions or need further assistance, don't hesitate to reach out. 60 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .svgnode import * 2 | 3 | NODE_CLASS_MAPPINGS = { 4 | "ConvertRasterToVectorColor": ConvertRasterToVectorColor, 5 | "ConvertRasterToVectorBW": ConvertRasterToVectorBW, 6 | "ConvertVectorToRaster": ConvertVectorToRaster, 7 | "SaveSVG": SaveSVG, 8 | "SVGPreview": SVGPreview, 9 | } 10 | 11 | NODE_DISPLAY_NAME_MAPPINGS = { 12 | "ConvertRasterToVectorColor": "Raster to Vector (SVG)Color", 13 | "ConvertRasterToVectorBW": "Raster to Vector (SVG)BW", 14 | "ConvertVectorToRaster": "Vector to Raster (SVG)", 15 | "SaveSVG": "Save SVG", 16 | "SVGPreview": "SVG Preview", 17 | } 18 | 19 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] 20 | -------------------------------------------------------------------------------- /__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yanick112/ComfyUI-ToSVG/09d6f6b89e52e7b529741cf82a9a15ab3f7fd857/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yanick112/ComfyUI-ToSVG/09d6f6b89e52e7b529741cf82a9a15ab3f7fd857/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/svgnode.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yanick112/ComfyUI-ToSVG/09d6f6b89e52e7b529741cf82a9a15ab3f7fd857/__pycache__/svgnode.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/svgnode.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yanick112/ComfyUI-ToSVG/09d6f6b89e52e7b529741cf82a9a15ab3f7fd857/__pycache__/svgnode.cpython-312.pyc -------------------------------------------------------------------------------- /examples/20241012_111028_265.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yanick112/ComfyUI-ToSVG/09d6f6b89e52e7b529741cf82a9a15ab3f7fd857/examples/20241012_111028_265.png -------------------------------------------------------------------------------- /examples/Workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 9, 3 | "last_link_id": 8, 4 | "nodes": [ 5 | { 6 | "id": 6, 7 | "type": "SaveSVG", 8 | "pos": { 9 | "0": 968, 10 | "1": 732 11 | }, 12 | "size": { 13 | "0": 315, 14 | "1": 106 15 | }, 16 | "flags": {}, 17 | "order": 4, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "svg_strings", 22 | "type": "LIST", 23 | "link": 4, 24 | "label": "svg_strings" 25 | } 26 | ], 27 | "outputs": [], 28 | "properties": { 29 | "Node name for S&R": "SaveSVG" 30 | }, 31 | "widgets_values": [ 32 | "ComfyUI_SVG", 33 | true, 34 | "" 35 | ] 36 | }, 37 | { 38 | "id": 9, 39 | "type": "PreviewImage", 40 | "pos": { 41 | "0": 1341, 42 | "1": 283 43 | }, 44 | "size": [ 45 | 210, 46 | 246 47 | ], 48 | "flags": {}, 49 | "order": 8, 50 | "mode": 0, 51 | "inputs": [ 52 | { 53 | "name": "images", 54 | "type": "IMAGE", 55 | "link": 8, 56 | "label": "images" 57 | } 58 | ], 59 | "outputs": [], 60 | "properties": { 61 | "Node name for S&R": "PreviewImage" 62 | } 63 | }, 64 | { 65 | "id": 5, 66 | "type": "PreviewImage", 67 | "pos": { 68 | "0": 1346, 69 | "1": 616 70 | }, 71 | "size": [ 72 | 210, 73 | 246 74 | ], 75 | "flags": {}, 76 | "order": 7, 77 | "mode": 0, 78 | "inputs": [ 79 | { 80 | "name": "images", 81 | "type": "IMAGE", 82 | "link": 3, 83 | "label": "images" 84 | } 85 | ], 86 | "outputs": [], 87 | "properties": { 88 | "Node name for S&R": "PreviewImage" 89 | } 90 | }, 91 | { 92 | "id": 3, 93 | "type": "LoadImage", 94 | "pos": { 95 | "0": 192, 96 | "1": 275 97 | }, 98 | "size": [ 99 | 315, 100 | 314 101 | ], 102 | "flags": {}, 103 | "order": 0, 104 | "mode": 0, 105 | "inputs": [], 106 | "outputs": [ 107 | { 108 | "name": "IMAGE", 109 | "type": "IMAGE", 110 | "links": [ 111 | 1, 112 | 5 113 | ], 114 | "label": "IMAGE", 115 | "slot_index": 0 116 | }, 117 | { 118 | "name": "MASK", 119 | "type": "MASK", 120 | "links": null, 121 | "label": "MASK" 122 | } 123 | ], 124 | "properties": { 125 | "Node name for S&R": "LoadImage" 126 | }, 127 | "widgets_values": [ 128 | "yanick_113_just_pure_cuteness_and_nothing_else_but_cuteness_T_cd5200d8-0281-46bc-a140-1b526ddfe2f2_1.png", 129 | "image" 130 | ] 131 | }, 132 | { 133 | "id": 1, 134 | "type": "ConvertRasterToVectorColor", 135 | "pos": { 136 | "0": 580, 137 | "1": 270 138 | }, 139 | "size": { 140 | "0": 340.20001220703125, 141 | "1": 274 142 | }, 143 | "flags": {}, 144 | "order": 2, 145 | "mode": 0, 146 | "inputs": [ 147 | { 148 | "name": "image", 149 | "type": "IMAGE", 150 | "link": 5, 151 | "label": "image" 152 | } 153 | ], 154 | "outputs": [ 155 | { 156 | "name": "LIST", 157 | "type": "LIST", 158 | "links": [ 159 | 6, 160 | 7 161 | ], 162 | "label": "LIST", 163 | "slot_index": 0 164 | } 165 | ], 166 | "properties": { 167 | "Node name for S&R": "ConvertRasterToVectorColor" 168 | }, 169 | "widgets_values": [ 170 | "stacked", 171 | "spline", 172 | 4, 173 | 6, 174 | 16, 175 | 60, 176 | 4, 177 | 10, 178 | 45, 179 | 3 180 | ] 181 | }, 182 | { 183 | "id": 2, 184 | "type": "ConvertRasterToVectorBW", 185 | "pos": { 186 | "0": 588, 187 | "1": 643 188 | }, 189 | "size": { 190 | "0": 315, 191 | "1": 154 192 | }, 193 | "flags": {}, 194 | "order": 1, 195 | "mode": 0, 196 | "inputs": [ 197 | { 198 | "name": "image", 199 | "type": "IMAGE", 200 | "link": 1, 201 | "label": "image" 202 | } 203 | ], 204 | "outputs": [ 205 | { 206 | "name": "LIST", 207 | "type": "LIST", 208 | "links": [ 209 | 2, 210 | 4 211 | ], 212 | "label": "LIST", 213 | "slot_index": 0 214 | } 215 | ], 216 | "properties": { 217 | "Node name for S&R": "ConvertRasterToVectorBW" 218 | }, 219 | "widgets_values": [ 220 | "spline", 221 | 4, 222 | 60, 223 | 4, 224 | 45 225 | ] 226 | }, 227 | { 228 | "id": 7, 229 | "type": "ConvertVectorToRaster", 230 | "pos": { 231 | "0": 991, 232 | "1": 284 233 | }, 234 | "size": { 235 | "0": 277.20001220703125, 236 | "1": 26 237 | }, 238 | "flags": {}, 239 | "order": 5, 240 | "mode": 0, 241 | "inputs": [ 242 | { 243 | "name": "svg_strings", 244 | "type": "LIST", 245 | "link": 6, 246 | "label": "svg_strings" 247 | } 248 | ], 249 | "outputs": [ 250 | { 251 | "name": "IMAGE", 252 | "type": "IMAGE", 253 | "links": [ 254 | 8 255 | ], 256 | "label": "IMAGE", 257 | "slot_index": 0 258 | } 259 | ], 260 | "properties": { 261 | "Node name for S&R": "ConvertVectorToRaster" 262 | } 263 | }, 264 | { 265 | "id": 8, 266 | "type": "SaveSVG", 267 | "pos": { 268 | "0": 983, 269 | "1": 385 270 | }, 271 | "size": { 272 | "0": 315, 273 | "1": 106 274 | }, 275 | "flags": {}, 276 | "order": 6, 277 | "mode": 0, 278 | "inputs": [ 279 | { 280 | "name": "svg_strings", 281 | "type": "LIST", 282 | "link": 7, 283 | "label": "svg_strings" 284 | } 285 | ], 286 | "outputs": [], 287 | "properties": { 288 | "Node name for S&R": "SaveSVG" 289 | }, 290 | "widgets_values": [ 291 | "ComfyUI_SVG", 292 | true, 293 | "" 294 | ] 295 | }, 296 | { 297 | "id": 4, 298 | "type": "ConvertVectorToRaster", 299 | "pos": { 300 | "0": 972, 301 | "1": 640 302 | }, 303 | "size": { 304 | "0": 277.20001220703125, 305 | "1": 26 306 | }, 307 | "flags": {}, 308 | "order": 3, 309 | "mode": 0, 310 | "inputs": [ 311 | { 312 | "name": "svg_strings", 313 | "type": "LIST", 314 | "link": 2, 315 | "label": "svg_strings" 316 | } 317 | ], 318 | "outputs": [ 319 | { 320 | "name": "IMAGE", 321 | "type": "IMAGE", 322 | "links": [ 323 | 3 324 | ], 325 | "label": "IMAGE", 326 | "slot_index": 0 327 | } 328 | ], 329 | "properties": { 330 | "Node name for S&R": "ConvertVectorToRaster" 331 | } 332 | } 333 | ], 334 | "links": [ 335 | [ 336 | 1, 337 | 3, 338 | 0, 339 | 2, 340 | 0, 341 | "IMAGE" 342 | ], 343 | [ 344 | 2, 345 | 2, 346 | 0, 347 | 4, 348 | 0, 349 | "LIST" 350 | ], 351 | [ 352 | 3, 353 | 4, 354 | 0, 355 | 5, 356 | 0, 357 | "IMAGE" 358 | ], 359 | [ 360 | 4, 361 | 2, 362 | 0, 363 | 6, 364 | 0, 365 | "LIST" 366 | ], 367 | [ 368 | 5, 369 | 3, 370 | 0, 371 | 1, 372 | 0, 373 | "IMAGE" 374 | ], 375 | [ 376 | 6, 377 | 1, 378 | 0, 379 | 7, 380 | 0, 381 | "LIST" 382 | ], 383 | [ 384 | 7, 385 | 1, 386 | 0, 387 | 8, 388 | 0, 389 | "LIST" 390 | ], 391 | [ 392 | 8, 393 | 7, 394 | 0, 395 | 9, 396 | 0, 397 | "IMAGE" 398 | ] 399 | ], 400 | "groups": [], 401 | "config": {}, 402 | "extra": { 403 | "ds": { 404 | "scale": 0.8954302432553057, 405 | "offset": [ 406 | -68.58408775957353, 407 | -60.800600936531914 408 | ] 409 | } 410 | }, 411 | "version": 0.4 412 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-tosvg" 3 | description = "This project converts raster images into SVG format using the [a/VTracer](https://github.com/visioncortex/vtracer) library. It's a handy tool for designers and developers who need to work with vector graphics programmatically." 4 | version = "1.0.0" 5 | license = { file = "LICENSE" } 6 | dependencies = ["numpy", "Pillow", "torch", "vtracer"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/Yanick112/ComfyUI-ToSVG" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "" 14 | DisplayName = "ComfyUI-ToSVG" 15 | Icon = "" 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | Pillow 3 | torch 4 | vtracer 5 | PyMuPDF -------------------------------------------------------------------------------- /svgnode.py: -------------------------------------------------------------------------------- 1 | import vtracer 2 | import os 3 | import time 4 | import folder_paths 5 | import numpy as np 6 | from PIL import Image 7 | from typing import List, Tuple 8 | import torch 9 | from io import BytesIO 10 | import fitz 11 | import random 12 | import folder_paths 13 | 14 | from PIL import Image 15 | from nodes import SaveImage 16 | 17 | # Tensor to PIL 18 | def tensor2pil(image): 19 | return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) 20 | 21 | # PIL to Tensor 22 | def pil2tensor(image): 23 | return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) 24 | 25 | class ConvertRasterToVectorColor: 26 | @classmethod 27 | def INPUT_TYPES(cls): 28 | return { 29 | "required": { 30 | "image": ("IMAGE",), 31 | "hierarchical": (["stacked", "cutout"], {"default": "stacked"}), 32 | "mode": (["spline", "polygon", "none"], {"default": "spline"}), 33 | "filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}), 34 | "color_precision": ("INT", {"default": 6, "min": 0, "max": 10, "step": 1}), 35 | "layer_difference": ("INT", {"default": 16, "min": 0, "max": 256, "step": 1}), 36 | "corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180, "step": 1}), 37 | "length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.1}), 38 | "max_iterations": ("INT", {"default": 10, "min": 1, "max": 70, "step": 1}), 39 | "splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180, "step": 1}), 40 | "path_precision": ("INT", {"default": 3, "min": 0, "max": 10, "step": 1}), 41 | } 42 | } 43 | 44 | RETURN_TYPES = ("STRING",) 45 | OUTPUT_IS_LIST = (True,) 46 | FUNCTION = "convert_to_svg" 47 | 48 | CATEGORY = "💎TOSVG" 49 | 50 | def convert_to_svg(self, image, hierarchical, mode, filter_speckle, color_precision, layer_difference, corner_threshold, 51 | length_threshold, max_iterations, splice_threshold, path_precision): 52 | 53 | svg_strings = [] 54 | 55 | for i in image: 56 | i = torch.unsqueeze(i, 0) 57 | _image = tensor2pil(i) 58 | 59 | if _image.mode != 'RGBA': 60 | alpha = Image.new('L', _image.size, 255) 61 | _image.putalpha(alpha) 62 | 63 | pixels = list(_image.getdata()) 64 | size = _image.size 65 | 66 | svg_str = vtracer.convert_pixels_to_svg( 67 | pixels, 68 | size=size, 69 | colormode="color", 70 | hierarchical=hierarchical, 71 | mode=mode, 72 | filter_speckle=filter_speckle, 73 | color_precision=color_precision, 74 | layer_difference=layer_difference, 75 | corner_threshold=corner_threshold, 76 | length_threshold=length_threshold, 77 | max_iterations=max_iterations, 78 | splice_threshold=splice_threshold, 79 | path_precision=path_precision, 80 | ) 81 | 82 | svg_strings.append(svg_str) 83 | 84 | return (svg_strings,) 85 | 86 | class ConvertRasterToVectorBW: 87 | @classmethod 88 | def INPUT_TYPES(cls): 89 | return { 90 | "required": { 91 | "image": ("IMAGE",), 92 | "mode": (["spline", "polygon", "none"], {"default": "spline"}), 93 | "filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}), 94 | "corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180, "step": 1}), 95 | "length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.1}), 96 | "splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180, "step": 1}), 97 | } 98 | } 99 | 100 | RETURN_TYPES = ("STRING",) 101 | OUTPUT_IS_LIST = (True,) 102 | FUNCTION = "convert_to_svg" 103 | 104 | CATEGORY = "💎TOSVG" 105 | 106 | def convert_to_svg(self, image, mode, filter_speckle, corner_threshold, length_threshold, splice_threshold): 107 | 108 | svg_strings = [] 109 | 110 | for i in image: 111 | i = torch.unsqueeze(i, 0) 112 | _image = tensor2pil(i) 113 | 114 | if _image.mode != 'RGBA': 115 | alpha = Image.new('L', _image.size, 255) 116 | _image.putalpha(alpha) 117 | 118 | pixels = list(_image.getdata()) 119 | size = _image.size 120 | 121 | svg_str = vtracer.convert_pixels_to_svg( 122 | pixels, 123 | size=size, 124 | colormode="binary", 125 | mode=mode, 126 | filter_speckle=filter_speckle, 127 | corner_threshold=corner_threshold, 128 | length_threshold=length_threshold, 129 | splice_threshold=splice_threshold, 130 | ) 131 | 132 | svg_strings.append(svg_str) 133 | 134 | return (svg_strings,) 135 | 136 | 137 | class ConvertVectorToRaster: 138 | @classmethod 139 | def INPUT_TYPES(cls): 140 | return { 141 | "required": { 142 | "svg_strings": ("STRING", {"forceInput": True}) 143 | } 144 | } 145 | 146 | RETURN_TYPES = ("IMAGE",) 147 | FUNCTION = "convert_svg_to_image" 148 | CATEGORY = "💎TOSVG" 149 | 150 | def convert_svg_to_image(self, svg_strings): 151 | 152 | doc = fitz.open(stream=svg_strings.encode('utf-8'), filetype="svg") 153 | page = doc.load_page(0) 154 | pix = page.get_pixmap() 155 | 156 | image_data = pix.tobytes("png") 157 | pil_image = Image.open(BytesIO(image_data)).convert("RGB") 158 | 159 | return (pil2tensor(pil_image),) 160 | 161 | 162 | class SaveSVG: 163 | def __init__(self): 164 | self.output_dir = folder_paths.get_output_directory() 165 | 166 | @classmethod 167 | def INPUT_TYPES(cls): 168 | return { 169 | "required": { 170 | "svg_strings": ("STRING", {"forceInput": True}), 171 | "filename_prefix": ("STRING", {"default": "ComfyUI_SVG"}), 172 | }, 173 | "optional": { 174 | "append_timestamp": ("BOOLEAN", {"default": True}), 175 | "custom_output_path": ("STRING", {"default": "", "multiline": False}), 176 | } 177 | } 178 | 179 | CATEGORY = "💎TOSVG" 180 | DESCRIPTION = "Save SVG data to a file." 181 | RETURN_TYPES = () 182 | OUTPUT_NODE = True 183 | FUNCTION = "save_svg_file" 184 | 185 | def generate_unique_filename(self, prefix, timestamp=False): 186 | if timestamp: 187 | timestamp_str = time.strftime("%Y%m%d%H%M%S") 188 | return f"{prefix}_{timestamp_str}.svg" 189 | else: 190 | return f"{prefix}.svg" 191 | 192 | def save_svg_file(self, svg_strings, filename_prefix="ComfyUI_SVG", append_timestamp=True, custom_output_path=""): 193 | 194 | output_path = custom_output_path if custom_output_path else self.output_dir 195 | os.makedirs(output_path, exist_ok=True) 196 | 197 | unique_filename = self.generate_unique_filename(f"{filename_prefix}", append_timestamp) 198 | final_filepath = os.path.join(output_path, unique_filename) 199 | 200 | 201 | with open(final_filepath, "w") as svg_file: 202 | svg_file.write(svg_strings) 203 | 204 | 205 | ui_info = {"ui": {"saved_svg": unique_filename, "path": final_filepath}} 206 | 207 | return ui_info 208 | 209 | 210 | 211 | 212 | class SVGPreview(SaveImage): 213 | @classmethod 214 | def INPUT_TYPES(s): 215 | return { 216 | "required": { 217 | "svg_strings": ("STRING", {"forceInput": True}) 218 | } 219 | } 220 | 221 | FUNCTION = "svg_preview" 222 | CATEGORY = "💎TOSVG" 223 | OUTPUT_NODE = True 224 | 225 | def __init__(self): 226 | self.output_dir = folder_paths.get_temp_directory() 227 | self.type = "temp" 228 | self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(5)) 229 | self.compress_level = 4 230 | 231 | def svg_preview(self, svg_strings): 232 | doc = fitz.open(stream=svg_strings.encode('utf-8'), filetype="svg") 233 | page = doc.load_page(0) 234 | pix = page.get_pixmap() 235 | 236 | image_data = pix.tobytes("png") 237 | pil_image = Image.open(BytesIO(image_data)).convert("RGB") 238 | 239 | preview = pil2tensor(pil_image) 240 | 241 | return self.save_images(preview, "PointPreview") 242 | --------------------------------------------------------------------------------