You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Retouches an image to make it suitable for laser engraving.
* This involves converting the image to grayscale, adjusting brightness and contrast,
* applying optional sharpening, and then converting it to pure black and white
* using dithering or a simple threshold.
*
* @param {Image} originalImg The original Image object to process.
* @param {number} [brightness=0] Brightness adjustment. Recommended range: -100 to 100.
* @param {number} [contrast=0] Contrast adjustment. Recommended range: -100 to 100.
* @param {number} [sharpen=0] Sharpening amount. Recommended range: 0 to 100.
* @param {string} [ditheringMethod='floyd-steinberg'] The quantization method. Options: 'none', 'bayer', 'floyd-steinberg'.
* @param {number} [threshold=128] The threshold for black/white conversion (0-255), only used if ditheringMethod is 'none'.
* @returns {HTMLCanvasElement} A canvas element with the processed image.
*/
function processImage(originalImg, brightness = 0, contrast = 0, sharpen = 0, ditheringMethod = 'floyd-steinberg', threshold = 128) {
// 1. Setup Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
canvas.width = width;
canvas.height = height;
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// Create a floating point array for more precise intermediate calculations
const grayData = new Float32Array(width * height);
// 2. Pass 1: Grayscale, Brightness & Contrast
// The contrast formula is taken from the CamanJS library.
const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast));
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
let gray = 0.299 * r + 0.587 * g + 0.114 * b;
// Apply brightness
gray += brightness;
// Apply contrast
gray = contrastFactor * (gray - 128) + 128;
// Clamp the value and store it
grayData[i / 4] = Math.max(0, Math.min(255, gray));
}
// 3. Pass 2: Sharpening (if requested)
if (sharpen > 0) {
const sharpenAmount = sharpen / 100;
const sharpenKernel = [
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]
];
const identityKernel = [
[0, 0, 0],
[0, 1, 0],
[0, 0, 0]
];
// Create a blended kernel based on the sharpen amount
const finalKernel = identityKernel.map((row, y) =>
row.map((val, x) => (1 - sharpenAmount) * val + sharpenAmount * sharpenKernel[y][x])
);
const grayCopy = new Float32Array(grayData); // Work on a copy
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sum = 0;
const i = y * width + x;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
sum += grayCopy[(y + ky) * width + (x + kx)] * finalKernel[ky + 1][kx + 1];
}
}
grayData[i] = sum;
}
}
}
// 4. Pass 3: Quantization (Dithering or Threshold) and Final Write
const method = ditheringMethod.toLowerCase();
if (method === 'floyd-steinberg') {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = y * width + x;
const dataIndex = i * 4;
const oldPixel = Math.max(0, Math.min(255, grayData[i]));
const newPixel = oldPixel < 128 ? 0 : 255;
const quantError = oldPixel - newPixel;
// Set the final pixel value in the image data
data[dataIndex] = data[dataIndex + 1] = data[dataIndex + 2] = newPixel;
// Distribute the error to neighboring pixels
if (x + 1 < width) grayData[i + 1] += quantError * 7 / 16;
if (y + 1 < height) {
if (x > 0) grayData[i + width - 1] += quantError * 3 / 16;
grayData[i + width] += quantError * 5 / 16;
if (x + 1 < width) grayData[i + width + 1] += quantError * 1 / 16;
}
}
}
} else if (method === 'bayer') {
const bayerMatrix = [
[1, 9, 3, 11],
[13, 5, 15, 7],
[4, 12, 2, 10],
[16, 8, 14, 6]
];
const matrixSize = 4;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = y * width + x;
const dataIndex = i * 4;
const grayValue = Math.max(0, Math.min(255, grayData[i]));
const bayerThreshold = ((bayerMatrix[y % matrixSize][x % matrixSize] - 1) / 16) * 255;
const newPixel = grayValue > bayerThreshold ? 255 : 0;
data[dataIndex] = data[dataIndex + 1] = data[dataIndex + 2] = newPixel;
}
}
} else { // 'none' or any other value defaults to simple threshold
for (let i = 0; i < grayData.length; i++) {
const dataIndex = i * 4;
const grayValue = Math.max(0, Math.min(255, grayData[i]));
const newPixel = grayValue > threshold ? 255 : 0;
data[dataIndex] = data[dataIndex + 1] = data[dataIndex + 2] = newPixel;
}
}
// 5. Put the modified data back onto the canvas
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes