Spaces:
Sleeping
Sleeping
| /** | |
| * 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 | |
| ? "<span style='color:#ff4757; font-weight:600;'>Insufficient Balance!</span><br>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 = '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>'; | |
| 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 = '<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"></path>'; | |
| 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 = '<polyline points="20 6 9 17 4 12"></polyline>'; | |
| 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 = '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>'; | |
| 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); | |
| } | |
| } |