You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, pixelSize = 8, paletteColorsStr = "maroon,darkblue,beige,saddlebrown,darkgreen,black") {
// Ensure originalImg is valid and loaded
if (!originalImg || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Persian Carpet Filter: Invalid image or image not loaded properly.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 1;
errorCanvas.height = 1;
// Optionally, draw a small indicator on the error canvas
// const errCtx = errorCanvas.getContext('2d');
// errCtx.fillStyle = 'red';
// errCtx.fillRect(0,0,1,1);
return errorCanvas;
}
pixelSize = Math.max(1, Math.floor(pixelSize));
const imgWidth = originalImg.naturalWidth;
const imgHeight = originalImg.naturalHeight;
// 1. Parse palette colors
let parsedPalette = [];
// Temporary canvas for converting color strings to RGB values
const tempColorCanvas = document.createElement('canvas');
tempColorCanvas.width = 1;
tempColorCanvas.height = 1;
// Use willReadFrequently for performance optimization if available, though for few colors it's minor.
const tempColorCtx = tempColorCanvas.getContext('2d', { willReadFrequently: true });
if (paletteColorsStr && typeof paletteColorsStr === 'string' && paletteColorsStr.trim() !== "") {
const colorStrings = paletteColorsStr.split(',').map(s => s.trim()).filter(s => s.length > 0);
for (const colorStr of colorStrings) {
// Clear previous color draw (important for transparent colors or if fillStyle fails silently)
tempColorCtx.clearRect(0, 0, 1, 1);
tempColorCtx.fillStyle = colorStr;
tempColorCtx.fillRect(0, 0, 1, 1);
const data = tempColorCtx.getImageData(0, 0, 1, 1).data;
// data will be [R, G, B, A]. We only need R, G, B.
// If colorStr is invalid, fillStyle usually defaults to black.
parsedPalette.push([data[0], data[1], data[2]]);
}
}
// If palette is empty after parsing (e.g., empty string, only commas, or all colors unparseable - though latter is unlikely)
// Fallback to a predefined default palette
if (parsedPalette.length === 0) {
console.warn("Persian Carpet Filter: Palette is empty or invalid. Using a default internal palette.");
parsedPalette = [
[128, 0, 0], // maroon
[0, 0, 139], // darkblue
[245, 245, 220], // beige
[139, 69, 19], // saddlebrown
[0, 100, 0], // darkgreen
[0, 0, 0] // black
];
}
// 2. Input canvas to get pixel data from the original image
const inputCanvas = document.createElement('canvas');
inputCanvas.width = imgWidth;
inputCanvas.height = imgHeight;
const inputCtx = inputCanvas.getContext('2d', { willReadFrequently: true });
inputCtx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
let imageData;
try {
imageData = inputCtx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
console.error("Persian Carpet Filter: Could not getImageData. This might be due to a tainted canvas (cross-origin image without CORS).", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = imgWidth || 1;
errorCanvas.height = imgHeight || 1;
return errorCanvas; // Return an empty canvas of original size or 1x1
}
const data = imageData.data;
// 3. Output canvas (the one to be returned)
const outputCanvas = document.createElement('canvas');
outputCanvas.width = imgWidth;
outputCanvas.height = imgHeight;
const outputCtx = outputCanvas.getContext('2d');
// Ensure sharp, pixelated blocks without anti-aliasing from browser defaults
outputCtx.imageSmoothingEnabled = false;
// 4. Process image in blocks
for (let y = 0; y < imgHeight; y += pixelSize) {
for (let x = 0; x < imgWidth; x += pixelSize) {
let sumR = 0, sumG = 0, sumB = 0, count = 0;
// Define the actual block dimensions, handling edges
const blockWidth = Math.min(pixelSize, imgWidth - x);
const blockHeight = Math.min(pixelSize, imgHeight - y);
// Calculate average color of the block in the original image
for (let blockY = 0; blockY < blockHeight; blockY++) {
for (let blockX = 0; blockX < blockWidth; blockX++) {
const currentY = y + blockY;
const currentX = x + blockX;
const i = (currentY * imgWidth + currentX) * 4;
sumR += data[i];
sumG += data[i+1];
sumB += data[i+2];
count++;
}
}
if (count === 0) continue; // Should not happen with correct blockWidth/Height logic
const avgR = sumR / count;
const avgG = sumG / count;
const avgB = sumB / count;
// Find the closest color in the palette
let closestColor = parsedPalette[0]; // Default to first palette color
let minDist = Infinity;
for (const paletteRgb of parsedPalette) {
// Euclidean distance in RGB space
const dist = Math.sqrt(
Math.pow(avgR - paletteRgb[0], 2) +
Math.pow(avgG - paletteRgb[1], 2) +
Math.pow(avgB - paletteRgb[2], 2)
);
if (dist < minDist) {
minDist = dist;
closestColor = paletteRgb;
}
}
// Fill the block on the output canvas with the closest palette color
outputCtx.fillStyle = `rgb(${closestColor[0]},${closestColor[1]},${closestColor[2]})`;
outputCtx.fillRect(x, y, blockWidth, blockHeight);
}
}
return outputCanvas;
}
Apply Changes