You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, charsAcross = "80", threshold = "128", invert = "false", fontSize = "10") {
const numCharsAcrossVal = parseInt(charsAcross) || 80;
const thresholdValue = parseFloat(threshold) || 128;
const invertOutput = String(invert).toLowerCase() === "true";
const outputFontSizeVal = parseInt(fontSize) || 10;
if (!originalImg || typeof originalImg.naturalWidth === 'undefined' || !originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Image is not fully loaded, is invalid, or has zero dimensions.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 350;
errorCanvas.height = 100;
const errorCtx = errorCanvas.getContext('2d');
errorCtx.fillStyle = '#fdd'; // Light red background
errorCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errorCtx.fillStyle = 'black';
errorCtx.font = '14px Arial';
errorCtx.textAlign = 'center';
errorCtx.textBaseline = 'middle';
errorCtx.fillText("Error: Image not loaded or invalid.", errorCanvas.width / 2, errorCanvas.height / 2 - 10);
errorCtx.fillText("Please provide a valid, loaded image.", errorCanvas.width / 2, errorCanvas.height / 2 + 10);
return errorCanvas;
}
const sourceCanvas = document.createElement('canvas');
const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
sourceCanvas.width = originalImg.naturalWidth;
sourceCanvas.height = originalImg.naturalHeight;
sourceCtx.drawImage(originalImg, 0, 0);
const imgWidth = sourceCanvas.width;
const imgHeight = sourceCanvas.height;
const typicalCharAspectRatio = 0.5; // Assumed width/height for a font character (e.g., 8px wide / 16px tall)
const numCharsDownVal = Math.max(1, Math.round(numCharsAcrossVal * (imgHeight / imgWidth) / typicalCharAspectRatio));
const cellWidthPx = imgWidth / numCharsAcrossVal;
const cellHeightPx = imgHeight / numCharsDownVal;
// Dimensions of the sub-region within an image cell that maps to one dot
const dotRegionWidthPx = cellWidthPx / 2; // Braille characters are 2 dots wide
const dotRegionHeightPx = cellHeightPx / 4; // Braille characters are 4 dots high
let imageData;
try {
imageData = sourceCtx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
console.error("Error getting image data (CORS issue or other):", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 350; errorCanvas.height = 150;
const errorCtx = errorCanvas.getContext('2d');
errorCtx.fillStyle = '#fdd';
errorCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errorCtx.fillStyle = 'black'; errorCtx.font = '14px Arial';
errorCtx.textAlign = 'center'; errorCtx.textBaseline = 'middle';
errorCtx.fillText("Error processing image data.", errorCanvas.width / 2, errorCanvas.height / 2 - 20);
errorCtx.fillText("This might be a CORS issue if", errorCanvas.width/2, errorCanvas.height/2);
errorCtx.fillText("the image is from another domain.", errorCanvas.width/2, errorCanvas.height/2 + 20);
return errorCanvas;
}
const pixels = imageData.data;
const brailleCharsMatrix = [];
for (let charY = 0; charY < numCharsDownVal; charY++) {
const rowChars = [];
for (let charX = 0; charX < numCharsAcrossVal; charX++) {
let brailleCode = 0; // Bitmask for Braille dots for U+28xx
// Iterate over the 8 dot positions within the Braille cell (2 columns x 4 rows)
for (let dotCol = 0; dotCol < 2; dotCol++) { // 0 for left, 1 for right column of dots
for (let dotRow = 0; dotRow < 4; dotRow++) { // 0 to 3 for rows of dots
// Calculate the image region for this specific dot
const regionXOrigin = charX * cellWidthPx + dotCol * dotRegionWidthPx;
const regionYOrigin = charY * cellHeightPx + dotRow * dotRegionHeightPx;
let sumLuminance = 0;
let numPixelsInRegion = 0;
// Define the pixel sampling loop bounds for this dot's region
// Ensure start coords are floored, end coords are ceiled, and within image bounds
const R_X_START = Math.floor(regionXOrigin);
const R_Y_START = Math.floor(regionYOrigin);
const R_X_END = Math.min(imgWidth, Math.ceil(regionXOrigin + dotRegionWidthPx));
const R_Y_END = Math.min(imgHeight, Math.ceil(regionYOrigin + dotRegionHeightPx));
for (let py = R_Y_START; py < R_Y_END; py++) {
for (let px = R_X_START; px < R_X_END; px++) {
// Pixels array is 1D: (y * width + x) * 4 channels (R,G,B,A)
const offset = (py * imgWidth + px) * 4;
const r = pixels[offset];
const g = pixels[offset + 1];
const b = pixels[offset + 2];
// Standard luminance calculation (Rec.709 formula)
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
sumLuminance += luminance;
numPixelsInRegion++;
}
}
// Calculate average luminance, or default if region was empty/outside image
// Default to background color (non-dot) if region empty.
const avgLuminance = (numPixelsInRegion > 0) ?
sumLuminance / numPixelsInRegion :
(invertOutput ? 0 : 255);
// Determine if dot is set based on threshold and invert logic
// Default: dark image part = dot (luminance < threshold).
// Inverted: light image part = dot (luminance > threshold).
let dotIsSet = (invertOutput) ? (avgLuminance > thresholdValue) : (avgLuminance < thresholdValue);
if (dotIsSet) {
// Braille dot mapping to bits (for U+2800 + bitmask)
// Dot Pattern | Bit | Value
// 1 * . | 0 | 0x01
// 2 * . | 1 | 0x02
// 3 * . | 2 | 0x04
// 4 . * | 3 | 0x08
// 5 . * | 4 | 0x10
// 6 . * | 5 | 0x20
// 7 * . | 6 | 0x40 (bottom-left)
// 8 . * | 7 | 0x80 (bottom-right)
let bitPosition = -1;
if (dotCol === 0) { // Left column of dots
if (dotRow === 0) bitPosition = 0; // Dot 1
else if (dotRow === 1) bitPosition = 1; // Dot 2
else if (dotRow === 2) bitPosition = 2; // Dot 3
else if (dotRow === 3) bitPosition = 6; // Dot 7
} else { // Right column of dots (dotCol === 1)
if (dotRow === 0) bitPosition = 3; // Dot 4
else if (dotRow === 1) bitPosition = 4; // Dot 5
else if (dotRow === 2) bitPosition = 5; // Dot 6
else if (dotRow === 3) bitPosition = 7; // Dot 8
}
if (bitPosition !== -1) {
brailleCode |= (1 << bitPosition);
}
}
}
}
rowChars.push(String.fromCharCode(0x2800 + brailleCode));
}
brailleCharsMatrix.push(rowChars);
}
// Render Braille characters to the output canvas
const outputCanvas = document.createElement('canvas');
const outputCtx = outputCanvas.getContext('2d');
// Use a font stack favoring those with good Braille character support
const fontFamily = '"DejaVu Sans Mono", Consolas, "Courier New", monospace';
outputCtx.font = `${outputFontSizeVal}px ${fontFamily}`;
// Measure character dimensions for canvas sizing
const testCharMetrics = outputCtx.measureText("⣿"); // A full Braille character (U+28FF)
let charDisplayWidth = testCharMetrics.width;
if (charDisplayWidth === 0) charDisplayWidth = outputFontSizeVal * 0.6; // Fallback width if measurement fails
// For line height, using the specified font size tends to be robust.
let charDisplayHeight = outputFontSizeVal;
outputCanvas.width = Math.max(1, charDisplayWidth * numCharsAcrossVal);
outputCanvas.height = Math.max(1, charDisplayHeight * numCharsDownVal);
// Re-apply font settings as canvas resize might reset them
outputCtx.font = `${outputFontSizeVal}px ${fontFamily}`;
outputCtx.textBaseline = "top"; // Align text rendering to the top
// Set background and foreground colors (dots are typically black on white)
outputCtx.fillStyle = "white"; // Canvas background
outputCtx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
outputCtx.fillStyle = "black"; // Braille dot color
for (let y = 0; y < numCharsDownVal; y++) {
for (let x = 0; x < numCharsAcrossVal; x++) {
outputCtx.fillText(brailleCharsMatrix[y][x], x * charDisplayWidth, y * charDisplayHeight);
}
}
return outputCanvas;
}
Apply Changes