Fix: Image drag and drop
This commit is contained in:
+181
@@ -1,7 +1,188 @@
|
||||
import logging
|
||||
import json
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from aiohttp import web
|
||||
from PIL import Image
|
||||
import server
|
||||
|
||||
logger = logging.getLogger("fl_fill_nodes")
|
||||
|
||||
|
||||
def _fl_is_api_json(data):
|
||||
return isinstance(data, dict) and data and all(
|
||||
isinstance(v, dict) and "class_type" in v for v in data.values()
|
||||
)
|
||||
|
||||
|
||||
def _fl_decode_jsonish(value):
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8", errors="ignore")
|
||||
if isinstance(value, str):
|
||||
return json.loads(value)
|
||||
return None
|
||||
|
||||
|
||||
def _fl_metadata_from_json(data):
|
||||
parsed = _fl_decode_jsonish(data)
|
||||
if not isinstance(parsed, dict):
|
||||
return {}
|
||||
if any(k in parsed for k in ("workflow", "Workflow", "prompt", "Prompt", "parameters", "templates")):
|
||||
return {
|
||||
"workflow": parsed.get("workflow", parsed.get("Workflow")),
|
||||
"prompt": parsed.get("prompt", parsed.get("Prompt")),
|
||||
"parameters": parsed.get("parameters"),
|
||||
"templates": parsed.get("templates"),
|
||||
}
|
||||
if parsed.get("templates"):
|
||||
return {"templates": parsed["templates"]}
|
||||
if _fl_is_api_json(parsed):
|
||||
return {"prompt": parsed}
|
||||
return {"workflow": parsed}
|
||||
|
||||
|
||||
def _fl_metadata_from_image(data):
|
||||
try:
|
||||
image = Image.open(BytesIO(data))
|
||||
info = dict(image.info or {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
for source_key, target_key in (
|
||||
("workflow", "workflow"),
|
||||
("Workflow", "workflow"),
|
||||
("prompt", "prompt"),
|
||||
("Prompt", "prompt"),
|
||||
("parameters", "parameters"),
|
||||
("parametersText", "parameters"),
|
||||
("templates", "templates"),
|
||||
):
|
||||
if source_key not in info:
|
||||
continue
|
||||
value = info[source_key]
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8", errors="ignore")
|
||||
result[target_key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _fl_read_u32(data, pos):
|
||||
if pos + 4 > len(data):
|
||||
return 0
|
||||
return struct.unpack(">I", data[pos:pos + 4])[0]
|
||||
|
||||
|
||||
def _fl_find_box(data, start, end, box_type):
|
||||
pos = start
|
||||
marker = box_type.encode("ascii")
|
||||
while pos < end - 8:
|
||||
size = _fl_read_u32(data, pos)
|
||||
if size < 8:
|
||||
pos += 1
|
||||
continue
|
||||
if data[pos + 4:pos + 8] == marker:
|
||||
return pos + 8, min(pos + size, len(data))
|
||||
if pos + size > end:
|
||||
return None
|
||||
pos += size
|
||||
return None
|
||||
|
||||
|
||||
def _fl_extract_json_from_range(data, start, end):
|
||||
json_start = data.find(b"{", start, end)
|
||||
if json_start < 0:
|
||||
return None
|
||||
try:
|
||||
return json.loads(data[json_start:end].decode("utf-8", errors="ignore"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fl_parse_keys_box(data, start, end):
|
||||
keys = {}
|
||||
pos = start + 4
|
||||
if pos + 4 > end:
|
||||
return keys
|
||||
count = _fl_read_u32(data, pos)
|
||||
pos += 4
|
||||
for index in range(1, count + 1):
|
||||
if pos + 8 > end:
|
||||
break
|
||||
key_size = _fl_read_u32(data, pos)
|
||||
key_name_start = pos + 8
|
||||
key_name_end = pos + key_size
|
||||
if key_size < 8 or key_name_end > end:
|
||||
break
|
||||
keys[index] = data[key_name_start:key_name_end].decode("utf-8", errors="ignore")
|
||||
pos = key_name_end
|
||||
return keys
|
||||
|
||||
|
||||
def _fl_metadata_from_isobmff(data):
|
||||
data = data[:64 * 1024 * 1024]
|
||||
udta = _fl_find_box(data, 0, len(data), "udta")
|
||||
if not udta:
|
||||
moov = _fl_find_box(data, 0, len(data), "moov")
|
||||
if moov:
|
||||
udta = _fl_find_box(data, moov[0], moov[1], "udta")
|
||||
if not udta:
|
||||
return {}
|
||||
|
||||
meta = _fl_find_box(data, udta[0], udta[1], "meta")
|
||||
if not meta:
|
||||
return {}
|
||||
meta_start = meta[0] + 4
|
||||
keys_box = _fl_find_box(data, meta_start, meta[1], "keys")
|
||||
ilst_box = _fl_find_box(data, meta_start, meta[1], "ilst")
|
||||
if not keys_box or not ilst_box:
|
||||
return {}
|
||||
|
||||
keys = _fl_parse_keys_box(data, keys_box[0], keys_box[1])
|
||||
result = {}
|
||||
pos = ilst_box[0]
|
||||
while pos < ilst_box[1] - 8:
|
||||
item_size = _fl_read_u32(data, pos)
|
||||
if item_size <= 8 or pos + item_size > ilst_box[1]:
|
||||
break
|
||||
index = _fl_read_u32(data, pos + 4)
|
||||
key = (keys.get(index) or "").lower()
|
||||
data_box = _fl_find_box(data, pos + 8, pos + item_size, "data")
|
||||
if data_box and key in ("workflow", "prompt"):
|
||||
value = _fl_extract_json_from_range(data, data_box[0] + 8, data_box[1])
|
||||
if value is not None:
|
||||
result[key] = value
|
||||
pos += item_size
|
||||
return result
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/load_image_drop_fix/workflow_metadata")
|
||||
async def _fl_load_image_drop_fix_workflow_metadata(request):
|
||||
post = await request.post()
|
||||
upload = post.get("file") or post.get("image")
|
||||
if upload is None or not hasattr(upload, "file"):
|
||||
return web.json_response({}, status=400)
|
||||
|
||||
filename = (getattr(upload, "filename", "") or "").lower()
|
||||
data = upload.file.read()
|
||||
try:
|
||||
if filename.endswith(".json"):
|
||||
metadata = _fl_metadata_from_json(data)
|
||||
elif filename.endswith((".png", ".webp", ".avif")):
|
||||
metadata = _fl_metadata_from_image(data)
|
||||
elif filename.endswith((".mp4", ".mov", ".m4v")):
|
||||
metadata = _fl_metadata_from_isobmff(data)
|
||||
else:
|
||||
metadata = {}
|
||||
except Exception as exc:
|
||||
logger.warning("Load image drop metadata parse failed: %s", exc)
|
||||
metadata = {}
|
||||
|
||||
return web.json_response(metadata)
|
||||
|
||||
# AI NODES
|
||||
from .nodes.ai.FL_Fal_Gemini_ImageEdit import FL_Fal_Gemini_ImageEdit
|
||||
from .nodes.ai.FL_Fal_GPTImage2_Edit import FL_Fal_GPTImage2_Edit
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import {
|
||||
getAvifMetadata,
|
||||
getPngMetadata,
|
||||
getWebpMetadata,
|
||||
} from "/scripts/pnginfo.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Local.LoadImageDropFix.FillNodesPath",
|
||||
setup() {
|
||||
console.warn("[LoadImageDropFix] extension loaded v2026-07-20-workflow-sanitize");
|
||||
let lastDragOverLogKey = null;
|
||||
let patchedDragOverNode = null;
|
||||
let lastPatchedDrag = null;
|
||||
|
||||
const findNodeById = (graph, id) => {
|
||||
if (!graph || id == null) return null;
|
||||
|
||||
const node = graph.getNodeById?.(id);
|
||||
if (node) return node;
|
||||
|
||||
for (const subgraph of graph.subgraphs?.values?.() ?? []) {
|
||||
const subgraphNode = findNodeById(subgraph, id);
|
||||
if (subgraphNode) return subgraphNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getNodeFromDomEvent = (event) => {
|
||||
const element = event.target?.closest?.("[data-node-id]");
|
||||
if (!element) return null;
|
||||
|
||||
return findNodeById(app.rootGraph ?? app.canvas?.graph, element.dataset.nodeId);
|
||||
};
|
||||
|
||||
const getNodeUnderEvent = (event) => {
|
||||
const domNode = getNodeFromDomEvent(event);
|
||||
if (domNode) return domNode;
|
||||
|
||||
const canvas = app.canvas;
|
||||
const graph = canvas?.graph;
|
||||
if (!canvas || !graph) return null;
|
||||
|
||||
canvas.adjustMouseEvent?.(event);
|
||||
return graph.getNodeOnPos?.(event.canvasX, event.canvasY) ?? null;
|
||||
};
|
||||
|
||||
const isLoadImageNode = (node) => {
|
||||
return node?.type === "LoadImage" || node?.type === "LoadImageMask";
|
||||
};
|
||||
|
||||
const isImageFile = (file) => file?.type?.startsWith?.("image/");
|
||||
|
||||
const isWorkflowFile = (file) => {
|
||||
return (
|
||||
file?.type === "application/json" ||
|
||||
file?.name?.toLowerCase?.().endsWith(".json")
|
||||
);
|
||||
};
|
||||
|
||||
const isWorkflowMetadataCandidate = (file) => {
|
||||
const name = file?.name?.toLowerCase?.() ?? "";
|
||||
return (
|
||||
isWorkflowFile(file) ||
|
||||
isImageFile(file) ||
|
||||
file?.type === "video/mp4" ||
|
||||
file?.type === "video/quicktime" ||
|
||||
name.endsWith(".mp4") ||
|
||||
name.endsWith(".mov") ||
|
||||
name.endsWith(".m4v")
|
||||
);
|
||||
};
|
||||
|
||||
const getFiles = (event) => {
|
||||
return Array.from(event.dataTransfer?.files ?? []);
|
||||
};
|
||||
|
||||
const getItems = (event) => {
|
||||
return Array.from(event.dataTransfer?.items ?? []);
|
||||
};
|
||||
|
||||
const isDraggingFiles = (event) => {
|
||||
return (
|
||||
getItems(event).some((item) => item.kind === "file") ||
|
||||
Array.from(event.dataTransfer?.types ?? []).includes("Files")
|
||||
);
|
||||
};
|
||||
|
||||
const isDraggingImages = (event) => {
|
||||
const items = getItems(event).filter((item) => item.kind === "file");
|
||||
if (!items.length) return isDraggingFiles(event);
|
||||
return items.some(
|
||||
(item) => !item.type || item.type.startsWith("image/"),
|
||||
);
|
||||
};
|
||||
|
||||
const hasWorkflowMetadata = async (file) => {
|
||||
try {
|
||||
const workflowData = await getWorkflowData(file);
|
||||
return !!(
|
||||
workflowData?.workflow ||
|
||||
workflowData?.Workflow ||
|
||||
workflowData?.prompt ||
|
||||
workflowData?.Prompt ||
|
||||
workflowData?.parameters ||
|
||||
workflowData?.templates
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("[LoadImageDropFix] workflow metadata check failed", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getImageMetadata = async (file) => {
|
||||
let metadata = {};
|
||||
try {
|
||||
if (file.type === "image/png") {
|
||||
metadata = await getPngMetadata(file);
|
||||
} else if (file.type === "image/webp") {
|
||||
metadata = await getWebpMetadata(file);
|
||||
} else if (file.type === "image/avif") {
|
||||
metadata = await getAvifMetadata(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[LoadImageDropFix] metadata read failed", error);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const readTextFile = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result ?? ""));
|
||||
reader.onerror = () => reject(reader.error ?? new Error("read failed"));
|
||||
reader.onabort = () => reject(new Error("read aborted"));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkflowDataFromServer = async (file) => {
|
||||
if (!isWorkflowMetadataCandidate(file)) return {};
|
||||
|
||||
const body = new FormData();
|
||||
body.append("file", file);
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open("POST", "/load_image_drop_fix/workflow_metadata");
|
||||
request.onload = () => {
|
||||
if (request.status < 200 || request.status >= 300) {
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(request.responseText || "{}"));
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
};
|
||||
request.onerror = () => resolve({});
|
||||
request.onabort = () => resolve({});
|
||||
request.send(body);
|
||||
});
|
||||
};
|
||||
|
||||
const parseJson = (value) => {
|
||||
if (!value) return undefined;
|
||||
if (typeof value === "object") return value;
|
||||
if (typeof value !== "string") return undefined;
|
||||
return JSON.parse(value);
|
||||
};
|
||||
|
||||
const cloneJsonData = (value) => {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
try {
|
||||
return structuredClone(value);
|
||||
} catch {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
const stripPreviewUrl = (value) => {
|
||||
if (typeof value !== "string") return value;
|
||||
return value.includes("rgthree.compare._temp_") ? "" : value;
|
||||
};
|
||||
|
||||
const normalizeMediaReference = (value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
|
||||
|
||||
let changed = 0;
|
||||
const filename = value.filename;
|
||||
const subfolder = value.subfolder;
|
||||
if (typeof filename === "string" && typeof subfolder === "string" && subfolder) {
|
||||
const normalizedSubfolder = subfolder.replace(/^\/+|\/+$/g, "");
|
||||
const prefix = `${normalizedSubfolder}/`;
|
||||
if (filename.startsWith(prefix)) {
|
||||
value.filename = filename.slice(prefix.length);
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
};
|
||||
|
||||
const sanitizeWorkflowForDrop = (workflow) => {
|
||||
const sanitized = cloneJsonData(workflow);
|
||||
const stats = { normalizedMediaRefs: 0, clearedTempPreviews: 0 };
|
||||
|
||||
const walk = (value) => {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
|
||||
stats.normalizedMediaRefs += normalizeMediaReference(value);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = value.length - 1; i >= 0; i--) {
|
||||
if (typeof value[i] === "string" && value[i].includes("rgthree.compare._temp_")) {
|
||||
value.splice(i, 1);
|
||||
stats.clearedTempPreviews++;
|
||||
} else {
|
||||
const next = walk(value[i]);
|
||||
if (next !== value[i]) value[i] = next;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (typeof child === "string") {
|
||||
const stripped = stripPreviewUrl(child);
|
||||
if (stripped !== child) {
|
||||
value[key] = stripped;
|
||||
stats.clearedTempPreviews++;
|
||||
}
|
||||
} else {
|
||||
const next = walk(child);
|
||||
if (next !== child) value[key] = next;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
walk(sanitized);
|
||||
|
||||
for (const node of sanitized?.nodes ?? []) {
|
||||
if (node?.type === "Image Comparer (rgthree)" && Array.isArray(node.widgets_values)) {
|
||||
for (let i = 0; i < node.widgets_values.length; i++) {
|
||||
const widgetValue = node.widgets_values[i];
|
||||
const asJson = JSON.stringify(widgetValue ?? "");
|
||||
if (asJson.includes("rgthree.compare._temp_")) {
|
||||
node.widgets_values[i] = [];
|
||||
stats.clearedTempPreviews++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.normalizedMediaRefs || stats.clearedTempPreviews) {
|
||||
console.warn("[LoadImageDropFix] sanitized dropped workflow", stats);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
const focusLoadedWorkflow = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const canvas = app.canvas;
|
||||
const graph = canvas?.graph ?? app.rootGraph;
|
||||
const firstNode = graph?.nodes?.[0];
|
||||
if (firstNode && canvas?.centerOnNode) {
|
||||
canvas.centerOnNode(firstNode);
|
||||
}
|
||||
canvas?.setDirty?.(true, true);
|
||||
});
|
||||
};
|
||||
|
||||
const isApiJson = (data) => {
|
||||
return (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
!Array.isArray(data) &&
|
||||
Object.keys(data).length > 0 &&
|
||||
Object.values(data).every((node) => node?.class_type)
|
||||
);
|
||||
};
|
||||
|
||||
const getWorkflowData = async (file) => {
|
||||
const serverWorkflowData = await getWorkflowDataFromServer(file);
|
||||
if (
|
||||
serverWorkflowData?.workflow ||
|
||||
serverWorkflowData?.prompt ||
|
||||
serverWorkflowData?.parameters ||
|
||||
serverWorkflowData?.templates
|
||||
) {
|
||||
return serverWorkflowData;
|
||||
}
|
||||
|
||||
if (isWorkflowFile(file)) {
|
||||
const data = parseJson(await readTextFile(file));
|
||||
if (data?.workflow || data?.Workflow || data?.prompt || data?.Prompt) {
|
||||
return {
|
||||
workflow: data.workflow ?? data.Workflow,
|
||||
prompt: data.prompt ?? data.Prompt,
|
||||
parameters: data.parameters,
|
||||
templates: data.templates,
|
||||
};
|
||||
}
|
||||
if (data?.templates) return { templates: data.templates };
|
||||
if (isApiJson(data)) return { prompt: data };
|
||||
return { workflow: data };
|
||||
}
|
||||
|
||||
if (!isImageFile(file)) return {};
|
||||
|
||||
const metadata = await getImageMetadata(file);
|
||||
return !!(
|
||||
metadata?.workflow ||
|
||||
metadata?.Workflow ||
|
||||
metadata?.prompt ||
|
||||
metadata?.Prompt ||
|
||||
metadata?.parameters ||
|
||||
metadata?.parametersText ||
|
||||
metadata?.templates
|
||||
)
|
||||
? {
|
||||
workflow: metadata.workflow ?? metadata.Workflow,
|
||||
prompt: metadata.prompt ?? metadata.Prompt,
|
||||
parameters: metadata.parameters ?? metadata.parametersText,
|
||||
templates: metadata.templates,
|
||||
}
|
||||
: {};
|
||||
};
|
||||
|
||||
const hasWorkflowData = (workflowData) => {
|
||||
return !!(
|
||||
workflowData?.workflow ||
|
||||
workflowData?.prompt ||
|
||||
workflowData?.parameters ||
|
||||
workflowData?.templates
|
||||
);
|
||||
};
|
||||
|
||||
const loadWorkflowFromFile = async (file, workflowData) => {
|
||||
workflowData ??= await getWorkflowData(file);
|
||||
const fileName = file.name.replace(/\.\w+$/, "");
|
||||
|
||||
if (workflowData?.templates && app.loadTemplateData) {
|
||||
app.loadTemplateData({ templates: workflowData.templates });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (workflowData?.workflow) {
|
||||
const workflow = parseJson(workflowData.workflow);
|
||||
if (workflow && typeof workflow === "object" && !Array.isArray(workflow)) {
|
||||
const sanitizedWorkflow = sanitizeWorkflowForDrop(workflow);
|
||||
console.warn("[LoadImageDropFix] workflow data parsed", {
|
||||
file: file.name,
|
||||
nodes: sanitizedWorkflow.nodes?.length ?? 0,
|
||||
version: sanitizedWorkflow.version,
|
||||
});
|
||||
await app.loadGraphData(sanitizedWorkflow, true, true, fileName, {
|
||||
openSource: "file_drop",
|
||||
deferWarnings: true,
|
||||
skipAssetScans: true,
|
||||
silentAssetErrors: true,
|
||||
});
|
||||
console.warn("[LoadImageDropFix] workflow loaded", file.name);
|
||||
focusLoadedWorkflow();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (workflowData?.prompt && app.loadApiJson) {
|
||||
const prompt = parseJson(workflowData.prompt);
|
||||
if (prompt) {
|
||||
console.warn("[LoadImageDropFix] api json parsed", {
|
||||
file: file.name,
|
||||
nodes: Object.keys(prompt).length,
|
||||
});
|
||||
await app.loadApiJson(prompt, fileName);
|
||||
console.warn("[LoadImageDropFix] api json loaded", file.name);
|
||||
focusLoadedWorkflow();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (workflowData?.prompt) {
|
||||
console.warn("[LoadImageDropFix] api json fallback to native loader", file.name);
|
||||
await app.handleFile(file, "file_drop", { deferWarnings: true });
|
||||
focusLoadedWorkflow();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (workflowData?.parameters) {
|
||||
await app.handleFile(file, "file_drop", { deferWarnings: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleWorkflowFile = async (file, workflowData) => {
|
||||
try {
|
||||
if (await loadWorkflowFromFile(file, workflowData)) return true;
|
||||
} catch (error) {
|
||||
console.warn("[LoadImageDropFix] direct workflow load failed", error);
|
||||
}
|
||||
|
||||
const originalLoadGraphData = app.loadGraphData;
|
||||
try {
|
||||
app.loadGraphData = function (
|
||||
graphData,
|
||||
clean,
|
||||
restoreView,
|
||||
workflow,
|
||||
options = {},
|
||||
) {
|
||||
return originalLoadGraphData.call(
|
||||
this,
|
||||
graphData,
|
||||
clean,
|
||||
restoreView,
|
||||
workflow,
|
||||
{
|
||||
...options,
|
||||
deferWarnings: true,
|
||||
skipAssetScans: true,
|
||||
silentAssetErrors: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
await app.handleFile(file, "file_drop", { deferWarnings: true });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("[LoadImageDropFix] native workflow load failed", error);
|
||||
return false;
|
||||
} finally {
|
||||
app.loadGraphData = originalLoadGraphData;
|
||||
}
|
||||
};
|
||||
|
||||
const uploadImage = async (file) => {
|
||||
const body = new FormData();
|
||||
body.append("image", file);
|
||||
body.append("type", "input");
|
||||
body.append("overwrite", "true");
|
||||
|
||||
const data = await new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open("POST", "/upload/image");
|
||||
request.onload = () => {
|
||||
if (request.status < 200 || request.status >= 300) {
|
||||
reject(new Error(`${request.status} ${request.statusText}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(request.responseText));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
request.onerror = () => reject(new Error("image upload failed"));
|
||||
request.onabort = () => reject(new Error("image upload aborted"));
|
||||
request.send(body);
|
||||
});
|
||||
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name;
|
||||
};
|
||||
|
||||
const addToComboValues = (widget, value) => {
|
||||
const values = widget?.options?.values;
|
||||
if (!Array.isArray(values)) return;
|
||||
|
||||
const valuesToAdd = Array.isArray(value) ? value : [value];
|
||||
for (const item of valuesToAdd) {
|
||||
if (!values.includes(item)) values.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
const setNodeImage = (node, value) => {
|
||||
const widget = node?.widgets?.find?.((widget) => widget.name === "image");
|
||||
if (!widget) {
|
||||
throw new Error(`LoadImage node ${node?.id ?? ""} has no image widget`);
|
||||
}
|
||||
|
||||
const oldValue = widget.value;
|
||||
addToComboValues(widget, value);
|
||||
widget.value = value;
|
||||
widget.callback?.(value);
|
||||
node.onWidgetChanged?.(widget.name, value, oldValue, widget);
|
||||
node.imgs = undefined;
|
||||
node.graph?.setDirtyCanvas?.(true, true);
|
||||
app.canvas?.setDirty?.(true, true);
|
||||
};
|
||||
|
||||
const createLoadImageNode = (event) => {
|
||||
const liteGraph = window.LiteGraph;
|
||||
const graph = app.canvas?.graph ?? app.rootGraph;
|
||||
if (!liteGraph?.createNode || !graph?.add) {
|
||||
throw new Error("LiteGraph is not ready");
|
||||
}
|
||||
|
||||
app.canvas?.adjustMouseEvent?.(event);
|
||||
const node = liteGraph.createNode("LoadImage");
|
||||
if (!node) throw new Error("Failed to create LoadImage node");
|
||||
|
||||
node.pos = [
|
||||
event.canvasX ?? app.canvas?.graph_mouse?.[0] ?? 0,
|
||||
event.canvasY ?? app.canvas?.graph_mouse?.[1] ?? 0,
|
||||
];
|
||||
graph.add(node);
|
||||
return node;
|
||||
};
|
||||
|
||||
const uploadImagesToNode = async (files, node, event) => {
|
||||
const imageFiles = files.filter(isImageFile);
|
||||
if (!imageFiles.length) return false;
|
||||
|
||||
node ??= createLoadImageNode(event);
|
||||
node.isUploading = true;
|
||||
try {
|
||||
node.imgs = undefined;
|
||||
node.graph?.setDirtyCanvas?.(true, true);
|
||||
|
||||
const paths = [];
|
||||
for (const file of imageFiles) {
|
||||
paths.push(await uploadImage(file));
|
||||
}
|
||||
|
||||
setNodeImage(node, paths[0]);
|
||||
return true;
|
||||
} finally {
|
||||
node.isUploading = false;
|
||||
node.graph?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
const clearPatchedDragOverNode = () => {
|
||||
if (patchedDragOverNode && app.dragOverNode === patchedDragOverNode) {
|
||||
app.dragOverNode = null;
|
||||
}
|
||||
patchedDragOverNode = null;
|
||||
lastPatchedDrag = null;
|
||||
};
|
||||
|
||||
const getDropNode = (event) => {
|
||||
const node = getNodeUnderEvent(event);
|
||||
if (isLoadImageNode(node)) return node;
|
||||
|
||||
if (!patchedDragOverNode || !lastPatchedDrag) return null;
|
||||
|
||||
const elapsed = performance.now() - lastPatchedDrag.time;
|
||||
const distance = Math.hypot(
|
||||
event.clientX - lastPatchedDrag.clientX,
|
||||
event.clientY - lastPatchedDrag.clientY,
|
||||
);
|
||||
|
||||
if (elapsed < 1000 && distance < 48) return patchedDragOverNode;
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
const node = getNodeUnderEvent(event);
|
||||
const canDrop =
|
||||
(isLoadImageNode(node) && isDraggingImages(event)) ||
|
||||
(!node && isDraggingFiles(event));
|
||||
|
||||
if (isLoadImageNode(node)) {
|
||||
const key = `${node.id}:${canDrop}`;
|
||||
if (key !== lastDragOverLogKey) {
|
||||
lastDragOverLogKey = key;
|
||||
console.warn("[LoadImageDropFix] dragover LoadImage", {
|
||||
id: node.id,
|
||||
canDrop,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!canDrop) {
|
||||
clearPatchedDragOverNode();
|
||||
return;
|
||||
}
|
||||
|
||||
patchedDragOverNode = node;
|
||||
lastPatchedDrag = {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
time: performance.now(),
|
||||
};
|
||||
app.dragOverNode = node;
|
||||
event.preventDefault();
|
||||
requestAnimationFrame(() => app.canvas?.setDirty?.(false, true));
|
||||
};
|
||||
|
||||
const handleDrop = async (event) => {
|
||||
const node = getDropNode(event);
|
||||
const files = getFiles(event);
|
||||
|
||||
if (!files.length) {
|
||||
clearPatchedDragOverNode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoadImageNode(node)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
try {
|
||||
console.warn("[LoadImageDropFix] upload image to node", node.id);
|
||||
await uploadImagesToNode(files, node, event);
|
||||
} finally {
|
||||
clearPatchedDragOverNode();
|
||||
app.canvas?.setDirty?.(false, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
clearPatchedDragOverNode();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
try {
|
||||
app.canvas?.adjustMouseEvent?.(event);
|
||||
if (app.canvas?.graph_mouse) {
|
||||
app.canvas.graph_mouse[0] = event.canvasX;
|
||||
app.canvas.graph_mouse[1] = event.canvasY;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const workflowData = isWorkflowMetadataCandidate(file)
|
||||
? await getWorkflowData(file)
|
||||
: {};
|
||||
|
||||
if (hasWorkflowData(workflowData)) {
|
||||
console.warn("[LoadImageDropFix] load workflow from drop", file.name);
|
||||
await handleWorkflowFile(file, workflowData);
|
||||
} else if (isImageFile(file)) {
|
||||
console.warn("[LoadImageDropFix] create LoadImage from drop", file.name);
|
||||
await uploadImagesToNode([file], createLoadImageNode(event), event);
|
||||
} else {
|
||||
await handleWorkflowFile(file, workflowData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[LoadImageDropFix] drop handling failed", error);
|
||||
for (const file of files.filter(isImageFile)) {
|
||||
try {
|
||||
await uploadImagesToNode([file], createLoadImageNode(event), event);
|
||||
} catch (uploadError) {
|
||||
console.warn("[LoadImageDropFix] image fallback failed", uploadError);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearPatchedDragOverNode();
|
||||
app.canvas?.setDirty?.(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("dragover", handleDragOver, true);
|
||||
document.addEventListener(
|
||||
"drop",
|
||||
(event) => {
|
||||
handleDrop(event).catch((error) => {
|
||||
console.warn("[LoadImageDropFix] unhandled drop failure", error);
|
||||
clearPatchedDragOverNode();
|
||||
app.canvas?.setDirty?.(true, true);
|
||||
});
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user