You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, punctuationChars = "`.'`,-_^~:;/\\!|()[]{}<>?=+\"*#$%&@", fontSize = 10, invertColors = "false", contrast = 1.0, brightness = 0.0, outputWidthChars = 100) {
// Parameter parsing
const chars = String(punctuationChars);
const parsedFontSize = Math.max(1, Number(fontSize) || 10); // Ensure font size is at least 1
const parsedInvert = String(invertColors).toLowerCase() === 'true';
let parsedContrast = Number(contrast);
if (isNaN(parsedContrast)) parsedContrast = 1.0; // Default contrast
parsedContrast = Math.max(0, parsedContrast); // Contrast should not be negative
let parsedBrightness = Number(brightness); // expecting -1.0 to 1.0
if (isNaN(parsedBrightness)) parsedBrightness = 0.0;
parsedBrightness = Math.max(-1.0, Math.min(1.0, parsedBrightness)); // Clamp brightness
const parsedWidthInChars = Math.max(10, Number(outputWidthChars) || 100);
// Handle cases of invalid original image dimensions
if (!originalImg || originalImg.width === 0 || originalImg.height === 0) {
console.error("Original image is invalid or has zero width or height.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = parsedWidthInChars * parsedFontSize * 0.6; // Approx width
errorCanvas.height = parsedFontSize * 5; // Some height for error message
const eCtx = errorCanvas.getContext('2d');
eCtx.fillStyle = 'red';
eCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
eCtx.fillStyle = 'white';
eCtx.font = `${parsedFontSize}px monospace`;
eCtx.textAlign = 'center';
eCtx.textBaseline = 'middle';
eCtx.fillText("Error: Invalid Image", errorCanvas.width / 2, errorCanvas.height / 2);
return errorCanvas;
}
// Create a canvas to draw the original image for pixel manipulation
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = originalImg.width;
sourceCanvas.height = originalImg.height;
const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
sourceCtx.drawImage(originalImg, 0, 0);
// Apply contrast and brightness adjustments to the source canvas
if (parsedContrast !== 1.0 || parsedBrightness !== 0.0) {
const imageData = sourceCtx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height);
const data = imageData.data;
// Brightness value is mapped from -1.0->1.0 to an offset for 0-1 normalized pixel values
const brightnessOffset = parsedBrightness;
for (let i = 0; i < data.length; i += 4) {
// Normalize, apply brightness, then contrast, then clamp & denormalize
for (let j = 0; j < 3; j++) { // R, G, B channels
let val = data[i + j] / 255.0; // Normalize to 0-1
val += brightnessOffset; // Apply brightness
// Apply contrast: val = (val - 0.5) * contrast + 0.5
val = (val - 0.5) * parsedContrast + 0.5;
val = Math.max(0, Math.min(1, val)); // Clamp to 0-1
data[i + j] = val * 255.0; // Denormalize
}
}
sourceCtx.putImageData(imageData, 0, 0);
}
// Determine dimensions of the output "character grid"
const numCharsAcross = parsedWidthInChars;
// Measure character dimensions using a temporary canvas
const tempCtx = document.createElement('canvas').getContext('2d');
tempCtx.font = `${parsedFontSize}px monospace`; // Monospace font is crucial
const metrics = tempCtx.measureText("M"); // 'M' is often used for representative width
const charPixelWidth = metrics.width;
const charPixelHeight = parsedFontSize; // Font size defines cell height
const imageAspectRatio = originalImg.width / originalImg.height;
// Calculate number of characters down to maintain image aspect ratio using character cell dimensions
const numCharsDown = Math.max(1, Math.floor(numCharsAcross / imageAspectRatio * (charPixelHeight / charPixelWidth)));
const blockWidthOnSource = sourceCanvas.width / numCharsAcross;
const blockHeightOnSource = sourceCanvas.height / numCharsDown;
// Create output canvas
const outputCanvas = document.createElement('canvas');
outputCanvas.width = Math.max(1, Math.floor(numCharsAcross * charPixelWidth));
outputCanvas.height = Math.max(1, Math.floor(numCharsDown * charPixelHeight));
const outCtx = outputCanvas.getContext('2d');
// Set background and foreground (text) colors based on invertColors
outCtx.fillStyle = parsedInvert ? 'black' : 'white';
outCtx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
outCtx.font = `${parsedFontSize}px monospace`;
outCtx.fillStyle = parsedInvert ? 'white' : 'black';
outCtx.textBaseline = 'top'; // Consistent text rendering aligned to top of cell
const numPunctuationGlyphs = chars.length;
if (numPunctuationGlyphs === 0) {
console.error("Punctuation character string is empty!");
outCtx.fillStyle = 'red';
outCtx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
outCtx.fillStyle = 'white';
outCtx.textAlign = 'center';
outCtx.textBaseline = 'middle';
outCtx.fillText("Error: No punctuation characters.", outputCanvas.width / 2, outputCanvas.height / 2);
return outputCanvas;
}
for (let y = 0; y < numCharsDown; y++) {
for (let x = 0; x < numCharsAcross; x++) {
const sx = Math.floor(x * blockWidthOnSource);
const sy = Math.floor(y * blockHeightOnSource);
// Use Math.ceil for sw, sh to cover fractional parts, but clip to canvas bounds
const sw = Math.min(Math.ceil(blockWidthOnSource), sourceCanvas.width - sx);
const sh = Math.min(Math.ceil(blockHeightOnSource), sourceCanvas.height - sy);
if (sw <= 0 || sh <= 0) continue;
const blockImageData = sourceCtx.getImageData(sx, sy, sw, sh);
const blockData = blockImageData.data;
let totalBrightness = 0;
let pixelCount = 0;
for (let i = 0; i < blockData.length; i += 4) {
const r = blockData[i];
const g = blockData[i + 1];
const b = blockData[i + 2];
// Standard luminance calculation (grayscale)
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
totalBrightness += brightness;
pixelCount++;
}
const averageBrightness = pixelCount > 0 ? totalBrightness / pixelCount : (parsedInvert ? 0 : 255);
// Map brightness to a character index.
// Assumes `chars` string is sorted from sparse (light) to dense (dark).
// High brightness (e.g., 255, white) maps to sparse characters (low index).
// Low brightness (e.g., 0, black) maps to dense characters (high index).
let charIdxNormalized = 1.0 - (averageBrightness / 255.0); // 0.0 for white, 1.0 for black
// If `invertColors` is true (dark mode: white text on black BG),
// then bright image areas should map to dense characters (which become bright white text),
// and dark image areas to sparse characters (fainter white text).
// This means the density mapping effectively flips.
if (parsedInvert) {
charIdxNormalized = 1.0 - charIdxNormalized; // Invert mapping for dark mode
}
const charIndex = Math.round(charIdxNormalized * (numPunctuationGlyphs - 1));
const selectedChar = chars[Math.max(0, Math.min(numPunctuationGlyphs - 1, charIndex))];
const drawX = x * charPixelWidth;
const drawY = y * charPixelHeight;
outCtx.fillText(selectedChar, drawX, drawY);
}
}
return outputCanvas;
}
Apply Changes