first commit
This commit is contained in:
@@ -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
@@ -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 |
@@ -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 |
@@ -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.
@@ -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?.();
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Pillow>=9.0.0
|
||||
cairosvg>=2.7.0
|
||||
numpy>=1.22.0
|
||||
Reference in New Issue
Block a user