Full Code of Yanick112/ComfyUI-ToSVG for AI

main 1022ba05318d cached
9 files
49.4 KB
13.0k tokens
41 symbols
1 requests
Download .txt
Repository: Yanick112/ComfyUI-ToSVG
Branch: main
Commit: 1022ba05318d
Files: 9
Total size: 49.4 KB

Directory structure:
gitextract_cfygdqbr/

├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── examples/
│   └── to_svg.json
├── pyproject.toml
├── requirements.txt
└── svgnode.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to Comfy registry
on:
  workflow_dispatch:
  push:
    branches:
      - main
      - master
    paths:
      - "pyproject.toml"

permissions:
  issues: write

jobs:
  publish-node:
    name: Publish Custom Node to registry
    runs-on: ubuntu-latest
    if: ${{ github.repository_owner == 'Yanick112' }}
    steps:
      - name: Check out code
        uses: actions/checkout@v4
      - name: Publish Custom Node
        uses: Comfy-Org/publish-node-action@v1
        with:
          ## Add your own personal access token to your Github Repository secrets and reference it here.
          personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyderworkspace

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/ 


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Yanick112

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# ComfyUI-ToSVG

Huge thanks to visioncortex and potracer for this amazing thing! Original repository: https://github.com/visioncortex/vtracer and https://github.com/tatarize/potrace

![截图_20240613204507](examples/workflow_20250618_000738.png)



## Update
### 06-17

- This update is a destructive update. Please check carefully in the production environment!
- Rename nodes to avoid conflicts
- Add new nodes
  - `Image Quantize`
  - `SVG String to SVG BytesIO`
  - `SVG BytesIO to SVG String`
  - `SVG String Path Simplify`
  - `Image to SVG String BW_Potracer`(thanks@ImagineerNL, Optimized integration based on his work)

## VTracer ComfyUI Non-Official Implementation

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.

### Installation

1. Navigate to your `/ComfyUI/custom_nodes/` folder.
2. Run the following command to clone the repository:

```shell
git clone https://github.com/Yanick112/ComfyUI-ToSVG/
```

4. Navigate to your `ComfyUI-ToSVG` folder.

- For Portable/venv:
- Run the following command:
  ```shell
  path/to/ComfUI/python_embeded/python.exe -s -m pip install -r requirements.txt
  ```
- With system Python:
- Run the following command:
  ```shell
  pip install -r requirements.txt
  ```

Enjoy setting up your ComfyUI-ToSVG tool! If you encounter any issues or need further help, feel free to reach out.

### Partial Parameter Description

- Filter Speckle (Cleaner)
- Color Precision (More accurate)
- Gradient Step (Less layers)
- Corner Threshold (Smoother)
- Segment Length (More coarse)
- Splice Threshold (Less accurate)

### Features

- Converts images to RGBA format if necessary
- Support batch conversion

- node `ConvertRasterToVector` to handle the conversion of raster images to SVG format with various parameters for customization.
- node `SaveSVG` to save the resulting SVG data into files.

### What's next?

- [x] Add SVG preview node
- [x] Color and BW mode split

---

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.


================================================
FILE: __init__.py
================================================
from .svgnode import *

__all__ = [
    "NODE_CLASS_MAPPINGS",
    "NODE_DISPLAY_NAME_MAPPINGS"
]


