286 lines
11 KiB
JavaScript
286 lines
11 KiB
JavaScript
import { ops } from "../ops.js";
|
|
import { isImageOpsClass } from "../shared/classes.js";
|
|
function className(node) {
|
|
return String(node?.comfyClass ?? "");
|
|
}
|
|
function classLower(node) {
|
|
return className(node).toLowerCase();
|
|
}
|
|
function isImageOps(node) {
|
|
return isImageOpsClass(node?.comfyClass);
|
|
}
|
|
function widgetNames(node) {
|
|
return (node?.widgets ?? []).map((widget) => String(widget?.name ?? "").toLowerCase());
|
|
}
|
|
function hasWidget(node, name) {
|
|
return (node?.widgets ?? []).some((widget) => widget?.name === name);
|
|
}
|
|
function hasAnyWidget(node, names) {
|
|
return names.some((name) => hasWidget(node, name));
|
|
}
|
|
function hasInputSlot(node, index) {
|
|
if (Array.isArray(node?.inputs) && node.inputs.length > index) return true;
|
|
return !!node?.getInputLink?.(index);
|
|
}
|
|
function hasConnectedInput(node, index) {
|
|
return (node.inputs?.[index]?.link ?? null) != null || !!node.getInputLink?.(index);
|
|
}
|
|
function findInputIndex(node, names) {
|
|
const lowered = names.map((name) => name.toLowerCase());
|
|
for (let index = 0; index < (node.inputs?.length ?? 0); index++) {
|
|
const name = String(node.inputs?.[index]?.name ?? "").toLowerCase();
|
|
if (lowered.includes(name)) return index;
|
|
}
|
|
return null;
|
|
}
|
|
function collectIndexes(node, groups, fallback) {
|
|
const indexes = [];
|
|
for (const group of groups) {
|
|
const index = findInputIndex(node, group);
|
|
if (index != null && hasConnectedInput(node, index)) indexes.push(index);
|
|
}
|
|
if (indexes.length > 0) return indexes;
|
|
return fallback.filter((index) => hasConnectedInput(node, index));
|
|
}
|
|
function defaultImageIndexes(node, maxCount = 2) {
|
|
const indexes = [];
|
|
for (let index = 0; index < (node.inputs?.length ?? 0); index++) {
|
|
const slot = node.inputs?.[index];
|
|
const name = String(slot?.name ?? "").toLowerCase();
|
|
const type = String(slot?.type ?? "").toLowerCase();
|
|
if (/mask|bbox|box|region/.test(name) || /mask|bbox|box/.test(type)) continue;
|
|
if (!hasConnectedInput(node, index)) continue;
|
|
if (/image|video|images|source|destination|background|foreground|layer|input|red|green|blue|channel/.test(name) || /image|video/.test(type)) {
|
|
indexes.push(index);
|
|
}
|
|
}
|
|
if (indexes.length > 0) return indexes.slice(0, maxCount);
|
|
return [...Array(maxCount).keys()].filter((index) => hasConnectedInput(node, index));
|
|
}
|
|
function genericAdapters() {
|
|
return [
|
|
{
|
|
name: "generic:color_correct_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const ws = widgetNames(node);
|
|
const has = (name) => ws.includes(name);
|
|
return has("temperature") && has("hue") && has("brightness") && has("contrast") && has("saturation") && has("gamma");
|
|
},
|
|
inputs: 1,
|
|
async apply({ ctx, canvasSize, node }) {
|
|
ops.colorCorrect(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:levels_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const ws = widgetNames(node);
|
|
const has = (name) => ws.includes(name);
|
|
return has("in_min") && has("in_max") && has("gamma");
|
|
},
|
|
inputs: 1,
|
|
async apply({ ctx, canvasSize, node }) {
|
|
ops.levels(ctx, canvasSize, node, { generic: true });
|
|
}
|
|
},
|
|
{
|
|
name: "generic:huesat_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const ws = widgetNames(node);
|
|
const has = (name) => ws.includes(name);
|
|
return (has("hue") || has("hue_deg")) && (has("saturation") || has("sat"));
|
|
},
|
|
inputs: 1,
|
|
async apply({ ctx, canvasSize, node }) {
|
|
ops.hueSat(ctx, canvasSize, node, { generic: true });
|
|
}
|
|
},
|
|
{
|
|
name: "generic:resize_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /resize|scale|upscale|downscale|megapixel|total pixels/.test(cls) && !/mask|latent/.test(cls) || hasAnyWidget(node, ["width", "height", "target_width", "target_height", "scale_by", "multiplier", "largest_size", "megapixels"]);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node }) {
|
|
return ops.resize(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:crop_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /crop/.test(cls) || hasAnyWidget(node, ["crop_region", "crop_w", "crop_h"]);
|
|
},
|
|
inputs: (node) => collectIndexes(node, [["image", "images"], ["mask", "crop_region", "bbox"]], [0, 1]).length || 1,
|
|
inputIndexes: (node) => collectIndexes(node, [["image", "images"], ["mask", "crop_region", "bbox"]], [0, 1]),
|
|
async apply({ ctx, canvasSize, node, inputs, inputInfos }) {
|
|
return ops.cropGeneric(ctx, canvasSize, node, inputs, inputInfos);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:transform_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /transform|shift/.test(cls) || hasAnyWidget(node, ["translate_x", "translate_y", "rotate_deg", "rotate", "mirror", "shift_x", "shift_y", "scale"]);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node }) {
|
|
return ops.transform(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:flip_rotate_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /flip|mirror|rotate/.test(cls) || hasAnyWidget(node, ["flip_method", "rotation"]);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node }) {
|
|
return ops.flipRotate(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:blur_like",
|
|
// IMPORTANT: the `isImageOps` guard MUST stay here. ImageOps nodes often have
|
|
// a "radius" widget, so without it this heuristic would shadow the exact
|
|
// imageOpsAdapter for every ImageOps node that has such a widget.
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /blur/.test(cls) || hasAnyWidget(node, ["radius", "blur", "blur_radius", "sigma"]);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node }) {
|
|
ops.blur(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:sharpen_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /sharpen|cas/.test(cls);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node }) {
|
|
ops.sharpen(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:blend_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
const looksLikeBlendClass = /blend|merge|mix/.test(cls);
|
|
const hasBlendControl = hasAnyWidget(node, ["mix", "opacity", "blend_mode", "mode", "blend_factor"]);
|
|
return hasInputSlot(node, 1) && (looksLikeBlendClass || hasBlendControl);
|
|
},
|
|
inputs: (node) => collectIndexes(node, [["background_image", "image1", "images_1", "destination"], ["layer_image", "image2", "images_2", "source"], ["layer_mask", "mask"]], [0, 1, 2]).length || 1,
|
|
inputIndexes: (node) => collectIndexes(node, [["background_image", "image1", "images_1", "destination"], ["layer_image", "image2", "images_2", "source"], ["layer_mask", "mask"]], [0, 1, 2]),
|
|
async apply({ ctx, canvasSize, node, inputs }) {
|
|
if (!inputs[1]) return;
|
|
if (inputs.length >= 3) return ops.composite(ctx, canvasSize, node, inputs);
|
|
ops.merge(ctx, canvasSize, node, inputs[1], { generic: true });
|
|
}
|
|
},
|
|
{
|
|
name: "generic:composite_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /composite|crossfade/.test(cls) && hasInputSlot(node, 1);
|
|
},
|
|
inputs: (node) => collectIndexes(node, [["destination", "image_from", "image_1"], ["source", "image_to", "image_2"], ["mask", "layer_mask"]], [0, 1, 2]).length || 1,
|
|
inputIndexes: (node) => collectIndexes(node, [["destination", "image_from", "image_1"], ["source", "image_to", "image_2"], ["mask", "layer_mask"]], [0, 1, 2]),
|
|
async apply({ ctx, canvasSize, node, inputs }) {
|
|
return ops.composite(ctx, canvasSize, node, inputs);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:channel_split_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /extract channel|channelsplit|splitimagechannels|channel split/.test(cls) || /channel/.test(cls) && hasAnyWidget(node, ["channel"]);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node, outputSlot }) {
|
|
if (hasWidget(node, "channel")) {
|
|
ops.channel(ctx, canvasSize, node);
|
|
return;
|
|
}
|
|
return ops.channelSplit(ctx, canvasSize, node, outputSlot ?? 0);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:channel_merge_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /channelmerge|mergeimagechannels|channel merge/.test(cls);
|
|
},
|
|
inputs: (node) => collectIndexes(node, [["red", "channel_1"], ["green", "channel_2"], ["blue", "channel_3"], ["alpha", "channel_4"]], [0, 1, 2, 3]).length || 3,
|
|
inputIndexes: (node) => collectIndexes(node, [["red", "channel_1"], ["green", "channel_2"], ["blue", "channel_3"], ["alpha", "channel_4"]], [0, 1, 2, 3]),
|
|
async apply({ ctx, canvasSize, node, inputs }) {
|
|
return ops.channelMerge(ctx, canvasSize, node, inputs);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:channel_apply_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /apply channel|channel apply/.test(cls);
|
|
},
|
|
inputs: (node) => collectIndexes(node, [["image", "images"], ["channel_data", "mask"]], [0, 1]).length || 1,
|
|
inputIndexes: (node) => collectIndexes(node, [["image", "images"], ["channel_data", "mask"]], [0, 1]),
|
|
async apply({ ctx, canvasSize, node, inputs }) {
|
|
return ops.channelApply(ctx, canvasSize, node, inputs);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:desaturate_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /desaturate|grayscale|greyscale/.test(cls);
|
|
},
|
|
inputs: 1,
|
|
inputIndexes: (node) => defaultImageIndexes(node, 1),
|
|
async apply({ ctx, canvasSize, node }) {
|
|
ops.desaturate(ctx, canvasSize, node);
|
|
}
|
|
},
|
|
{
|
|
name: "generic:stitch_like",
|
|
match(node) {
|
|
if (isImageOps(node)) return false;
|
|
const cls = classLower(node);
|
|
return /stitch|concatenate|concat|join/.test(cls) && hasInputSlot(node, 1);
|
|
},
|
|
inputs: (node) => collectIndexes(node, [["image1"], ["image2"]], [0, 1]).length || 1,
|
|
inputIndexes: (node) => collectIndexes(node, [["image1"], ["image2"]], [0, 1]),
|
|
async apply({ ctx, canvasSize, node, inputs }) {
|
|
return ops.stitch(ctx, canvasSize, node, inputs);
|
|
}
|
|
}
|
|
];
|
|
}
|
|
export {
|
|
genericAdapters
|
|
};
|