[
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to Comfy registry\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - master\n    paths:\n      - \"pyproject.toml\"\n\npermissions:\n  issues: write\n\njobs:\n  publish-node:\n    name: Publish Custom Node to registry\n    runs-on: ubuntu-latest\n    if: ${{ github.repository_owner == 'Yanick112' }}\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Publish Custom Node\n        uses: Comfy-Org/publish-node-action@v1\n        with:\n          ## Add your own personal access token to your Github Repository secrets and reference it here.\n          personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyderworkspace\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/ \n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Yanick112\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ComfyUI-ToSVG\n\nHuge thanks to visioncortex and potracer for this amazing thing! Original repository: https://github.com/visioncortex/vtracer and https://github.com/tatarize/potrace\n\n![截图_20240613204507](examples/workflow_20250618_000738.png)\n\n\n\n## Update\n### 06-17\n\n- This update is a destructive update. Please check carefully in the production environment!\n- Rename nodes to avoid conflicts\n- Add new nodes\n  - `Image Quantize`\n  - `SVG String to SVG BytesIO`\n  - `SVG BytesIO to SVG String`\n  - `SVG String Path Simplify`\n  - `Image to SVG String BW_Potracer`(thanks@ImagineerNL, Optimized integration based on his work)\n\n## VTracer ComfyUI Non-Official Implementation\n\nWelcome 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.\n\n### Installation\n\n1. Navigate to your `/ComfyUI/custom_nodes/` folder.\n2. Run the following command to clone the repository:\n\n```shell\ngit clone https://github.com/Yanick112/ComfyUI-ToSVG/\n```\n\n4. Navigate to your `ComfyUI-ToSVG` folder.\n\n- For Portable/venv:\n- Run the following command:\n  ```shell\n  path/to/ComfUI/python_embeded/python.exe -s -m pip install -r requirements.txt\n  ```\n- With system Python:\n- Run the following command:\n  ```shell\n  pip install -r requirements.txt\n  ```\n\nEnjoy setting up your ComfyUI-ToSVG tool! If you encounter any issues or need further help, feel free to reach out.\n\n### Partial Parameter Description\n\n- Filter Speckle (Cleaner)\n- Color Precision (More accurate)\n- Gradient Step (Less layers)\n- Corner Threshold (Smoother)\n- Segment Length (More coarse)\n- Splice Threshold (Less accurate)\n\n### Features\n\n- Converts images to RGBA format if necessary\n- Support batch conversion\n\n- node `ConvertRasterToVector` to handle the conversion of raster images to SVG format with various parameters for customization.\n- node `SaveSVG` to save the resulting SVG data into files.\n\n### What's next?\n\n- [x] Add SVG preview node\n- [x] Color and BW mode split\n\n---\n\nEnjoy 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.\n"
  },
  {
    "path": "__init__.py",
    "content": "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",
    "content": "{\n  \"id\": \"fd87519e-4b4d-4a9a-9516-9e1ba29ea4a9\",\n  \"revision\": 0,\n  \"last_node_id\": 14,\n  \"last_link_id\": 12,\n  \"nodes\": [\n    {\n      \"id\": 8,\n      \"type\": \"TS_ImageToSVGStringBW_Vtracer\",\n      \"pos\": [\n        1550,\n        425\n      ],\n      \"size\": [\n        307.6441345214844,\n        154\n      ],\n      \"flags\": {},\n      \"order\": 3,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"image\",\n          \"name\": \"image\",\n          \"type\": \"IMAGE\",\n          \"link\": 6\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"STRING\",\n          \"name\": \"STRING\",\n          \"shape\": 6,\n          \"type\": \"STRING\",\n          \"links\": [\n            1\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_ImageToSVGStringBW_Vtracer\"\n      },\n      \"widgets_values\": [\n        \"spline\",\n        4,\n        60,\n        4,\n        45\n      ]\n    },\n    {\n      \"id\": 7,\n      \"type\": \"TS_ImageToSVGStringColor_Vtracer\",\n      \"pos\": [\n        1550,\n        100\n      ],\n      \"size\": [\n        318.5474548339844,\n        274\n      ],\n      \"flags\": {},\n      \"order\": 2,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"image\",\n          \"name\": \"image\",\n          \"type\": \"IMAGE\",\n          \"link\": 5\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"STRING\",\n          \"name\": \"STRING\",\n          \"shape\": 6,\n          \"type\": \"STRING\",\n          \"links\": [\n            3\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_ImageToSVGStringColor_Vtracer\"\n      },\n      \"widgets_values\": [\n        \"stacked\",\n        \"spline\",\n        4,\n        6,\n        16,\n        60,\n        4,\n        10,\n        45,\n        3\n      ]\n    },\n    {\n      \"id\": 5,\n      \"type\": \"TS_SVGPathSimplify\",\n      \"pos\": [\n        1900,\n        100\n      ],\n      \"size\": [\n        270,\n        82\n      ],\n      \"flags\": {},\n      \"order\": 5,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_String\",\n          \"name\": \"SVG_String\",\n          \"type\": \"STRING\",\n          \"link\": 3\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"STRING\",\n          \"name\": \"STRING\",\n          \"type\": \"STRING\",\n          \"links\": [\n            4\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SVGPathSimplify\"\n      },\n      \"widgets_values\": [\n        5,\n        false\n      ]\n    },\n    {\n      \"id\": 9,\n      \"type\": \"TS_SVGStringToImage\",\n      \"pos\": [\n        1925,\n        425\n      ],\n      \"size\": [\n        168.29257202148438,\n        26\n      ],\n      \"flags\": {},\n      \"order\": 6,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_String\",\n          \"name\": \"SVG_String\",\n          \"type\": \"STRING\",\n          \"link\": 1\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"IMAGE\",\n          \"name\": \"IMAGE\",\n          \"type\": \"IMAGE\",\n          \"links\": [\n            10\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SVGStringToImage\"\n      }\n    },\n    {\n      \"id\": 6,\n      \"type\": \"TS_ImageQuantize\",\n      \"pos\": [\n        1150,\n        450\n      ],\n      \"size\": [\n        270,\n        82\n      ],\n      \"flags\": {},\n      \"order\": 1,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"image\",\n          \"name\": \"image\",\n          \"type\": \"IMAGE\",\n          \"link\": 11\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"IMAGE\",\n          \"name\": \"IMAGE\",\n          \"type\": \"IMAGE\",\n          \"links\": [\n            5,\n            6,\n            7\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_ImageQuantize\"\n      },\n      \"widgets_values\": [\n        16,\n        \"Clear\"\n      ]\n    },\n    {\n      \"id\": 13,\n      \"type\": \"LoadImage\",\n      \"pos\": [\n        825,\n        450\n      ],\n      \"size\": [\n        270,\n        314\n      ],\n      \"flags\": {},\n      \"order\": 0,\n      \"mode\": 0,\n      \"inputs\": [],\n      \"outputs\": [\n        {\n          \"label\": \"IMAGE\",\n          \"name\": \"IMAGE\",\n          \"type\": \"IMAGE\",\n          \"links\": [\n            11\n          ]\n        },\n        {\n          \"label\": \"MASK\",\n          \"name\": \"MASK\",\n          \"type\": \"MASK\",\n          \"links\": null\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"comfy-core\",\n        \"ver\": \"0.3.41\",\n        \"Node name for S&R\": \"LoadImage\"\n      },\n      \"widgets_values\": [\n        \"0_1 (1).jpg\",\n        \"image\"\n      ]\n    },\n    {\n      \"id\": 3,\n      \"type\": \"TS_SVGStringToSVGBytesIO\",\n      \"pos\": [\n        1900,\n        725\n      ],\n      \"size\": [\n        212.63046264648438,\n        26\n      ],\n      \"flags\": {},\n      \"order\": 7,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_String\",\n          \"name\": \"SVG_String\",\n          \"type\": \"STRING\",\n          \"link\": 2\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"SVG\",\n          \"name\": \"SVG\",\n          \"type\": \"SVG\",\n          \"links\": [\n            8\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SVGStringToSVGBytesIO\"\n      }\n    },\n    {\n      \"id\": 11,\n      \"type\": \"TS_ImageToSVGStringBW_Potracer\",\n      \"pos\": [\n        1550,\n        725\n      ],\n      \"size\": [\n        315.4302673339844,\n        322\n      ],\n      \"flags\": {},\n      \"order\": 4,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"image\",\n          \"name\": \"image\",\n          \"type\": \"IMAGE\",\n          \"link\": 7\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"STRING\",\n          \"name\": \"STRING\",\n          \"type\": \"STRING\",\n          \"links\": [\n            2\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_ImageToSVGStringBW_Potracer\"\n      },\n      \"widgets_values\": [\n        128,\n        \"Black on White\",\n        \"minority\",\n        2,\n        1,\n        false,\n        0.2,\n        true,\n        {},\n        {},\n        {},\n        0\n      ]\n    },\n    {\n      \"id\": 1,\n      \"type\": \"TS_SaveSVGString\",\n      \"pos\": [\n        2425,\n        725\n      ],\n      \"size\": [\n        272.744140625,\n        106\n      ],\n      \"flags\": {},\n      \"order\": 11,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_String\",\n          \"name\": \"SVG_String\",\n          \"type\": \"STRING\",\n          \"link\": 9\n        }\n      ],\n      \"outputs\": [],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SaveSVGString\"\n      },\n      \"widgets_values\": [\n        {},\n        true,\n        {}\n      ]\n    },\n    {\n      \"id\": 2,\n      \"type\": \"TS_SVGStringPreview\",\n      \"pos\": [\n        2200,\n        100\n      ],\n      \"size\": [\n        210,\n        258\n      ],\n      \"flags\": {},\n      \"order\": 8,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_String\",\n          \"name\": \"SVG_String\",\n          \"type\": \"STRING\",\n          \"link\": 4\n        }\n      ],\n      \"outputs\": [],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SVGStringPreview\"\n      },\n      \"widgets_values\": []\n    },\n    {\n      \"id\": 12,\n      \"type\": \"PreviewImage\",\n      \"pos\": [\n        2175,\n        400\n      ],\n      \"size\": [\n        210,\n        258\n      ],\n      \"flags\": {},\n      \"order\": 9,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"images\",\n          \"name\": \"images\",\n          \"type\": \"IMAGE\",\n          \"link\": 10\n        }\n      ],\n      \"outputs\": [],\n      \"properties\": {\n        \"cnr_id\": \"comfy-core\",\n        \"ver\": \"0.3.41\",\n        \"Node name for S&R\": \"PreviewImage\"\n      },\n      \"widgets_values\": []\n    },\n    {\n      \"id\": 4,\n      \"type\": \"TS_SVGBytesIOToString\",\n      \"pos\": [\n        2150,\n        725\n      ],\n      \"size\": [\n        212.63046264648438,\n        26\n      ],\n      \"flags\": {},\n      \"order\": 10,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_BytesIO\",\n          \"name\": \"SVG_BytesIO\",\n          \"type\": \"SVG\",\n          \"link\": 8\n        }\n      ],\n      \"outputs\": [\n        {\n          \"label\": \"STRING\",\n          \"name\": \"STRING\",\n          \"type\": \"STRING\",\n          \"links\": [\n            9,\n            12\n          ]\n        }\n      ],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SVGBytesIOToString\"\n      }\n    },\n    {\n      \"id\": 14,\n      \"type\": \"TS_SVGStringPreview\",\n      \"pos\": [\n        2425,\n        900\n      ],\n      \"size\": [\n        210,\n        258\n      ],\n      \"flags\": {},\n      \"order\": 12,\n      \"mode\": 0,\n      \"inputs\": [\n        {\n          \"label\": \"SVG_String\",\n          \"name\": \"SVG_String\",\n          \"type\": \"STRING\",\n          \"link\": 12\n        }\n      ],\n      \"outputs\": [],\n      \"properties\": {\n        \"cnr_id\": \"ComfyUI-ToSVG\",\n        \"ver\": \"2626a6cc885a23e973715b5b5baf37799a7d3d41\",\n        \"Node name for S&R\": \"TS_SVGStringPreview\"\n      },\n      \"widgets_values\": []\n    }\n  ],\n  \"links\": [\n    [\n      1,\n      8,\n      0,\n      9,\n      0,\n      \"STRING\"\n    ],\n    [\n      2,\n      11,\n      0,\n      3,\n      0,\n      \"STRING\"\n    ],\n    [\n      3,\n      7,\n      0,\n      5,\n      0,\n      \"STRING\"\n    ],\n    [\n      4,\n      5,\n      0,\n      2,\n      0,\n      \"STRING\"\n    ],\n    [\n      5,\n      6,\n      0,\n      7,\n      0,\n      \"IMAGE\"\n    ],\n    [\n      6,\n      6,\n      0,\n      8,\n      0,\n      \"IMAGE\"\n    ],\n    [\n      7,\n      6,\n      0,\n      11,\n      0,\n      \"IMAGE\"\n    ],\n    [\n      8,\n      3,\n      0,\n      4,\n      0,\n      \"SVG\"\n    ],\n    [\n      9,\n      4,\n      0,\n      1,\n      0,\n      \"STRING\"\n    ],\n    [\n      10,\n      9,\n      0,\n      12,\n      0,\n      \"IMAGE\"\n    ],\n    [\n      11,\n      13,\n      0,\n      6,\n      0,\n      \"IMAGE\"\n    ],\n    [\n      12,\n      4,\n      0,\n      14,\n      0,\n      \"STRING\"\n    ]\n  ],\n  \"groups\": [],\n  \"config\": {},\n  \"extra\": {\n    \"ds\": {\n      \"scale\": 0.6934334949441344,\n      \"offset\": [\n        468.60443938760835,\n        382.7150165816723\n      ]\n    },\n    \"frontendVersion\": \"1.21.7\",\n    \"VHS_latentpreview\": false,\n    \"VHS_latentpreviewrate\": 0,\n    \"VHS_MetadataImage\": true,\n    \"VHS_KeepIntermediate\": true\n  },\n  \"version\": 0.4\n}"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"comfyui-tosvg\"\ndescription = \"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.\"\nversion = \"1.0.1\"\nlicense = { file = \"LICENSE\" }\ndependencies = [\"numpy\", \"Pillow\", \"torch\", \"vtracer\", \"potracer\", \"PyMuPDF\"]\n\n[project.urls]\nRepository = \"https://github.com/Yanick112/ComfyUI-ToSVG\"\n#  Used by Comfy Registry https://comfyregistry.org\n\n[tool.comfy]\nPublisherId = \"yanick\"\nDisplayName = \"ComfyUI-ToSVG\"\nIcon = \"\"\n"
  },
  {
    "path": "requirements.txt",
    "content": "vtracer\r\nnumpy\r\nPillow\r\ntorch\r\nPyMuPDF\r\npotracer"
  },
  {
    "path": "svgnode.py",
    "content": "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 random\r\nimport folder_paths\r\nimport potrace\r\nfrom io import BytesIO\r\nfrom PIL import Image\r\nfrom comfy_extras.nodes_images import SVG\r\nfrom nodes import SaveImage\r\nimport re\r\nimport xml.etree.ElementTree as ET\r\n\r\ndef tensor2pil(image):\r\n    \"\"\"Tensor转PIL图像\"\"\"\r\n    return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))\r\n\r\ndef pil2tensor(image):\r\n    \"\"\"PIL图像转Tensor\"\"\"\r\n    return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)\r\n\r\nclass TS_ImageQuantize:\r\n    \"\"\"\r\n    图像量化：通过减少图像中的颜色数量来优化矢量转换过程。\r\n    \"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        \"\"\"\r\n        定义节点的输入参数。\r\n        \"\"\"\r\n        return {\r\n            \"required\": {\r\n                \"image\": (\"IMAGE\",),\r\n                \"colors\": (\"INT\", {\"default\": 16, \"min\": 2, \"max\": 256, \"step\": 1}),\r\n                \"dither\": ([\"Clear\", \"Smooth\"], {\"default\": \"Clear\"}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"IMAGE\",)\r\n    FUNCTION = \"quantize_image\"\r\n    CATEGORY = \"💎TOSVG/Tools\"\r\n\r\n    def quantize_image(self, image, colors, dither):\r\n        \"\"\"\r\n        执行图像量化处理。\r\n        \"\"\"\r\n        quantized_images = []\r\n        \r\n        dither_method = Image.Dither.NONE\r\n        if dither == \"Smooth\":\r\n            dither_method = Image.Dither.FLOYDSTEINBERG\r\n\r\n        for i in image:\r\n            pil_image = tensor2pil(torch.unsqueeze(i, 0))\r\n\r\n            quantized_pil = pil_image.convert('RGB').quantize(colors=colors, dither=dither_method)\r\n            \r\n            quantized_pil_rgb = quantized_pil.convert('RGB')\r\n            \r\n            tensor_image = pil2tensor(quantized_pil_rgb)\r\n            quantized_images.append(tensor_image.squeeze(0))\r\n\r\n        if not quantized_images:\r\n            return (image,)\r\n\r\n        return (torch.stack(quantized_images),)\r\n\r\nclass TS_ImageToSVGStringColor_Vtracer:\r\n    \"\"\"图像转彩色SVG字符串\"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"image\": (\"IMAGE\",),\r\n                \"hierarchical\": ([\"stacked\", \"cutout\"], {\"default\": \"stacked\"}),\r\n                \"mode\": ([\"spline\", \"polygon\", \"none\"], {\"default\": \"spline\"}),\r\n                \"filter_speckle\": (\"INT\", {\"default\": 4, \"min\": 0, \"max\": 100, \"step\": 1}),\r\n                \"color_precision\": (\"INT\", {\"default\": 6, \"min\": 0, \"max\": 10, \"step\": 1}),\r\n                \"layer_difference\": (\"INT\", {\"default\": 16, \"min\": 0, \"max\": 256, \"step\": 1}),\r\n                \"corner_threshold\": (\"INT\", {\"default\": 60, \"min\": 0, \"max\": 180, \"step\": 1}),\r\n                \"length_threshold\": (\"FLOAT\", {\"default\": 4.0, \"min\": 0.0, \"max\": 10.0, \"step\": 0.1}),\r\n                \"max_iterations\": (\"INT\", {\"default\": 10, \"min\": 1, \"max\": 70, \"step\": 1}),\r\n                \"splice_threshold\": (\"INT\", {\"default\": 45, \"min\": 0, \"max\": 180, \"step\": 1}),\r\n                \"path_precision\": (\"INT\", {\"default\": 3, \"min\": 0, \"max\": 10, \"step\": 1}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"STRING\",)\r\n    OUTPUT_IS_LIST = (True,)\r\n    FUNCTION = \"convert_to_svg\"\r\n\r\n    CATEGORY = \"💎TOSVG/Convert\"\r\n\r\n    def convert_to_svg(self, image, hierarchical, mode, filter_speckle, color_precision, layer_difference, corner_threshold,\r\n                       length_threshold, max_iterations, splice_threshold, path_precision):\r\n        \r\n        svg_strings = []\r\n\r\n        for i in image:\r\n            i = torch.unsqueeze(i, 0)\r\n            _image = tensor2pil(i)\r\n            \r\n            if _image.mode != 'RGBA':\r\n                alpha = Image.new('L', _image.size, 255)\r\n                _image.putalpha(alpha)\r\n\r\n            pixels = list(_image.getdata())\r\n            size = _image.size\r\n\r\n            svg_str = vtracer.convert_pixels_to_svg(\r\n                pixels,\r\n                size=size,\r\n                colormode=\"color\",\r\n                hierarchical=hierarchical,\r\n                mode=mode,\r\n                filter_speckle=filter_speckle,\r\n                color_precision=color_precision,\r\n                layer_difference=layer_difference,\r\n                corner_threshold=corner_threshold,\r\n                length_threshold=length_threshold,\r\n                max_iterations=max_iterations,\r\n                splice_threshold=splice_threshold,\r\n                path_precision=path_precision,\r\n            )\r\n            \r\n            svg_strings.append(svg_str)\r\n\r\n        return (svg_strings,)\r\n\r\nclass TS_ImageToSVGStringBW_Vtracer:\r\n    \"\"\"图像转黑白SVG字符串\"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"image\": (\"IMAGE\",),\r\n                \"mode\": ([\"spline\", \"polygon\", \"none\"], {\"default\": \"spline\"}),\r\n                \"filter_speckle\": (\"INT\", {\"default\": 4, \"min\": 0, \"max\": 100, \"step\": 1}),\r\n                \"corner_threshold\": (\"INT\", {\"default\": 60, \"min\": 0, \"max\": 180, \"step\": 1}),\r\n                \"length_threshold\": (\"FLOAT\", {\"default\": 4.0, \"min\": 0.0, \"max\": 10.0, \"step\": 0.1}),\r\n                \"splice_threshold\": (\"INT\", {\"default\": 45, \"min\": 0, \"max\": 180, \"step\": 1}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"STRING\",)\r\n    OUTPUT_IS_LIST = (True,)\r\n    FUNCTION = \"convert_to_svg\"\r\n\r\n    CATEGORY = \"💎TOSVG/Convert\"\r\n\r\n    def convert_to_svg(self, image, mode, filter_speckle, corner_threshold, length_threshold, splice_threshold):\r\n        \r\n        svg_strings = []\r\n\r\n        for i in image:\r\n            i = torch.unsqueeze(i, 0)\r\n            _image = tensor2pil(i)\r\n            \r\n            if _image.mode != 'RGBA':\r\n                alpha = Image.new('L', _image.size, 255)\r\n                _image.putalpha(alpha)\r\n\r\n            pixels = list(_image.getdata())\r\n            size = _image.size\r\n\r\n            svg_str = vtracer.convert_pixels_to_svg(\r\n                pixels,\r\n                size=size,\r\n                colormode=\"binary\",\r\n                mode=mode,\r\n                filter_speckle=filter_speckle,\r\n                corner_threshold=corner_threshold,\r\n                length_threshold=length_threshold,\r\n                splice_threshold=splice_threshold,\r\n            )\r\n            \r\n            svg_strings.append(svg_str)\r\n\r\n        return (svg_strings,)\r\n\r\n\r\nclass TS_SVGStringToImage:\r\n    \"\"\"SVG字符串转图像\"\"\"  \r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"SVG_String\": (\"STRING\", {\"forceInput\": True})\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"IMAGE\",)\r\n    FUNCTION = \"convert_svg_to_image\"\r\n    CATEGORY = \"💎TOSVG/Convert\"\r\n\r\n    def convert_svg_to_image(self, SVG_String):\r\n\r\n        doc = fitz.open(stream=SVG_String.encode('utf-8'), filetype=\"svg\")\r\n        page = doc.load_page(0)\r\n        pix = page.get_pixmap()\r\n\r\n        image_data = pix.tobytes(\"png\")\r\n        pil_image = Image.open(BytesIO(image_data)).convert(\"RGB\")\r\n\r\n        return (pil2tensor(pil_image),)\r\n    \r\n    \r\nclass TS_SaveSVGString:\r\n    \"\"\"保存SVG字符串到文件\"\"\"\r\n    def __init__(self):\r\n        self.output_dir = folder_paths.get_output_directory()\r\n\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"SVG_String\": (\"STRING\", {\"forceInput\": True}),              \r\n                \"filename_prefix\": (\"STRING\", {\"default\": \"ComfyUI_SVG\"}),\r\n            },\r\n            \"optional\": {\r\n                \"append_timestamp\": (\"BOOLEAN\", {\"default\": True}),\r\n                \"custom_output_path\": (\"STRING\", {\"default\": \"\", \"multiline\": False}),\r\n            }\r\n        }\r\n\r\n    CATEGORY = \"💎TOSVG/Tools\"\r\n    RETURN_TYPES = ()\r\n    OUTPUT_NODE = True\r\n    FUNCTION = \"save_svg_file\"\r\n\r\n    def generate_unique_filename(self, prefix, timestamp=False):\r\n        if timestamp:\r\n            timestamp_str = time.strftime(\"%Y%m%d%H%M%S\")\r\n            return f\"{prefix}_{timestamp_str}.svg\"\r\n        else:\r\n            return f\"{prefix}.svg\"\r\n\r\n    def save_svg_file(self, SVG_String, filename_prefix=\"ComfyUI_SVG\", append_timestamp=True, custom_output_path=\"\"):\r\n        \r\n        output_path = custom_output_path if custom_output_path else self.output_dir\r\n        os.makedirs(output_path, exist_ok=True)\r\n        \r\n        unique_filename = self.generate_unique_filename(f\"{filename_prefix}\", append_timestamp)\r\n        final_filepath = os.path.join(output_path, unique_filename)\r\n            \r\n            \r\n        with open(final_filepath, \"w\") as svg_file:\r\n            svg_file.write(SVG_String)\r\n            \r\n            \r\n        ui_info = {\"ui\": {\"saved_svg\": unique_filename, \"path\": final_filepath}}\r\n\r\n        return ui_info\r\n\r\n\r\n\r\n\r\nclass TS_SVGStringPreview(SaveImage):\r\n    \"\"\"SVG字符串预览\"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(s):\r\n        return {\r\n            \"required\": {\r\n                \"SVG_String\": (\"STRING\", {\"forceInput\": True})\r\n            }\r\n        }\r\n\r\n    FUNCTION = \"svg_preview\"\r\n    CATEGORY = \"💎TOSVG/Tools\"\r\n    OUTPUT_NODE = True\r\n\r\n    def __init__(self):\r\n        self.output_dir = folder_paths.get_temp_directory()\r\n        self.type = \"temp\"\r\n        self.prefix_append = \"_temp_\" + ''.join(random.choice(\"abcdefghijklmnopqrstupvxyz1234567890\") for x in range(5))\r\n        self.compress_level = 4\r\n\r\n    def svg_preview(self, SVG_String):\r\n        doc = fitz.open(stream=SVG_String.encode('utf-8'), filetype=\"svg\")\r\n        page = doc.load_page(0)\r\n        pix = page.get_pixmap()\r\n\r\n        image_data = pix.tobytes(\"png\")\r\n        pil_image = Image.open(BytesIO(image_data)).convert(\"RGB\")\r\n\r\n        preview = pil2tensor(pil_image)\r\n\r\n        return self.save_images(preview, \"PointPreview\")\r\n\r\nclass TS_SVGStringToSVGBytesIO:\r\n    \"\"\"SVG字符串转BytesIO\"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"SVG_String\": (\"STRING\", {\"forceInput\": True}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"SVG\",)\r\n    FUNCTION = \"convert_string_to_svg\"\r\n    CATEGORY = \"💎TOSVG/Tools\"\r\n\r\n    def convert_string_to_svg(self, SVG_String):\r\n        svg_bytes = BytesIO(SVG_String.encode('utf-8'))\r\n        return (SVG([svg_bytes]),)\r\n\r\nclass TS_SVGBytesIOToString:\r\n    \"\"\"BytesIO转SVG字符串\"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"SVG_BytesIO\": (\"SVG\", {\"forceInput\": True}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"STRING\",)\r\n    FUNCTION = \"convert_svg_to_string\"\r\n    CATEGORY = \"💎TOSVG/Tools\"\r\n\r\n    def convert_svg_to_string(self, SVG_BytesIO):\r\n        if not SVG_BytesIO.data:\r\n            return (\"\",)\r\n        \r\n        svg_bytes = SVG_BytesIO.data[0].getvalue()\r\n        svg_string = svg_bytes.decode('utf-8')\r\n        \r\n        return (svg_string,)\r\n\r\n\r\nclass TS_SVGPathSimplify:\r\n    \"\"\"SVG路径简化\"\"\"\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        return {\r\n            \"required\": {\r\n                \"SVG_String\": (\"STRING\", {\"forceInput\": True}),\r\n                \"tolerance\": (\"FLOAT\", {\"default\": 5.0, \"min\": 0.1, \"max\": 50.0, \"step\": 0.1}),\r\n                \"preserve_curves\": (\"BOOLEAN\", {\"default\": False}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"STRING\",)\r\n    FUNCTION = \"simplify_svg_paths\"\r\n    CATEGORY = \"💎TOSVG/Tools\"\r\n\r\n    def douglas_peucker(self, points, tolerance):\r\n        \"\"\"Douglas-Peucker算法简化路径点\"\"\"\r\n        if len(points) <= 2:\r\n            return points\r\n            \r\n        # 找到距离起点和终点连线最远的点\r\n        max_distance = 0\r\n        max_index = 0\r\n        \r\n        start = points[0]\r\n        end = points[-1]\r\n        \r\n        if abs(start[0] - end[0]) < 0.01 and abs(start[1] - end[1]) < 0.01:\r\n            for i in range(1, len(points) - 1):\r\n                distance = ((points[i][0] - start[0]) ** 2 + (points[i][1] - start[1]) ** 2) ** 0.5\r\n                if distance > max_distance:\r\n                    max_distance = distance\r\n                    max_index = i\r\n        else:\r\n            for i in range(1, len(points) - 1):\r\n                distance = self.point_to_line_distance(points[i], start, end)\r\n                if distance > max_distance:\r\n                    max_distance = distance\r\n                    max_index = i\r\n        \r\n        if max_distance > tolerance and max_index > 0:\r\n            left_part = self.douglas_peucker(points[:max_index + 1], tolerance)  \r\n            right_part = self.douglas_peucker(points[max_index:], tolerance)\r\n            return left_part[:-1] + right_part\r\n        else:\r\n            return [start, end]\r\n\r\n    def point_to_line_distance(self, point, line_start, line_end):\r\n        \"\"\"计算点到直线距离\"\"\"\r\n        x0, y0 = point\r\n        x1, y1 = line_start\r\n        x2, y2 = line_end\r\n        \r\n        if x1 == x2 and y1 == y2:\r\n            return ((x0 - x1) ** 2 + (y0 - y1) ** 2) ** 0.5\r\n        \r\n        numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)\r\n        denominator = ((y2 - y1) ** 2 + (x2 - x1) ** 2) ** 0.5\r\n        \r\n        return numerator / denominator if denominator > 0 else 0\r\n\r\n    def parse_path_commands(self, path_data):\r\n        \"\"\"解析SVG路径命令\"\"\"\r\n        commands = re.findall(r'[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*', path_data)\r\n        return commands\r\n\r\n    def extract_points_from_path(self, path_data):\r\n        \"\"\"提取路径中的坐标点\"\"\"\r\n        points = []\r\n        commands = self.parse_path_commands(path_data)\r\n        current_pos = (0, 0)\r\n        \r\n        for command in commands:\r\n            cmd_type = command[0]\r\n            params = re.findall(r'-?\\d*\\.?\\d+', command[1:])\r\n            params = [float(p) for p in params]\r\n            \r\n            if cmd_type in 'Mm':\r\n                if len(params) >= 2:\r\n                    if cmd_type == 'M':\r\n                        current_pos = (params[0], params[1])\r\n                    else:  \r\n                        current_pos = (current_pos[0] + params[0], current_pos[1] + params[1])\r\n                    points.append(current_pos)\r\n                    \r\n            elif cmd_type in 'LlHhVv':\r\n                if cmd_type in 'Ll':\r\n                    for i in range(0, len(params), 2):\r\n                        if i + 1 < len(params):\r\n                            if cmd_type == 'L':\r\n                                current_pos = (params[i], params[i + 1])\r\n                            else:\r\n                                current_pos = (current_pos[0] + params[i], current_pos[1] + params[i + 1])\r\n                            points.append(current_pos)\r\n                elif cmd_type in 'Hh':\r\n                    for param in params:\r\n                        if cmd_type == 'H':\r\n                            current_pos = (param, current_pos[1])\r\n                        else:\r\n                            current_pos = (current_pos[0] + param, current_pos[1])\r\n                        points.append(current_pos)\r\n                elif cmd_type in 'Vv':\r\n                    for param in params:\r\n                        if cmd_type == 'V':\r\n                            current_pos = (current_pos[0], param)\r\n                        else:\r\n                            current_pos = (current_pos[0], current_pos[1] + param)\r\n                        points.append(current_pos)\r\n                        \r\n            elif cmd_type in 'CcSsQqTt':\r\n                if cmd_type in 'Cc':\r\n                    for i in range(0, len(params), 6):\r\n                        if i + 5 < len(params):\r\n                            if cmd_type == 'C':\r\n                                control1 = (params[i], params[i + 1])\r\n                                control2 = (params[i + 2], params[i + 3])\r\n                                end_point = (params[i + 4], params[i + 5])\r\n                                points.extend([control1, control2, end_point])\r\n                                current_pos = end_point\r\n                            else:\r\n                                control1 = (current_pos[0] + params[i], current_pos[1] + params[i + 1])\r\n                                control2 = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])\r\n                                end_point = (current_pos[0] + params[i + 4], current_pos[1] + params[i + 5])\r\n                                points.extend([control1, control2, end_point])\r\n                                current_pos = end_point\r\n                elif cmd_type in 'Ss':\r\n                    for i in range(0, len(params), 4):\r\n                        if i + 3 < len(params):\r\n                            if cmd_type == 'S':\r\n                                control2 = (params[i], params[i + 1])\r\n                                end_point = (params[i + 2], params[i + 3])\r\n                                points.extend([control2, end_point])\r\n                                current_pos = end_point\r\n                            else:\r\n                                control2 = (current_pos[0] + params[i], current_pos[1] + params[i + 1])\r\n                                end_point = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])\r\n                                points.extend([control2, end_point])\r\n                                current_pos = end_point\r\n                elif cmd_type in 'Qq':\r\n                    for i in range(0, len(params), 4):\r\n                        if i + 3 < len(params):\r\n                            if cmd_type == 'Q':\r\n                                control = (params[i], params[i + 1])\r\n                                end_point = (params[i + 2], params[i + 3])\r\n                                points.extend([control, end_point])\r\n                                current_pos = end_point\r\n                            else:\r\n                                control = (current_pos[0] + params[i], current_pos[1] + params[i + 1])\r\n                                end_point = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])\r\n                                points.extend([control, end_point])\r\n                                current_pos = end_point\r\n                elif cmd_type in 'Tt':\r\n                    for i in range(0, len(params), 2):\r\n                        if i + 1 < len(params):\r\n                            if cmd_type == 'T':\r\n                                end_point = (params[i], params[i + 1])\r\n                            else:\r\n                                end_point = (current_pos[0] + params[i], current_pos[1] + params[i + 1])\r\n                            points.append(end_point)\r\n                            current_pos = end_point\r\n        \r\n        return points\r\n\r\n    def simplify_path_data(self, path_data, tolerance, preserve_curves, stats):\r\n        \"\"\"简化路径数据\"\"\"\r\n        original_points = self.extract_points_from_path(path_data)\r\n        stats['original_points'] += len(original_points)\r\n        \r\n        if len(original_points) < 3:\r\n            stats['simplified_points'] += len(original_points)\r\n            return path_data\r\n        \r\n        if not preserve_curves:\r\n            filtered_points = [original_points[0]]\r\n            for i in range(1, len(original_points)):\r\n                if (abs(original_points[i][0] - filtered_points[-1][0]) > 0.1 or \r\n                    abs(original_points[i][1] - filtered_points[-1][1]) > 0.1):\r\n                    filtered_points.append(original_points[i])\r\n            original_points = filtered_points\r\n        \r\n        simplified_points = self.douglas_peucker(original_points, tolerance)\r\n        stats['simplified_points'] += len(simplified_points)\r\n        \r\n        if preserve_curves and len(simplified_points) >= len(original_points) * 0.9:\r\n            stats['simplified_points'] = stats['simplified_points'] - len(simplified_points) + len(original_points)\r\n            return path_data\r\n        \r\n        if len(simplified_points) < 2:\r\n            return path_data\r\n            \r\n        path_parts = [f\"M{simplified_points[0][0]:.1f},{simplified_points[0][1]:.1f}\"]\r\n        \r\n        for i in range(1, len(simplified_points)):\r\n            x, y = simplified_points[i]\r\n            path_parts.append(f\"L{x:.1f},{y:.1f}\")\r\n        \r\n        if path_data.strip().endswith('Z') or path_data.strip().endswith('z'):\r\n            path_parts.append('Z')\r\n        \r\n        return ' '.join(path_parts)\r\n\r\n    def simplify_svg_paths(self, SVG_String, tolerance, preserve_curves):\r\n        \"\"\"简化SVG路径\"\"\"\r\n        effective_tolerance = tolerance\r\n        \r\n        stats = {\r\n            'original_points': 0,\r\n            'simplified_points': 0,\r\n            'paths_processed': 0,\r\n            'original_size': len(SVG_String),\r\n            'simplified_size': 0\r\n        }\r\n        \r\n        try:\r\n            root = ET.fromstring(SVG_String)\r\n            \r\n            paths = root.findall('.//{http://www.w3.org/2000/svg}path')\r\n            if not paths:\r\n                paths = root.findall('.//path')\r\n            \r\n            for path in paths:\r\n                if 'd' in path.attrib:\r\n                    original_data = path.attrib['d']\r\n                    simplified_data = self.simplify_path_data(original_data, effective_tolerance, preserve_curves, stats)\r\n                    path.attrib['d'] = simplified_data\r\n                    stats['paths_processed'] += 1\r\n            \r\n            ET.register_namespace('', 'http://www.w3.org/2000/svg')\r\n            simplified_svg = ET.tostring(root, encoding='unicode')\r\n            stats['simplified_size'] = len(simplified_svg)\r\n            \r\n        except Exception as e:\r\n            simplified_svg = self.simplify_svg_regex(SVG_String, effective_tolerance, preserve_curves, stats)\r\n            stats['simplified_size'] = len(simplified_svg)\r\n        \r\n        reduction_ratio = (stats['original_points'] - stats['simplified_points']) / max(stats['original_points'], 1) * 100\r\n        print(f\"SVG Path Simplified: {reduction_ratio:.1f}% points reduced\")\r\n        \r\n        return (simplified_svg,)\r\n\r\n    def simplify_svg_regex(self, SVG_String, tolerance, preserve_curves, stats):\r\n        \"\"\"\r\n        使用正则表达式方法简化SVG路径（备选方案）\r\n        \"\"\"\r\n        def replace_path(match):\r\n            path_data = match.group(1)\r\n            simplified = self.simplify_path_data(path_data, tolerance, preserve_curves, stats)\r\n            stats['paths_processed'] += 1\r\n            return f'd=\"{simplified}\"'\r\n        \r\n        simplified_svg = re.sub(r'd=\"([^\"]*)\"', replace_path, SVG_String)\r\n        \r\n        return simplified_svg\r\n\r\n\r\nclass TS_ImageToSVGStringBW_Potracer:\r\n    \"\"\"Potracer矢量化为SVG\"\"\"\r\n    turnpolicy_map = {\r\n        \"minority\": potrace.POTRACE_TURNPOLICY_MINORITY,\r\n        \"black\": potrace.POTRACE_TURNPOLICY_BLACK,\r\n        \"white\": potrace.POTRACE_TURNPOLICY_WHITE,\r\n        \"left\": potrace.POTRACE_TURNPOLICY_LEFT,\r\n        \"right\": potrace.POTRACE_TURNPOLICY_RIGHT,\r\n        \"majority\": potrace.POTRACE_TURNPOLICY_MAJORITY,\r\n    }\r\n\r\n    @classmethod\r\n    def INPUT_TYPES(cls):\r\n        policy_options = list(cls.turnpolicy_map.keys())\r\n        return {\r\n            \"required\": {\r\n                \"image\": (\"IMAGE\",),\r\n                \"threshold\": (\"INT\", {\"default\": 128, \"min\": 0, \"max\": 255}),\r\n            },\r\n            \"optional\": {\r\n                \"input_foreground\": ([\"White on Black\", \"Black on White\"], {\"default\": \"Black on White\"}),\r\n                \"turnpolicy\": (policy_options, {\"default\": \"minority\"}),\r\n                \"turdsize\": (\"INT\", {\"default\": 2, \"min\": 0}),\r\n                \"corner_threshold\": (\"FLOAT\", {\"default\": 1.0, \"min\": 0.0, \"max\": 1.34, \"step\": 0.01}),\r\n                \"zero_sharp_corners\": (\"BOOLEAN\", {\"default\": False}),\r\n                \"opttolerance\": (\"FLOAT\", {\"default\": 0.2, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01}),\r\n                \"optimize_curve\": (\"BOOLEAN\", {\"default\": True}),\r\n                \"foreground_color\": (\"STRING\", {\"widget\": \"color\", \"default\": \"#000000\"}),\r\n                \"background_color\": (\"STRING\", {\"widget\": \"color\", \"default\": \"#FFFFFF\"}),\r\n                \"stroke_color\": (\"STRING\", {\"widget\": \"color\", \"default\": \"#FF0000\"}),\r\n                \"stroke_width\": (\"FLOAT\", {\"default\": 0.0, \"min\": 0.0, \"step\": 0.5}),\r\n            }\r\n        }\r\n\r\n    RETURN_TYPES = (\"STRING\",)\r\n    FUNCTION = \"vectorize\"\r\n    CATEGORY = \"💎TOSVG/Convert\"\r\n\r\n    def vectorize(self, image, threshold, turnpolicy, turdsize, corner_threshold, opttolerance,\r\n                  input_foreground=\"Black on White\", optimize_curve=True,\r\n                  zero_sharp_corners=False,\r\n                  foreground_color=\"#000000\", background_color=\"#FFFFFF\",\r\n                  stroke_color=\"#FF0000\", stroke_width=0.0):\r\n        \r\n        image_np = image.cpu().numpy()\r\n        batch_svg_strings = []\r\n\r\n        for i, single_image_np in enumerate(image_np):\r\n            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)\r\n            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>'\r\n\r\n            try:\r\n                pil_img = Image.fromarray((single_image_np * 255).astype(np.uint8))\r\n                orig_width, orig_height = pil_img.size\r\n\r\n                if orig_width <= 0 or orig_height <= 0:\r\n                    error_svg = f'<svg width=\"1\" height=\"1\"><desc>Error: Invalid image dimensions for image {i}</desc></svg>'\r\n                    batch_svg_strings.append(error_svg)\r\n                    continue\r\n\r\n                threshold_norm = threshold / 255.0\r\n                if single_image_np.ndim == 3:\r\n                    binary_np = single_image_np[:, :, 0] < threshold_norm if single_image_np.shape[2] > 1 else single_image_np[:,:,0] < threshold_norm\r\n                elif single_image_np.ndim == 2:\r\n                    binary_np = single_image_np < threshold_norm\r\n                else:\r\n                    error_svg = f'<svg width=\"{orig_width}\" height=\"{orig_height}\"><desc>Error: Unexpected image dimensions for image {i}</desc></svg>'\r\n                    batch_svg_strings.append(error_svg)\r\n                    continue\r\n\r\n                if input_foreground == \"Black on White\":\r\n                    binary_np = ~binary_np\r\n\r\n                if np.all(binary_np) or not np.any(binary_np):\r\n                    skipped_svg = f'<svg width=\"{orig_width}\" height=\"{orig_height}\"><desc>Potracer: Skipped blank image {i}</desc></svg>'\r\n                    batch_svg_strings.append(skipped_svg)\r\n                    continue\r\n\r\n                turdsize_int = int(turdsize) if turdsize is not None else 0\r\n                policy_arg = self.turnpolicy_map.get(turnpolicy, turnpolicy)\r\n                alphamax_value_to_use = 1.34 if zero_sharp_corners else corner_threshold\r\n                scale = 1.0\r\n\r\n                bm = potrace.Bitmap(binary_np)\r\n                plist = bm.trace(\r\n                    turdsize=turdsize_int,\r\n                    turnpolicy=policy_arg,\r\n                    alphamax=alphamax_value_to_use,\r\n                    opticurve=optimize_curve,\r\n                    opttolerance=opttolerance\r\n                )\r\n\r\n                scaled_width = max(1, round(orig_width * scale))\r\n                scaled_height = max(1, round(orig_height * scale))\r\n                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}\">'\r\n                svg_footer = \"</svg>\"\r\n                background_rect = \"\"\r\n                bg_color_lower = background_color.lower()\r\n\r\n                if bg_color_lower != \"none\" and bg_color_lower != \"\":\r\n                    background_rect = f'<rect width=\"100%\" height=\"100%\" fill=\"{background_color}\"/>'\r\n\r\n                scaled_stroke_width = stroke_width * scale\r\n                stroke_attr = f'stroke=\"{stroke_color}\" stroke-width=\"{scaled_stroke_width}\"' if scaled_stroke_width > 0 and stroke_color.lower() != \"none\" else 'stroke=\"none\"'\r\n                fill_attr = f'fill=\"{foreground_color}\"' if foreground_color.lower() != \"none\" else 'fill=\"none\"'\r\n                if fill_attr == 'fill=\"none\"' and stroke_attr == 'stroke=\"none\"':\r\n                    fill_attr = 'fill=\"black\"'\r\n\r\n                all_paths_svg_parts = []\r\n                if plist:\r\n                    fill_rule_to_use = \"evenodd\"\r\n                    for curve in plist:\r\n                        if not (hasattr(curve, 'start_point') and hasattr(curve.start_point, 'x') and hasattr(curve.start_point, 'y')):\r\n                            continue\r\n                        fs = curve.start_point\r\n                        all_paths_svg_parts.append(f\"M{fs.x * scale:.2f},{fs.y * scale:.2f}\")\r\n\r\n                        if not hasattr(curve, 'segments'):\r\n                            continue\r\n                        for segment in curve.segments:\r\n                            valid_segment = True\r\n                            if not (hasattr(segment, 'is_corner') and hasattr(segment, 'end_point') and hasattr(segment.end_point, 'x') and hasattr(segment.end_point, 'y')):\r\n                                valid_segment = False\r\n\r\n                            if valid_segment and segment.is_corner:\r\n                                if not (hasattr(segment, 'c') and hasattr(segment.c, 'x') and hasattr(segment.c, 'y')):\r\n                                    valid_segment = False\r\n                                else:\r\n                                    c_x = segment.c.x * scale\r\n                                    c_y = segment.c.y * scale\r\n                                    ep_x = segment.end_point.x * scale\r\n                                    ep_y = segment.end_point.y * scale\r\n                                    all_paths_svg_parts.append(f\"L{c_x:.2f},{c_y:.2f}L{ep_x:.2f},{ep_y:.2f}\")\r\n                            elif valid_segment:\r\n                                if not (hasattr(segment, 'c1') and hasattr(segment.c1, 'x') and hasattr(segment.c1, 'y') and \\\r\n                                        hasattr(segment, 'c2') and hasattr(segment.c2, 'x') and hasattr(segment.c2, 'y')):\r\n                                    valid_segment = False\r\n                                else:\r\n                                    c1_x = segment.c1.x * scale; c1_y = segment.c1.y * scale\r\n                                    c2_x = segment.c2.x * scale; c2_y = segment.c2.y * scale\r\n                                    ep_x = segment.end_point.x * scale; ep_y = segment.end_point.y * scale\r\n                                    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}\")\r\n                        all_paths_svg_parts.append(\"Z\")\r\n\r\n                    if all_paths_svg_parts:\r\n                        path_d_attribute = \"\".join(all_paths_svg_parts)\r\n                        path_element = f'<path {stroke_attr} {fill_attr} fill-rule=\"{fill_rule_to_use}\" d=\"{path_d_attribute}\"/>'\r\n                        svg_data_for_current_image = svg_header + background_rect + path_element + svg_footer\r\n                    else:\r\n                        svg_data_for_current_image = f'{svg_header}<desc>Potracer: Path data generation failed for image {i}</desc>{svg_footer}'\r\n                else:\r\n                    svg_data_for_current_image = f'{svg_header}<desc>Potracer: No paths found for image {i}</desc>{svg_footer}'\r\n\r\n                batch_svg_strings.append(svg_data_for_current_image)\r\n\r\n            except Exception as e:\r\n                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>'\r\n                batch_svg_strings.append(error_svg_content)\r\n                continue\r\n\r\n        output_string_joined = \"\\n\".join(batch_svg_strings)\r\n\r\n        return (output_string_joined,)\r\n\r\n\r\n\r\nNODE_CLASS_MAPPINGS = {\r\n    \"TS_ImageQuantize\": TS_ImageQuantize,\r\n    \"TS_ImageToSVGStringColor_Vtracer\": TS_ImageToSVGStringColor_Vtracer,\r\n    \"TS_ImageToSVGStringBW_Vtracer\": TS_ImageToSVGStringBW_Vtracer,\r\n    \"TS_SVGStringToImage\": TS_SVGStringToImage,\r\n    \"TS_SaveSVGString\": TS_SaveSVGString,\r\n    \"TS_SVGStringPreview\": TS_SVGStringPreview,\r\n    \"TS_SVGStringToSVGBytesIO\": TS_SVGStringToSVGBytesIO,\r\n    \"TS_SVGBytesIOToString\": TS_SVGBytesIOToString,\r\n    \"TS_SVGPathSimplify\": TS_SVGPathSimplify,\r\n    \"TS_ImageToSVGStringBW_Potracer\": TS_ImageToSVGStringBW_Potracer,\r\n}\r\n\r\nNODE_DISPLAY_NAME_MAPPINGS = {\r\n    \"TS_ImageQuantize\": \"Image Quantize\",\r\n    \"TS_ImageToSVGStringColor_Vtracer\": \"Image to SVG String Color_Vtracer\",\r\n    \"TS_ImageToSVGStringBW_Vtracer\": \"Image to SVG String BW_Vtracer\",\r\n    \"TS_SVGStringToImage\": \"SVG String to Image\",\r\n    \"TS_SaveSVGString\": \"Save SVG String\",\r\n    \"TS_SVGStringPreview\": \"SVG String Preview\",\r\n    \"TS_SVGStringToSVGBytesIO\": \"SVG String to SVG BytesIO\",\r\n    \"TS_SVGBytesIOToString\": \"SVG BytesIO to SVG String\",\r\n    \"TS_SVGPathSimplify\": \"SVG String Path Simplify\",\r\n    \"TS_ImageToSVGStringBW_Potracer\": \"Image to SVG String BW_Potracer\",\r\n}"
  }
]