================================================
FILE: examples/to_svg.json
================================================
{
  "id": "fd87519e-4b4d-4a9a-9516-9e1ba29ea4a9",
  "revision": 0,
  "last_node_id": 14,
  "last_link_id": 12,
  "nodes": [
    {
      "id": 8,
      "type": "TS_ImageToSVGStringBW_Vtracer",
      "pos": [
        1550,
        425
      ],
      "size": [
        307.6441345214844,
        154
      ],
      "flags": {},
      "order": 3,
      "mode": 0,
      "inputs": [
        {
          "label": "image",
          "name": "image",
          "type": "IMAGE",
          "link": 6
        }
      ],
      "outputs": [
        {
          "label": "STRING",
          "name": "STRING",
          "shape": 6,
          "type": "STRING",
          "links": [
            1
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_ImageToSVGStringBW_Vtracer"
      },
      "widgets_values": [
        "spline",
        4,
        60,
        4,
        45
      ]
    },
    {
      "id": 7,
      "type": "TS_ImageToSVGStringColor_Vtracer",
      "pos": [
        1550,
        100
      ],
      "size": [
        318.5474548339844,
        274
      ],
      "flags": {},
      "order": 2,
      "mode": 0,
      "inputs": [
        {
          "label": "image",
          "name": "image",
          "type": "IMAGE",
          "link": 5
        }
      ],
      "outputs": [
        {
          "label": "STRING",
          "name": "STRING",
          "shape": 6,
          "type": "STRING",
          "links": [
            3
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_ImageToSVGStringColor_Vtracer"
      },
      "widgets_values": [
        "stacked",
        "spline",
        4,
        6,
        16,
        60,
        4,
        10,
        45,
        3
      ]
    },
    {
      "id": 5,
      "type": "TS_SVGPathSimplify",
      "pos": [
        1900,
        100
      ],
      "size": [
        270,
        82
      ],
      "flags": {},
      "order": 5,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_String",
          "name": "SVG_String",
          "type": "STRING",
          "link": 3
        }
      ],
      "outputs": [
        {
          "label": "STRING",
          "name": "STRING",
          "type": "STRING",
          "links": [
            4
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SVGPathSimplify"
      },
      "widgets_values": [
        5,
        false
      ]
    },
    {
      "id": 9,
      "type": "TS_SVGStringToImage",
      "pos": [
        1925,
        425
      ],
      "size": [
        168.29257202148438,
        26
      ],
      "flags": {},
      "order": 6,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_String",
          "name": "SVG_String",
          "type": "STRING",
          "link": 1
        }
      ],
      "outputs": [
        {
          "label": "IMAGE",
          "name": "IMAGE",
          "type": "IMAGE",
          "links": [
            10
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SVGStringToImage"
      }
    },
    {
      "id": 6,
      "type": "TS_ImageQuantize",
      "pos": [
        1150,
        450
      ],
      "size": [
        270,
        82
      ],
      "flags": {},
      "order": 1,
      "mode": 0,
      "inputs": [
        {
          "label": "image",
          "name": "image",
          "type": "IMAGE",
          "link": 11
        }
      ],
      "outputs": [
        {
          "label": "IMAGE",
          "name": "IMAGE",
          "type": "IMAGE",
          "links": [
            5,
            6,
            7
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_ImageQuantize"
      },
      "widgets_values": [
        16,
        "Clear"
      ]
    },
    {
      "id": 13,
      "type": "LoadImage",
      "pos": [
        825,
        450
      ],
      "size": [
        270,
        314
      ],
      "flags": {},
      "order": 0,
      "mode": 0,
      "inputs": [],
      "outputs": [
        {
          "label": "IMAGE",
          "name": "IMAGE",
          "type": "IMAGE",
          "links": [
            11
          ]
        },
        {
          "label": "MASK",
          "name": "MASK",
          "type": "MASK",
          "links": null
        }
      ],
      "properties": {
        "cnr_id": "comfy-core",
        "ver": "0.3.41",
        "Node name for S&R": "LoadImage"
      },
      "widgets_values": [
        "0_1 (1).jpg",
        "image"
      ]
    },
    {
      "id": 3,
      "type": "TS_SVGStringToSVGBytesIO",
      "pos": [
        1900,
        725
      ],
      "size": [
        212.63046264648438,
        26
      ],
      "flags": {},
      "order": 7,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_String",
          "name": "SVG_String",
          "type": "STRING",
          "link": 2
        }
      ],
      "outputs": [
        {
          "label": "SVG",
          "name": "SVG",
          "type": "SVG",
          "links": [
            8
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SVGStringToSVGBytesIO"
      }
    },
    {
      "id": 11,
      "type": "TS_ImageToSVGStringBW_Potracer",
      "pos": [
        1550,
        725
      ],
      "size": [
        315.4302673339844,
        322
      ],
      "flags": {},
      "order": 4,
      "mode": 0,
      "inputs": [
        {
          "label": "image",
          "name": "image",
          "type": "IMAGE",
          "link": 7
        }
      ],
      "outputs": [
        {
          "label": "STRING",
          "name": "STRING",
          "type": "STRING",
          "links": [
            2
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_ImageToSVGStringBW_Potracer"
      },
      "widgets_values": [
        128,
        "Black on White",
        "minority",
        2,
        1,
        false,
        0.2,
        true,
        {},
        {},
        {},
        0
      ]
    },
    {
      "id": 1,
      "type": "TS_SaveSVGString",
      "pos": [
        2425,
        725
      ],
      "size": [
        272.744140625,
        106
      ],
      "flags": {},
      "order": 11,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_String",
          "name": "SVG_String",
          "type": "STRING",
          "link": 9
        }
      ],
      "outputs": [],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SaveSVGString"
      },
      "widgets_values": [
        {},
        true,
        {}
      ]
    },
    {
      "id": 2,
      "type": "TS_SVGStringPreview",
      "pos": [
        2200,
        100
      ],
      "size": [
        210,
        258
      ],
      "flags": {},
      "order": 8,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_String",
          "name": "SVG_String",
          "type": "STRING",
          "link": 4
        }
      ],
      "outputs": [],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SVGStringPreview"
      },
      "widgets_values": []
    },
    {
      "id": 12,
      "type": "PreviewImage",
      "pos": [
        2175,
        400
      ],
      "size": [
        210,
        258
      ],
      "flags": {},
      "order": 9,
      "mode": 0,
      "inputs": [
        {
          "label": "images",
          "name": "images",
          "type": "IMAGE",
          "link": 10
        }
      ],
      "outputs": [],
      "properties": {
        "cnr_id": "comfy-core",
        "ver": "0.3.41",
        "Node name for S&R": "PreviewImage"
      },
      "widgets_values": []
    },
    {
      "id": 4,
      "type": "TS_SVGBytesIOToString",
      "pos": [
        2150,
        725
      ],
      "size": [
        212.63046264648438,
        26
      ],
      "flags": {},
      "order": 10,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_BytesIO",
          "name": "SVG_BytesIO",
          "type": "SVG",
          "link": 8
        }
      ],
      "outputs": [
        {
          "label": "STRING",
          "name": "STRING",
          "type": "STRING",
          "links": [
            9,
            12
          ]
        }
      ],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SVGBytesIOToString"
      }
    },
    {
      "id": 14,
      "type": "TS_SVGStringPreview",
      "pos": [
        2425,
        900
      ],
      "size": [
        210,
        258
      ],
      "flags": {},
      "order": 12,
      "mode": 0,
      "inputs": [
        {
          "label": "SVG_String",
          "name": "SVG_String",
          "type": "STRING",
          "link": 12
        }
      ],
      "outputs": [],
      "properties": {
        "cnr_id": "ComfyUI-ToSVG",
        "ver": "2626a6cc885a23e973715b5b5baf37799a7d3d41",
        "Node name for S&R": "TS_SVGStringPreview"
      },
      "widgets_values": []
    }
  ],
  "links": [
    [
      1,
      8,
      0,
      9,
      0,
      "STRING"
    ],
    [
      2,
      11,
      0,
      3,
      0,
      "STRING"
    ],
    [
      3,
      7,
      0,
      5,
      0,
      "STRING"
    ],
    [
      4,
      5,
      0,
      2,
      0,
      "STRING"
    ],
    [
      5,
      6,
      0,
      7,
      0,
      "IMAGE"
    ],
    [
      6,
      6,
      0,
      8,
      0,
      "IMAGE"
    ],
    [
      7,
      6,
      0,
      11,
      0,
      "IMAGE"
    ],
    [
      8,
      3,
      0,
      4,
      0,
      "SVG"
    ],
    [
      9,
      4,
      0,
      1,
      0,
      "STRING"
    ],
    [
      10,
      9,
      0,
      12,
      0,
      "IMAGE"
    ],
    [
      11,
      13,
      0,
      6,
      0,
      "IMAGE"
    ],
    [
      12,
      4,
      0,
      14,
      0,
      "STRING"
    ]
  ],
  "groups": [],
  "config": {},
  "extra": {
    "ds": {
      "scale": 0.6934334949441344,
      "offset": [
        468.60443938760835,
        382.7150165816723
      ]
    },
    "frontendVersion": "1.21.7",
    "VHS_latentpreview": false,
    "VHS_latentpreviewrate": 0,
    "VHS_MetadataImage": true,
    "VHS_KeepIntermediate": true
  },
  "version": 0.4
}

