From 96c6d998dd931e4a4b3f295db2d53fb3b7c2f785 Mon Sep 17 00:00:00 2001 From: Aaron Burton Date: Wed, 18 Mar 2026 17:23:52 -0700 Subject: [PATCH] Initial release: Vectorize Pro PNG-to-SVG node for ComfyUI --- .gitignore | 12 ++++ LICENCE.MD | 9 +++ README.MD | 75 ++++++++++++++++++++++++ __init__.py | 44 ++++++++++++++ png_to_svg_pro.py | 123 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 21 +++++++ trigger.py | 99 ++++++++++++++++++++++++++++++++ web/js/augment_style.js | 65 +++++++++++++++++++++ 8 files changed, 448 insertions(+) create mode 100644 .gitignore create mode 100644 LICENCE.MD create mode 100644 README.MD create mode 100644 __init__.py create mode 100644 png_to_svg_pro.py create mode 100644 pyproject.toml create mode 100644 trigger.py create mode 100644 web/js/augment_style.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f6e4bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +.env +.venv/ +*.log +.DS_Store +Thumbs.db +node.zip diff --git a/LICENCE.MD b/LICENCE.MD new file mode 100644 index 0000000..1580b89 --- /dev/null +++ b/LICENCE.MD @@ -0,0 +1,9 @@ + + +Copyright 2026 Augment Studio - Vectprize Pro + +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. diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..c92bec7 --- /dev/null +++ b/README.MD @@ -0,0 +1,75 @@ +# Augment Vectorize Pro: PNG to SVG Vectorizer for ComfyUI + +Convert raster images into clean, scalable vector graphics, right inside ComfyUI. Augment Vectorize Pro takes your PNGs (logos, icons, illustrations, brand marks) and returns production-ready SVG files with crisp edges, accurate colors, and none of the artifacts you get from auto-trace tools. + +Built for designers, studios, and agencies that need vector output they can actually hand off to a client or send to print without cleanup. + +## Why this exists + +Most PNG-to-SVG converters produce noisy paths, stray anchor points, and muddy fills. Vectorize Pro is powered by the Augment Studio API and tuned specifically for **design assets**: flat artwork, logomarks, icons, and brand graphics. The result is a minimal, well-structured SVG you can open in Illustrator, Figma, or Affinity and use as-is. + +## What you get + +- **True vector output.** Paths, not embedded rasters. Scale to any size without quality loss. +- **Clean geometry.** Smooth curves, minimal nodes, no stray fragments. +- **Accurate color mapping.** Fills and strokes match the source, ready for brand guidelines. +- **Native ComfyUI SVG type.** Outputs the ComfyUI `SVG` type so you can preview, save, or pipe it downstream. +- **Chainable workflows.** Includes Trigger and Wait nodes for wiring multi-step pipelines (upscale first, then vectorize, etc.). + +## Getting started + +1. Grab an API key at [augmentstudio.app/pricing](https://augmentstudio.app/pricing) +2. Install from ComfyUI Manager (search **"augment Vectorize Pro"**) +3. Add the **Vectorize Pro** node to your workflow, paste in your key, and connect an image + +The node submits your image, polls for completion, and returns the SVG automatically. No extra steps. + +## Included nodes + + +| Node | Category | Description | +| ----------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ | +| **Vectorize Pro** | Augment / Enhance | Converts a PNG image to a production-ready SVG via the Augment Studio API. | +| **Trigger** | Augment / Utils | Converts any input into a trigger signal for sequencing nodes. | +| **Wait (Int, Float, String, Bool, Image, Latent, ...)** | Augment / Utils | Holds a value until a trigger fires. Use these to enforce execution order in complex workflows. | + + +## Example workflow + +``` +Load Image > Vectorize Pro > Save SVG +``` + +Or chain it with [Augment Upscale Pro](https://github.com/augment-lib/upscale-pro) for maximum quality. Upscale your asset 2x first, then vectorize: + +``` +Load Image > Upscale Pro (2x) > Vectorize Pro > Save SVG +``` + +## Manual install + +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/augment-lib/vectorize-pro.git augment-vectorize-pro +``` + +Restart ComfyUI and the nodes will register automatically. + +## Requirements + +- ComfyUI (latest recommended) +- Python 3.10+ +- An Augment Studio API key ([get one here](https://augmentstudio.app/pricing)) + +Dependencies (`numpy`, `Pillow`, `requests`, `torch`) are installed automatically by ComfyUI Manager or listed in `pyproject.toml` for manual installs. + +## Who it's for + +- **Brand designers** cleaning up logo assets for style guides +- **Agencies** converting client-supplied rasters into print-ready vectors +- **Icon designers** producing SVG icon sets from pixel art or AI-generated images +- **ComfyUI power users** building end-to-end asset pipelines that output scalable graphics + +## License + +MIT. See [LICENCE.md](LICENCE.MD) for details. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e5bb471 --- /dev/null +++ b/__init__.py @@ -0,0 +1,44 @@ +""" +Augment - Vectorize Pro +""" + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} +_failed = [] + +# ── Flow Control ── +try: + from .trigger import GateNode, make_wait_node, WAIT_TYPES + NODE_CLASS_MAPPINGS["GateNode"] = GateNode + NODE_DISPLAY_NAME_MAPPINGS["GateNode"] = "Trigger" + for _name, _type_str in WAIT_TYPES.items(): + _cls_name = f"AugmentWait{_name}" + try: + NODE_CLASS_MAPPINGS[_cls_name] = make_wait_node(_name, _type_str) + NODE_DISPLAY_NAME_MAPPINGS[_cls_name] = f"{_name} (Improved)" + except Exception as e: + _failed.append((_cls_name, e)) + print(f"[augment-vectorize] ⚠ {_cls_name} unavailable: {e}") +except Exception as e: + _failed.append(("Wait nodes", e)) + print(f"[augment-vectorize] ⚠ Flow control nodes unavailable: {e}") + +# ── PNG to SVG Pro (Paid) ── +try: + from .png_to_svg_pro import AugmentPNGToSVGPro + NODE_CLASS_MAPPINGS["AugmentPNGToSVGPro"] = AugmentPNGToSVGPro + NODE_DISPLAY_NAME_MAPPINGS["AugmentPNGToSVGPro"] = "Vectorize Pro" +except Exception as e: + _failed.append(("AugmentPNGToSVGPro", e)) + print(f"[augment-vectorize] ⚠ AugmentPNGToSVGPro unavailable: {e}") + +# ── Summary ── +WEB_DIRECTORY = "./web/js" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] + +if _failed: + print(f"[augment-vectorize] ✓ Registered {len(NODE_CLASS_MAPPINGS)} nodes ({len(_failed)} failed)") + for node_id, err in _failed: + print(f"[augment-vectorize] ✗ {node_id}: {err}") +else: + print(f"[augment-vectorize] ✓ Registered {len(NODE_CLASS_MAPPINGS)} nodes") diff --git a/png_to_svg_pro.py b/png_to_svg_pro.py new file mode 100644 index 0000000..ddc2f69 --- /dev/null +++ b/png_to_svg_pro.py @@ -0,0 +1,123 @@ +# "Design-focused node suite for ComfyUI." +# Copyright 2026 Augment Studio +# Paid Node, credit values may change. +# Augmentstudio.app + +import io +import json +import requests +import time +import numpy as np +from PIL import Image + + +NODE_ID = "png_to_svg_pro" +API_URL = "https://augmentstudio.app/api" + + +class AugmentPNGToSVGPro: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "api_key": ("STRING", {"default": ""}), + "image": ("IMAGE",), + }, + "optional": { + "trigger": ("TRIGGER",), + }, + } + + RETURN_TYPES = ("SVG", "AUGMENT_JSON", "TRIGGER") + RETURN_NAMES = ("svg", "json_result", "trigger") + FUNCTION = "execute" + CATEGORY = "Augment/Enhance" + OUTPUT_NODE = True + + @classmethod + def IS_CHANGED(cls, **kwargs): + return time.time() + + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + return True + + def execute(self, api_key, image, trigger=None): + api_url = API_URL.rstrip("/") + auth = {"Authorization": f"Bearer {api_key}"} + + img_np = (image[0].cpu().numpy() * 255).astype(np.uint8) + pil_img = Image.fromarray(img_np, "RGB") + buf = io.BytesIO() + pil_img.save(buf, format="PNG") + img_bytes = buf.getvalue() + + print(f"[Augment API] PNG to SVG Pro: submitting {img_np.shape[1]}x{img_np.shape[0]} image ({len(img_bytes)} bytes)") + try: + r = requests.post( + f"{api_url}/process", + files={"image": ("image_input.png", img_bytes, "image/png")}, + data={"node_id": NODE_ID}, + headers=auth, + timeout=30, + ) + except requests.exceptions.RequestException as e: + print(f"[Augment API] Submit failed: {e}") + raise + + if r.status_code != 200: + raise RuntimeError(f"Submit error {r.status_code}: {r.text[:500]}") + + request_id = r.json().get("request_id") + if not request_id: + raise RuntimeError(f"No request_id: {r.text[:500]}") + + print(f"[Augment API] Job submitted: {request_id}") + + max_wait = 60 + elapsed = 0 + job_status = "unknown" + + while elapsed < max_wait: + time.sleep(1) + elapsed += 1 + try: + status_r = requests.get( + f"{api_url}/job/{request_id}/status", + headers=auth, timeout=10, + ) + job_status = status_r.json().get("status", "unknown") + except Exception as e: + print(f"[Augment API] Poll error: {e}") + continue + + if job_status == "done": + break + elif job_status == "error": + raise RuntimeError(f"Job failed: {status_r.json().get('error')}") + + if job_status != "done": + raise RuntimeError(f"Timed out after {max_wait}s") + + svg_r = requests.get( + f"{api_url}/job/{request_id}/image", + headers=auth, timeout=60, + ) + if svg_r.status_code != 200: + raise RuntimeError(f"SVG fetch error: {svg_r.status_code}") + + from comfy_api.latest._util.image_types import SVG + svg_bytes = io.BytesIO(svg_r.content) + size = svg_bytes.getbuffer().nbytes + print(f"[Augment API] Done! Received SVG ({size} bytes)") + + json_result = json.dumps({"node": "PNGToSVGPro", "size_bytes": size}) + return {"ui": {"text": [svg_r.text[:500]]}, "result": (SVG([svg_bytes]), json_result, "done")} + + +NODE_CLASS_MAPPINGS = { + "AugmentPNGToSVGPro": AugmentPNGToSVGPro, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "AugmentPNGToSVGPro": "PNG to SVG Pro", +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d72f34b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "augment-VectorizePro" +description = "✨ augment Vectorize — PNG to SVG vectorizer for ComfyUI. Convert raster images, logos, and icons into clean scalable vector graphics. A professional vectorization tool built directly into your ComfyUI workflow via the Augment Studio API with automatic job polling and SVG output. Ideal for logo design, brand asset pipelines, and any workflow that needs crisp vector conversion. Includes built-in flow control trigger and wait nodes for chaining multi-step pipelines. Get your API key at https://augmentstudio.app/pricing" +version = "1.0.0" +licence = { file = "LICENCE.md" } + +dependencies = [ + "numpy>=1.19.0", + "torch>=1.9.0", + "Pillow>=8.0.0", + "requests>=2.25.0" +] + +[project.urls] +Repository = "https://github.com/augment-lib/vectorize-pro" +Website = "https://augmentstudio.app/" + +[tool.comfy] +PublisherId = "augmentstudio" +DisplayName = "✨ augment Vectorize Pro - PNG to SVG Vectorizer for ComfyUI" +Icon = "https://avatars.githubusercontent.com/u/243209371?s=400&u=db6e0d310dcf8863dd5c735d3ebbdaa5c317530d&v=4" diff --git a/trigger.py b/trigger.py new file mode 100644 index 0000000..fc6fb60 --- /dev/null +++ b/trigger.py @@ -0,0 +1,99 @@ +# "Design-focused node suite for ComfyUI." +# Copyright 2026 Augment Studio +# Augmentstudio.app + +import json + + +class GateNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input": ("*",), + }, + } + + RETURN_TYPES = ("TRIGGER", "*", "STRING") + RETURN_NAMES = ("trigger", "value", "json") + OUTPUT_NODE = True + FUNCTION = "execute" + CATEGORY = "Augment/Utils" + + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + return True + + def execute(self, input): + json_result = json.dumps({"node": "Convert Any", "type": type(input).__name__, "value": str(input)}) + return {"ui": {"text": [str(input)]}, "result": ("done", input, json_result)} + + +WAIT_DEFAULTS = { + "INT": {"default": 0}, + "FLOAT": {"default": 0.0, "step": 0.01}, + "STRING": {"default": ""}, + "BOOLEAN": {"default": False}, +} + + +def make_wait_node(type_name, type_str): + class WaitNode: + @classmethod + def INPUT_TYPES(cls): + has_widget = type_str in WAIT_DEFAULTS + inputs = { + "required": {}, + "optional": { + "trigger": ("TRIGGER",), + }, + } + if has_widget: + inputs["required"]["value"] = (type_str, WAIT_DEFAULTS[type_str]) + inputs["optional"]["input"] = (type_str, {"forceInput": True}) + else: + inputs["required"]["value"] = (type_str,) + return inputs + + RETURN_TYPES = (type_str, "AUGMENT_JSON", "TRIGGER") + RETURN_NAMES = ("value", "json", "trigger") + OUTPUT_NODE = True + FUNCTION = "execute" + CATEGORY = "Augment/Utils" + + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + return True + + def execute(self, value=None, trigger=None, input=None): + v = input if input is not None else value + json_result = json.dumps({"node": type_str, "type": type_str.lower(), "value": str(v)}) + return {"ui": {"text": [str(v)]}, "result": (v, json_result, "done")} + + WaitNode.__name__ = f"Wait{type_name}Node" + WaitNode.__qualname__ = f"Wait{type_name}Node" + return WaitNode + + +WAIT_TYPES = { + "Int": "INT", + "Float": "FLOAT", + "String": "STRING", + "Bool": "BOOLEAN", + "Image": "IMAGE", + "Latent": "LATENT", + "Condition": "CONDITIONING", + "VAE": "VAE", + "Model": "MODEL", + "Clip": "CLIP", + "Mask": "MASK", +} + +NODE_CLASS_MAPPINGS = {"GateNode": GateNode} +NODE_DISPLAY_NAME_MAPPINGS = {"GateNode": "Trigger"} + +for name, type_str in WAIT_TYPES.items(): + cls_name = f"Wait{name}Node" + node_cls = make_wait_node(name, type_str) + NODE_CLASS_MAPPINGS[cls_name] = node_cls + NODE_DISPLAY_NAME_MAPPINGS[cls_name] = f"{name} (Improved)" diff --git a/web/js/augment_style.js b/web/js/augment_style.js new file mode 100644 index 0000000..98723e1 --- /dev/null +++ b/web/js/augment_style.js @@ -0,0 +1,65 @@ +/*"Augment Vectorize — AI PNG to SVG Vectorizer for ComfyUI." +# Copyright 2026 Augment Studio +Augmentstudio.app */ + +import { app } from "../../../scripts/app.js"; + +const TITLE_BG = "#443322"; +const NODE_BG = "#665533"; + +const PAID_NODES = [ + "AugmentPNGToSVGPro", +]; + +const BADGE_FONT = "bold 9px sans-serif"; +const BADGE_RADIUS = 4; +const BADGE_PAD_X = 6; + +app.registerExtension({ + name: "augment-vectorize.NodeStyle", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (!PAID_NODES.includes(nodeData.name)) return; + + const origCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + origCreated?.apply(this, arguments); + this.addWidget("button", "Get Tokens", null, () => { + window.open("https://augmentstudio.app/pricing", "_blank"); + }); + + const jsonIdx = this.outputs?.findIndex(o => o.name === "json_result"); + if (jsonIdx >= 0) { + this.outputs.splice(jsonIdx, 1); + } + }; + + const origDraw = nodeType.prototype.onDrawForeground; + nodeType.prototype.onDrawForeground = function (ctx) { + this.color = TITLE_BG; + this.bgcolor = NODE_BG; + + origDraw?.apply(this, arguments); + + ctx.save(); + ctx.font = BADGE_FONT; + + const creditsText = "🔸 1 export / run"; + const creditsW = ctx.measureText(creditsText).width + BADGE_PAD_X * 2; + const creditsX = this.size[0] - creditsW - 6; + const y = -22; + + ctx.beginPath(); + ctx.roundRect(creditsX, y, creditsW, 14, BADGE_RADIUS); + ctx.fillStyle = "#8d6932"; + ctx.fill(); + + ctx.fillStyle = "#ffffff"; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText(creditsText, creditsX + BADGE_PAD_X, y + 7); + + ctx.restore(); + }; + }, +});