You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, threshold = 50, lineColorStr = "0,0,0", backgroundColorStr = "255,255,255", lineThickness = 1) {
// Helper function to parse color strings (e.g., "255,0,0") into [R, G, B] arrays
const parseColorVal = (colorStr, defaultArr) => {
const parts = colorStr.split(',').map(c => parseInt(c, 10));
if (parts.length === 3 && parts.every(num => !isNaN(num) && num >= 0 && num <= 255)) {
return parts;
}
// If parsing fails, return the specified default color array
return defaultArr;
};
const lineColor = parseColorVal(lineColorStr, [0, 0, 0]); // Default to black if parsing fails
const backgroundColor = parseColorVal(backgroundColorStr, [255, 255, 255]); // Default to white if parsing fails
// Ensure lineThickness is an integer and at least 1
const actualLineThickness = Math.max(1, Math.floor(lineThickness));
const imgWidth = originalImg.width;
const imgHeight = originalImg.height;
// Handle cases where the image might not be loaded properly or is empty
if (imgWidth === 0 || imgHeight === 0) {
console.warn("Image has zero width or height.");
const emptyCanvas = document.createElement('canvas');
// Set canvas to a minimal 1x1 size if original dimensions are 0
emptyCanvas.width = imgWidth || 1;
emptyCanvas.height = imgHeight || 1;
const emptyCtx = emptyCanvas.getContext('2d');
if (emptyCtx) {
emptyCtx.fillStyle = `rgb(${backgroundColor[0]},${backgroundColor[1]},${backgroundColor[2]})`;
emptyCtx.fillRect(0,0,emptyCanvas.width, emptyCanvas.height);
}
return emptyCanvas;
}
// Create a temporary canvas to draw the image and get its pixel data
const canvas = document.createElement('canvas');
canvas.width = imgWidth;
canvas.height = imgHeight;
// Add willReadFrequently hint for potential performance optimization
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
let imageData;
try {
imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
// Handle potential security errors if the image is cross-origin
console.error("Could not get ImageData (e.g., cross-origin issue):", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = imgWidth || 150; // Fallback size
errorCanvas.height = imgHeight || 100; // Fallback size
const errCtx = errorCanvas.getContext('2d');
if (errCtx) {
errCtx.fillStyle = `rgb(${backgroundColor[0]},${backgroundColor[1]},${backgroundColor[2]})`;
errCtx.fillRect(0,0,errorCanvas.width, errorCanvas.height);
errCtx.fillStyle = `rgb(${lineColor[0]},${lineColor[1]},${lineColor[2]})`;
errCtx.font = "16px Arial";
errCtx.textAlign = "center";
errCtx.textBaseline = "middle";
errCtx.fillText("Error: Cannot process image.", errorCanvas.width/2, errorCanvas.height/2);
}
return errorCanvas;
}
const data = imageData.data;
const width = imgWidth;
const height = imgHeight;
// 1. Convert image to grayscale
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 formula for grayscale conversion
const gray = 0.2989 * r + 0.5870 * g + 0.1140 * b;
grayscaleData[i / 4] = gray;
}
// Prepare output ImageData, initialized with the background color
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
for (let i = 0; i < outputData.length; i += 4) {
outputData[i] = backgroundColor[0];
outputData[i + 1] = backgroundColor[1];
outputData[i + 2] = backgroundColor[2];
outputData[i + 3] = 255; // Opaque alpha
}
// Sobel kernels for edge detection
const sobelXKernel = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const sobelYKernel = [
[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]
];
// Calculate offsets for line thickness application
const thicknessStartOffset = -Math.floor((actualLineThickness - 1) / 2);
const thicknessEndOffset = Math.ceil((actualLineThickness - 1) / 2);
// Or simpler for centered square:
// const halfThick = Math.floor(actualLineThickness / 2); // (actualLineThickness-1)/2 for odd, actualLineThickness/2 for even?
// The startOffset/endOffset as define above are correct for N=1 (0,0), N=2 (0,1), N=3 (-1,1)
// 2. Apply Sobel operator and thresholding
// Iterate pixels, excluding a 1-pixel border where the kernel wouldn't fit
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0; // Gradient in X direction
let gy = 0; // Gradient in Y direction
// Apply Sobel kernels to the 3x3 neighborhood
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const K_val_x = sobelXKernel[ky + 1][kx + 1];
const K_val_y = sobelYKernel[ky + 1][kx + 1];
const pixelIndex = (y + ky) * width + (x + kx); // Index in 1D grayscale array
const grayVal = grayscaleData[pixelIndex];
gx += grayVal * K_val_x;
gy += grayVal * K_val_y;
}
}
// Calculate gradient magnitude
const magnitude = Math.sqrt(gx * gx + gy * gy);
// If magnitude exceeds threshold, it's an edge point
if (magnitude > threshold) {
// Draw the line pixel with specified thickness
for (let dy = thicknessStartOffset; dy <= thicknessEndOffset; dy++) {
for (let dx = thicknessStartOffset; dx <= thicknessEndOffset; dx++) {
const currentX = x + dx;
const currentY = y + dy;
// Check bounds to ensure thickened line pixels are within the image
if (currentY >= 0 && currentY < height && currentX >= 0 && currentX < width) {
const outputPixelIndex = (currentY * width + currentX) * 4;
outputData[outputPixelIndex] = lineColor[0]; // R
outputData[outputPixelIndex + 1] = lineColor[1]; // G
outputData[outputPixelIndex + 2] = lineColor[2]; // B
// Alpha (outputData[outputPixelIndex + 3]) is already 255
}
}
}
}
}
}
// Create the final output canvas and put the processed image data onto it
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outputCtx = outputCanvas.getContext('2d');
outputCtx.putImageData(outputImageData, 0, 0);
return outputCanvas;
}
Apply Changes