Add Export Workflow as PNG node
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user