ComfyUI custom node: resize an image to the closest standard aspect ratio at a target megapixel count (or a custom resolution), snapped down to a selectable pixel multiple, with image, VAE latent, empty latent (SDXL / Flux.2 Klein), width and height outputs and an on-node resolution readout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
4.6 KiB
JavaScript
120 lines
4.6 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
const NODE_NAME = "ResizeToMultiple";
|
|
const LINE_HEIGHT = 16;
|
|
const PADDING = 10;
|
|
|
|
// Same widget mechanism the core "Preview as Text" node uses; exposed by the
|
|
// frontend for reuse. Absent on old frontends, where we fall back to drawing
|
|
// on the node canvas.
|
|
function textPreviewAPI() {
|
|
const tpw = window.comfyAPI?.textPreviewWidgets;
|
|
if (tpw?.addTextPreviewWidgets && tpw?.updateTextPreviewWidgets) return tpw;
|
|
return null;
|
|
}
|
|
|
|
function toLines(output) {
|
|
const t = output?.text;
|
|
return Array.isArray(t) ? t.map(String) : t ? [String(t)] : [];
|
|
}
|
|
|
|
function updateReadout(node, output) {
|
|
const lines = toLines(output);
|
|
if (node.__rtm_widget) {
|
|
// The widget joins arrays with spaces, so pre-join with newlines.
|
|
textPreviewAPI()?.updateTextPreviewWidgets(node, { text: [lines.join("\n")] });
|
|
} else {
|
|
node.__rtm_lines = lines;
|
|
if (lines.length) {
|
|
const base = node.computeSize();
|
|
const needed = base[1] + lines.length * LINE_HEIGHT + PADDING;
|
|
if (node.size[1] < needed) node.setSize([node.size[0], needed]);
|
|
}
|
|
}
|
|
node.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
function findSubgraphByUuid(graph, uuid) {
|
|
if (graph?.subgraphs instanceof Map) return graph.subgraphs.get(uuid) ?? null;
|
|
for (const n of graph?.nodes ?? graph?._nodes ?? []) {
|
|
if (n.subgraph) {
|
|
if (n.subgraph.id === uuid) return n.subgraph;
|
|
const found = findSubgraphByUuid(n.subgraph, uuid);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveNodeByLocatorId(locatorId) {
|
|
const idStr = String(locatorId);
|
|
const root = app.rootGraph ?? app.graph;
|
|
const colon = idStr.indexOf(":");
|
|
if (colon === -1) return root?.getNodeById?.(idStr) ?? null;
|
|
const sub = findSubgraphByUuid(root, idStr.slice(0, colon));
|
|
return sub?.getNodeById?.(idStr.slice(colon + 1)) ?? null;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "resize_to_multiple.resolution_display",
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== NODE_NAME) return;
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
onNodeCreated?.apply(this, arguments);
|
|
const tpw = textPreviewAPI();
|
|
if (!tpw) return;
|
|
try {
|
|
tpw.addTextPreviewWidgets(this);
|
|
this.__rtm_widget = true;
|
|
// addTextPreviewWidgets also adds a Markdown/Plain-text
|
|
// toggle, pointless for a resolution readout.
|
|
const mode = this.widgets?.find((w) => w.name === "preview_mode");
|
|
if (mode) {
|
|
mode.hidden = true;
|
|
if (mode.options) mode.options.hidden = true;
|
|
}
|
|
} catch (e) {
|
|
console.warn("ResizeToMultiple: text preview widget unavailable, using canvas fallback", e);
|
|
this.__rtm_widget = false;
|
|
}
|
|
};
|
|
|
|
const onExecuted = nodeType.prototype.onExecuted;
|
|
nodeType.prototype.onExecuted = function (message) {
|
|
onExecuted?.apply(this, arguments);
|
|
updateReadout(this, message);
|
|
};
|
|
|
|
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
|
onDrawForeground?.apply(this, arguments);
|
|
if (this.__rtm_widget) return;
|
|
const lines = this.__rtm_lines;
|
|
if (!lines?.length || this.flags?.collapsed) return;
|
|
ctx.save();
|
|
ctx.font = "12px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.fillStyle = window.LiteGraph?.NODE_TEXT_COLOR || "#AAA";
|
|
let y = this.size[1] - PADDING / 2 - (lines.length - 1) * LINE_HEIGHT;
|
|
for (const line of lines) {
|
|
ctx.fillText(line, this.size[0] / 2, y);
|
|
y += LINE_HEIGHT;
|
|
}
|
|
ctx.restore();
|
|
};
|
|
},
|
|
// Restores the readout when outputs are re-applied, e.g. after switching
|
|
// between open workflows. Keys are NodeLocatorIds: plain ids for root-graph
|
|
// nodes, "subgraphUuid:localId" for nodes inside subgraphs.
|
|
onNodeOutputsUpdated(nodeOutputs) {
|
|
for (const [locatorId, output] of Object.entries(nodeOutputs)) {
|
|
const node = resolveNodeByLocatorId(locatorId);
|
|
if (!node) continue;
|
|
if ((node.comfyClass ?? node.type) !== NODE_NAME) continue;
|
|
if (output?.text) updateReadout(node, output);
|
|
}
|
|
},
|
|
});
|