================================================
FILE: pyproject.toml
================================================
[project]
name = "comfyui-tosvg"
description = "This project converts raster images into SVG format using the [a/VTracer](https://github.com/visioncortex/vtracer) library and [a/Potracer](https://github.com/tatarize/potrace). It's a handy tool for designers and developers who need to work with vector graphics programmatically."
version = "1.0.1"
license = { file = "LICENSE" }
dependencies = ["numpy", "Pillow", "torch", "vtracer", "potracer", "PyMuPDF"]

[project.urls]
Repository = "https://github.com/Yanick112/ComfyUI-ToSVG"
#  Used by Comfy Registry https://comfyregistry.org

[tool.comfy]
PublisherId = "yanick"
DisplayName = "ComfyUI-ToSVG"
Icon = ""


================================================
FILE: requirements.txt
================================================
vtracer
numpy
Pillow
torch
PyMuPDF
potracer

================================================
FILE: svgnode.py
================================================
import vtracer
import os
import time
import folder_paths
import numpy as np
import torch
import fitz
import random
import folder_paths
import potrace
from io import BytesIO
from PIL import Image
from comfy_extras.nodes_images import SVG
from nodes import SaveImage
import re
import xml.etree.ElementTree as ET

def tensor2pil(image):
    """Tensor转PIL图像"""
    return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))

def pil2tensor(image):
    """PIL图像转Tensor"""
    return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)

class TS_ImageQuantize:
    """
    图像量化:通过减少图像中的颜色数量来优化矢量转换过程。
    """
    @classmethod
    def INPUT_TYPES(cls):
        """
        定义节点的输入参数。
        """
        return {
            "required": {
                "image": ("IMAGE",),
                "colors": ("INT", {"default": 16, "min": 2, "max": 256, "step": 1}),
                "dither": (["Clear", "Smooth"], {"default": "Clear"}),
            }
        }

    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "quantize_image"
    CATEGORY = "💎TOSVG/Tools"

    def quantize_image(self, image, colors, dither):
        """
        执行图像量化处理。
        """
        quantized_images = []
        
        dither_method = Image.Dither.NONE
        if dither == "Smooth":
            dither_method = Image.Dither.FLOYDSTEINBERG

        for i in image:
            pil_image = tensor2pil(torch.unsqueeze(i, 0))

            quantized_pil = pil_image.convert('RGB').quantize(colors=colors, dither=dither_method)
            
            quantized_pil_rgb = quantized_pil.convert('RGB')
            
            tensor_image = pil2tensor(quantized_pil_rgb)
            quantized_images.append(tensor_image.squeeze(0))

        if not quantized_images:
            return (image,)

        return (torch.stack(quantized_images),)

class TS_ImageToSVGStringColor_Vtracer:
    """图像转彩色SVG字符串"""
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "hierarchical": (["stacked", "cutout"], {"default": "stacked"}),
                "mode": (["spline", "polygon", "none"], {"default": "spline"}),
                "filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}),
                "color_precision": ("INT", {"default": 6, "min": 0, "max": 10, "step": 1}),
                "layer_difference": ("INT", {"default": 16, "min": 0, "max": 256, "step": 1}),
                "corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180, "step": 1}),
                "length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.1}),
                "max_iterations": ("INT", {"default": 10, "min": 1, "max": 70, "step": 1}),
                "splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180, "step": 1}),
                "path_precision": ("INT", {"default": 3, "min": 0, "max": 10, "step": 1}),
            }
        }

    RETURN_TYPES = ("STRING",)
    OUTPUT_IS_LIST = (True,)
    FUNCTION = "convert_to_svg"

    CATEGORY = "💎TOSVG/Convert"

    def convert_to_svg(self, image, 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="color",
                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 TS_ImageToSVGStringBW_Vtracer:
    """图像转黑白SVG字符串"""
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "mode": (["spline", "polygon", "none"], {"default": "spline"}),
                "filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}),
                "corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180, "step": 1}),
                "length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.1}),
                "splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180, "step": 1}),
            }
        }

    RETURN_TYPES = ("STRING",)
    OUTPUT_IS_LIST = (True,)
    FUNCTION = "convert_to_svg"

    CATEGORY = "💎TOSVG/Convert"

    def convert_to_svg(self, image, mode, filter_speckle, corner_threshold, length_threshold, splice_threshold):
        
        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="binary",
                mode=mode,
                filter_speckle=filter_speckle,
                corner_threshold=corner_threshold,
                length_threshold=length_threshold,
                splice_threshold=splice_threshold,
            )
            
            svg_strings.append(svg_str)

        return (svg_strings,)


