Add Export Workflow as PNG node

This commit is contained in:
samuelxuliu
2025-10-22 16:52:30 +08:00
parent 56e1577f70
commit 6fb76c5c77
4 changed files with 245 additions and 87 deletions
+3 -29
View File
@@ -1,6 +1,6 @@
# ComfyUI-Prepack
**Version: 1.4.3**
**Version: 1.5.0**
A comprehensive workflow optimization toolkit for ComfyUI, providing essential nodes for model management, sampling control, and workflow enhancement with professional-grade reliability.
@@ -86,33 +86,6 @@ All dependencies are typically already available in standard ComfyUI installatio
- ✅ **Logic Operations** - Comprehensive logic and comparison tools
- ✅ **File Management** - Smart file saving with format preservation and custom naming
## 📁 Project Structure
```
ComfyUI-Prepack/
├── py/ # Core node implementations
│ ├── getpipe.py # Pipeline retrieval
│ ├── setpipe.py # Pipeline storage
│ ├── seed.py # Seed management
│ ├── loras.py # LoRA management
│ ├── ksampler.py # Basic sampling
│ ├── ksamplerAdvanced.py # Advanced sampling
│ ├── modelDualCLIP.py # Dual CLIP models
│ ├── modelSingleCLIP.py # Single CLIP models
│ ├── logicInt.py # Integer logic
│ ├── logicString.py # String logic
│ ├── intCombine.py # Integer combination
│ ├── intSplit.py # Integer splitting
│ └── saveByFileName.py # Smart file saving
├── js/ # JavaScript UI extensions
│ ├── seed.js # Seed management UI
│ ├── loraText.js # LoRA text integration
│ └── setgetnodes.js # Pipeline node UI
├── summary_md/ # Documentation and summaries
├── __init__.py # Plugin initialization
└── requirements.txt # Minimal dependencies
```
## 🤝 Contributing
Contributions are welcome! Please feel free to submit pull requests or report issues.
@@ -125,4 +98,5 @@ This project is open source. Please respect the licensing terms.
**Author:** S4MUEL
**Website:** [s4muel.com](https://s4muel.com)
**Version:** 1.4.3
**GitHub:** [https://github.com/S4MUEL-404/ComfyUI-Prepack](https://github.com/S4MUEL-404/ComfyUI-Prepack)
**Version:** 1.5.0
+3 -29
View File
@@ -1,6 +1,6 @@
# ComfyUI-Prepack
**版本: 1.4.3**
**版本: 1.5.0**
专为 ComfyUI 设计的综合工作流优化工具包,提供模型管理、采样控制和工作流增强的核心节点,具备专业级可靠性。
@@ -86,33 +86,6 @@
- ✅ **逻辑运算** - 综合逻辑和比较工具
- ✅ **文件管理** - 智能文件保存,具备格式保持和自定义命名功能
## 📁 项目结构
```
ComfyUI-Prepack/
├── py/ # 核心节点实现
│ ├── getpipe.py # 管道获取
│ ├── setpipe.py # 管道存储
│ ├── seed.py # 种子管理
│ ├── loras.py # LoRA 管理
│ ├── ksampler.py # 基本采样
│ ├── ksamplerAdvanced.py # 高级采样
│ ├── modelDualCLIP.py # 双 CLIP 模型
│ ├── modelSingleCLIP.py # 单 CLIP 模型
│ ├── logicInt.py # 整数逻辑
│ ├── logicString.py # 字符串逻辑
│ ├── intCombine.py # 整数合并
│ ├── intSplit.py # 整数拆分
│ └── saveByFileName.py # 智能文件保存
├── js/ # JavaScript UI 扩展
│ ├── seed.js # 种子管理 UI
│ ├── loraText.js # LoRA 文本集成
│ └── setgetnodes.js # 管道节点 UI
├── summary_md/ # 文档和总结
├── __init__.py # 插件初始化
└── requirements.txt # 最小依赖包
```
## 🤝 贡献
欢迎贡献!请随时提交 Pull Request 或报告问题。
@@ -125,4 +98,5 @@ ComfyUI-Prepack/
**作者:** S4MUEL
**网站:** [s4muel.com](https://s4muel.com)
**版本:** 1.4.3
**GitHub:** [https://github.com/S4MUEL-404/ComfyUI-Prepack](https://github.com/S4MUEL-404/ComfyUI-Prepack)
**版本:** 1.5.0
+3 -29
View File
@@ -1,6 +1,6 @@
# ComfyUI-Prepack
**版本: 1.4.3**
**版本: 1.5.0**
專為 ComfyUI 設計的綜合工作流優化工具包,提供模型管理、採樣控制和工作流強化的核心節點,具備專業級可靠性。
@@ -86,33 +86,6 @@
- ✅ **邏輯運算** - 綜合邏輯和比較工具
- ✅ **文件管理** - 智慧文件保存,具備格式保持和自訂命名功能
## 📁 專案結構
```
ComfyUI-Prepack/
├── py/ # 核心節點實作
│ ├── getpipe.py # 管道取得
│ ├── setpipe.py # 管道儲存
│ ├── seed.py # 種子管理
│ ├── loras.py # LoRA 管理
│ ├── ksampler.py # 基本採樣
│ ├── ksamplerAdvanced.py # 進階採樣
│ ├── modelDualCLIP.py # 雙 CLIP 模型
│ ├── modelSingleCLIP.py # 單 CLIP 模型
│ ├── logicInt.py # 整數邏輯
│ ├── logicString.py # 字串邏輯
│ ├── intCombine.py # 整數合併
│ ├── intSplit.py # 整數拆分
│ └── saveByFileName.py # 智慧文件保存
├── js/ # JavaScript UI 擴展
│ ├── seed.js # 種子管理 UI
│ ├── loraText.js # LoRA 文本整合
│ └── setgetnodes.js # 管道節點 UI
├── summary_md/ # 文件和總結
├── __init__.py # 外掛程式初始化
└── requirements.txt # 最小相依套件
```
## 🤝 貢獻
歡迎貢獻!請隨時提交 Pull Request 或回報問題。
@@ -125,4 +98,5 @@ ComfyUI-Prepack/
**作者:** S4MUEL
**網站:** [s4muel.com](https://s4muel.com)
**版本:** 1.4.3
**GitHub:** [https://github.com/S4MUEL-404/ComfyUI-Prepack](https://github.com/S4MUEL-404/ComfyUI-Prepack)
**版本:** 1.5.0
+236
View File
@@ -0,0 +1,236 @@
import { app } from "../../../scripts/app.js";
class WorkflowScreenshotExporter {
constructor() {
this.state = null;
}
// Get bounds of all nodes in the workflow
getBounds() {
const bounds = app.graph._nodes.reduce(
(p, n) => {
if (n.pos[0] < p[0]) p[0] = n.pos[0];
if (n.pos[1] < p[1]) p[1] = n.pos[1];
const nodeBounds = n.getBounding();
const r = n.pos[0] + nodeBounds[2];
const b = n.pos[1] + nodeBounds[3];
if (r > p[2]) p[2] = r;
if (b > p[3]) p[3] = b;
return p;
},
[99999, 99999, -99999, -99999]
);
// Add padding: top(2 grid), left(1 grid), bottom(1 grid), right(1 grid)
// ComfyUI grid size is approximately 10px, so multiply by grid size
const gridSize = LiteGraph.NODE_SLOT_HEIGHT || 10;
bounds[0] -= gridSize * 10; // left: 1 grid
bounds[1] -= gridSize * 20; // top: 2 grids
bounds[2] += gridSize * 10; // right: 1 grid
bounds[3] += gridSize * 10; // bottom: 1 grid
return bounds;
}
// Save current canvas state
saveState() {
this.state = {
scale: app.canvas.ds.scale,
width: app.canvas.canvas.width,
height: app.canvas.canvas.height,
offset: app.canvas.ds.offset,
transform: app.canvas.canvas.getContext("2d").getTransform(),
};
}
// Restore canvas state
restoreState() {
if (!this.state) return;
app.canvas.ds.scale = this.state.scale;
app.canvas.canvas.width = this.state.width;
app.canvas.canvas.height = this.state.height;
app.canvas.ds.offset = this.state.offset;
app.canvas.canvas.getContext("2d").setTransform(this.state.transform);
}
// Update view to fit entire workflow
updateView(bounds) {
const scale = window.devicePixelRatio || 1;
app.canvas.ds.scale = 1;
app.canvas.canvas.width = (bounds[2] - bounds[0]) * scale;
app.canvas.canvas.height = (bounds[3] - bounds[1]) * scale;
app.canvas.ds.offset = [-bounds[0], -bounds[1]];
app.canvas.canvas.getContext("2d").setTransform(scale, 0, 0, scale, 0, 0);
}
// Generate PNG blob with optional workflow data
async getBlob(includeWorkflow) {
return new Promise((resolve) => {
app.canvasEl.toBlob(async (blob) => {
if (includeWorkflow) {
// Embed workflow data in PNG metadata
const workflow = JSON.stringify(app.graph.serialize());
const buffer = await blob.arrayBuffer();
const typedArr = new Uint8Array(buffer);
const view = new DataView(buffer);
const data = new TextEncoder().encode(`tEXtworkflow\0${workflow}`);
const chunk = this.joinArrayBuffer(
this.n2b(data.byteLength - 4),
data,
this.n2b(this.crc32(data))
);
const sz = view.getUint32(8) + 20;
const result = this.joinArrayBuffer(
typedArr.subarray(0, sz),
chunk,
typedArr.subarray(sz)
);
blob = new Blob([result], { type: "image/png" });
}
resolve(blob);
});
});
}
// Helper: Convert number to 4-byte array
n2b(n) {
return new Uint8Array([
(n >> 24) & 0xff,
(n >> 16) & 0xff,
(n >> 8) & 0xff,
n & 0xff,
]);
}
// Helper: Join multiple array buffers
joinArrayBuffer(...bufs) {
const result = new Uint8Array(
bufs.reduce((totalSize, buf) => totalSize + buf.byteLength, 0)
);
bufs.reduce((offset, buf) => {
result.set(buf, offset);
return offset + buf.byteLength;
}, 0);
return result;
}
// Helper: Calculate CRC32 checksum
crc32(data) {
const crcTable =
WorkflowScreenshotExporter.crcTable ||
(WorkflowScreenshotExporter.crcTable = (() => {
let c;
const crcTable = [];
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
crcTable[n] = c;
}
return crcTable;
})());
let crc = 0 ^ -1;
for (let i = 0; i < data.byteLength; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ data[i]) & 0xff];
}
return (crc ^ -1) >>> 0;
}
// Download the generated PNG
download(blob, filename = "workflow_screenshot.png") {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
Object.assign(a, {
href: url,
download: filename,
style: "display: none",
});
document.body.append(a);
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
// Main export function
async export(includeWorkflow = true, filename = null) {
try {
// Save current state
this.saveState();
// Update view to show entire workflow
const bounds = this.getBounds();
this.updateView(bounds);
// Render canvas
app.canvas.draw(true, true);
// Generate PNG blob
const blob = await this.getBlob(includeWorkflow);
// Restore state
this.restoreState();
app.canvas.draw(true, true);
// Generate filename with timestamp if not provided
if (!filename) {
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.slice(0, -5);
filename = `workflow_${timestamp}.png`;
}
// Download
this.download(blob, filename);
return true;
} catch (error) {
console.error("Export workflow screenshot failed:", error);
this.restoreState();
app.canvas.draw(true, true);
return false;
}
}
}
// Register extension
app.registerExtension({
name: "Prepack.ExportWorkflowPNG",
async setup() {
const exporter = new WorkflowScreenshotExporter();
// Add menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
options.push(null, {
content: "💀Export Workflow as PNG",
submenu: {
options: [
{
content: "With workflow data",
callback: () => {
exporter.export(true);
},
},
{
content: "Image Only",
callback: () => {
exporter.export(false);
},
},
],
},
});
return options;
};
},
});