/** * 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); } }