class TS_SVGStringToImage:
    """SVG字符串转图像"""  
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "SVG_String": ("STRING", {"forceInput": True})
            }
        }

    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "convert_svg_to_image"
    CATEGORY = "💎TOSVG/Convert"

    def convert_svg_to_image(self, SVG_String):

        doc = fitz.open(stream=SVG_String.encode('utf-8'), filetype="svg")
        page = doc.load_page(0)
        pix = page.get_pixmap()

        image_data = pix.tobytes("png")
        pil_image = Image.open(BytesIO(image_data)).convert("RGB")

        return (pil2tensor(pil_image),)
    
    
class TS_SaveSVGString:
    """保存SVG字符串到文件"""
    def __init__(self):
        self.output_dir = folder_paths.get_output_directory()

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "SVG_String": ("STRING", {"forceInput": True}),              
                "filename_prefix": ("STRING", {"default": "ComfyUI_SVG"}),
            },
            "optional": {
                "append_timestamp": ("BOOLEAN", {"default": True}),
                "custom_output_path": ("STRING", {"default": "", "multiline": False}),
            }
        }

    CATEGORY = "💎TOSVG/Tools"
    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_String, 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)
        
        unique_filename = self.generate_unique_filename(f"{filename_prefix}", 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}}

        return ui_info




class TS_SVGStringPreview(SaveImage):
    """SVG字符串预览"""
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "SVG_String": ("STRING", {"forceInput": True})
            }
        }

    FUNCTION = "svg_preview"
    CATEGORY = "💎TOSVG/Tools"
    OUTPUT_NODE = True

    def __init__(self):
        self.output_dir = folder_paths.get_temp_directory()
        self.type = "temp"
        self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(5))
        self.compress_level = 4

    def svg_preview(self, SVG_String):
        doc = fitz.open(stream=SVG_String.encode('utf-8'), filetype="svg")
        page = doc.load_page(0)
        pix = page.get_pixmap()

        image_data = pix.tobytes("png")
        pil_image = Image.open(BytesIO(image_data)).convert("RGB")

        preview = pil2tensor(pil_image)

        return self.save_images(preview, "PointPreview")

class TS_SVGStringToSVGBytesIO:
    """SVG字符串转BytesIO"""
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "SVG_String": ("STRING", {"forceInput": True}),
            }
        }

    RETURN_TYPES = ("SVG",)
    FUNCTION = "convert_string_to_svg"
    CATEGORY = "💎TOSVG/Tools"

    def convert_string_to_svg(self, SVG_String):
        svg_bytes = BytesIO(SVG_String.encode('utf-8'))
        return (SVG([svg_bytes]),)

class TS_SVGBytesIOToString:
    """BytesIO转SVG字符串"""
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "SVG_BytesIO": ("SVG", {"forceInput": True}),
            }
        }

    RETURN_TYPES = ("STRING",)
    FUNCTION = "convert_svg_to_string"
    CATEGORY = "💎TOSVG/Tools"

    def convert_svg_to_string(self, SVG_BytesIO):
        if not SVG_BytesIO.data:
            return ("",)
        
        svg_bytes = SVG_BytesIO.data[0].getvalue()
        svg_string = svg_bytes.decode('utf-8')
        
        return (svg_string,)


class TS_SVGPathSimplify:
    """SVG路径简化"""
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "SVG_String": ("STRING", {"forceInput": True}),
                "tolerance": ("FLOAT", {"default": 5.0, "min": 0.1, "max": 50.0, "step": 0.1}),
                "preserve_curves": ("BOOLEAN", {"default": False}),
            }
        }

    RETURN_TYPES = ("STRING",)
    FUNCTION = "simplify_svg_paths"
    CATEGORY = "💎TOSVG/Tools"

    def douglas_peucker(self, points, tolerance):
        """Douglas-Peucker算法简化路径点"""
        if len(points) <= 2:
            return points
            
        # 找到距离起点和终点连线最远的点
        max_distance = 0
        max_index = 0
        
        start = points[0]
        end = points[-1]
        
        if abs(start[0] - end[0]) < 0.01 and abs(start[1] - end[1]) < 0.01:
            for i in range(1, len(points) - 1):
                distance = ((points[i][0] - start[0]) ** 2 + (points[i][1] - start[1]) ** 2) ** 0.5
                if distance > max_distance:
                    max_distance = distance
                    max_index = i
        else:
            for i in range(1, len(points) - 1):
                distance = self.point_to_line_distance(points[i], start, end)
                if distance > max_distance:
                    max_distance = distance
                    max_index = i
        
        if max_distance > tolerance and max_index > 0:
            left_part = self.douglas_peucker(points[:max_index + 1], tolerance)  
            right_part = self.douglas_peucker(points[max_index:], tolerance)
            return left_part[:-1] + right_part
        else:
            return [start, end]

    def point_to_line_distance(self, point, line_start, line_end):
        """计算点到直线距离"""
        x0, y0 = point
        x1, y1 = line_start
        x2, y2 = line_end
        
        if x1 == x2 and y1 == y2:
            return ((x0 - x1) ** 2 + (y0 - y1) ** 2) ** 0.5
        
        numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
        denominator = ((y2 - y1) ** 2 + (x2 - x1) ** 2) ** 0.5
        
        return numerator / denominator if denominator > 0 else 0

    def parse_path_commands(self, path_data):
        """解析SVG路径命令"""
        commands = re.findall(r'[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*', path_data)
        return commands

    def extract_points_from_path(self, path_data):
        """提取路径中的坐标点"""
        points = []
        commands = self.parse_path_commands(path_data)
        current_pos = (0, 0)
        
        for command in commands:
            cmd_type = command[0]
            params = re.findall(r'-?\d*\.?\d+', command[1:])
            params = [float(p) for p in params]
            
            if cmd_type in 'Mm':
                if len(params) >= 2:
                    if cmd_type == 'M':
                        current_pos = (params[0], params[1])
                    else:  
                        current_pos = (current_pos[0] + params[0], current_pos[1] + params[1])
                    points.append(current_pos)
                    
            elif cmd_type in 'LlHhVv':
                if cmd_type in 'Ll':
                    for i in range(0, len(params), 2):
                        if i + 1 < len(params):
                            if cmd_type == 'L':
                                current_pos = (params[i], params[i + 1])
                            else:
                                current_pos = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
                            points.append(current_pos)
                elif cmd_type in 'Hh':
                    for param in params:
                        if cmd_type == 'H':
                            current_pos = (param, current_pos[1])
                        else:
                            current_pos = (current_pos[0] + param, current_pos[1])
                        points.append(current_pos)
                elif cmd_type in 'Vv':
                    for param in params:
                        if cmd_type == 'V':
                            current_pos = (current_pos[0], param)
                        else:
                            current_pos = (current_pos[0], current_pos[1] + param)
                        points.append(current_pos)
                        
            elif cmd_type in 'CcSsQqTt':
                if cmd_type in 'Cc':
                    for i in range(0, len(params), 6):
                        if i + 5 < len(params):
                            if cmd_type == 'C':
                                control1 = (params[i], params[i + 1])
                                control2 = (params[i + 2], params[i + 3])
                                end_point = (params[i + 4], params[i + 5])
                                points.extend([control1, control2, end_point])
                                current_pos = end_point
                            else:
                                control1 = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
                                control2 = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])
                                end_point = (current_pos[0] + params[i + 4], current_pos[1] + params[i + 5])
                                points.extend([control1, control2, end_point])
                                current_pos = end_point
                elif cmd_type in 'Ss':
                    for i in range(0, len(params), 4):
                        if i + 3 < len(params):
                            if cmd_type == 'S':
                                control2 = (params[i], params[i + 1])
                                end_point = (params[i + 2], params[i + 3])
                                points.extend([control2, end_point])
                                current_pos = end_point
                            else:
                                control2 = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
                                end_point = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])
                                points.extend([control2, end_point])
                                current_pos = end_point
                elif cmd_type in 'Qq':
                    for i in range(0, len(params), 4):
                        if i + 3 < len(params):
                            if cmd_type == 'Q':
                                control = (params[i], params[i + 1])
                                end_point = (params[i + 2], params[i + 3])
                                points.extend([control, end_point])
                                current_pos = end_point
                            else:
                                control = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
                                end_point = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])
                                points.extend([control, end_point])
                                current_pos = end_point
                elif cmd_type in 'Tt':
                    for i in range(0, len(params), 2):
                        if i + 1 < len(params):
                            if cmd_type == 'T':
                                end_point = (params[i], params[i + 1])
                            else:
                                end_point = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
                            points.append(end_point)
                            current_pos = end_point
        
        return points

    def simplify_path_data(self, path_data, tolerance, preserve_curves, stats):
        """简化路径数据"""
        original_points = self.extract_points_from_path(path_data)
        stats['original_points'] += len(original_points)
        
        if len(original_points) < 3:
            stats['simplified_points'] += len(original_points)
            return path_data
        
        if not preserve_curves:
            filtered_points = [original_points[0]]
            for i in range(1, len(original_points)):
                if (abs(original_points[i][0] - filtered_points[-1][0]) > 0.1 or 
                    abs(original_points[i][1] - filtered_points[-1][1]) > 0.1):
                    filtered_points.append(original_points[i])
            original_points = filtered_points
        
        simplified_points = self.douglas_peucker(original_points, tolerance)
        stats['simplified_points'] += len(simplified_points)
        
        if preserve_curves and len(simplified_points) >= len(original_points) * 0.9:
            stats['simplified_points'] = stats['simplified_points'] - len(simplified_points) + len(original_points)
            return path_data
        
        if len(simplified_points) < 2:
            return path_data
            
        path_parts = [f"M{simplified_points[0][0]:.1f},{simplified_points[0][1]:.1f}"]
        
        for i in range(1, len(simplified_points)):
            x, y = simplified_points[i]
            path_parts.append(f"L{x:.1f},{y:.1f}")
        
        if path_data.strip().endswith('Z') or path_data.strip().endswith('z'):
            path_parts.append('Z')
        
        return ' '.join(path_parts)

    def simplify_svg_paths(self, SVG_String, tolerance, preserve_curves):
        """简化SVG路径"""
        effective_tolerance = tolerance
        
        stats = {
            'original_points': 0,
            'simplified_points': 0,
            'paths_processed': 0,
            'original_size': len(SVG_String),
            'simplified_size': 0
        }
        
        try:
            root = ET.fromstring(SVG_String)
            
            paths = root.findall('.//{http://www.w3.org/2000/svg}path')
            if not paths:
                paths = root.findall('.//path')
            
            for path in paths:
                if 'd' in path.attrib:
                    original_data = path.attrib['d']
                    simplified_data = self.simplify_path_data(original_data, effective_tolerance, preserve_curves, stats)
                    path.attrib['d'] = simplified_data
                    stats['paths_processed'] += 1
            
            ET.register_namespace('', 'http://www.w3.org/2000/svg')
            simplified_svg = ET.tostring(root, encoding='unicode')
            stats['simplified_size'] = len(simplified_svg)
            
        except Exception as e:
            simplified_svg = self.simplify_svg_regex(SVG_String, effective_tolerance, preserve_curves, stats)
            stats['simplified_size'] = len(simplified_svg)
        
        reduction_ratio = (stats['original_points'] - stats['simplified_points']) / max(stats['original_points'], 1) * 100
        print(f"SVG Path Simplified: {reduction_ratio:.1f}% points reduced")
        
        return (simplified_svg,)

    def simplify_svg_regex(self, SVG_String, tolerance, preserve_curves, stats):
        """
        使用正则表达式方法简化SVG路径(备选方案)
        """
        def replace_path(match):
            path_data = match.group(1)
            simplified = self.simplify_path_data(path_data, tolerance, preserve_curves, stats)
            stats['paths_processed'] += 1
            return f'd="{simplified}"'
        
        simplified_svg = re.sub(r'd="([^"]*)"', replace_path, SVG_String)
        
        return simplified_svg


