import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; import { ComfyWidgets } from "/scripts/widgets.js"; import { applyPrimereButtonStyle, showToast } from "./frontend_helper.js"; let hasShownAlertForUpdatingInt = false; let currentClass = false; let outputEventListenerInit = false; let ImagePath = null; const realPath = "/extensions/ComfyUI_Primere_Nodes"; const prwPath = "/extensions/ComfyUI_Primere_Nodes"; let pendingSaveNode = null; const NodenameByType = { 'Checkpoint': 'PrimereVisualCKPT', 'CSV Prompt': 'PrimereVisualStyle', 'Lora': 'PrimereVisualLORA', 'Lycoris': 'PrimereVisualLYCORIS', 'Hypernetwork': 'PrimereVisualHypernetwork', 'Embedding': 'PrimereVisualEmbedding' } const NodesubdirByType = { 'Checkpoint': 'checkpoints', 'CSV Prompt': 'styles', 'Lora': 'loras', 'Lycoris': 'lycoris', 'Hypernetwork': 'hypernetworks', 'Embedding': 'embeddings' } const OutputToNode = ['PrimereAnyOutput', 'PrimereTextOutput', 'PrimereAestheticCKPTScorer', 'PrimereFastSeed']; // ── Per-node state ──────────────────────────────────────────────────────────── function initNodeState() { return { workflowData: {}, origSizeString: '', saveIsValid: false, targetFileName: null, targetSelValues: ['select target...'], selectedTarget: null, previewExist: false, previewURL: null, }; } function ensureNodeState(node) { if (!node.psState) node.psState = initNodeState(); return node.psState; } function getWidgetValues(node) { const state = ensureNodeState(node); const vals = { previewTarget: 'Checkpoint', saveMode: true, imgType: 'jpeg', maxSide: -1, targetQuality: 95, prwSaveMode: 'Overwrite', }; for (const w of node.widgets) { switch (w.name) { case 'preview_target': vals.previewTarget = w.value; break; case 'image_save_as': vals.saveMode = w.value; break; case 'image_type': vals.imgType = w.value; break; case 'image_resize': vals.maxSide = w.value; break; case 'image_quality': vals.targetQuality = w.value; break; case 'preview_save_mode': vals.prwSaveMode = w.value; break; case 'target_selection': state.selectedTarget = w.value; break; } } return vals; } // ── Node registration ───────────────────────────────────────────────────────── app.registerExtension({ name: "Primere.PrimereOutputs", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (OutputToNode.includes(nodeData.name) === true) { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { onNodeCreated ? onNodeCreated.apply(this, []) : undefined; this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget; this.showValueWidget.inputEl.readOnly = true; this.showValueWidget.serializeValue = async (node, index) => { if (typeof node.widgets_values != "undefined") { node.widgets_values[index] = ""; return ""; } }; }; const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]); this.showValueWidget.value = message.text[0]; app.canvas.setDirty(true); }; } if (nodeData.name === "PrimerePreviewImage") { if (nodeData.input.hasOwnProperty('hidden') === true) { ImagePath = nodeData.input.hidden['image_path'][0]; } const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { onNodeCreated ? onNodeCreated.apply(this, []) : undefined; new MiniPreviewControl(this); PrimerePreviewSaverWidget.apply(this, [this, 'PrimerePreviewSaver']); }; } }, }); class MiniPreviewControl { constructor(node) { node.onMouseDown = function(event, pos, graphcanvas) { if (event.type == 'pointerdown') { if (pos[1] >= 212 && pos[1] <= 265) { showPreviewIfExist(node, event.clientX, event.clientY); } else { checkPreviewExample(); } } }; node.onMouseMove = function(event, pos, graphcanvas) { if (event.type == 'pointermove') { const previewBox = document.querySelector('div#primere_previewbox'); const btn = node.widgets.find(w => w.type === 'button'); if (!previewBox || previewBox.style.display === 'none') { if (pos[1] >= btn.last_y && pos[1] <= btn.last_y + 30 && pos[0] >= 14 && pos[0] <= 160) { showPreviewIfExist(node, event.clientX, event.clientY); } else { checkPreviewExample(); } } else { if (pos[1] <= btn.last_y || pos[1] >= btn.last_y + 30 || pos[0] <= 14 || pos[0] >= 160) { checkPreviewExample(); } } } }; } } function showPreviewIfExist(node, coordX, coordY) { const state = ensureNodeState(node); const wv = getWidgetValues(node); if (state.saveIsValid && wv.saveMode && state.previewExist) { const previewBox = document.querySelector('div#primere_previewbox'); const previewImage = document.querySelector('div#primere_previewbox img.privewbox_image'); if (!previewBox || !previewImage) return; previewBox.style.top = coordY + 'px'; previewBox.style.left = coordX + 'px'; previewImage.src = state.previewURL; previewBox.style.display = 'block'; } } function checkPreviewExample() { const previewBox = document.querySelector('div#primere_previewbox'); if (previewBox && previewBox.style.display !== 'none') { previewBox.style.display = 'none'; } } async function PrimerePreviewSaverWidget(node, inputName) { if (inputName == 'PrimerePreviewSaver') { node.name = inputName; ensureNodeState(node); const widget = { type: "preview_saver_widget", name: `w${inputName}`, callback: () => {}, }; node.onWidgetChanged = function (name, value, old_value) { ButtonLabelCreator(node); return false; }; node.addWidget("combo", "target_selection", 'select target...', () => {}, { values: ["select target..."], }); const saveBtn = node.addWidget("button", '⛔ Image not available for save. Please load one.', null, () => { const state = ensureNodeState(node); if (state.saveIsValid === true) { if (typeof node['imgs'] != "undefined") { pendingSaveNode = node; node.PreviewSaver = new PreviewSaver(node); } else { state.saveIsValid = false; const errTitle = '⛔ Image not available for save. Please load one.'; applyWidgetValues(node, errTitle, state.targetSelValues); showToast("error", 'Current settings is invalid to save image. ERROR: ' + errTitle); } } else { const btn = node.widgets.find(w => w.type === 'button'); showToast("error", 'Current settings is invalid to save image. ERROR: ' + (btn ? btn.name : 'Unknown')); } }); applyPrimereButtonStyle(saveBtn); return { widget: widget }; } } app.registerExtension({ name: "Primere.PrimerePreviewImage", async init(app) { function PreviewHandler(app) { outputEventListenerInit = true; let head = document.getElementsByTagName('HEAD')[0]; let js1 = document.createElement("script"); js1.src = realPath + "/vendor/LoadImage/load-image.js"; head.appendChild(js1); let js2 = document.createElement("script"); js2.src = realPath + "/vendor/LoadImage/load-image-scale.js"; head.appendChild(js2); let link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = realPath + '/css/visual.css'; head.appendChild(link); let previewbox = document.getElementById("primere_previewbox"); if (!previewbox) { previewbox = document.createElement("div"); previewbox.setAttribute('style', 'display:none;'); previewbox.setAttribute("id", "primere_previewbox"); previewbox.innerHTML = '
';
document.body.appendChild(previewbox);
}
document.addEventListener('click', function(e) {
if (e.target.closest('div#primere_previewbox div.preview_closebutton')) {
checkPreviewExample();
return;
}
if (e.target.closest('body')) {
checkPreviewExample();
}
});
}
if (outputEventListenerInit == false) {
PreviewHandler(app);
}
},
});
// ── Event: node executed ──────────────────────────────────────────────────────
api.addEventListener("getVisualTargets", VisualDataReceiver);
function VisualDataReceiver(event) {
const data = event.detail;
let targetNode = null;
const nodeId = data['node_id'];
if (nodeId && app.graph) {
targetNode = app.graph.getNodeById(parseInt(nodeId));
}
if (!targetNode && app.graph && app.graph._nodes) {
targetNode = app.graph._nodes.find(n => n.type === 'PrimerePreviewImage');
}
if (!targetNode) return;
const state = ensureNodeState(targetNode);
state.workflowData = data;
if (data['ImagePath']) ImagePath = data['ImagePath'];
const imgs = data['SaveImages'];
if (!imgs || imgs.length === 0) return;
const newLoadedURL = window.location.origin + '/view?filename=' + imgs[0]['filename'] + '&type=' + imgs[0]['type'] + '&subfolder=' + imgs[0]['subfolder'];
UrlExists(newLoadedURL, function (status) {
if (status === 200) {
ButtonLabelCreator(targetNode, newLoadedURL);
} else {
state.origSizeString = '[Unknown dimensions]';
console.log('new image loaded - ERROR status ' + status + ': - ' + newLoadedURL);
}
});
}
// ── Utility ───────────────────────────────────────────────────────────────────
function UrlExists(url, cb) {
fetch(url, {
method: 'HEAD',
}).then((response) => {
if (typeof cb === 'function') {
cb.apply(this, [response.status]);
}
}).catch(() => {
if (typeof cb === 'function') {
cb.apply(this, [0]);
}
});
}
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[arr.length - 1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
}
function downloadImage(url, extension, PreviewTarget) {
fetch(url, {
mode : 'no-cors',
})
.then(response => response.blob())
.then(blob => {
let blobUrl = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.download = PreviewTarget + '.' + extension;
a.href = blobUrl;
document.body.appendChild(a);
a.click();
a.remove();
});
}
// ── PreviewSaver ──────────────────────────────────────────────────────────────
class PreviewSaver {
constructor(node) {
const state = ensureNodeState(node);
const wv = getWidgetValues(node);
let maxWidth = null;
let maxHeight = null;
if (wv.saveMode === true) {
maxWidth = 400;
maxHeight = 220;
}
let imgMime = "image/jpeg";
let extension = 'jpg';
if (wv.saveMode === false) {
if (wv.imgType === 'jpeg') {
imgMime = "image/jpeg";
extension = 'jpg';
} else if (wv.imgType === 'png') {
imgMime = "image/png";
extension = 'png';
} else if (wv.imgType === 'webp') {
imgMime = "image/webp";
extension = 'webp';
}
}
if (wv.maxSide >= 64 && wv.saveMode === false) {
maxWidth = wv.maxSide;
maxHeight = wv.maxSide;
}
const sizeStringFN = wv.maxSide >= 64 ? wv.maxSide + 'px_' : '';
const imageSource = node['imgs'][0]['src'];
const imageName = node['images'][0]['filename'];
let saveImageName = 'PreviewImage_' + sizeStringFN + '_QTY' + wv.targetQuality + '_' + (Math.random() + 1).toString(36).substring(5);
if (state.targetFileName !== null) {
saveImageName = state.targetFileName;
}
const renderToCanvas = (blob) => new Promise((resolve, reject) => {
const blobUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(blobUrl);
const canvas = document.createElement('canvas');
let w = img.naturalWidth;
let h = img.naturalHeight;
if (maxWidth && w > maxWidth) { h = Math.round(h * maxWidth / w); w = maxWidth; }
if (maxHeight && h > maxHeight) { w = Math.round(w * maxHeight / h); h = maxHeight; }
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, w, h);
resolve(canvas);
};
img.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error('Failed to decode image: ' + imageName));
};
img.src = blobUrl;
});
fetch(imageSource)
.then((res) => res.blob())
.then((blob) => renderToCanvas(blob))
.then((canvas) => {
const resampledImage = canvas.toDataURL(imgMime, wv.targetQuality / 100);
if (wv.saveMode === false) {
downloadImage(resampledImage, extension, saveImageName);
} else {
sendPOSTmessage(JSON.stringify({
"PreviewTarget": wv.previewTarget,
"PreviewTargetOriginal": state.selectedTarget,
"extension": extension,
"ImageName": imageName,
"ImagePath": ImagePath,
"SaveImageName": saveImageName,
"maxWidth": canvas.width,
"maxHeight": canvas.height,
"TargetQuality": wv.targetQuality,
"PrwSaveMode": wv.prwSaveMode,
}));
}
})
.catch((err) => {
showToast("error", 'Failed to load source image: ' + err.message);
});
}
}
// ── Button label / target list ────────────────────────────────────────────────
function TargetListCreator(node) {
const state = ensureNodeState(node);
const wv = getWidgetValues(node);
const workflowData = state.workflowData;
const origKey = NodenameByType[wv.previewTarget] + '_ORIGINAL';
if (workflowData[origKey] !== undefined) {
state.targetSelValues = workflowData[origKey];
if (state.targetSelValues.length === 0) {
state.saveIsValid = false;
state.targetSelValues = ['ERROR: Cannot list target for ' + wv.previewTarget];
}
} else {
state.saveIsValid = false;
state.targetSelValues = ['ERROR: Cannot list target for ' + wv.previewTarget];
}
return state.targetSelValues;
}
function ButtonLabelCreator(node, url = false) {
const state = ensureNodeState(node);
state.previewExist = false;
state.previewURL = null;
const wv = getWidgetValues(node);
state.targetSelValues = TargetListCreator(node);
if (typeof state.targetSelValues === "object") {
let targetIndexChanged = 0;
if (state.targetSelValues.includes(state.selectedTarget)) {
targetIndexChanged = state.targetSelValues.indexOf(state.selectedTarget);
}
state.selectedTarget = state.targetSelValues[targetIndexChanged];
}
const imgTypeString = wv.imgType.toUpperCase() + ' format';
const imgSizeString = wv.maxSide < 64 ? "at original size" : "resized to " + wv.maxSide + 'px';
const targetQuality = wv.imgType === 'png' ? 100 : wv.targetQuality;
state.targetFileName = null;
state.saveIsValid = false;
const workflowData = state.workflowData;
if (Object.keys(workflowData).length < 1) {
if (wv.saveMode === true) {
applyWidgetValues(node, state.selectedTarget, state.targetSelValues);
} else {
state.saveIsValid = true;
if (url !== false) {
getMeta(url).then(img => {
if (img) state.origSizeString = '[' + img.naturalHeight + ' X ' + img.naturalWidth + ']';
const title = '💾 Save image as ' + imgTypeString + ' | ' + state.origSizeString + ' ' + imgSizeString + ' | QTY: ' + targetQuality + '%';
applyWidgetValues(node, title, state.targetSelValues);
});
} else {
applyWidgetValues(node, '⛔ Image not available for save. Please load one.', state.targetSelValues);
}
}
} else {
if (wv.saveMode === true) {
if (url !== false) {
getMeta(url).then(img => {
if (img) state.origSizeString = '[' + img.naturalHeight + ' X ' + img.naturalWidth + ']';
});
}
const nodeName = NodenameByType[wv.previewTarget];
if (workflowData[nodeName] !== undefined && state.selectedTarget !== undefined) {
if (workflowData[nodeName].length < 1) {
const title = '❌ No resource selected for preview target: [' + wv.previewTarget + ']';
applyWidgetValues(node, title, state.targetSelValues);
} else {
state.saveIsValid = true;
const origKey = nodeName + '_ORIGINAL';
const targetIndex = workflowData[origKey].indexOf(state.selectedTarget);
if (targetIndex > -1) {
state.targetFileName = workflowData[nodeName][targetIndex];
}
let prwpathNew = state.selectedTarget.replaceAll('\\', '/');
const dotLastIndex = prwpathNew.lastIndexOf('.');
const finalName = (dotLastIndex > 1 ? prwpathNew.substring(0, dotLastIndex) : prwpathNew).replaceAll(' ', '_');
const imgsrc = prwPath + '/images/' + NodesubdirByType[wv.previewTarget] + '/' + finalName + '.jpg';
fetch(imgsrc, { method: 'HEAD' })
.then(response => {
state.previewExist = response.ok;
state.previewURL = response.ok ? imgsrc : null;
const prwMode = wv.prwSaveMode.split(' ').map(n => n[0]).join('').toUpperCase();
const imgExistLink = state.previewExist ? ' [' + prwMode + ']' : ' [C]';
const title = '🏙️ Save preview as: [' + state.targetFileName + '.jpg] to [' + wv.previewTarget + '] folder.' + imgExistLink;
applyWidgetValues(node, title, state.targetSelValues);
})
.catch(() => {
state.previewExist = false;
state.previewURL = null;
const title = '🏙️ Save preview as: [' + state.targetFileName + '.jpg] to [' + wv.previewTarget + '] folder. [C]';
applyWidgetValues(node, title, state.targetSelValues);
});
}
} else {
const title = '❌ Required node: [' + NodenameByType[wv.previewTarget] + '] not available in workflow for target: [' + wv.previewTarget + ']';
applyWidgetValues(node, title, state.targetSelValues);
}
} else {
state.saveIsValid = true;
if (url !== false) {
getMeta(url).then(img => {
if (img) state.origSizeString = '[' + img.naturalHeight + ' X ' + img.naturalWidth + ']';
const title = '💾 Save image as ' + imgTypeString + ' | ' + state.origSizeString + ' ' + imgSizeString + ' | QTY: ' + targetQuality + '%';
applyWidgetValues(node, title, state.targetSelValues);
});
} else {
const title = '💾 Save image as ' + imgTypeString + ' | ' + state.origSizeString + ' ' + imgSizeString + ' | QTY: ' + targetQuality + '%';
applyWidgetValues(node, title, state.targetSelValues);
}
}
}
}
function applyWidgetValues(node, buttontitle, targetSelValues) {
const state = ensureNodeState(node);
for (const w of node.widgets) {
if (w.type == 'button') {
w.name = buttontitle;
}
if (w.name == 'target_selection') {
w.options.values = targetSelValues;
if (targetSelValues.length > 0) {
let targetIndexChanged = 0;
if (targetSelValues.includes(state.selectedTarget)) {
targetIndexChanged = targetSelValues.indexOf(state.selectedTarget);
}
w.value = targetSelValues[targetIndexChanged];
state.selectedTarget = w.value;
}
}
}
app.canvas.setDirty(true);
}
// ── POST save ─────────────────────────────────────────────────────────────────
function sendPOSTmessage(message) {
const body = new FormData();
body.append('previewdata', message);
api.fetchApi("/primere_preview_post", {method: "POST", body,});
}
api.addEventListener("PreviewSaveResponse", PreviewSaveResponse);
function PreviewSaveResponse(event) {
showToast("success", event.detail);
if (pendingSaveNode) {
ButtonLabelCreator(pendingSaveNode);
pendingSaveNode = null;
}
}
// ── getMeta ───────────────────────────────────────────────────────────────────
const getMeta = (url) => new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = url;
}).catch(function() {
return false;
});