Modification of the RS Crop Image node
This commit is contained in:
+14
-11
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user