class TS_ImageToSVGStringBW_Potracer:
    """Potracer矢量化为SVG"""
    turnpolicy_map = {
        "minority": potrace.POTRACE_TURNPOLICY_MINORITY,
        "black": potrace.POTRACE_TURNPOLICY_BLACK,
        "white": potrace.POTRACE_TURNPOLICY_WHITE,
        "left": potrace.POTRACE_TURNPOLICY_LEFT,
        "right": potrace.POTRACE_TURNPOLICY_RIGHT,
        "majority": potrace.POTRACE_TURNPOLICY_MAJORITY,
    }

    @classmethod
    def INPUT_TYPES(cls):
        policy_options = list(cls.turnpolicy_map.keys())
        return {
            "required": {
                "image": ("IMAGE",),
                "threshold": ("INT", {"default": 128, "min": 0, "max": 255}),
            },
            "optional": {
                "input_foreground": (["White on Black", "Black on White"], {"default": "Black on White"}),
                "turnpolicy": (policy_options, {"default": "minority"}),
                "turdsize": ("INT", {"default": 2, "min": 0}),
                "corner_threshold": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.34, "step": 0.01}),
                "zero_sharp_corners": ("BOOLEAN", {"default": False}),
                "opttolerance": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
                "optimize_curve": ("BOOLEAN", {"default": True}),
                "foreground_color": ("STRING", {"widget": "color", "default": "#000000"}),
                "background_color": ("STRING", {"widget": "color", "default": "#FFFFFF"}),
                "stroke_color": ("STRING", {"widget": "color", "default": "#FF0000"}),
                "stroke_width": ("FLOAT", {"default": 0.0, "min": 0.0, "step": 0.5}),
            }
        }

    RETURN_TYPES = ("STRING",)
    FUNCTION = "vectorize"
    CATEGORY = "💎TOSVG/Convert"

    def vectorize(self, image, threshold, turnpolicy, turdsize, corner_threshold, opttolerance,
                  input_foreground="Black on White", optimize_curve=True,
                  zero_sharp_corners=False,
                  foreground_color="#000000", background_color="#FFFFFF",
                  stroke_color="#FF0000", stroke_width=0.0):
        
        image_np = image.cpu().numpy()
        batch_svg_strings = []

        for i, single_image_np in enumerate(image_np):
            orig_width_temp, orig_height_temp = (single_image_np.shape[1], single_image_np.shape[0]) if single_image_np.ndim >= 2 else (100,100)
            svg_data_for_current_image = f'<svg width="{orig_width_temp}" height="{orig_height_temp}"><desc>Error: Processing failed before SVG generation for image {i}</desc></svg>'

            try:
                pil_img = Image.fromarray((single_image_np * 255).astype(np.uint8))
                orig_width, orig_height = pil_img.size

                if orig_width <= 0 or orig_height <= 0:
                    error_svg = f'<svg width="1" height="1"><desc>Error: Invalid image dimensions for image {i}</desc></svg>'
                    batch_svg_strings.append(error_svg)
                    continue

                threshold_norm = threshold / 255.0
                if single_image_np.ndim == 3:
                    binary_np = single_image_np[:, :, 0] < threshold_norm if single_image_np.shape[2] > 1 else single_image_np[:,:,0] < threshold_norm
                elif single_image_np.ndim == 2:
                    binary_np = single_image_np < threshold_norm
                else:
                    error_svg = f'<svg width="{orig_width}" height="{orig_height}"><desc>Error: Unexpected image dimensions for image {i}</desc></svg>'
                    batch_svg_strings.append(error_svg)
                    continue

                if input_foreground == "Black on White":
                    binary_np = ~binary_np

                if np.all(binary_np) or not np.any(binary_np):
                    skipped_svg = f'<svg width="{orig_width}" height="{orig_height}"><desc>Potracer: Skipped blank image {i}</desc></svg>'
                    batch_svg_strings.append(skipped_svg)
                    continue

                turdsize_int = int(turdsize) if turdsize is not None else 0
                policy_arg = self.turnpolicy_map.get(turnpolicy, turnpolicy)
                alphamax_value_to_use = 1.34 if zero_sharp_corners else corner_threshold
                scale = 1.0

                bm = potrace.Bitmap(binary_np)
                plist = bm.trace(
                    turdsize=turdsize_int,
                    turnpolicy=policy_arg,
                    alphamax=alphamax_value_to_use,
                    opticurve=optimize_curve,
                    opttolerance=opttolerance
                )

                scaled_width = max(1, round(orig_width * scale))
                scaled_height = max(1, round(orig_height * scale))
                svg_header = f'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{scaled_width}" height="{scaled_height}" viewBox="0 0 {scaled_width} {scaled_height}">'
                svg_footer = "</svg>"
                background_rect = ""
                bg_color_lower = background_color.lower()

                if bg_color_lower != "none" and bg_color_lower != "":
                    background_rect = f'<rect width="100%" height="100%" fill="{background_color}"/>'

                scaled_stroke_width = stroke_width * scale
                stroke_attr = f'stroke="{stroke_color}" stroke-width="{scaled_stroke_width}"' if scaled_stroke_width > 0 and stroke_color.lower() != "none" else 'stroke="none"'
                fill_attr = f'fill="{foreground_color}"' if foreground_color.lower() != "none" else 'fill="none"'
                if fill_attr == 'fill="none"' and stroke_attr == 'stroke="none"':
                    fill_attr = 'fill="black"'

                all_paths_svg_parts = []
                if plist:
                    fill_rule_to_use = "evenodd"
                    for curve in plist:
                        if not (hasattr(curve, 'start_point') and hasattr(curve.start_point, 'x') and hasattr(curve.start_point, 'y')):
                            continue
                        fs = curve.start_point
                        all_paths_svg_parts.append(f"M{fs.x * scale:.2f},{fs.y * scale:.2f}")

                        if not hasattr(curve, 'segments'):
                            continue
                        for segment in curve.segments:
                            valid_segment = True
                            if not (hasattr(segment, 'is_corner') and hasattr(segment, 'end_point') and hasattr(segment.end_point, 'x') and hasattr(segment.end_point, 'y')):
                                valid_segment = False

                            if valid_segment and segment.is_corner:
                                if not (hasattr(segment, 'c') and hasattr(segment.c, 'x') and hasattr(segment.c, 'y')):
                                    valid_segment = False
                                else:
                                    c_x = segment.c.x * scale
                                    c_y = segment.c.y * scale
                                    ep_x = segment.end_point.x * scale
                                    ep_y = segment.end_point.y * scale
                                    all_paths_svg_parts.append(f"L{c_x:.2f},{c_y:.2f}L{ep_x:.2f},{ep_y:.2f}")
                            elif valid_segment:
                                if not (hasattr(segment, 'c1') and hasattr(segment.c1, 'x') and hasattr(segment.c1, 'y') and \
                                        hasattr(segment, 'c2') and hasattr(segment.c2, 'x') and hasattr(segment.c2, 'y')):
                                    valid_segment = False
                                else:
                                    c1_x = segment.c1.x * scale; c1_y = segment.c1.y * scale
                                    c2_x = segment.c2.x * scale; c2_y = segment.c2.y * scale
                                    ep_x = segment.end_point.x * scale; ep_y = segment.end_point.y * scale
                                    all_paths_svg_parts.append(f"C{c1_x:.2f},{c1_y:.2f} {c2_x:.2f},{c2_y:.2f} {ep_x:.2f},{ep_y:.2f}")
                        all_paths_svg_parts.append("Z")

                    if all_paths_svg_parts:
                        path_d_attribute = "".join(all_paths_svg_parts)
                        path_element = f'<path {stroke_attr} {fill_attr} fill-rule="{fill_rule_to_use}" d="{path_d_attribute}"/>'
                        svg_data_for_current_image = svg_header + background_rect + path_element + svg_footer
                    else:
                        svg_data_for_current_image = f'{svg_header}<desc>Potracer: Path data generation failed for image {i}</desc>{svg_footer}'
                else:
                    svg_data_for_current_image = f'{svg_header}<desc>Potracer: No paths found for image {i}</desc>{svg_footer}'

                batch_svg_strings.append(svg_data_for_current_image)

            except Exception as e:
                error_svg_content = f'<svg width="100" height="100"><desc>Error processing image {i}: {type(e).__name__} - {str(e).replace("<", "&lt;").replace(">", "&gt;")}</desc></svg>'
                batch_svg_strings.append(error_svg_content)
                continue

        output_string_joined = "\n".join(batch_svg_strings)

        return (output_string_joined,)



