From d0c835700d2989286e914d3914686083f55a48ce Mon Sep 17 00:00:00 2001 From: InpaintRegionEditor Dev Date: Fri, 27 Feb 2026 20:47:25 +0800 Subject: [PATCH] fix: Properly export layer mask from Photopea - Use Photopea's native layer mask feature - Users can: Layer > Layer Mask > Reveal All - Draw mask with brush (black/white) - Apply feathering via Properties panel - Export mask correctly from Mask channel --- web/extension.js | 117 ++++++++++++----------------------------------- 1 file changed, 28 insertions(+), 89 deletions(-) diff --git a/web/extension.js b/web/extension.js index bbf3b17..b2ee0bc 100644 --- a/web/extension.js +++ b/web/extension.js @@ -1,7 +1,5 @@ /** * InpaintRegionEditor Extension - * - * 流程:图像 + 蒙版 <-> Photopea */ import { app } from "../../scripts/app.js"; @@ -21,13 +19,12 @@ const PhotopeaBridge = { var self = this; return new Promise(function(resolve, reject) { if (!self.photopeaWindow) { - reject(new Error("Photopea 未初始化")); + reject(new Error("Photopea not initialized")); return; } var responses = []; var timeoutId = null; var done = false; - var handler = function(event) { if (event.origin !== self.PHOTOPEA_ORIGIN) return; responses.push(event.data); @@ -38,15 +35,13 @@ const PhotopeaBridge = { resolve(responses); } }; - window.addEventListener("message", handler); self.photopeaWindow.postMessage(message, "*"); - timeoutId = setTimeout(function() { if (!done) { done = true; window.removeEventListener("message", handler); - reject(new Error("操作超时")); + reject(new Error("Timeout")); } }, 60000); }); @@ -61,9 +56,7 @@ const PhotopeaBridge = { await self.postMessage('app.open("' + reader.result + '", null, false);'); await self.postMessage('app.activeDocument.activeLayer.rasterize();'); resolve(); - } catch (e) { - reject(e); - } + } catch (e) { reject(e); } }; reader.onerror = reject; reader.readAsDataURL(imageBlob); @@ -77,18 +70,16 @@ const PhotopeaBridge = { return new Blob([result[i]], { type: "image/png" }); } } - throw new Error("导出图像失败"); + throw new Error("Export failed"); }, - async exportMaskOrSelection() { - // 方法1: 尝试从选区创建蒙版 - var script = '(function(){var d=app.activeDocument;if(d.selection.bounds){var t=d.artLayers.add();d.selection.invert();d.selection.fill({r:0,g:0,b:0});d.selection.invert();d.selection.fill({r:255,g:255,b:255});d.saveToOE("png");t.remove();return;}app.echoToOE("NO_SELECTION");})();'; - + async exportLayerMask() { + var script = '(function(){var d=app.activeDocument;var l=d.activeLayer;if(!l.mask||!l.mask.enabled){app.echoToOE("NO_MASK");return;}var mc=null;for(var i=0;i' + - ' ' + - '
' + - '
用选区工具(M)绘制重绘区域,然后保存
' + - '
' + - ' ' + - ' ' + - '
' + - '
' + - '
正在加载...
' + - ''; - + this.modal.innerHTML = "
图层>图层蒙版>显示全部,用画笔绘制蒙版
加载中...
"; this.injectStyles(); document.body.appendChild(this.modal); - var iframe = document.getElementById("pp-iframe"); iframe.onload = async function() { if (!self.isOpen) return; PhotopeaBridge.init(iframe); - self.setStatus("正在加载图像..."); + self.setStatus("加载图像..."); self.setupEvents(); await self.loadImage(); }; @@ -145,15 +122,11 @@ const PhotopeaModal = { if (document.getElementById("pp-styles")) return; var s = document.createElement("style"); s.id = "pp-styles"; - s.textContent = ".photopea-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:99999;display:flex;align-items:center;justify-content:center}.photopea-container{width:95%;height:95%;display:flex;flex-direction:column;background:#1e1e1e;border-radius:8px;overflow:hidden}#pp-iframe{flex:1;width:100%;border:none}.pp-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#2d2d2d;border-top:1px solid #444;gap:10px}.pp-left{display:flex;align-items:center}.pp-right{display:flex;gap:10px}.pp-hint{color:#999;font-size:13px}.pp-btn{padding:10px 18px;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500}.pp-btn:hover{transform:translateY(-1px)}.pp-save{background:#107c10;color:#fff}.pp-cancel{background:#555;color:#fff}.pp-status{padding:8px 16px;background:#1a1a1a;color:#888;font-size:12px}"; + s.textContent = ".photopea-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:99999;display:flex;align-items:center;justify-content:center}.pp-container{width:95%;height:95%;display:flex;flex-direction:column;background:#1e1e1e;border-radius:8px;overflow:hidden}#pp-iframe{flex:1;width:100%;border:none}.pp-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#2d2d2d;border-top:1px solid #444;gap:10px}.pp-left{display:flex;align-items:center}.pp-right{display:flex;gap:10px}.pp-hint{color:#999;font-size:13px}.pp-btn{padding:10px 18px;border:none;border-radius:6px;cursor:pointer;font-size:14px}.pp-btn:hover{transform:translateY(-1px)}.pp-save{background:#107c10;color:#fff}.pp-cancel{background:#555;color:#fff}.pp-status{padding:8px 16px;background:#1a1a1a;color:#888;font-size:12px}"; document.head.appendChild(s); }, - setStatus(t) { - if (!this.isOpen) return; - var el = document.getElementById("pp-status-text"); - if (el) el.textContent = t; - }, + setStatus(t) { if (this.isOpen) { var el = document.getElementById("pp-status"); if (el) el.textContent = t; } }, setupEvents() { var self = this; @@ -167,62 +140,38 @@ const PhotopeaModal = { try { var path = this.imagePath || ""; var filename = path, type = "input", subfolder = ""; - var m = path.match(/^(.+?)\s*\[(\w+)\]$/); if (m) { filename = m[1].trim(); type = m[2]; } var idx = filename.lastIndexOf("/"); if (idx !== -1) { subfolder = filename.substring(0, idx); filename = filename.substring(idx + 1); } - var url = "/view?filename=" + encodeURIComponent(filename) + "&type=" + type; if (subfolder) url += "&subfolder=" + encodeURIComponent(subfolder); - var resp = await fetch(url); if (!resp.ok) { this.setStatus("加载失败"); return; } - await PhotopeaBridge.openImage(await resp.blob()); - this.setStatus("图像已加载。用选区工具(M)绘制重绘区域,然后保存"); - this.setStatus("加载失败: " + e.message); - } + this.setStatus("图像已加载。用 图层>图层蒙版>显示全部,画笔绘制"); + } catch (e) { this.setStatus("加载失败: " + e.message); } }, async saveAll() { if (!this.isOpen) return; - try { - this.setStatus("正在导出图像..."); + this.setStatus("导出图像..."); var imageBlob = await PhotopeaBridge.exportImage(); - - this.setStatus("正在导出蒙版..."); - var maskBlob = null; - try { maskBlob = await PhotopeaBridge.exportMaskOrSelection(); } catch (e) {} - - this.setStatus("正在上传..."); - + this.setStatus("导出蒙版..."); + var maskBlob = await PhotopeaBridge.exportLayerMask(); + this.setStatus("上传..."); var imgResult = await this.upload(imageBlob, "edited.png"); - if (imgResult.name) { - var w = this.node.widgets && this.node.widgets.find(function(x) { return x.name === "image"; }); - if (w) w.value = imgResult.name; - } - + if (imgResult.name) { var w = this.node.widgets && this.node.widgets.find(function(x) { return x.name === "image"; }); if (w) w.value = imgResult.name; } if (maskBlob) { var maskResult = await this.upload(maskBlob, "mask.png"); - if (maskResult.name) { - var mw = this.node.widgets && this.node.widgets.find(function(x) { return x.name === "mask_path"; }); - if (mw) mw.value = maskResult.name; - } + if (maskResult.name) { var mw = this.node.widgets && this.node.widgets.find(function(x) { return x.name === "mask_path"; }); if (mw) mw.value = maskResult.name; } } - this.node.setDirtyCanvas(true); - this.setStatus("已保存"); - - // 自动关闭 + this.setStatus(maskBlob ? "已保存(含蒙版)" : "已保存(无蒙版)"); var self = this; - setTimeout(function() { self.close(); }, 300); - - } catch (e) { - this.setStatus("保存失败: " + e.message); - alert("保存失败: " + e.message); - } + setTimeout(function() { self.close(); }, 500); + } catch (e) { console.error(e); this.setStatus("失败: " + e.message); alert("保存失败: " + e.message); } }, async upload(blob, name) { @@ -243,29 +192,19 @@ const PhotopeaModal = { app.registerExtension({ name: "comfyui.inpaint_region_editor", - async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name !== "InpaintRegionEditor") return; - var getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function(canvas, options) { getExtraMenuOptions && getExtraMenuOptions.apply(this, arguments); var node = this; options.push(null); - options.push({ - content: "Open in Photopea", - callback: function() { - var w = node.widgets && node.widgets.find(function(x) { return x.name === "image"; }); - if (w && w.value) { - PhotopeaModal.show(node, w.value); - } else { - alert("请先上传图像"); - } - } - }); + options.push({ content: "Open in Photopea", callback: function() { + var w = node.widgets && node.widgets.find(function(x) { return x.name === "image"; }); + if (w && w.value) { PhotopeaModal.show(node, w.value); } else { alert("请先上传图像"); } + }}); return options; }; - var onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { var r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;