You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
passportType = "P",
countryCode = "UTO", // Utopian States of Oceania
passportNumber = "L898902C",
surname = "DOE",
givenNames = "JANE ANNA",
nationality = "UTOPIAN",
dateOfBirth = "01 JAN 1990", // Display format: DD MMM YYYY
sex = "F",
placeOfBirth = "CAPITAL CITY",
dateOfIssue = "04 JUL 2023",
dateOfExpiry = "04 JUL 2033",
issuingAuthority = "MINISTRY OF STATE AFFAIRS",
countryFullName = "UTOPIAN STATES OF OCEANIA",
personalNumber = "" // Optional, for MRZ
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const W = 800; // Width of the passport page
const H = 560; // Height of the passport page
canvas.width = W;
canvas.height = H;
// Ensure image is decoded, especially if it was just new Image() and src set
// However, assuming originalImg is a fully loaded Image object as per common interpretation
try {
if (!(originalImg.complete && originalImg.naturalHeight !== 0)) {
await originalImg.decode();
}
} catch (error) {
console.error("Image decoding failed:", error);
// Fallback: draw error message in photo area if image cannot be decoded
// This part will be handled later when drawing the photo if needed
}
// 1. Background
ctx.fillStyle = '#F0F8FF'; // AliceBlue, a very light blue, good for official docs
ctx.fillRect(0, 0, W, H);
// Optional: A thin border for aesthetics
ctx.strokeStyle = '#B0C4DE'; // LightSteelBlue
ctx.lineWidth = 2;
ctx.strokeRect(5, 5, W - 10, H - 10);
// Placeholder for a subtle background pattern (e.g. country emblem repeated)
// This is complex; for now, a simple background.
// 2. Header Text
ctx.fillStyle = '#003366'; // Dark blue, common official color
ctx.textAlign = 'center';
ctx.font = 'bold 28px "Times New Roman", serif';
ctx.fillText("PASSPORT", W / 2, 45);
ctx.font = 'bold 20px "Times New Roman", serif';
ctx.fillText(countryFullName.toUpperCase(), W / 2, 75);
// Horizontal line separator
ctx.beginPath();
ctx.moveTo(30, 90);
ctx.lineTo(W - 30, 90);
ctx.strokeStyle = '#003366'; // Dark blue line
ctx.lineWidth = 1;
ctx.stroke();
// 3. Photo Area & Photo
const photoX = 40;
const photoY = 110;
const photoW = 130;
const photoH = 170;
ctx.strokeStyle = '#666666'; // Border for photo
ctx.lineWidth = 1;
ctx.strokeRect(photoX, photoY, photoW, photoH);
// Draw the image, scaled to fit (cover behavior)
if (originalImg.complete && originalImg.naturalHeight !== 0) {
const imgAR = originalImg.width / originalImg.height;
const photoAR = photoW / photoH;
let drawW, drawH, imgDrawX, imgDrawY;
if (imgAR > photoAR) { // Image wider than area
drawH = photoH;
drawW = drawH * imgAR;
imgDrawX = photoX - (drawW - photoW) / 2;
imgDrawY = photoY;
} else { // Image taller or same AR
drawW = photoW;
drawH = drawW / imgAR;
imgDrawX = photoX;
imgDrawY = photoY - (drawH - photoH) / 2;
}
// Create a clipping path for the photo
ctx.save();
ctx.beginPath();
ctx.rect(photoX, photoY, photoW, photoH);
ctx.clip();
ctx.drawImage(originalImg, imgDrawX, imgDrawY, drawW, drawH);
ctx.restore();
} else {
// Fallback if image is not loaded or errored
ctx.fillStyle = '#EEEEEE';
ctx.fillRect(photoX, photoY, photoW, photoH);
ctx.fillStyle = '#888888';
ctx.textAlign = 'center';
ctx.font = '12px Arial';
ctx.fillText("Photo N/A", photoX + photoW / 2, photoY + photoH / 2);
}
// 4. Data Fields Area
const fieldBlockX = photoX + photoW + 30; // X start for data fields
let currentY = photoY - 5; // Align data fields with top of photo slightly
function drawDataEntry(label, value, x, y, valueMaxWidth, labelFont='11px Arial', valueFont='bold 13px Arial') {
ctx.font = labelFont;
ctx.fillStyle = '#555';
ctx.fillText(label.toUpperCase(), x, y);
ctx.font = valueFont;
ctx.fillStyle = '#000';
let displayValue = value.toUpperCase();
if (valueMaxWidth && ctx.measureText(displayValue).width > valueMaxWidth) {
let fittingValue = displayValue;
while (ctx.measureText(fittingValue + "...").width > valueMaxWidth && fittingValue.length > 0) {
fittingValue = fittingValue.slice(0, -1);
}
displayValue = fittingValue + "...";
}
ctx.fillText(displayValue, x, y + 16);
}
const col1X = fieldBlockX;
const col2X = fieldBlockX + 220;
const col3X = fieldBlockX + 380;
const valWidthShort = 80;
const valWidthMedium = W - col2X - 30; // Max width for a value in col2
const valWidthFull = W - col1X - 30; // Max width for a value spanning the block
// Row 1: Type, Code, Passport No.
drawDataEntry("Type/Type", passportType, col1X, currentY, valWidthShort);
drawDataEntry("Issuing State Code/Code État", countryCode, col2X, currentY, valWidthShort);
drawDataEntry("Passport No./Nº Passeport", passportNumber, col3X, currentY, W - col3X - 30);
currentY += 38;
// Row 2: Surname
drawDataEntry("Surname/Nom", surname, col1X, currentY, valWidthFull);
currentY += 38;
// Row 3: Given Names
drawDataEntry("Given Names/Prénoms", givenNames, col1X, currentY, valWidthFull);
currentY += 38;
// Row 4: Nationality
drawDataEntry("Nationality/Nationalité", nationality, col1X, currentY, valWidthFull);
currentY += 38;
// Row 5: Date of Birth, Sex
drawDataEntry("Date of Birth/Date de naissance", dateOfBirth, col1X, currentY, valWidthMedium);
drawDataEntry("Sex/Sexe", sex, col2X, currentY, valWidthShort);
currentY += 38;
// Row 6: Place of Birth
drawDataEntry("Place of Birth/Lieu de naissance", placeOfBirth, col1X, currentY, valWidthFull);
currentY += 38; // This takes it to currentY for next data row.
// Signature Area (Below Photo)
const sigAreaY = photoY + photoH + 10;
ctx.font = '10px Arial';
ctx.fillStyle = '#555';
ctx.textAlign = 'center'; // Center "Signature of bearer"
ctx.fillText("SIGNATURE OF BEARER / SIGNATURE DU TITULAIRE", photoX + photoW/2, sigAreaY + 10);
// Line for signature:
ctx.beginPath();
ctx.moveTo(photoX + 5, sigAreaY + 15);
ctx.lineTo(photoX + photoW - 5, sigAreaY + 15);
ctx.strokeStyle = '#AAAAAA';
ctx.lineWidth = 0.75;
ctx.stroke();
// Placeholder for actual signature image or drawing with italic font as a visual cue
ctx.font = 'italic 18px "Brush Script MT", cursive'; // Fallback to generic cursive
ctx.fillStyle = '#333333';
// Truncate name for signature if too long
let sigName = surname.length > 12 ? surname.substring(0,10)+"." : surname;
ctx.fillText(givenNames.split(" ")[0].charAt(0) + ". " + sigName, photoX + photoW/2, sigAreaY + 35);
// Row 7: Dates of Issue and Expiry (align with potentially lower signature area)
// currentY is height of data block. Ensure it's below photo+signature block.
currentY = Math.max(currentY, sigAreaY + 45);
drawDataEntry("Date of Issue/Date de délivrance", dateOfIssue, col1X, currentY, valWidthMedium);
drawDataEntry("Date of Expiry/Date d'expiration", dateOfExpiry, col2X, currentY, valWidthMedium);
currentY += 38;
// Row 8: Authority
drawDataEntry("Authority/Autorité", issuingAuthority, col1X, currentY, valWidthFull);
currentY += 38;
// 5. Machine-Readable Zone (MRZ)
const mrzStartY = H - 70; // Y position for the top of MRZ block
ctx.beginPath(); // Line above MRZ
ctx.moveTo(20, mrzStartY - 10);
ctx.lineTo(W - 20, mrzStartY - 10);
ctx.strokeStyle = '#AAAAAA';
ctx.lineWidth = 1;
ctx.stroke();
ctx.font = 'bold 20px "Courier New", monospace';
ctx.fillStyle = '#000000';
ctx.textAlign = 'left';
function mrzSanitize(str, maxLength) {
let s = str.toUpperCase().replace(/[^A-Z0-9<]/g, '<').replace(/\s+/g, '<');
return s.padEnd(maxLength, '<').substring(0, maxLength);
}
function formatDateForMRZ(dateStr) {
try {
const date = new Date(dateStr.replace(/(\d+)\s([A-Z]+)\s(\d+)/, "$2 $1, $3")); // JAN 01 1990
const year = date.getFullYear().toString().slice(-2);
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
if (isNaN(date.getFullYear())) return "YYMMDD"; // Invalid date
return `${year}${month}${day}`;
} catch (e) {
return "??????"; // Fallback for truly invalid date strings
}
}
function calculateCheckDigit(str) {
let allFiller = true;
for (let i = 0; i < str.length; i++) {
if (str[i] !== '<') { allFiller = false; break; }
}
if (allFiller && str.length > 0) return '<';
const weights = [7, 3, 1];
let sum = 0;
for (let i = 0; i < str.length; i++) {
const char = str[i];
let val = 0;
if (char >= '0' && char <= '9') val = parseInt(char, 10);
else if (char >= 'A' && char <= 'Z') val = char.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
else if (char === '<') val = 0;
// Other characters are ignored or treated as 0 per ICAO if they slip through sanitize
sum += val * weights[i % 3];
}
return (sum % 10).toString();
}
// MRZ Line 1
let mrzL1 = "P";
mrzL1 += (passportType.toUpperCase() === "P" || passportType === "") ? "<" : mrzSanitize(passportType.charAt(0), 1);
mrzL1 += mrzSanitize(countryCode, 3);
const mrzSurname = surname.toUpperCase().replace(/[^A-Z0-9<]/g, '');
const mrzGivenNames = givenNames.toUpperCase().replace(/[^A-Z0-9<\s]/g, '').replace(/\s+/g, '<');
const nameField = mrzSurname + "<<" + mrzGivenNames;
mrzL1 += mrzSanitize(nameField, 39);
mrzL1 = mrzL1.substring(0, 44);
// MRZ Line 2
let mrzL2_PassNum = mrzSanitize(passportNumber, 9);
let mrzL2_cd_PassNum = calculateCheckDigit(mrzL2_PassNum);
let mrzL2_Nationality = mrzSanitize(countryCode, 3); // Typically issuing state code
let mrzL2_DOB = formatDateForMRZ(dateOfBirth);
let mrzL2_cd_DOB = calculateCheckDigit(mrzL2_DOB);
let mrzL2_Sex = (sex.charAt(0).toUpperCase() === 'M' || sex.charAt(0).toUpperCase() === 'F') ? sex.charAt(0).toUpperCase() : '<';
let mrzL2_Expiry = formatDateForMRZ(dateOfExpiry);
let mrzL2_cd_Expiry = calculateCheckDigit(mrzL2_Expiry);
let mrzL2_PersonalNum = mrzSanitize(personalNumber, 14);
let mrzL2_cd_PersonalNum = calculateCheckDigit(mrzL2_PersonalNum);
let overallCDString = mrzL2_PassNum + mrzL2_cd_PassNum +
mrzL2_DOB + mrzL2_cd_DOB +
mrzL2_Expiry + mrzL2_cd_Expiry +
mrzL2_PersonalNum + mrzL2_cd_PersonalNum;
let mrzL2_OverallCD = calculateCheckDigit(overallCDString);
let mrzL2 = mrzL2_PassNum + mrzL2_cd_PassNum +
mrzL2_Nationality +
mrzL2_DOB + mrzL2_cd_DOB +
mrzL2_Sex +
mrzL2_Expiry + mrzL2_cd_Expiry +
mrzL2_PersonalNum + mrzL2_cd_PersonalNum +
mrzL2_OverallCD;
mrzL2 = mrzL2.substring(0,44);
ctx.fillText(mrzL1, 20, mrzStartY + 5); // Adjusted Y for MRZ text
ctx.fillText(mrzL2, 20, mrzStartY + 30);
// Optional: Faint "seal" or emblem as watermark underneath data fields
ctx.globalAlpha = 0.08;
ctx.font = 'bold 80px "Times New Roman", serif';
ctx.fillStyle = "#003366";
ctx.textAlign = 'center';
// Simple text based seal: Country code
// Position it somewhat centrally in the data area
const sealX = fieldBlockX + (W - fieldBlockX) / 2 - 25;
const sealY = photoY + (currentY - photoY) / 2 + 20; // Dynamically center based on data block height
ctx.fillText(countryCode, sealX, sealY);
ctx.globalAlpha = 1.0; // Reset alpha
return canvas;
}
Apply Changes