NODE_CLASS_MAPPINGS = {
    "TS_ImageQuantize": TS_ImageQuantize,
    "TS_ImageToSVGStringColor_Vtracer": TS_ImageToSVGStringColor_Vtracer,
    "TS_ImageToSVGStringBW_Vtracer": TS_ImageToSVGStringBW_Vtracer,
    "TS_SVGStringToImage": TS_SVGStringToImage,
    "TS_SaveSVGString": TS_SaveSVGString,
    "TS_SVGStringPreview": TS_SVGStringPreview,
    "TS_SVGStringToSVGBytesIO": TS_SVGStringToSVGBytesIO,
    "TS_SVGBytesIOToString": TS_SVGBytesIOToString,
    "TS_SVGPathSimplify": TS_SVGPathSimplify,
    "TS_ImageToSVGStringBW_Potracer": TS_ImageToSVGStringBW_Potracer,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "TS_ImageQuantize": "Image Quantize",
    "TS_ImageToSVGStringColor_Vtracer": "Image to SVG String Color_Vtracer",
    "TS_ImageToSVGStringBW_Vtracer": "Image to SVG String BW_Vtracer",
    "TS_SVGStringToImage": "SVG String to Image",
    "TS_SaveSVGString": "Save SVG String",
    "TS_SVGStringPreview": "SVG String Preview",
    "TS_SVGStringToSVGBytesIO": "SVG String to SVG BytesIO",
    "TS_SVGBytesIOToString": "SVG BytesIO to SVG String",
    "TS_SVGPathSimplify": "SVG String Path Simplify",
    "TS_ImageToSVGStringBW_Potracer": "Image to SVG String BW_Potracer",
}
Download .txt
gitextract_cfygdqbr/

├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── examples/
│   └── to_svg.json
├── pyproject.toml
├── requirements.txt
└── svgnode.py
Download .txt
SYMBOL INDEX (41 symbols across 1 files)

