You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, targetLang = 'en') {
/**
* Dynamically loads a script if it's not already present in the document.
* @param {string} url The URL of the script to load.
* @returns {Promise<void>} A promise that resolves when the script is loaded.
*/
const loadScript = (url) => {
return new Promise((resolve, reject) => {
// Check if Tesseract is already available on the window object
if (window.Tesseract) {
return resolve();
}
// Check if the script tag already exists
if (document.querySelector(`script[src="${url}"]`)) {
// If it exists but Tesseract is not ready, wait for it
const interval = setInterval(() => {
if (window.Tesseract) {
clearInterval(interval);
resolve();
}
}, 100);
return;
}
const script = document.createElement('script');
script.src = url;
script.onload = () => resolve();
script.onerror = (err) => reject(new Error(`Script load error for ${url}: ${err}`));
document.head.appendChild(script);
});
};
/**
* Maps 3-letter Tesseract language codes to 2-letter ISO 639-1 codes for the translation API.
* @param {string} tessLang The 3-letter language code from Tesseract (e.g., 'eng').
* @returns {string|null} The 2-letter code (e.g., 'en') or null if not found.
*/
const mapLangCode = (tessLang) => {
const map = {
'eng': 'en', 'rus': 'ru', 'deu': 'de', 'fra': 'fr', 'spa': 'es', 'ita': 'it',
'por': 'pt', 'nld': 'nl', 'jpn': 'ja', 'chi_sim': 'zh-CN', 'chi_tra': 'zh-TW', 'kor': 'ko'
};
return map[tessLang] || null;
};
/**
* Gets an average color from the border of a bounding box for inpainting.
* @param {CanvasRenderingContext2D} ctx The canvas context.
* @param {object} bbox The bounding box object from Tesseract.
* @param {number} margin The distance from the box to sample pixels.
* @returns {string} An CSS rgb color string.
*/
const getAverageBorderColor = (ctx, bbox, margin = 3) => {
const { x0, y0, x1, y1 } = bbox;
const { width, height } = ctx.canvas;
const pointsData = [];
const samplePoints = [
{x: x0 + bbox.width / 2, y: y0 - margin}, {x: x0 + bbox.width / 2, y: y1 + margin},
{x: x0 - margin, y: y0 + bbox.height / 2}, {x: x1 + margin, y: y0 + bbox.height / 2},
{x: x0 - margin, y: y0 - margin}, {x: x1 + margin, y: y0 - margin},
{x: x0 - margin, y: y1 + margin}, {x: x1 + margin, y: y1 + margin}
];
for (const p of samplePoints) {
if (p.x > 0 && p.x < width && p.y > 0 && p.y < height) {
pointsData.push(ctx.getImageData(p.x, p.y, 1, 1).data);
}
}
if (pointsData.length === 0) return '#FFFFFF'; // Fallback
let r = 0, g = 0, b = 0;
pointsData.forEach(p => { r += p[0]; g += p[1]; b += p[2]; });
r = Math.floor(r / pointsData.length);
g = Math.floor(g / pointsData.length);
b = Math.floor(b / pointsData.length);
return `rgb(${r}, ${g}, ${b})`;
};
/**
* Draws wrapped text, attempting to fit it within a given bounding box.
* @param {CanvasRenderingContext2D} ctx The canvas context.
* @param {string} text The text to draw.
* @param {object} bbox The bounding box to draw inside.
* @param {string} bgColor The background color, used to determine text color.
*/
const drawTextInBox = (ctx, text, bbox, bgColor) => {
const colorMatch = bgColor.match(/rgb\((\d+), (\d+), (\d+)\)/);
let r = 0, g = 0, b = 0;
if (colorMatch) { [_, r, g, b] = colorMatch.map(Number); }
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
ctx.fillStyle = luminance > 0.5 ? '#111111' : '#FFFFFF';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const fontFamily = 'Arial, "Helvetica Neue", Helvetica, sans-serif';
let fontSize = bbox.height;
let lines = [];
// Iteratively find a font size that allows the text to fit
while (fontSize > 6) {
ctx.font = `bold ${fontSize}px ${fontFamily}`;
const words = text.split(' ');
lines = [];
if (words.length > 0) {
let currentLine = words[0] || '';
for (let i = 1; i < words.length; i++) {
const word = words[i];
const testLine = currentLine + ' ' + word;
if (ctx.measureText(testLine).width > bbox.width && i > 0) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
lines.push(currentLine);
}
if (lines.length * fontSize < bbox.height) break;
fontSize -= 1;
}
const lineHeight = fontSize * 1.1;
const totalTextHeight = lines.length * lineHeight;
const startY = bbox.y0 + (bbox.height - totalTextHeight) / 2 + lineHeight / 2 - (lineHeight * 0.1);
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], bbox.x0 + bbox.width / 2, startY + (i * lineHeight));
}
};
// --- Main Function Logic ---
// 1. Load Tesseract.js library
try {
await loadScript('https://unpkg.com/tesseract.js@5/dist/tesseract.min.js');
} catch (error) {
console.error(error);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = originalImg.naturalWidth || 500;
errorCanvas.height = originalImg.naturalHeight || 300;
const errorCtx = errorCanvas.getContext('2d');
errorCtx.drawImage(originalImg, 0, 0);
errorCtx.fillStyle = 'rgba(255, 0, 0, 0.7)';
errorCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errorCtx.fillStyle = 'white';
errorCtx.textAlign = 'center';
errorCtx.font = '24px Arial';
errorCtx.fillText('Error: Could not load OCR library.', errorCanvas.width/2, errorCanvas.height/2);
return errorCanvas;
}
// 2. Setup canvas and OCR worker
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
ctx.drawImage(originalImg, 0, 0);
const worker = await Tesseract.createWorker('eng+rus+deu+fra+spa', 1, {
logger: m => console.log(m) // Logs progress to the console
});
// 3. Perform OCR
const { data: { lines } } = await worker.recognize(canvas);
// 4. Process each detected line of text
for (const line of lines) {
if (line.confidence < 50 || line.text.trim().length <= 1) continue;
const textToTranslate = line.text;
const sourceLangIso2 = mapLangCode(line.language);
if (!sourceLangIso2 || sourceLangIso2 === targetLang) continue;
let translatedText = textToTranslate;
try {
const apiUrl = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(textToTranslate)}&langpair=${sourceLangIso2}|${targetLang}`;
const response = await fetch(apiUrl);
if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
const translationData = await response.json();
if (translationData.responseStatus === 200) {
translatedText = translationData.responseData.translatedText;
} else {
console.warn(`Translation service issue for: "${textToTranslate}"`, translationData.responseDetails);
}
} catch (error) {
console.error('Translation API error:', error);
continue; // Skip this line on error
}
if (translatedText.trim().toLowerCase() === textToTranslate.trim().toLowerCase()) continue;
// 5. Inpaint (erase original text)
const bbox = line.bbox;
const avgColor = getAverageBorderColor(ctx, bbox);
ctx.fillStyle = avgColor;
ctx.fillRect(bbox.x0 - 2, bbox.y0 - 2, bbox.width + 4, bbox.height + 4);
// 6. Draw translated text
drawTextInBox(ctx, translatedText, bbox, avgColor);
}
// 7. Cleanup
await worker.terminate();
// 8. Return result canvas
return canvas;
}
Apply Changes