You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
name = "UNSUB",
aliases = "NONE KNOWN",
description = "Sex: Unknown\nRace: Unknown\nAge: Approx. 30-40 years\nHeight: Approx. 5'10\" - 6'2\"\nWeight: Approx. 170-190 lbs\nHair: Unknown\nEyes: Unknown\nScars/Marks: May have distinguishing features not visible in photo.",
crime = "MULTIPLE FELONIES INCLUDING BUT NOT LIMITED TO:\nBANK ROBBERY, ARMED ASSAULT, INTERSTATE FLIGHT TO AVOID PROSECUTION",
rewardText = "REWARD OF UP TO $1,000,000\nFOR INFORMATION LEADING DIRECTLY TO THE ARREST OF THIS INDIVIDUAL",
cautionText = "CONSIDERED ARMED AND EXTREMELY DANGEROUS\nDO NOT ATTEMPT TO APPREHEND THIS INDIVIDUAL YOURSELF"
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const canvasWidth = 800;
const canvasHeight = 1100;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx.textBaseline = 'top'; // Easier Y coordinate management
// Background
ctx.fillStyle = '#FDF5E6'; // Old Lace / very light beige, common for aged paper
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// Border
ctx.strokeStyle = '#000000';
ctx.lineWidth = 5; // Outer thick border
ctx.strokeRect(10, 10, canvasWidth - 20, canvasHeight - 20);
ctx.lineWidth = 2; // Inner thin border
ctx.strokeRect(20, 20, canvasWidth - 40, canvasHeight - 40);
let currentY = 25; // Start currentY from top margin (inside borders)
const margin = 40; // Margin for content from the inner border edge
const contentWidth = canvasWidth - 2 * margin;
// 1. "WANTED" Header
let textBlockY = currentY + 35; // Add some top padding for the first element
ctx.font = 'bold 120px Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif';
ctx.fillStyle = '#A52A2A'; // Brownish Red (less bright than Firebrick)
ctx.textAlign = 'center';
ctx.fillText("WANTED", canvasWidth / 2, textBlockY);
currentY = textBlockY + 120; // Height of "WANTED" text (approx font size)
// 2. "BY THE..." Sub-Header
textBlockY = currentY + 10; // Spacing from "WANTED"
ctx.font = 'bold 22px "Times New Roman", Times, serif';
ctx.fillStyle = '#000000'; // Black
ctx.fillText("BY THE FEDERAL BUREAU OF INVESTIGATION", canvasWidth / 2, textBlockY);
currentY = textBlockY + 22 + 15; // Height of sub-header + spacing after it
// 3. Image Area
const imgMaxHeight = 300; // Max height for the suspect's image
const imgMaxWidth = contentWidth * 0.7; // Max width for the image, allowing some side margin
let imgHeight = originalImg.height;
let imgWidth = originalImg.width;
const aspectRatio = imgWidth / imgHeight;
// Scale image to fit constraints
if (imgHeight > imgMaxHeight) {
imgHeight = imgMaxHeight;
imgWidth = imgHeight * aspectRatio;
}
if (imgWidth > imgMaxWidth) {
imgWidth = imgMaxWidth;
imgHeight = imgWidth / aspectRatio;
}
const imgX = (canvasWidth - imgWidth) / 2; // Center the image
const imgY = currentY + 5; // Small space above image
ctx.fillStyle = '#FFFFFF'; // White "matting" behind photo
ctx.fillRect(imgX - 7, imgY - 7, imgWidth + 14, imgHeight + 14);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2; // Border for the photo
ctx.strokeRect(imgX - 5, imgY - 5, imgWidth + 10, imgHeight + 10);
ctx.drawImage(originalImg, imgX, imgY, imgWidth, imgHeight);
currentY = imgY + imgHeight + 10 + 15; // Y below image border + spacing
// Text Fields Section Helper Function
const labelFontSize = 18;
const valueFontSize = 16;
const fieldLineSpacing = valueFontSize * 1.3; // Line height for multi-line values
const labelFont = '"Arial Black", Gadget, sans-serif';
const valueFont = '"Courier New", Courier, monospace'; // "Typewriter" style for values
const valueIndent = margin + 170; // X position where value text starts (after label)
const maxValueWidth = contentWidth - (valueIndent - margin); // Max width for value text
function drawLabelAndValue(label, valueString, startDrawY) {
let currentLineY = startDrawY;
ctx.textAlign = 'left';
ctx.font = `bold ${labelFontSize}px ${labelFont}`;
ctx.fillStyle = '#000000';
ctx.fillText(label.toUpperCase() + ":", margin, currentLineY);
ctx.font = `normal ${valueFontSize}px ${valueFont}`;
const valueLines = valueString.split('\n'); // Handle explicit newlines in value
for (let i = 0; i < valueLines.length; i++) {
const line = valueLines[i];
const words = line.split(' '); // For word wrapping
let textLine = '';
for (let n = 0; n < words.length; n++) {
const testLine = textLine + words[n] + ' ';
if (ctx.measureText(testLine).width > maxValueWidth && textLine !== '') {
ctx.fillText(textLine.trim(), valueIndent, currentLineY);
textLine = words[n] + ' ';
currentLineY += fieldLineSpacing;
} else {
textLine = testLine;
}
}
ctx.fillText(textLine.trim(), valueIndent, currentLineY);
if (i < valueLines.length - 1) { // If more pre-split lines follow
currentLineY += fieldLineSpacing;
}
}
const labelBottom = startDrawY + labelFontSize;
const valueBottom = currentLineY + valueFontSize; // Bottom of last line of value text
return Math.max(labelBottom, valueBottom) + fieldLineSpacing * 0.7; // Y for next field, with spacing
}
currentY += 10; // Space before first field
currentY = drawLabelAndValue("NAME", name, currentY);
currentY = drawLabelAndValue("ALIASES", aliases, currentY);
// Description field (special handling for full width value)
ctx.textAlign = 'left';
ctx.font = `bold ${labelFontSize}px ${labelFont}`;
ctx.fillStyle = '#000000';
ctx.fillText("DESCRIPTION:", margin, currentY);
currentY += labelFontSize + 8; // Move Y below "DESCRIPTION:" label text + small gap
ctx.font = `normal ${valueFontSize}px ${valueFont}`;
const descriptionLines = description.split('\n');
let descInnerY = currentY;
for (let i = 0; i < descriptionLines.length; i++) {
const line = descriptionLines[i];
const words = line.split(' ');
let textLine = '';
for (let n = 0; n < words.length; n++) {
const testLine = textLine + words[n] + ' ';
// For description, value text uses full contentWidth
if (ctx.measureText(testLine).width > contentWidth && textLine !== '') {
ctx.fillText(textLine.trim(), margin, descInnerY);
textLine = words[n] + ' ';
descInnerY += fieldLineSpacing;
} else {
textLine = testLine;
}
}
ctx.fillText(textLine.trim(), margin, descInnerY);
if (i < descriptionLines.length - 1) {
descInnerY += fieldLineSpacing;
}
}
currentY = descInnerY + valueFontSize + fieldLineSpacing * 0.7; // Y for next element
currentY = drawLabelAndValue("CRIME", crime, currentY);
// Helper for centered, multi-line (from \n) text blocks (e.g. Reward, Caution)
function drawCenteredTextBlock(text, startBlockY, fontStyle, fontSize, fontFamily, color, lineHeightFactor = 1.2) {
ctx.font = `${fontStyle} ${fontSize}px ${fontFamily}`;
ctx.fillStyle = color;
ctx.textAlign = 'center';
const lines = text.toUpperCase().split('\n');
let lineDrawY = startBlockY;
for(const line of lines) {
ctx.fillText(line.trim(), canvasWidth / 2, lineDrawY);
lineDrawY += fontSize * lineHeightFactor;
}
return lineDrawY - (fontSize * (lineHeightFactor - 1)); // Return Y at conceptual bottom of last text line
}
currentY += 20; // Extra space before reward
currentY = drawCenteredTextBlock(rewardText, currentY, 'bold', 26, '"Times New Roman", Times, serif', '#A52A2A', 1.2);
currentY += 20; // Extra space
currentY = drawCenteredTextBlock(cautionText, currentY, 'bold', 20, 'Arial, sans-serif', '#000000', 1.2);
// Footer Text
const footerFontSize = 11; // Slightly smaller for dense footer
const footerFontFamily = '"Verdana", Geneva, sans-serif';
const footerTopMargin = 30;
const footerLineHeight = footerFontSize * 1.25;
const footerTextContent = "IF YOU HAVE ANY INFORMATION CONCERNING THIS PERSON, PLEASE CONTACT YOUR LOCAL FBI OFFICE OR THE NEAREST U.S. EMBASSY OR CONSULATE. YOU CAN ALSO SUBMIT A TIP ONLINE AT TIPS.FBI.GOV.";
const footerWords = footerTextContent.split(' ');
const tempFooterLines = [];
const originalCtxFontForMeasure = ctx.font;
ctx.font = `bold ${footerFontSize}px ${footerFontFamily}`; // Use footer font for measuring
let currentLineForFooter = "";
for (const word of footerWords) {
const testLine = currentLineForFooter + word + " ";
if (ctx.measureText(testLine).width > contentWidth * 0.95 && currentLineForFooter !== "") {
tempFooterLines.push(currentLineForFooter.trim());
currentLineForFooter = word + " ";
} else {
currentLineForFooter = testLine;
}
}
tempFooterLines.push(currentLineForFooter.trim());
ctx.font = originalCtxFontForMeasure;
let footerStartY = canvasHeight - 20 - (tempFooterLines.length * footerLineHeight) - (footerLineHeight * 0.5); // Position from inner border-bottom
// Ensure footer doesn't overlap content above it if content is very long
if (footerStartY < currentY + footerTopMargin) {
footerStartY = currentY + footerTopMargin;
}
currentY = drawCenteredTextBlock(tempFooterLines.join('\n'), footerStartY, 'bold', footerFontSize, footerFontFamily, '#000000', 1.25);
// FBI Seal (simplified placeholder)
const sealSize = 45;
const sealMarginFromLeft = margin;
const sealBottomRefY = canvasHeight - 20 - 10; // Above inner border bottom + some padding
const sealTopY = sealBottomRefY - sealSize;
// Check if there's a clear spot for the seal (e.g., not overlapping image, not too close to footer start)
const spaceBelowImage = imgY + imgHeight + 20;
const spaceAboveFooter = footerStartY - sealSize - 15; // Ensure seal ends 15px above footer start
if (sealTopY > spaceBelowImage && sealTopY < spaceAboveFooter) {
ctx.beginPath();
const sealCenterX = sealMarginFromLeft + sealSize / 2;
const sealCenterY = sealTopY + sealSize / 2;
ctx.arc(sealCenterX, sealCenterY, sealSize / 2, 0, 2 * Math.PI);
ctx.fillStyle = '#002868'; // FBI Blue
ctx.fill();
// Text inside seal
ctx.font = 'bold 11px Arial';
ctx.fillStyle = '#FFFFFF'; // White text
ctx.textAlign = 'center';
// Adjust Y for two lines of text, considering textBaseline='top'
const textLineHeightSeal = 11;
ctx.fillText("FBI", sealCenterX, sealCenterY - textLineHeightSeal * 0.8);
ctx.fillText("SEAL", sealCenterX, sealCenterY + textLineHeightSeal * 0.3);
}
return canvas;
}
Apply Changes