/**
* pdf-generator.js
* Generates High-Resolution Image PDF using html2canvas
* to preserve exact Devanagari ligatures and fonts,
* optimized for 2-5MB file sizes via JPEG compression.
*/
async function generateAadhaarPDF(isFree = false) {
const { jsPDF } = window.jspdf;
// 1. Inject Base64 Fonts into Main Document for absolute reliability
if (!document.getElementById('injected-fonts')) {
const style = document.createElement('style');
style.id = 'injected-fonts';
style.innerHTML = `
@font-face {
font-family: 'AadhaarEnglish';
src: url('data:font/ttf;base64,${FONT_ENGLISH_B64}') format('truetype');
font-display: block;
}
@font-face {
font-family: 'AadhaarHindi';
src: url('data:font/ttf;base64,${FONT_HINDI_B64}') format('truetype');
font-display: block;
}
@font-face {
font-family: 'AadhaarDigits';
src: url('data:font/ttf;base64,${FONT_DIGITS_B64}') format('truetype');
font-display: block;
}
`;
document.head.appendChild(style);
}
// 2. Wait for fonts to be ready before proceeding
try {
await document.fonts.ready;
// Force load specific fonts
await Promise.all([
document.fonts.load('12px AadhaarEnglish'),
document.fonts.load('12px AadhaarHindi'),
document.fonts.load('12px AadhaarDigits')
]);
console.log("All fonts loaded successfully");
} catch (e) {
console.warn("Font loading wait failed, proceeding anyway:", e);
}
// UI Elements for status updates
const popupMessage = document.getElementById('popup-message');
const popup = document.getElementById('validation-popup');
const closeBtn = document.getElementById('popup-close-btn');
const iconContainer = document.getElementById('popup-icon-container');
const popupTitle = document.getElementById('popup-title');
const popupSvg = document.getElementById('popup-svg');
// Initial UI State
if (popup && !popup.classList.contains('active')) {
popup.classList.add('active');
}
if (closeBtn) closeBtn.style.display = 'none';
// If it's a free reprint (from history), skip the wallet deduction
if (isFree) {
if (popupMessage) popupMessage.innerText = "Reprinting from History...";
startPDFGenerationFlow();
return;
}
// Otherwise, check and deduct wallet balance
const cost = 10;
if (popupMessage) popupMessage.innerText = "Checking wallet balance...";
// Call deduction API with Aadhaar Number in description
const adharNo = document.getElementById('in-aadhaar')?.value || "Unknown";
const serviceDesc = `Aadhaar Advance: ${adharNo}`;
fetch('/api/wallet/deduct', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: cost, service: serviceDesc })
})
.then(async res => {
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Failed to deduct balance");
}
// Success: Deduction done, now proceed with PDF generation
if (popupMessage) popupMessage.innerText = "Processing Aadhaar PDF... Please wait.";
startPDFGenerationFlow();
})
.catch(err => {
console.error("Deduction failed:", err);
const isBalanceError = err.message.toLowerCase().includes('balance');
if (popupMessage) {
popupMessage.innerHTML = isBalanceError
? "Insufficient Balance!
Redirecting to Wallet page in 2s..."
: "System Error: " + err.message;
}
// Reset popup to Error State
if (iconContainer) {
iconContainer.style.background = '#fff0f0';
iconContainer.style.color = '#ff4757';
iconContainer.style.boxShadow = '0 0 0 10px rgba(255, 71, 87, 0.1)';
}
if (popupTitle) popupTitle.innerText = isBalanceError ? "Low Balance" : "Payment Error";
if (popupSvg) {
popupSvg.innerHTML = '';
popupSvg.classList.remove('spinner-animation');
}
// Final Action: Hide close button for balance errors (since it's automated)
if (closeBtn) closeBtn.style.display = isBalanceError ? 'none' : 'block';
if (isBalanceError) {
setTimeout(() => {
if (popup) popup.classList.remove('active');
window.location.href = '/wallet';
}, 2000);
}
// Refresh balance in case of sync issues
if (typeof updateWalletPill === 'function') updateWalletPill();
});
}
function startPDFGenerationFlow() {
const { jsPDF } = window.jspdf;
// Re-fetch UI Elements for the flow (avoiding scope issues)
const popupMessage = document.getElementById('popup-message');
const popup = document.getElementById('validation-popup');
const closeBtn = document.getElementById('popup-close-btn');
const iconContainer = document.getElementById('popup-icon-container');
const popupTitle = document.getElementById('popup-title');
const popupSvg = document.getElementById('popup-svg');
const now = new Date();
const timeStamp = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
const nameInput = document.getElementById('in-name-en');
const nameVal = nameInput && nameInput.value ? nameInput.value.trim().replace(/\s+/g, '_') : 'Applicant';
const fileName = `${nameVal}_Aadhaar_${timeStamp}.pdf`;
const ws = document.getElementById('workspace');
const previewArea = document.querySelector('.preview-area');
// Update Popup to "Processing" State (variables are already defined in outer scope)
if (iconContainer) {
iconContainer.style.background = '#e7f3ff';
iconContainer.style.color = '#007bff';
iconContainer.style.boxShadow = '0 0 0 10px rgba(0, 123, 255, 0.1)';
}
if (popupTitle) popupTitle.innerText = "Processing";
if (popupSvg) {
popupSvg.innerHTML = '';
popupSvg.classList.add('spinner-animation');
}
// Show preview area off-screen so html2canvas can capture it
const originalPreviewDisplay = previewArea ? previewArea.style.display : '';
const originalPreviewPosition = previewArea ? previewArea.style.position : '';
const originalPreviewLeft = previewArea ? previewArea.style.left : '';
const originalPreviewVisibility = previewArea ? previewArea.style.visibility : '';
if (previewArea) {
previewArea.style.setProperty('display', 'flex', 'important');
previewArea.style.position = 'fixed';
previewArea.style.left = '-9999px';
previewArea.style.visibility = 'visible';
previewArea.style.top = '0';
}
// Temporarily remove scale transforms so html2canvas captures full A4 layout
const originalTransform = ws.style.transform;
const originalShadow = ws.style.boxShadow;
ws.style.transform = 'none';
ws.style.boxShadow = 'none';
// FIX: html2canvas blurs "background-size: cover" CSS images and misaligns them
// Calculate precise mathematical dimensions mirroring 'cover' logic in mm natively
const bgImgNative = new Image();
bgImgNative.onload = () => {
const pageRatio = 210 / 297;
const imgRatio = bgImgNative.naturalWidth / bgImgNative.naturalHeight;
let drawW, drawH, drawX, drawY;
if (imgRatio > pageRatio) {
drawH = 297;
drawW = (297 / bgImgNative.naturalHeight) * bgImgNative.naturalWidth;
drawX = (210 - drawW) / 2;
drawY = 0;
} else {
drawW = 210;
drawH = (210 / bgImgNative.naturalWidth) * bgImgNative.naturalHeight;
drawX = 0;
drawY = (297 - drawH) / 2;
}
bgImgNative.style.position = 'absolute';
bgImgNative.style.width = drawW + 'mm';
bgImgNative.style.height = drawH + 'mm';
bgImgNative.style.left = drawX + 'mm';
bgImgNative.style.top = drawY + 'mm';
bgImgNative.style.objectFit = 'fill';
bgImgNative.style.zIndex = '0';
bgImgNative.id = 'temp-bg-native';
const originalBgLayer = document.querySelector('.bg-layer');
const oldDisplay = originalBgLayer ? originalBgLayer.style.display : '';
if (originalBgLayer) originalBgLayer.style.display = 'none';
ws.prepend(bgImgNative);
// Timeout ensures DOM repaints and font loading before capture
setTimeout(() => {
html2canvas(ws, {
scale: 4, // High-res capture
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false,
onclone: (clonedDoc) => {
// 1. Inject Base64 Fonts into Cloned Document for absolute reliability
const style = clonedDoc.createElement('style');
style.innerHTML = `
@font-face {
font-family: 'AadhaarEnglish';
src: url('data:font/ttf;base64,${FONT_ENGLISH_B64}') format('truetype');
}
@font-face {
font-family: 'AadhaarHindi';
src: url('data:font/ttf;base64,${FONT_HINDI_B64}') format('truetype');
}
@font-face {
font-family: 'AadhaarDigits';
src: url('data:font/ttf;base64,${FONT_DIGITS_B64}') format('truetype');
}
`;
clonedDoc.head.appendChild(style);
const clonedWs = clonedDoc.getElementById('workspace');
if (clonedWs) {
clonedWs.style.transform = 'none';
clonedWs.style.boxShadow = 'none';
}
// 2. Force exact font-family and sizes on requested text elements
const englishEls = clonedDoc.querySelectorAll('.inroltext, .english-name, .address-en-text, .address-en-label, .enrolment-no, .inrol-date, .download-date');
englishEls.forEach(el => {
el.style.fontFamily = "'AadhaarEnglish', Arial, sans-serif";
if (!el.classList.contains('enrolment-no')) {
el.style.fontSize = '8.4px';
}
});
const hindiEls = clonedDoc.querySelectorAll('.hindi-name, .address-hi-text, .address-hi-label, .gender-text');
hindiEls.forEach(el => {
el.style.fontFamily = "'AadhaarHindi', Arial, sans-serif";
el.style.fontSize = '8.4px';
});
const digitEls = clonedDoc.querySelectorAll('.aadhar-no-top, .aadhar-no-middle, .aadhar-no-bottom, .dob-text, .enrolment-no');
digitEls.forEach(el => {
el.style.fontFamily = "'AadhaarDigits', Arial, sans-serif";
});
// 3. Fix Rotation and Visibility for Dates
const rotatedEls = clonedDoc.querySelectorAll('.inrol-date, .download-date');
rotatedEls.forEach(el => {
el.style.display = 'block';
el.style.visibility = 'visible';
el.style.opacity = '1';
el.style.color = '#000000';
el.style.whiteSpace = 'nowrap';
el.style.fontFamily = "'AadhaarEnglish', Arial, sans-serif";
el.style.fontSize = '7.5px';
el.style.zIndex = '100';
el.style.letterSpacing = '0.2px'; // Increased spacing to prevent mixing
// Force layout properties that html2canvas likes for rotated elements
el.style.transformOrigin = 'center center';
el.style.transform = 'rotate(-90deg)';
el.style.width = '200px'; // Increased width
el.style.textAlign = 'center';
});
}
}).then(canvas => {
// Restore UI immediately
ws.style.transform = originalTransform;
ws.style.boxShadow = originalShadow;
if (originalBgLayer) originalBgLayer.style.display = oldDisplay;
const tempBg = document.getElementById('temp-bg-native');
if (tempBg) tempBg.remove();
// Restore preview area state
if (previewArea) {
previewArea.style.removeProperty('display');
previewArea.style.position = originalPreviewPosition;
previewArea.style.left = originalPreviewLeft;
previewArea.style.visibility = originalPreviewVisibility;
previewArea.style.top = '';
}
// JPEG encoding at 1.0 provides the absolute maximum fidelity
// resulting in a beautiful 5-7MB file size as requested
const imgData = canvas.toDataURL('image/jpeg', 1.0);
const doc = new jsPDF({
orientation: 'portrait',
unit: 'pt',
format: 'letter'
});
// Aspect Ratio Calculation to prevent stretching
// Workspace: 794x1123, PDF: 612x792
// To keep ratio, Width = (794 / 1123) * 792 = ~560pt
// (612 - 560) / 2 = 26pt margin on each side to center it
doc.addImage(imgData, 'JPEG', 26, 0, 560, 792);
if (popupMessage) popupMessage.innerText = `PDF Saved: ${fileName}`;
// Update Popup to "Success" State
if (iconContainer) {
iconContainer.style.background = '#e6ffed';
iconContainer.style.color = '#28a745';
iconContainer.style.boxShadow = '0 0 0 10px rgba(40, 167, 69, 0.1)';
}
if (popupTitle) popupTitle.innerText = "Success!";
if (popupSvg) {
popupSvg.innerHTML = '';
popupSvg.classList.remove('spinner-animation');
}
setTimeout(() => {
if (popup) popup.classList.remove('active');
}, 1500);
doc.save(fileName);
// Save to History
saveToHistory();
if (typeof updateWalletPill === 'function') updateWalletPill();
}).catch(err => {
console.error("PDF Generation Error:", err);
ws.style.transform = originalTransform;
ws.style.boxShadow = originalShadow;
if (originalBgLayer) originalBgLayer.style.display = oldDisplay;
const tempBg = document.getElementById('temp-bg-native');
if (tempBg) tempBg.remove();
if (previewArea) {
previewArea.style.removeProperty('display');
previewArea.style.position = originalPreviewPosition;
previewArea.style.left = originalPreviewLeft;
previewArea.style.top = '';
}
if (popupMessage) popupMessage.innerText = "Error generating PDF. Check console.";
// Show close button so user can exit error state
if (closeBtn) closeBtn.style.display = 'block';
// Reset to Error State
if (iconContainer) {
iconContainer.style.background = '#fff0f0';
iconContainer.style.color = '#ff4757';
iconContainer.style.boxShadow = '0 0 0 10px rgba(255, 71, 87, 0.1)';
}
if (popupTitle) popupTitle.innerText = "Error";
if (popupSvg) {
popupSvg.innerHTML = '';
popupSvg.classList.remove('spinner-animation');
}
setTimeout(() => { if (popup) popup.classList.remove('active'); }, 2000);
});
}, 1000);
};
bgImgNative.src = EMBEDDED_BG;
}
/**
* Collects form data and saves it to the backend history API
*/
async function saveToHistory() {
const data = {
id: (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).substring(2, 11),
timestamp: new Date().toISOString()
};
// Collect all in-* inputs and radio buttons
const inputs = document.querySelectorAll('input[id^="in-"], textarea[id^="in-"]');
inputs.forEach(input => {
if (input.type === 'radio') {
if (input.checked) data[input.id] = input.value;
} else if (input.type === 'file') {
// Skip file input's fakepath, we'll handle the base64 photo separately
} else {
data[input.id] = input.value;
}
});
// Also save the photo data if it exists in the preview
const photoPreview = document.getElementById('out-photo');
if (photoPreview && photoPreview.src && photoPreview.src.startsWith('data:image')) {
data['photo_data'] = photoPreview.src;
}
try {
const response = await fetch('/api/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
console.log("History saved successfully, ID:", result.id || data.id);
// Update local ID if backend returned a different one (e.g. for duplicates)
if (result.id) data.id = result.id;
// Refresh badge if function exists in generate.js
if (typeof fetchHistoryCount === 'function') fetchHistoryCount();
}
} catch (err) {
console.error("Failed to save history:", err);
}
}