FILE: svgnode.py
  function tensor2pil (line 18) | def tensor2pil(image):
  function pil2tensor (line 22) | def pil2tensor(image):
  class TS_ImageQuantize (line 26) | class TS_ImageQuantize:
    method INPUT_TYPES (line 31) | def INPUT_TYPES(cls):
    method quantize_image (line 47) | def quantize_image(self, image, colors, dither):
  class TS_ImageToSVGStringColor_Vtracer (line 72) | class TS_ImageToSVGStringColor_Vtracer:
    method INPUT_TYPES (line 75) | def INPUT_TYPES(cls):
    method convert_to_svg (line 98) | def convert_to_svg(self, image, hierarchical, mode, filter_speckle, co...
  class TS_ImageToSVGStringBW_Vtracer (line 134) | class TS_ImageToSVGStringBW_Vtracer:
    method INPUT_TYPES (line 137) | def INPUT_TYPES(cls):
    method convert_to_svg (line 155) | def convert_to_svg(self, image, mode, filter_speckle, corner_threshold...
  class TS_SVGStringToImage (line 186) | class TS_SVGStringToImage:
    method INPUT_TYPES (line 189) | def INPUT_TYPES(cls):
    method convert_svg_to_image (line 200) | def convert_svg_to_image(self, SVG_String):
  class TS_SaveSVGString (line 212) | class TS_SaveSVGString:
    method __init__ (line 214) | def __init__(self):
    method INPUT_TYPES (line 218) | def INPUT_TYPES(cls):
    method generate_unique_filename (line 235) | def generate_unique_filename(self, prefix, timestamp=False):
    method save_svg_file (line 242) | def save_svg_file(self, SVG_String, filename_prefix="ComfyUI_SVG", app...
  class TS_SVGStringPreview (line 262) | class TS_SVGStringPreview(SaveImage):
    method INPUT_TYPES (line 265) | def INPUT_TYPES(s):
    method __init__ (line 276) | def __init__(self):
    method svg_preview (line 282) | def svg_preview(self, SVG_String):
  class TS_SVGStringToSVGBytesIO (line 294) | class TS_SVGStringToSVGBytesIO:
    method INPUT_TYPES (line 297) | def INPUT_TYPES(cls):
    method convert_string_to_svg (line 308) | def convert_string_to_svg(self, SVG_String):
  class TS_SVGBytesIOToString (line 312) | class TS_SVGBytesIOToString:
    method INPUT_TYPES (line 315) | def INPUT_TYPES(cls):
    method convert_svg_to_string (line 326) | def convert_svg_to_string(self, SVG_BytesIO):
  class TS_SVGPathSimplify (line 336) | class TS_SVGPathSimplify:
    method INPUT_TYPES (line 339) | def INPUT_TYPES(cls):
    method douglas_peucker (line 352) | def douglas_peucker(self, points, tolerance):
    method point_to_line_distance (line 384) | def point_to_line_distance(self, point, line_start, line_end):
    method parse_path_commands (line 398) | def parse_path_commands(self, path_data):
    method extract_points_from_path (line 403) | def extract_points_from_path(self, path_data):
    method simplify_path_data (line 500) | def simplify_path_data(self, path_data, tolerance, preserve_curves, st...
    method simplify_svg_paths (line 538) | def simplify_svg_paths(self, SVG_String, tolerance, preserve_curves):
    method simplify_svg_regex (line 577) | def simplify_svg_regex(self, SVG_String, tolerance, preserve_curves, s...
  class TS_ImageToSVGStringBW_Potracer (line 592) | class TS_ImageToSVGStringBW_Potracer:
    method INPUT_TYPES (line 604) | def INPUT_TYPES(cls):
    method vectorize (line 630) | def vectorize(self, image, threshold, turnpolicy, turdsize, corner_thr...
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (56K chars).
[
  {
    "path": ".github/workflows/publish.yml",
    "chars": 663,
    "preview": "name: Publish to Comfy registry\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - master\n    paths:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 1461,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2024 Yanick112\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 2265,
    "preview": "# ComfyUI-ToSVG\n\nHuge thanks to visioncortex and potracer for this amazing thing! Original repository: https://github.co"
  },
  {
    "path": "__init__.py",
    "chars": 104,
    "preview": "from .svgnode import *\r\n\r\n__all__ = [\r\n    \"NODE_CLASS_MAPPINGS\",\r\n    \"NODE_DISPLAY_NAME_MAPPINGS\"\r\n]\r\n"
  },
  {
    "path": "examples/to_svg.json",
    "chars": 11173,
    "preview": "{\n  \"id\": \"fd87519e-4b4d-4a9a-9516-9e1ba29ea4a9\",\n  \"revision\": 0,\n  \"last_node_id\": 14,\n  \"last_link_id\": 12,\n  \"nodes\""
  },
  {
    "path": "pyproject.toml",
    "chars": 660,
    "preview": "[project]\nname = \"comfyui-tosvg\"\ndescription = \"This project converts raster images into SVG format using the [a/VTracer"
  },
  {
    "path": "requirements.txt",
    "chars": 48,
    "preview": "vtracer\r\nnumpy\r\nPillow\r\ntorch\r\nPyMuPDF\r\npotracer"
  },
  {
    "path": "svgnode.py",
    "chars": 33172,
    "preview": "import vtracer\r\nimport os\r\nimport time\r\nimport folder_paths\r\nimport numpy as np\r\nimport torch\r\nimport fitz\r\nimport rando"
  }
]

About this extraction

This page contains the full source code of the Yanick112/ComfyUI-ToSVG GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (49.4 KB), approximately 13.0k tokens, and a symbol index with 41 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!