first commit

This commit is contained in:
stormcenter
2024-11-24 23:41:12 +08:00
commit ca7267461d
9 changed files with 714 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
# ComfyUI-SVGFullfill
A ComfyUI custom node that enables SVG file integration and editing. This node allows you to upload SVG files, preview them in real-time, replace images and text elements within the SVG, and export the result as a PNG image.
## Features
- 🖼️ SVG file upload and preview
- 🔄 Replace up to 3 images in SVG
- ✏️ Edit up to 10 text elements in SVG
- 🈯 Full Chinese text support
- 🖼️ Real-time canvas preview
- 📤 PNG export
## Installation
1. Clone this repository to your ComfyUI custom_nodes directory:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/YourUsername/ComfyUI-SVGFullfill.git
```
2. Install the required dependencies:
```bash
pip install cairosvg Pillow
```
3. Copy your fonts into the font directory:
```bash
cd ComfyUI-SVGFullfill
mkdir font
# Copy your .ttf or .otf fonts into the font directory
```
## Usage
### SVG Requirements
Your SVG file should:
1. Have text elements with id attributes like `text1`, `text2`, etc., for text replacement
2. Have `<image>` elements for image replacement
3. Use UTF-8 encoding (especially important for Chinese text)
Example SVG structure:
```xml
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
<text id="text1">Replaceable Text 1</text>
<text id="text2">替换文本2</text>
<image x="10" y="10" width="200" height="200" />
</svg>
```
### Nodes
The extension provides two main nodes:
1. **SVG Uploader**
- Allows SVG file upload
- Provides real-time canvas preview
- Outputs SVG content to SVG Editor
2. **SVG Editor**
- Accepts SVG file input from SVG Uploader
- Has optional inputs for 3 images and 10 text replacements
- Outputs the final PNG image
### Workflow Example
1. Add SVG Uploader node
2. Add SVG Editor node
3. Connect SVG Uploader's output to SVG Editor's input
4. Upload your SVG file using the SVG Uploader
5. Connect your images to SVG Editor's image inputs (optional)
6. Input text replacements in SVG Editor (optional)
7. Run the workflow to get your final image
### Example Workflow
```mermaid
graph TD
A[SVG Uploader] -->|SVG File| B[SVG Editor]
C[Image 1] -->|Optional| B
D[Image 2] -->|Optional| B
E[Image 3] -->|Optional| B
B -->|Final Image| F[Output]
```
## Font Support
To use custom fonts (especially for Chinese text):
1. Place your `.ttf` or `.otf` font files in the `font` directory
2. The fonts will be automatically detected and used for rendering
## Known Limitations
- Maximum 3 image replacements per SVG
- Maximum 10 text replacements per SVG
- SVG animations are not supported
- External images in SVG must be base64 encoded
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Credits
Thanks to the ComfyUI community for their support and inspiration.
+11
View File
@@ -0,0 +1,11 @@
try:
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
except ImportError:
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
__version__ = "1.0.0"
WEB_DIRECTORY = "./js"
TYPE_NAME = "SVG"
EXTENSION_NAME = "ComfyUI-SVGFullfill"
Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="600" viewBox="0 0 800 600">
<!-- 背景 -->
<rect width="100%" height="100%" fill="#f0f0f0"/>
<!-- 标题文本 -->
<text id="text1" x="400" y="80" font-family="Arial" font-size="40" text-anchor="middle" fill="#333333">
主标题文本
</text>
<!-- 副标题 -->
<text id="text2" x="400" y="130" font-family="Arial" font-size="24" text-anchor="middle" fill="#666666">
副标题内容
</text>
<!-- 左侧图片位置 -->
<image id="image1" x="50" y="180" width="200" height="200" xlink:href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2NjY2Ij5JbWFnZSAxPC90ZXh0Pjwvc3ZnPg=="/>
<!-- 中间图片位置 -->
<image id="image2" x="300" y="180" width="200" height="200" xlink:href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2NjY2Ij5JbWFnZSAyPC90ZXh0Pjwvc3ZnPg=="/>
<!-- 右侧图片位置 -->
<image id="image3" x="550" y="180" width="200" height="200" xlink:href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2NjY2Ij5JbWFnZSAzPC90ZXh0Pjwvc3ZnPg=="/>
<!-- 描述文本 -->
<text id="text3" x="100" y="420" font-family="Arial" font-size="18" fill="#444444">
左侧描述文本
</text>
<text id="text4" x="400" y="420" font-family="Arial" font-size="18" text-anchor="middle" fill="#444444">
中间描述文本
</text>
<text id="text5" x="700" y="420" font-family="Arial" font-size="18" text-anchor="end" fill="#444444">
右侧描述文本
</text>
<!-- 底部文本区域 -->
<text id="text6" x="400" y="480" font-family="Arial" font-size="16" text-anchor="middle" fill="#666666">
底部补充说明文本1
</text>
<text id="text7" x="400" y="510" font-family="Arial" font-size="16" text-anchor="middle" fill="#666666">
底部补充说明文本2
</text>
<text id="text8" x="400" y="540" font-family="Arial" font-size="16" text-anchor="middle" fill="#666666">
底部补充说明文本3
</text>
<!-- 页脚文本 -->
<text id="text9" x="50" y="580" font-family="Arial" font-size="14" fill="#999999">
左侧页脚文本
</text>
<text id="text10" x="750" y="580" font-family="Arial" font-size="14" text-anchor="end" fill="#999999">
右侧页脚文本
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+185
View File
@@ -0,0 +1,185 @@
{
"last_node_id": 16,
"last_link_id": 8,
"nodes": [
{
"id": 15,
"type": "SVGEditor",
"pos": {
"0": -837,
"1": 112
},
"size": {
"0": 400,
"1": 586
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "svg_file",
"type": "SVG_FILE",
"link": 7,
"label": "svg_file"
},
{
"name": "image1",
"type": "IMAGE",
"link": null,
"shape": 7,
"label": "image1"
},
{
"name": "image2",
"type": "IMAGE",
"link": null,
"shape": 7,
"label": "image2"
},
{
"name": "image3",
"type": "IMAGE",
"link": null,
"shape": 7,
"label": "image3"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
8
],
"slot_index": 0,
"label": "IMAGE"
}
],
"properties": {
"Node name for S&R": "SVGEditor"
},
"widgets_values": [
"11111",
"",
"",
"",
"",
"",
"",
"",
"",
""
]
},
{
"id": 14,
"type": "SVGUploader",
"pos": {
"0": -1389,
"1": 116
},
"size": {
"0": 422.3057556152344,
"1": 513.9155883789062
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "svg_file",
"type": "SVG_FILE",
"links": [
7
],
"slot_index": 0,
"label": "svg_file"
}
],
"properties": {
"Node name for S&R": "SVGUploader"
},
"widgets_values": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"800\" height=\"600\" viewBox=\"0 0 800 600\">\n <!-- 背景 -->\n <rect width=\"100%\" height=\"100%\" fill=\"#f0f0f0\"/>\n \n <!-- 标题文本 -->\n <text id=\"text1\" x=\"400\" y=\"80\" font-family=\"Arial\" font-size=\"40\" text-anchor=\"middle\" fill=\"#333333\">\n 主标题文本\n </text>\n \n <!-- 副标题 -->\n <text id=\"text2\" x=\"400\" y=\"130\" font-family=\"Arial\" font-size=\"24\" text-anchor=\"middle\" fill=\"#666666\">\n 副标题内容\n </text>\n \n <!-- 左侧图片位置 -->\n <image id=\"image1\" x=\"50\" y=\"180\" width=\"200\" height=\"200\" xlink:href=\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2NjY2Ij5JbWFnZSAxPC90ZXh0Pjwvc3ZnPg==\"/>\n \n <!-- 中间图片位置 -->\n <image id=\"image2\" x=\"300\" y=\"180\" width=\"200\" height=\"200\" xlink:href=\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2NjY2Ij5JbWFnZSAyPC90ZXh0Pjwvc3ZnPg==\"/>\n \n <!-- 右侧图片位置 -->\n <image id=\"image3\" x=\"550\" y=\"180\" width=\"200\" height=\"200\" xlink:href=\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjY2NjIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2NjY2Ij5JbWFnZSAzPC90ZXh0Pjwvc3ZnPg==\"/>\n \n <!-- 描述文本 -->\n <text id=\"text3\" x=\"100\" y=\"420\" font-family=\"Arial\" font-size=\"18\" fill=\"#444444\">\n 左侧描述文本\n </text>\n \n <text id=\"text4\" x=\"400\" y=\"420\" font-family=\"Arial\" font-size=\"18\" text-anchor=\"middle\" fill=\"#444444\">\n 中间描述文本\n </text>\n \n <text id=\"text5\" x=\"700\" y=\"420\" font-family=\"Arial\" font-size=\"18\" text-anchor=\"end\" fill=\"#444444\">\n 右侧描述文本\n </text>\n \n <!-- 底部文本区域 -->\n <text id=\"text6\" x=\"400\" y=\"480\" font-family=\"Arial\" font-size=\"16\" text-anchor=\"middle\" fill=\"#666666\">\n 底部补充说明文本1\n </text>\n \n <text id=\"text7\" x=\"400\" y=\"510\" font-family=\"Arial\" font-size=\"16\" text-anchor=\"middle\" fill=\"#666666\">\n 底部补充说明文本2\n </text>\n \n <text id=\"text8\" x=\"400\" y=\"540\" font-family=\"Arial\" font-size=\"16\" text-anchor=\"middle\" fill=\"#666666\">\n 底部补充说明文本3\n </text>\n \n <!-- 页脚文本 -->\n <text id=\"text9\" x=\"50\" y=\"580\" font-family=\"Arial\" font-size=\"14\" fill=\"#999999\">\n 左侧页脚文本\n </text>\n \n <text id=\"text10\" x=\"750\" y=\"580\" font-family=\"Arial\" font-size=\"14\" text-anchor=\"end\" fill=\"#999999\">\n 右侧页脚文本\n </text>\n</svg>\n",
null,
null
]
},
{
"id": 16,
"type": "PreviewImage",
"pos": {
"0": -358,
"1": 109
},
"size": [
455.8845222550208,
406.6712703372059
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 8,
"label": "images"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
2,
1,
0,
3,
0,
"IMAGE"
],
[
6,
10,
0,
1,
0,
"SVG_FILE"
],
[
7,
14,
0,
15,
0,
"SVG_FILE"
],
[
8,
15,
0,
16,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.5445000000000001,
"offset": [
784.5764511150434,
328.22220991991054
]
}
},
"version": 0.4
}
Binary file not shown.
+156
View File
@@ -0,0 +1,156 @@
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js";
app.registerExtension({
name: "Comfy.SVGFullfill",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SVGUploader") {
nodeType.prototype.onNodeCreated = function () {
console.log("Node created: SVGUploader");
// 隐藏默认的字符串输入
const svgInput = this.widgets.find(w => w.name === "svg_string");
if (svgInput) {
svgInput.hidden = true;
}
// 创建上传按钮(先创建按钮,这样它会在预览区域上方)
this.addWidget("button", "Upload SVG", null, () => {
console.log("Upload button clicked");
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".svg";
fileInput.style.display = "none";
document.body.appendChild(fileInput);
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
console.log("File selected:", file.name);
const reader = new FileReader();
reader.onload = async (e) => {
const svgContent = e.target.result;
console.log("SVG content loaded");
// 存储SVG内容
const svgInput = this.widgets.find(w => w.name === "svg_string");
if (svgInput) {
svgInput.value = svgContent;
}
// 更新预览
try {
console.log("Parsing SVG content...");
const parser = new DOMParser();
const doc = parser.parseFromString(svgContent, "image/svg+xml");
const svgElement = doc.documentElement;
if (!svgElement || svgElement.nodeName !== "svg") {
throw new Error("Invalid SVG content");
}
console.log("Setting SVG attributes...");
// 设置SVG属性
svgElement.style.width = "100%";
svgElement.style.height = "100%";
svgElement.setAttribute("preserveAspectRatio", "xMidYMid meet");
// 更新预览
console.log("Updating preview container...");
widget.div.innerHTML = "";
widget.div.appendChild(svgElement.cloneNode(true));
console.log("Preview updated successfully");
// 强制重绘
app.canvas.setDirty(true);
} catch (error) {
console.error("Error updating preview:", error);
widget.div.innerHTML = `
<div style="color: red; text-align: center; padding: 10px;">
Error loading SVG: ${error.message}
</div>
`;
}
};
reader.readAsText(file);
}
document.body.removeChild(fileInput);
};
fileInput.click();
});
// 创建预览widget
const widget = {
type: "div",
name: "preview",
draw(ctx, node, widget_width, y, widget_height) {
const margin = 10;
const buttonHeight = 40; // 为按钮预留空间
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y + buttonHeight); // 添加buttonHeight偏移
Object.assign(this.div.style, {
transformOrigin: "0 0",
transform: transform,
position: "absolute",
left: document.querySelector('.comfy-menu').style.display === 'none' ? '60px' : '0',
top: "0",
width: `${widget_width - margin * 2}px`,
height: "200px", // 减小高度,给按钮留空间
zIndex: 1
});
}
};
// 创建预览容器
widget.div = $el("div", {
style: {
width: "100%",
height: "100%",
border: "2px solid #ccc",
borderRadius: "4px",
backgroundColor: "#f0f0f0",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center"
}
});
// 创建占位符
const placeholderText = $el("div", {
textContent: "SVG Preview",
style: {
color: "#666",
textAlign: "center",
userSelect: "none",
pointerEvents: "none"
}
});
widget.div.appendChild(placeholderText);
// 添加预览widget到节点
document.body.appendChild(widget.div);
this.addCustomWidget(widget);
// 设置节点大小
this.setSize([220, 320]);
// 清理函数
const onRemoved = this.onRemoved;
this.onRemoved = () => {
widget.div.remove();
return onRemoved?.();
};
};
}
}
});
+191
View File
@@ -0,0 +1,191 @@
import os
import base64
from PIL import Image
import numpy as np
from xml.etree import ElementTree as ET
import cairosvg
from io import BytesIO
import torch
import re
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
class SVGUploader:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"svg_string": ("STRING", {"default": "", "multiline": False, "visible": False}),
}}
RETURN_TYPES = ("SVG_FILE",)
RETURN_NAMES = ("svg_file",)
OUTPUT_NODE = True
FUNCTION = "upload_svg"
CATEGORY = "SVGFullfill"
# 添加节点尺寸定义
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
return float("NaN") # 总是更新
# 定义节点的默认大小
SIZE = [200, 320] # [宽度, 高度]
def __init__(self):
self._svg_content = None
def upload_svg(self, svg_string):
if not svg_string:
return ("")
return (svg_string,)
class SVGEditor:
def __init__(self):
self.font_dir = os.path.join(os.path.dirname(__file__), "font")
if not os.path.exists(self.font_dir):
os.makedirs(self.font_dir)
# 获取字体目录下的所有字体文件
self.fonts = [f for f in os.listdir(self.font_dir) if f.endswith(('.ttf', '.otf'))]
if not self.fonts:
print("警告:font 目录下没有找到字体文件(.ttf 或 .otf)")
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"svg_file": ("SVG_FILE",),
},
"optional": {
"image1": ("IMAGE", {"default": None}),
"image2": ("IMAGE", {"default": None}),
"image3": ("IMAGE", {"default": None}),
"text1": ("STRING", {"default": "", "multiline": True}),
"text2": ("STRING", {"default": "", "multiline": True}),
"text3": ("STRING", {"default": "", "multiline": True}),
"text4": ("STRING", {"default": "", "multiline": True}),
"text5": ("STRING", {"default": "", "multiline": True}),
"text6": ("STRING", {"default": "", "multiline": True}),
"text7": ("STRING", {"default": "", "multiline": True}),
"text8": ("STRING", {"default": "", "multiline": True}),
"text9": ("STRING", {"default": "", "multiline": True}),
"text10": ("STRING", {"default": "", "multiline": True}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "edit_svg"
CATEGORY = "SVGFullfill"
def edit_svg(self, svg_file, image1=None, image2=None, image3=None, **kwargs):
try:
if not svg_file or len(svg_file.strip()) == 0:
print("No SVG content received")
blank_image = torch.zeros((1, 64, 64, 3))
return (blank_image,)
# 使用XML解析器处理SVG
parser = ET.XMLParser(encoding='utf-8')
root = ET.fromstring(svg_file.encode('utf-8'), parser=parser)
# 在处理文本之前,添加字体样式定义
# 查找或创建 defs 元素
defs = root.find('{http://www.w3.org/2000/svg}defs')
if defs is None:
defs = ET.SubElement(root, '{http://www.w3.org/2000/svg}defs')
# 添加字体样式
if self.fonts:
font_path = os.path.join(self.font_dir, self.fonts[0])
style = ET.SubElement(defs, '{http://www.w3.org/2000/svg}style')
style.set('type', 'text/css')
font_face = f"""
@font-face {{
font-family: 'CustomFont';
src: url('file://{font_path}');
}}
"""
style.text = font_face
# 处理文本替换时设置字体
for i in range(1, 11):
text_key = f"text{i}"
if text_key in kwargs and kwargs[text_key]:
for text_elem in root.findall(f".//*[@id='{text_key}']"):
if text_elem is not None:
text_elem.text = kwargs[text_key].strip()
# 设置字体样式
text_elem.set('style', 'font-family: CustomFont;')
print(f"Updated text {i}: {kwargs[text_key]}")
# 处理图片替换
for i, img_batch in enumerate([image1, image2, image3], 1):
if img_batch is not None and img_batch.shape[0] > 0:
print(f"Processing image {i}")
# 将tensor转换为PIL图像
img_tensor = img_batch[0]
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8)
img = Image.fromarray(img_np)
# 转换为PNG base64
buffer = BytesIO()
img.save(buffer, format="PNG")
img_str = base64.b64encode(buffer.getvalue()).decode()
# 修改查找方式,增加命名空间处理
for image_elem in root.findall(f".//*[@id='image{i}']"):
if image_elem is not None:
# 保持原有的宽度和高度属性
current_width = image_elem.get('width', '200')
current_height = image_elem.get('height', '200')
image_elem.set('width', current_width)
image_elem.set('height', current_height)
# 保持原有的位置属性
current_x = image_elem.get('x', str(50 + (i-1)*250))
current_y = image_elem.get('y', '180')
image_elem.set('x', current_x)
image_elem.set('y', current_y)
# 更新图片内容
image_elem.set('{http://www.w3.org/1999/xlink}href', f'data:image/png;base64,{img_str}')
# 转换回字符串
svg_content = ET.tostring(root, encoding='utf-8', method='xml').decode('utf-8')
# 使用cairosvg进行转换
png_data = cairosvg.svg2png(
bytestring=svg_content.encode('utf-8'),
output_width=800,
output_height=600,
scale=1.0,
background_color='white',
unsafe=True,
dpi=96
)
# 转换为PIL图像
png_image = Image.open(BytesIO(png_data))
png_image = png_image.convert('RGB')
# 转换为tensor(值范围0-1)
image_tensor = torch.from_numpy(np.array(png_image).astype(np.float32) / 255.0)
image_tensor = image_tensor.unsqueeze(0)
return (image_tensor,)
except Exception as e:
print(f"Error processing SVG: {str(e)}")
import traceback
traceback.print_exc()
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
return (blank_image,)
# Register nodes
NODE_CLASS_MAPPINGS = {
"SVGUploader": SVGUploader,
"SVGEditor": SVGEditor
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SVGUploader": "SVG Uploader",
"SVGEditor": "SVG Editor"
}
+3
View File
@@ -0,0 +1,3 @@
Pillow>=9.0.0
cairosvg>=2.7.0
numpy>=1.22.0