Files
speedyrulzandClaude Fable 5 1c35317466 Resize to Multiple v1.2.0
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>
2026-08-15 20:50:07 -04:00

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);
}
},
});