You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, censorColor = "#222222") {
// Create the final canvas to be returned
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const ctx = canvas.getContext('2d');
// Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0);
// Limit image size for OCR to avoid browser crashes and speed up processing
const MAX_OCR_DIMENSION = 1200;
let scale = 1;
let ocrCanvas = canvas;
if (canvas.width > MAX_OCR_DIMENSION || canvas.height > MAX_OCR_DIMENSION) {
scale = MAX_OCR_DIMENSION / Math.max(canvas.width, canvas.height);
ocrCanvas = document.createElement('canvas');
ocrCanvas.width = canvas.width * scale;
ocrCanvas.height = canvas.height * scale;
const ocrCtx = ocrCanvas.getContext('2d');
ocrCtx.drawImage(originalImg, 0, 0, ocrCanvas.width, ocrCanvas.height);
}
// Dynamically load Tesseract.js if not already present
if (typeof Tesseract === 'undefined') {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Initialize Tesseract Worker
const worker = await Tesseract.createWorker('eng');
// Perform Optical Character Recognition
const { data } = await worker.recognize(ocrCanvas);
await worker.terminate();
const lines = data.lines;
const keywordBBoxes = { name: [], dob: [], address: [] };
// Helper to translate bounding box back to original image scale
const adjustBBox = (b) => ({
x0: b.x0 / scale,
y0: b.y0 / scale,
x1: b.x1 / scale,
y1: b.y1 / scale
});
// Pass 1: Identify keywords and map their locations
for (const line of lines) {
for (const word of line.words) {
const cleanText = word.text.replace(/[^a-z]/gi, '').toLowerCase();
if (['name', 'first', 'last', 'full'].includes(cleanText)) {
keywordBBoxes.name.push(adjustBBox(word.bbox));
} else if (['dob', 'birth', 'date'].includes(cleanText)) {
keywordBBoxes.dob.push(adjustBBox(word.bbox));
} else if (['address', 'addr'].includes(cleanText)) {
keywordBBoxes.address.push(adjustBBox(word.bbox));
}
}
}
// Helper function used to check if a word is contextually placed next to or immediately below a keyword
const isRelatedLocally = (targetBbox, keywordBbox) => {
const height = keywordBbox.y1 - keywordBbox.y0;
const wordYCenter = (targetBbox.y0 + targetBbox.y1) / 2;
// Checks if the target is to the right of the keyword explicitly on the same line
const isRight = wordYCenter >= keywordBbox.y0 - 10 &&
wordYCenter <= keywordBbox.y1 + 10 &&
targetBbox.x0 > keywordBbox.x0;
// Checks if the target is placed on the line immediately below the keyword label
const isBelow = targetBbox.y0 >= keywordBbox.y1 - 5 &&
targetBbox.y0 <= keywordBbox.y1 + (height * 3) &&
targetBbox.x0 >= keywordBbox.x0 - (height * 5) &&
targetBbox.x0 <= keywordBbox.x1 + (height * 20);
return isRight || isBelow;
};
ctx.fillStyle = censorColor;
// RegEx patterns for standard Personal Identifiable Information details (PII)
const addressLine1Regex = /\b\d{1,5}\s+[a-z0-9\s]+\s+(st|street|ave|avenue|blvd|boulevard|rd|road|dr|drive|ln|lane|ct|court|apt|suite|unit|pl|place|hw|hwy|highway|pkwy)\b/i;
const poBoxRegex = /\bp\.?o\.?\s*box\s+\d+\b/i;
// Pass 2: Analyze & redact matched details
for (const line of lines) {
const lineText = line.text;
// Address checks (street address, po box, or zip code format states)
const hasAddress = addressLine1Regex.test(lineText) ||
poBoxRegex.test(lineText) ||
/\b[A-Z]{2}[,]?\s+\d{5}(-\d{4})?\b/.test(lineText);
// General Date checks
const hasMonth = /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\b/i.test(lineText);
const has4Digits = /\b\d{4}\b/.test(lineText);
for (const word of line.words) {
const wordText = word.text.trim();
const cleanWord = wordText.replace(/[^a-z]/gi, '').toLowerCase();
// Do not paint over structural document labels to maintain visibility of layout context
if (/^(name|first|last|dob|birth|date|address|addr|street|city|state|zip|sex|gender|height|weight|eyes|hair|class|exp|expires|issued)$/i.test(cleanWord)) {
continue;
}
let shouldCensor = false;
// 1. Redact Address lines completely
if (hasAddress) shouldCensor = true;
// 2. Redact strictly numerical Dates (e.g. DD/MM/YYYY)
if (/\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})\b/.test(wordText)) {
shouldCensor = true;
}
// 3. Redact descriptive string formatted dates (e.g. Month Day Year)
if (hasMonth && has4Digits && (/\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\b/i.test(wordText) || /\d/.test(wordText))) {
shouldCensor = true;
}
// 4. Redact numbers that resemble Zip Codes or SSNs
if (/\b\d{5}(-\d{4})?\b/.test(wordText) || /\b\d{3}-\d{2}-\d{4}\b/.test(wordText)) {
shouldCensor = true;
}
const adjustedBBox = adjustBBox(word.bbox);
// 5. Redact contextually via logical alignment (placed directly right or accurately below a label indicator)
for (const kb of keywordBBoxes.name) if (isRelatedLocally(adjustedBBox, kb)) shouldCensor = true;
for (const kb of keywordBBoxes.dob) if (isRelatedLocally(adjustedBBox, kb)) shouldCensor = true;
for (const kb of keywordBBoxes.address) if (isRelatedLocally(adjustedBBox, kb)) shouldCensor = true;
// Paint Censor block with minor 3px padding
if (shouldCensor) {
const { x0, y0, x1, y1 } = adjustedBBox;
ctx.fillRect(Math.max(0, x0 - 3), Math.max(0, y0 - 3), (x1 - x0) + 6, (y1 - y0) + 6);
}
}
}
return canvas;
}
Apply Changes