Files
EwaldandClaude Sonnet 5 019a5c9172 Load through comfy_entrypoint and adopt current ComfyUI APIs
Defining NODE_CLASS_MAPPINGS made ComfyUI skip comfy_entrypoint, so
/imageops/viewmedia and node replacements were never registered. Load
as a V3 extension only, move server.py to routes.py so it stops
shadowing ComfyUI's server module, and transcode animated previews
with PyAV instead of an external ffmpeg.

- Require ComfyUI 0.19.0 (has_intermediate_output); CI floor updated.
- Process 11 nodes on ComfyUI's torch device when the frames fit in
  free memory (IMAGEOPS_COMPUTE_DEVICE=cpu disables it).
- Add core CURVE input to Color Correct, BOUNDING_BOX output/input to
  Crop and Crop Stitch, LAYERS output to Comp (ComfyUI 0.31+),
  essentials_category, and Merge blend modes with golden parity.
- Report progress through the public set_progress API and drop the
  hidden unique_id input.
- Add ImageOps settings and a refresh-previews command.
- Fix ImageOpsComp rotation and preview saving (undefined
  to_display_range) and masked Transform edge colors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 14:53:14 +02:00

264 lines
9.2 KiB
JavaScript

const COMP_BLEND_MODES = [
"over",
"add",
"multiply",
"screen",
"overlay",
"soft_light",
"difference",
"lighten",
"darken",
"color_dodge",
"color_burn",
"exclusion",
"hard_light"
];
function inputName(input) {
return String(input?.name ?? "");
}
function clampCompCenter(value) {
if (!Number.isFinite(value)) return 0.5;
return Math.max(-2, Math.min(3, value));
}
function clampCompScale(value) {
if (!Number.isFinite(value)) return 1;
return Math.max(0.05, Math.min(8, value));
}
function clampCompRotation(value) {
if (!Number.isFinite(value)) return 0;
const normalized = ((value + 180) % 360 + 360) % 360 - 180;
return Math.max(-180, Math.min(180, normalized));
}
function clampCompPoint(value) {
if (value == null || value === "") return null;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
return clampCompCenter(numeric);
}
function hasFiniteCompPoint(value) {
if (value == null || value === "") return false;
return Number.isFinite(Number(value));
}
function hasNonDegenerateCompQuad(layer) {
const xs = [Number(layer.tlX), Number(layer.trX), Number(layer.blX), Number(layer.brX)];
const ys = [Number(layer.tlY), Number(layer.trY), Number(layer.blY), Number(layer.brY)];
return Math.max(...xs) - Math.min(...xs) > 1e-4 && Math.max(...ys) - Math.min(...ys) > 1e-4;
}
function defaultCompLayer(slot, index) {
const offset = Math.min(index, 4) * 0.04;
return {
slot,
centerX: 0.5 + offset,
centerY: 0.5 + offset,
scale: index === 0 ? 1 : 0.5,
rotationDeg: 0,
opacity: 1,
mode: "over",
enabled: true
};
}
function normalizeCompLayer(slot, index, layer) {
const fallback = defaultCompLayer(slot, index);
const anyLayer = layer ?? {};
const mode = String(anyLayer.mode ?? fallback.mode).trim().toLowerCase().replace(/[-\s]+/g, "_");
return {
slot,
centerX: clampCompCenter(Number(anyLayer.centerX ?? anyLayer.center_x ?? fallback.centerX)),
centerY: clampCompCenter(Number(anyLayer.centerY ?? anyLayer.center_y ?? fallback.centerY)),
scale: clampCompScale(Number(anyLayer.scale ?? fallback.scale)),
rotationDeg: clampCompRotation(Number(anyLayer.rotationDeg ?? anyLayer.rotate_deg ?? anyLayer.rotation_deg ?? anyLayer.rotation ?? fallback.rotationDeg)),
opacity: Math.max(0, Math.min(1, Number(anyLayer.opacity ?? fallback.opacity))),
mode: COMP_BLEND_MODES.includes(mode) ? mode : "over",
enabled: anyLayer.enabled !== false,
tlX: clampCompPoint(anyLayer.tlX ?? anyLayer.tl_x),
tlY: clampCompPoint(anyLayer.tlY ?? anyLayer.tl_y),
trX: clampCompPoint(anyLayer.trX ?? anyLayer.tr_x),
trY: clampCompPoint(anyLayer.trY ?? anyLayer.tr_y),
blX: clampCompPoint(anyLayer.blX ?? anyLayer.bl_x),
blY: clampCompPoint(anyLayer.blY ?? anyLayer.bl_y),
brX: clampCompPoint(anyLayer.brX ?? anyLayer.br_x),
brY: clampCompPoint(anyLayer.brY ?? anyLayer.br_y)
};
}
function getCompSlots(node) {
const slotMap = /* @__PURE__ */ new Map();
for (let inputIndex = 0; inputIndex < (node.inputs?.length ?? 0); inputIndex++) {
const name = inputName(node.inputs?.[inputIndex]);
const match = /^image_(\d+)$/.exec(name);
if (match) {
const layerNumber = Number(match[1]);
slotMap.set(layerNumber, {
slot: name,
inputIndex,
layerNumber,
maskSlot: `mask_${layerNumber}`,
maskInputIndex: null
});
}
}
for (let inputIndex = 0; inputIndex < (node.inputs?.length ?? 0); inputIndex++) {
const name = inputName(node.inputs?.[inputIndex]);
const match = /^mask_(\d+)$/.exec(name);
if (!match) continue;
const layerNumber = Number(match[1]);
const existing = slotMap.get(layerNumber);
if (existing) {
existing.maskInputIndex = inputIndex;
} else {
slotMap.set(layerNumber, {
slot: `image_${layerNumber}`,
inputIndex: -1,
layerNumber,
maskSlot: name,
maskInputIndex: inputIndex
});
}
}
return [...slotMap.values()].filter((slot) => slot.inputIndex >= 0).sort((a, b) => a.layerNumber - b.layerNumber);
}
function parseCompLayers(raw) {
if (!raw) return [];
let parsed = raw;
if (typeof raw === "string") {
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
}
const source = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.layers) ? parsed.layers : [];
const layers = [];
for (let index = 0; index < source.length; index++) {
const entry = source[index];
if (!entry || typeof entry !== "object") continue;
const slot = String(entry.slot ?? "").trim();
if (!slot) continue;
layers.push(normalizeCompLayer(slot, index, entry));
}
return layers;
}
function syncCompLayers(raw, slots) {
const existing = parseCompLayers(raw);
const bySlot = new Map(existing.map((layer) => [layer.slot, layer]));
return slots.map((slot, index) => normalizeCompLayer(slot.slot, index, bySlot.get(slot.slot)));
}
function hasCompLayerCornerPin(layer) {
if (!layer) return false;
const hasAllPoints = [layer.tlX, layer.tlY, layer.trX, layer.trY, layer.blX, layer.blY, layer.brX, layer.brY].every(hasFiniteCompPoint);
return hasAllPoints && hasNonDegenerateCompQuad(layer);
}
function serializeCompLayers(layers) {
return JSON.stringify({
version: 1,
layers: layers.map((layer) => ({
slot: layer.slot,
center_x: layer.centerX,
center_y: layer.centerY,
scale: layer.scale,
rotate_deg: layer.rotationDeg,
opacity: layer.opacity,
mode: layer.mode,
enabled: layer.enabled,
...hasCompLayerCornerPin(layer) ? {
tl_x: layer.tlX,
tl_y: layer.tlY,
tr_x: layer.trX,
tr_y: layer.trY,
bl_x: layer.blX,
bl_y: layer.blY,
br_x: layer.brX,
br_y: layer.brY
} : {}
}))
});
}
function getCompLayerOutputCorners(outputWidth, outputHeight, sourceWidth, sourceHeight, layer) {
if (hasCompLayerCornerPin(layer)) {
const maxX = Math.max(1, outputWidth - 1);
const maxY = Math.max(1, outputHeight - 1);
return {
tl: { x: Number(layer.tlX) * maxX, y: Number(layer.tlY) * maxY },
tr: { x: Number(layer.trX) * maxX, y: Number(layer.trY) * maxY },
bl: { x: Number(layer.blX) * maxX, y: Number(layer.blY) * maxY },
br: { x: Number(layer.brX) * maxX, y: Number(layer.brY) * maxY }
};
}
const rect = computeCompRect(outputWidth, outputHeight, sourceWidth, sourceHeight, { ...layer, tlX: null, tlY: null, trX: null, trY: null, blX: null, blY: null, brX: null, brY: null });
const angleRad = rect.rotationDeg * Math.PI / 180;
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);
const halfWidth = rect.drawWidth / 2;
const halfHeight = rect.drawHeight / 2;
const makeCorner = (localX, localY) => ({
x: rect.centerX + localX * cos - localY * sin,
y: rect.centerY + localX * sin + localY * cos
});
return {
tl: makeCorner(-halfWidth, -halfHeight),
tr: makeCorner(halfWidth, -halfHeight),
bl: makeCorner(-halfWidth, halfHeight),
br: makeCorner(halfWidth, halfHeight)
};
}
function computeCompRect(outputWidth, outputHeight, sourceWidth, sourceHeight, layer) {
const drawWidth = Math.max(1, Math.round(Math.max(1, sourceWidth) * clampCompScale(layer.scale)));
const drawHeight = Math.max(1, Math.round(Math.max(1, sourceHeight) * clampCompScale(layer.scale)));
if (hasCompLayerCornerPin(layer)) {
const points = getCompLayerOutputCorners(outputWidth, outputHeight, sourceWidth, sourceHeight, layer);
const xs = [points.tl.x, points.tr.x, points.bl.x, points.br.x];
const ys = [points.tl.y, points.tr.y, points.bl.y, points.br.y];
const left2 = Math.round(Math.min(...xs));
const top2 = Math.round(Math.min(...ys));
const right = Math.round(Math.max(...xs));
const bottom = Math.round(Math.max(...ys));
return {
left: left2,
top: top2,
width: Math.max(1, right - left2),
height: Math.max(1, bottom - top2),
right,
bottom,
drawWidth,
drawHeight,
centerX: (points.tl.x + points.tr.x + points.bl.x + points.br.x) / 4,
centerY: (points.tl.y + points.tr.y + points.bl.y + points.br.y) / 4,
rotationDeg: clampCompRotation(layer.rotationDeg)
};
}
const centerX = clampCompCenter(layer.centerX) * Math.max(1, outputWidth);
const centerY = clampCompCenter(layer.centerY) * Math.max(1, outputHeight);
const rad = clampCompRotation(layer.rotationDeg) * Math.PI / 180;
const absCos = Math.abs(Math.cos(rad));
const absSin = Math.abs(Math.sin(rad));
const width = Math.max(1, Math.ceil(drawWidth * absCos + drawHeight * absSin));
const height = Math.max(1, Math.ceil(drawWidth * absSin + drawHeight * absCos));
const left = Math.round(centerX - width / 2);
const top = Math.round(centerY - height / 2);
return {
left,
top,
width,
height,
right: left + width,
bottom: top + height,
drawWidth,
drawHeight,
centerX,
centerY,
rotationDeg: clampCompRotation(layer.rotationDeg)
};
}
export {
COMP_BLEND_MODES,
clampCompCenter,
clampCompRotation,
clampCompScale,
computeCompRect,
getCompLayerOutputCorners,
getCompSlots,
hasCompLayerCornerPin,
parseCompLayers,
serializeCompLayers,
syncCompLayers
};