import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js" function showImageOnNode(node, name, imageType) { const img = new Image(); img.onload = () => { node.imgs = [img]; app.graph.setDirtyCanvas(true); }; let folder_separator = name.lastIndexOf("/"); let subfolder = ""; if (imageType === "temp") { subfolder = ""; } else if (folder_separator > -1) { subfolder = name.substring(0, folder_separator); name = name.substring(folder_separator + 1); } img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=${imageType}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`); node.setSizeForImage?.(); } // Global variables var uploadByPathNodes = []; // Used by widget: IMAGEUPLOAD_BYPATH // Handle Python -> JS messages function updatePreviewSignalHandle(evt) { const name = evt.detail.preview_filename; const imageType = evt.detail.preview_type; const ogPath = evt.detail.path; for (let i = uploadByPathNodes.length - 1; i >=0; i--) { const node = uploadByPathNodes[i]; let stopTrackingNode = false; if (!node || !(node.id) || !(app.graph._nodes_by_id[node.id])) { stopTrackingNode = true; } else { try { const doUpdate = node.isAutoPreviewUpdate() && !!(node.getPath()) && (ogPath === node.getPath()); if (doUpdate) { console.log("Update preview for node " + node.id); showImageOnNode(node, name, imageType); } } catch (error) { console.log(error); stopTrackingNode = true; } } if (stopTrackingNode) uploadByPathNodes.splice(i, 1); } } api.addEventListener("kap-update-preview", updatePreviewSignalHandle); // Load image by path app.registerExtension({ name: "kap.loadimage.dedup", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData?.input?.required?.image?.[1]?.kap_load_image_dedup === true) { nodeData.input.required.upload = ["IMAGEUPLOAD_DEDUP"]; } if (nodeType.comfyClass === "KLoadImageByPathAdvanced" || nodeType.comfyClass === "KLoadImageByPath") { //if (nodeData?.input?.required?.image?.[1]?.kap_load_image_by_path === true) { nodeData.input.required.upload = ["IMAGEUPLOAD_BYPATH"]; } }, getCustomWidgets(app) { return { // Ref: ComfyWidgets.IMAGEUPLOAD (https://github.com/comfyanonymous/ComfyUI/blob/7f4725f6b3f72dd8bdb60dae5dd2c3e943263bcf/web/scripts/widgets.js#L359) IMAGEUPLOAD_DEDUP(node, inputName, inputData, app) { const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image")); let uploadWidget; function showImage(name) { showImageOnNode(node, name, "input"); } var default_value = imageWidget.value; Object.defineProperty(imageWidget, "value", { set : function(value) { this._real_value = value; }, get : function() { let value = ""; if (this._real_value) { value = this._real_value; } else { return default_value; } if (value.filename) { let real_value = value; value = ""; if (real_value.subfolder) { value = real_value.subfolder + "/"; } value += real_value.filename; if(real_value.type && real_value.type !== "input") value += ` [${real_value.type}]`; } return value; } }); // Add our own callback to the combo widget to render an image when it changes const cb = node.callback; imageWidget.callback = function () { showImage(imageWidget.value); if (cb) { return cb.apply(this, arguments); } }; // On load if we have a value then render the image // The value isnt set immediately so we need to wait a moment // No change callbacks seem to be fired on initial setting of the value requestAnimationFrame(() => { if (imageWidget.value) { showImage(imageWidget.value); } }); async function uploadFile(file, updateNode, pasted = false) { try { // Wrap file in formdata so it includes filename const body = new FormData(); body.append("image", file); const overwriteWidget = node.widgets.find(w => w.name === "overwrite_option"); body.append("overwrite", overwriteWidget.value); if (pasted) body.append("subfolder", "pasted"); const resp = await api.fetchApi("/kap/upload/image-dedup", { method: "POST", body, }); if (resp.status === 200) { const data = await resp.json(); // Add the file to the dropdown list and update the widget value let path = data.name; if (data.subfolder) path = data.subfolder + "/" + path; if (!imageWidget.options.values.includes(path)) { imageWidget.options.values.push(path); } if (updateNode) { showImage(path); imageWidget.value = path; } } else { alert(resp.status + " - " + resp.statusText); } } catch (error) { alert(error); } } const fileInput = document.createElement("input"); Object.assign(fileInput, { type: "file", accept: "image/jpeg,image/png,image/webp", style: "display: none", onchange: async () => { if (fileInput.files.length) { await uploadFile(fileInput.files[0], true); } }, }); document.body.append(fileInput); // Create the button widget for selecting the files uploadWidget = node.addWidget("button", inputName, "image", () => { fileInput.click(); }); uploadWidget.label = "choose file to upload"; uploadWidget.serialize = false; // Add handler to check if an image is being dragged over our node node.onDragOver = function (e) { if (e.dataTransfer && e.dataTransfer.items) { const image = [...e.dataTransfer.items].find((f) => f.kind === "file"); return !!image; } return false; }; // On drop upload files node.onDragDrop = function (e) { console.log("onDragDrop called"); let handled = false; for (const file of e.dataTransfer.files) { if (file.type.startsWith("image/")) { uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one handled = true; } } return handled; }; node.pasteFile = function(file) { if (file.type.startsWith("image/")) { const is_pasted = (file.name === "image.png") && (file.lastModified - Date.now() < 2000); uploadFile(file, true, is_pasted); return true; } return false; } return { widget: uploadWidget }; }, IMAGEUPLOAD_BYPATH(node, inputName, inputData, app) { // Widget returned //let refreshPreviewWidget; const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image")); const isAdvanced = node.comfyClass === "KLoadImageByPathAdvanced"; Object.assign(node, { getPath: function() { updateImageWidgetValue(); let path = imageWidget.value; return path; }, isAutoPreviewUpdate: function() { const w = node.widgets.find(w => w.name === "auto_preview_update"); return w?.value ?? true; }, }); //var default_value = imageWidget.value; //Object.defineProperty(imageWidget, "value", { // set : function(value) { // this._real_value = value; // }, // get : function() { // let value = ""; // if (this._real_value) { // value = this._real_value; // } else { // return default_value; // } // if (value.filename) { // let real_value = value; // value = ""; // if (real_value.subfolder) { // value = real_value.subfolder + "/"; // } // value += real_value.filename; // if(real_value.type && real_value.type !== "input") // value += ` [${real_value.type}]`; // } // return value; // } //}); // Add our own callback to the string widget to render an image when it changes const cb = node.callback; imageWidget.callback = function () { updateWatchlist({ updateNode: true }); if (cb) { return cb.apply(this, arguments); } }; // On load if we have a value then render the image // The value isnt set immediately so we need to wait a moment // No change callbacks seem to be fired on initial setting of the value requestAnimationFrame(() => { if (imageWidget.value) { //showImageOnNode(node, imageWidget.value); updateWatchlist({ updateNode: true }); } }); async function updatePreview() { const body = new FormData(); body.append("image_path", node.getPath()); const resp = await api.fetchApi("/kap/upload/update-preview", { method: "POST", body, }); if (resp.status === 200) { const data = await resp.json(); const previewName = data.preview_filename; const previewImageType = data.preview_image_type; showImageOnNode(node, previewName, previewImageType); } else { alert(`Generating preview for '${node.getPath()}' was not successful (node id: ${node.id})`); } } async function updateWatchlist({ updateNode = true }) { try { // Watchlist consists of images from all loadbypath widgets, and the first item is the path of the image in the current node const allImagePaths = []; const selfInWatchlist = node.isAutoPreviewUpdate() && !!(node.getPath()); if (selfInWatchlist) allImagePaths.push(node.getPath()); for (let i = uploadByPathNodes.length - 1; i >= 0; i--) { const nd = uploadByPathNodes[i]; if (!nd || !(nd.id) || !(app.graph._nodes_by_id[nd.id])) { uploadByPathNodes.splice(i, 1); } else { if (nd.id === node.id) continue; const path = nd.getPath?.(); if (path && nd.isAutoPreviewUpdate()) allImagePaths.push(path); } } const body = new FormData(); body.append("all_image_paths", JSON.stringify(allImagePaths)); const resp = await api.fetchApi("/kap/upload/update-watchlist", { method: "POST", body, }); if (resp.status === 200) { const data = await resp.json(); // We are only updating for the image in the current node, which is the first item if (selfInWatchlist && data.success[0]) { const previewName = data.preview_names[0]; if (updateNode) { showImageOnNode(node, previewName, data.preview_images_type); } } else if (!selfInWatchlist) { // No preview update } else { // TODO Display error more elegantly alert(`Generating preview for '${node.getPath()}' was not successful (node id: ${node.id}).` + `\n Watchlist for files for auto preview update refreshed (#=${data.watchlist_size}).`); } } else { alert(resp.status + " - " + resp.statusText); } } catch (error) { alert(error); } } // Refresh preview button widget const updatePreviewWidget = node.addWidget("button", null, null, () => { updatePreview(); }); updatePreviewWidget.label = "update preview"; updatePreviewWidget.serialize = false; // Refresh watchers button widget const refreshWatchWidget = node.addWidget("button", null, null, () => { updateWatchlist({ updateNode: true }); }); refreshWatchWidget.label = "refresh auto preview"; refreshWatchWidget.serialize = false; // Auto preview update let autoPreviewUpdateWidget = null; if (isAdvanced) { autoPreviewUpdateWidget = node.addWidget("toggle", "auto_preview_update", true, () => { updateWatchlist({ updateNode: true }); } ); autoPreviewUpdateWidget.label = "update preview automatically"; autoPreviewUpdateWidget.serialize = true; } // Prefix substitution function updateImageWidgetValue() { let path = imageWidget.value ?? ""; if ((path.startsWith('"') && path.endsWith('"')) || (path.startsWith("'") && path.endsWith("'"))) { path = path.substring(1, path.length - 1); } if (path && isPrefixSubEnabled()) { const oldPrefix = prefixSubSrcWidget.value; const newPrefix = prefixSubDestWidget.value; if (path.startsWith(oldPrefix)) { path = newPrefix + path.substring(oldPrefix.length); } else { console.log("The path '" + path + "' does not begin with prefix: '" + oldPrefix + "'; nothing to replace."); } console.log(`Path (node id: ${node.id}): '${path}'`); } imageWidget.value = path; } function isPrefixSubEnabled() { return enablePrefixSubWidget?.value ?? false; } function onPrefixChange() { updateImageWidgetValue(); updateWatchlist({ updateNode: true }); } let enablePrefixSubWidget = null; let prefixSubSrcWidget = null; let prefixSubDestWidget = null; if (isAdvanced) { enablePrefixSubWidget = node.addWidget("toggle", "enable_prefix_substitution", false, () => { onPrefixChange(); } ); enablePrefixSubWidget.label = "enable prefix substitution"; enablePrefixSubWidget.serialize = true; prefixSubSrcWidget = node.addWidget("text", "prefix_substitution_source", "/mnt/point/", () => { onPrefixChange(); } ); prefixSubSrcWidget.label = "prefix to substitute"; prefixSubSrcWidget.serialize = true; prefixSubDestWidget = node.addWidget("text", "prefix_substitution_destination", "~/", () => { onPrefixChange(); } ); prefixSubDestWidget.label = "new prefix"; prefixSubDestWidget.serialize = true; } // uploadByPathNodes.push(node); node.onRemoved = function() { for (let i = uploadByPathNodes.length - 1; i >= 0; i--) { if (uploadByPathNodes[i].id === node.id) uploadByPathNodes.splice(i, 1); } } //return { widget: updatePreviewWidget }; return {}; }, }; }, });