Modification of the RS Crop Image node

This commit is contained in:
Raykosan
2026-06-02 13:07:41 +03:00
parent 350a7136f6
commit d582c8649b
5 changed files with 209 additions and 33 deletions
+14 -11
View File
@@ -36,6 +36,14 @@ class RSCropImage:
inputs = {
"required": {
"image": ("IMAGE",),
"multiple_mode": ("BOOLEAN", {
"default": False,
"label_on": "ON",
"label_off": "OFF",
}),
"multiple_of": (["4", "8", "16", "32", "64"], {
"default": "8",
}),
},
"optional": {
"crop_data": ("STRING", {"multiline": False, "default": "{}"}),
@@ -54,7 +62,8 @@ class RSCropImage:
CATEGORY = "🦊 RaykoStudio"
OUTPUT_NODE = True
def crop_image(self, image, crop_data="{}", unique_id=None, prompt_id=None):
def crop_image(self, image, multiple_mode=False, multiple_of="8",
crop_data="{}", unique_id=None, prompt_id=None):
unique_id = str(unique_id) if unique_id else "unknown"
if SERVER_AVAILABLE and unique_id:
@@ -120,10 +129,10 @@ class RSCropImage:
except Exception:
crop_dict = {}
x = int(crop_dict.get("x", 0))
y = int(crop_dict.get("y", 0))
width = int(crop_dict.get("width", 512))
height = int(crop_dict.get("height", 512))
x = round(crop_dict.get("x", 0))
y = round(crop_dict.get("y", 0))
width = round(crop_dict.get("width", 512))
height = round(crop_dict.get("height", 512))
if image is None or len(image) == 0:
h, w = 512, 512
@@ -180,19 +189,15 @@ if SERVER_AVAILABLE:
PENDING_DECISIONS[node_id]["status"] = "approved"
if crop_data:
PENDING_DECISIONS[node_id]["crop_data"] = crop_data
elif decision == "reject":
PENDING_DECISIONS[node_id]["status"] = "rejected"
if crop_data is not None:
PENDING_DECISIONS[node_id]["crop_data"] = crop_data
elif decision == "cancel":
PENDING_DECISIONS[node_id]["status"] = "cancelled"
return web.Response(status=200, text="Decision recorded")
else:
return web.Response(status=404, text=f"Node {node_id} not waiting")
except Exception as e:
return web.Response(status=500, text=str(e))
@@ -201,13 +206,11 @@ if SERVER_AVAILABLE:
try:
data = await request.json()
node_id = str(data.get("node_id"))
if node_id in PENDING_DECISIONS:
PENDING_DECISIONS[node_id]["status"] = "removed"
return web.Response(status=200, text="Cleanup recorded")
else:
return web.Response(status=200, text="Node not found")
except Exception as e:
return web.Response(status=500, text=str(e))
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.20.1"
__version__ = "0.20.2"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.20.1",
"version": "0.20.2",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.20.1"
version = "0.20.2"
license = { file = "LICENSE" }
dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"]
+192 -19
View File
@@ -30,6 +30,42 @@ app.registerExtension({
};
}
const modeWidget = node.widgets?.find(w => w.name === "multiple_mode");
const ofWidget = node.widgets?.find(w => w.name === "multiple_of");
if (modeWidget && ofWidget) {
const applyDisabled = () => {
ofWidget.disabled = !modeWidget.value;
node.setDirtyCanvas(true, true);
};
let _modeValue = modeWidget.value;
Object.defineProperty(modeWidget, 'value', {
get: function() { return _modeValue; },
set: function(v) {
const wasOn = _modeValue;
_modeValue = v;
applyDisabled();
if (!wasOn && v && node.imageReady) {
applyAlignment();
updateCropRect();
}
},
configurable: true
});
const originalOfCallback = ofWidget.callback;
ofWidget.callback = function(value) {
if (originalOfCallback) originalOfCallback.apply(this, arguments);
if (modeWidget.value && node.imageReady) {
applyAlignment();
updateCropRect();
}
};
applyDisabled();
}
node.addButton = (label, color, callback) => {
node.buttons.push({
label: label, color: color, callback: callback,
@@ -61,8 +97,115 @@ app.registerExtension({
} catch (e) {}
}
const clampCropRect = (rect) => {
if (!node.imageReady || !node.imageWidth || !node.imageHeight) return rect;
const imgW = node.imageWidth;
const imgH = node.imageHeight;
let w = Math.max(1, Math.min(rect.width, imgW));
let h = Math.max(1, Math.min(rect.height, imgH));
let x = Math.max(0, Math.min(rect.x, imgW - w));
let y = Math.max(0, Math.min(rect.y, imgH - h));
w = Math.min(w, imgW - x);
h = Math.min(h, imgH - y);
return { x, y, width: w, height: h };
};
const applyClamp = () => {
const clamped = clampCropRect(node._cropRect);
node._cropRect.x = clamped.x;
node._cropRect.y = clamped.y;
node._cropRect.width = clamped.width;
node._cropRect.height = clamped.height;
};
const alignToMultiple = (rect, m, imgW, imgH) => {
if (!m || m < 1 || !imgW || !imgH) return rect;
const origX = rect.x;
const origY = rect.y;
const origW = Math.max(1, rect.width);
const origH = Math.max(1, rect.height);
const centerX = origX + origW / 2;
const centerY = origY + origH / 2;
let newW = Math.ceil(origW / m) * m;
let newH = Math.ceil(origH / m) * m;
const maxW = ((imgW - origX) / m) | 0;
if (maxW < 1 || (maxW * m) < m) {
newW = Math.floor(origW / m) * m;
} else if (origX + newW > imgW) {
newW = maxW * m;
}
const maxH = ((imgH - origY) / m) | 0;
if (maxH < 1 || (maxH * m) < m) {
newH = Math.floor(origH / m) * m;
} else if (origY + newH > imgH) {
newH = maxH * m;
}
if (newW < m) newW = m;
if (newH < m) newH = m;
let newX = Math.round(centerX - newW / 2);
let newY = Math.round(centerY - newH / 2);
newX = Math.max(0, Math.min(newX, imgW - newW));
newY = Math.max(0, Math.min(newY, imgH - newH));
return { x: newX, y: newY, width: newW, height: newH };
};
const applyAlignment = () => {
if (!modeWidget || !ofWidget || !modeWidget.value || !node.imageReady) return;
const m = parseInt(ofWidget.value);
if (!m) return;
const aligned = alignToMultiple(node._cropRect, m, node.imageWidth, node.imageHeight);
node._cropRect.x = aligned.x;
node._cropRect.y = aligned.y;
node._cropRect.width = aligned.width;
node._cropRect.height = aligned.height;
};
const roundRect = (rect) => ({
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
});
const isNodeFullyVisible = () => {
if (!app.canvas) return false;
const ds = app.canvas.ds;
const scale = ds.scale;
const canvasEl = app.canvas.canvas;
const canvasRect = canvasEl.getBoundingClientRect();
const nodeLeft = node.pos[0];
const nodeTop = node.pos[1];
const nodeRight = node.pos[0] + node.size[0];
const nodeBottom = node.pos[1] + node.size[1];
const viewportLeft = -ds.offset[0];
const viewportTop = -ds.offset[1];
const viewportRight = viewportLeft + canvasRect.width / scale;
const viewportBottom = viewportTop + canvasRect.height / scale;
return nodeLeft >= viewportLeft &&
nodeTop >= viewportTop &&
nodeRight <= viewportRight &&
nodeBottom <= viewportBottom;
};
const updateCropRect = () => {
const jsonStr = JSON.stringify(node._cropRect);
const jsonStr = JSON.stringify(roundRect(node._cropRect));
node.properties = node.properties || {};
node.properties.crop_rect = jsonStr;
if (cropDataWidget) cropDataWidget.value = jsonStr;
@@ -303,6 +446,7 @@ app.registerExtension({
}
}
applyClamp();
updateCropRect();
});
@@ -311,6 +455,12 @@ app.registerExtension({
_isDragging = false;
_isResizing = false;
_resizeHandle = null;
if (modeWidget && ofWidget && modeWidget.value && node.imageReady) {
applyAlignment();
updateCropRect();
}
node._overlayCanvas.releasePointerCapture(e.pointerId || 1);
});
@@ -350,10 +500,6 @@ app.registerExtension({
ctx.fillRect(cropX + cropW - handleSize/2, cropY - handleSize/2, handleSize, handleSize);
ctx.fillRect(cropX - handleSize/2, cropY + cropH - handleSize/2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize/2, cropY + cropH - handleSize/2, handleSize, handleSize);
ctx.fillStyle = "#ffffff";
ctx.font = "bold 12px Arial";
ctx.fillText(`${Math.round(node._cropRect.width)} x ${Math.round(node._cropRect.height)}`, cropX + 5, cropY + 20);
};
node.onDrawForeground = function(ctx) {
@@ -378,6 +524,34 @@ app.registerExtension({
const availableHeight = h - startY - footerHeight - padding;
const availableWidth = w - (padding * 2);
const cropSizeText = `${Math.round(node._cropRect.width)} x ${Math.round(node._cropRect.height)}`;
const isMultipleOn = modeWidget && modeWidget.value && ofWidget;
const multipleSuffix = isMultipleOn ? ` (×${parseInt(ofWidget.value)})` : "";
const fullText = cropSizeText + multipleSuffix;
const statusLineY = titleBarHeight + widgetsTotalHeight + 30;
ctx.font = "14px Arial";
ctx.textBaseline = "top";
const mainWidth = ctx.measureText(cropSizeText).width;
if (isMultipleOn) {
const fullWidth = ctx.measureText(fullText).width;
let drawX = (w - fullWidth) / 2;
ctx.fillStyle = "#aaaaaa";
ctx.textAlign = "left";
ctx.fillText(cropSizeText, drawX, statusLineY);
ctx.fillStyle = "#FF0000";
ctx.fillText(multipleSuffix, drawX + mainWidth, statusLineY);
} else {
ctx.fillStyle = "#aaaaaa";
ctx.textAlign = "center";
ctx.fillText(cropSizeText, w / 2, statusLineY);
}
if (this.imageReady && this.image) {
const imgRatio = this.imageWidth / this.imageHeight;
let drawW, drawH;
@@ -407,10 +581,10 @@ app.registerExtension({
ctx.fillText("Waiting for Image...", w / 2, startY + availableHeight / 2);
}
ctx.fillStyle = "#888";
ctx.fillStyle = "#aaaaaa";
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.fillText(this.currentStatus, w / 2, h - 50);
ctx.fillText(this.currentStatus, w / 2, h - 60);
const btnH = 28;
const btnY = h - 40;
@@ -470,7 +644,7 @@ app.registerExtension({
};
node.sendDecision = async function(decision) {
const currentCropData = JSON.stringify(node._cropRect);
const currentCropData = JSON.stringify(roundRect(node._cropRect));
node.properties = node.properties || {};
node.properties.crop_rect = currentCropData;
@@ -482,11 +656,13 @@ app.registerExtension({
if (decision === "cancel") {
try {
await api.interrupt();
} catch (e) { console.error("Interrupt failed:", e); }
} catch (e) {}
}
if (decision === "reject") {
node._cropRect = { x: 0, y: 0, width: 256, height: 256 };
applyClamp();
applyAlignment();
updateCropRect();
}
@@ -531,7 +707,7 @@ app.registerExtension({
body: JSON.stringify({
node_id: this.id.toString()
})
}).catch(err => {});
}).catch(() => {});
};
api.addEventListener("rayko.rscrop.show", (event) => {
@@ -541,25 +717,20 @@ app.registerExtension({
if (String(node_id) === String(currentId)) {
node.image.src = image_url + "&t=" + Date.now();
node.image.onload = () => {
console.log("[RS Crop 🦊] Image loaded, showing overlay");
node.imageReady = true;
node.imageWidth = image_width || node.image.width;
node.imageHeight = image_height || node.image.height;
if (node._overlayCanvas) {
node._overlayCanvas.style.display = "block";
console.log("[RS Crop 🦊] Overlay display set to block");
}
if (node.properties?.crop_rect) {
try {
const r = JSON.parse(node.properties.crop_rect);
if (r.x >= node.imageWidth) r.x = 0;
if (r.y >= node.imageHeight) r.y = 0;
if (r.x + r.width > node.imageWidth) r.width = Math.max(64, node.imageWidth - r.x);
if (r.y + r.height > node.imageHeight) r.height = Math.max(64, node.imageHeight - r.y);
node.properties.crop_rect = JSON.stringify(r);
node._cropRect = r;
if (r.x !== undefined) node._cropRect = r;
applyClamp();
applyAlignment();
} catch (e) {}
}
@@ -573,7 +744,9 @@ app.registerExtension({
if (node.size[1] < 650) node.setSize([node.size[0], 650]);
node.currentStatus = "🖱️ Drag to select crop area, then APPROVE!";
app.canvas.centerOnNode(node);
if (!isNodeFullyVisible()) {
app.canvas.centerOnNode(node);
}
node.setDirtyCanvas(true, true);
}
});