You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, threshold = -1, invertOutput = "false") {
// The function is async to align with potential needs for dynamic imports or Web Workers,
// though this specific implementation doesn't require them.
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Use originalImg.width and originalImg.height for Image objects.
// naturalWidth/Height are more for <img> elements in the DOM.
// For an Image object, .width and .height give its intrinsic dimensions once loaded.
const width = originalImg.width;
const height = originalImg.height;
if (width === 0 || height === 0) {
// Handle unloaded or zero-size image
canvas.width = 0;
canvas.height = 0;
// Return an empty canvas, which is valid and displayable.
return canvas;
}
canvas.width = width;
canvas.height = height;
try {
// Draw the image onto the canvas to access its pixel data
ctx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
console.error("Error drawing image to canvas:", e);
// Fallback for drawing error: return a canvas with an error message
// This might happen if originalImg is not a valid image source (e.g. broken data URI)
ctx.fillStyle = "rgba(200, 200, 200, 1)"; // Light gray background
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.font = "14px Arial";
ctx.fillText("Error: Invalid image source.", width / 2, height / 2);
return canvas;
}
let imageData;
try {
imageData = ctx.getImageData(0, 0, width, height);
} catch (e) {
// This error often occurs due to CORS policy if the image is from a different origin
// and the server doesn't send appropriate CORS headers.
console.error("Error getting ImageData:", e);
// The original image is already drawn on 'canvas'. We'll overlay an error message.
ctx.fillStyle = "rgba(255, 0, 0, 0.6)"; // Semi-transparent red overlay
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "bold 16px Arial";
let errorMessage = "Error Processing Image";
if (e.name === 'SecurityError') {
errorMessage = "Cannot process: Image is from another domain (CORS issue).\nEnsure the image server allows cross-origin access.";
} else {
errorMessage = `Cannot process: ${e.name}\n${e.message}`;
}
const lines = errorMessage.split('\n');
const lineHeight = 20; // Approximate line height
const totalTextHeight = lines.length * lineHeight;
let startY = height / 2 - totalTextHeight / 2 + lineHeight / 2 + 5; // Adjusted for better centering
lines.forEach((line, index) => {
ctx.fillText(line, width / 2, startY + index * lineHeight);
});
return canvas; // Return the canvas with the original image + error overlay
}
const data = imageData.data; // Pixel data (RGBA sequence)
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
// 1. Convert image to grayscale
// We'll store grayscale values in a separate array for easier access during convolution.
const grayscaleData = new Uint8ClampedArray(width * height);
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Standard luminance calculation (coefficients for perceived brightness)
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
grayscaleData[i / 4] = Math.round(gray); // Store as integer 0-255
}
// 2. Define Sobel kernels for edge detection
const Gx = [ // Kernel for detecting horizontal edges
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
];
const Gy = [ // Kernel for detecting vertical edges
-1, -2, -1,
0, 0, 0,
1, 2, 1
];
// Parse parameters
const shouldInvert = invertOutput.toString().toLowerCase() === "true";
// A threshold must be a number between 0 and 255 (inclusive) to be active.
// Default threshold is -1, which means grayscale output (no thresholding).
const useThreshold = typeof threshold === 'number' && threshold >= 0 && threshold <= 255;
// 3. Apply Sobel operator (convolution)
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sumX = 0; // Gradient in X direction
let sumY = 0; // Gradient in Y direction
// Apply 3x3 kernel for Gx and Gy
for (let ky = -1; ky <= 1; ky++) { // Kernel Y offset
for (let kx = -1; kx <= 1; kx++) { // Kernel X offset
const Gx_val = Gx[(ky + 1) * 3 + (kx + 1)];
const Gy_val = Gy[(ky + 1) * 3 + (kx + 1)];
// Get coordinates of the neighbor pixel, clamping to image bounds
const currentPixelX = Math.min(width - 1, Math.max(0, x + kx));
const currentPixelY = Math.min(height - 1, Math.max(0, y + ky));
// Get grayscale value of the neighbor pixel
const grayVal = grayscaleData[currentPixelY * width + currentPixelX];
sumX += grayVal * Gx_val;
sumY += grayVal * Gy_val;
}
}
// Calculate gradient magnitude: sqrt(Gx^2 + Gy^2)
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
let finalValue;
if (useThreshold) {
// Binarize the output based on the threshold
if (magnitude > threshold) {
finalValue = shouldInvert ? 0 : 255; // Edge pixel
} else {
finalValue = shouldInvert ? 255 : 0; // Non-edge pixel
}
} else {
// Output grayscale gradient magnitude (clamped to 0-255)
finalValue = Math.min(255, Math.round(magnitude));
}
// Set the output pixel (R, G, B all same for grayscale/binary)
const N = (y * width + x) * 4; // Index for the current pixel in outputData
outputData[N] = finalValue; // Red
outputData[N + 1] = finalValue; // Green
outputData[N + 2] = finalValue; // Blue
outputData[N + 3] = 255; // Alpha (fully opaque)
}
}
// 4. Put the processed pixel data back onto the canvas
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes