From 6fb76c5c776eac5a4e2a6c71b35b72745e2248a8 Mon Sep 17 00:00:00 2001 From: samuelxuliu Date: Wed, 22 Oct 2025 16:52:30 +0800 Subject: [PATCH] Add Export Workflow as PNG node --- README.md | 32 +------ README_ZHS.md | 32 +------ README_ZHT.md | 32 +------ js/workflow.js | 236 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 87 deletions(-) create mode 100644 js/workflow.js diff --git a/README.md b/README.md index 4bab769..0b8eb1c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_ZHS.md b/README_ZHS.md index 78272fd..977f5e1 100644 --- a/README_ZHS.md +++ b/README_ZHS.md @@ -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 diff --git a/README_ZHT.md b/README_ZHT.md index e7ccfc6..5630f33 100644 --- a/README_ZHT.md +++ b/README_ZHT.md @@ -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 diff --git a/js/workflow.js b/js/workflow.js new file mode 100644 index 0000000..b285085 --- /dev/null +++ b/js/workflow.js @@ -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; + }; + }, +});