/** * PDF Editor Module */ let pdfBeforeUrl = null; let pdfAfterUrl = null; let isSplitView = false; let pdfStepIndicator = null; // Config storage key and expiry (24 hours) const PDF_CONFIG_KEY = 'pdfEditorConfig'; const CONFIG_EXPIRY_MS = 24 * 60 * 60 * 1000; function savePdfConfig() { const config = { remove_pages: document.getElementById('remove_pages').value, unit: document.getElementById('unit').value, top: document.getElementById('top').value, bottom: document.getElementById('bottom').value, left: document.getElementById('left').value, right: document.getElementById('right').value, watermark_text: document.getElementById('watermark_text').value, watermark_size: document.getElementById('watermark_size').value, watermark_rotate: document.getElementById('watermark_rotate').value, output_name: document.getElementById('output_name').value, savedAt: Date.now() }; localStorage.setItem(PDF_CONFIG_KEY, JSON.stringify(config)); } function loadPdfConfig() { try { const stored = localStorage.getItem(PDF_CONFIG_KEY); if (!stored) return null; const config = JSON.parse(stored); // Check if expired (24 hours) if (Date.now() - config.savedAt > CONFIG_EXPIRY_MS) { localStorage.removeItem(PDF_CONFIG_KEY); return null; } return config; } catch { return null; } } function applyPdfConfig(config) { if (!config) return; if (config.remove_pages) document.getElementById('remove_pages').value = config.remove_pages; if (config.unit) document.getElementById('unit').value = config.unit; if (config.top) document.getElementById('top').value = config.top; if (config.bottom) document.getElementById('bottom').value = config.bottom; if (config.left) document.getElementById('left').value = config.left; if (config.right) document.getElementById('right').value = config.right; if (config.watermark_text) document.getElementById('watermark_text').value = config.watermark_text; if (config.watermark_size) document.getElementById('watermark_size').value = config.watermark_size; if (config.watermark_rotate) document.getElementById('watermark_rotate').value = config.watermark_rotate; if (config.output_name) document.getElementById('output_name').value = config.output_name; } function clearPdfConfig() { localStorage.removeItem(PDF_CONFIG_KEY); document.getElementById('remove_pages').value = ''; document.getElementById('unit').value = 'mm'; document.getElementById('top').value = ''; document.getElementById('bottom').value = ''; document.getElementById('left').value = ''; document.getElementById('right').value = ''; document.getElementById('watermark_text').value = ''; document.getElementById('watermark_size').value = '36'; document.getElementById('watermark_rotate').value = '45'; document.getElementById('output_name').value = 'cropped.pdf'; showToast('Config Cleared', 'Settings reset to defaults', 'success', 2000); } function initPdfEditor() { const elements = { btnBefore: document.getElementById('btnBefore'), btnAfter: document.getElementById('btnAfter'), frameBefore: document.getElementById('frameBefore'), frameAfter: document.getElementById('frameAfter'), splitFrameBefore: document.getElementById('splitFrameBefore'), splitFrameAfter: document.getElementById('splitFrameAfter'), previewTabs: document.querySelectorAll('.preview-tab'), dropZone: document.getElementById('pdfDropZone'), fileInput: document.getElementById('pdf_file'), urlInput: document.getElementById('pdf_url'), progress: document.getElementById('pdfProgress'), btnSplitView: document.getElementById('btnSplitView'), btnFullscreen: document.getElementById('btnFullscreen'), splitView: document.getElementById('splitView'), fullscreenOverlay: document.getElementById('fullscreenOverlay'), fullscreenFrame: document.getElementById('fullscreenFrame'), fullscreenClose: document.getElementById('fullscreenClose'), previewBefore: document.getElementById('preview-before'), previewAfter: document.getElementById('preview-after') }; // Initialize step indicator pdfStepIndicator = new StepIndicator('pdfStepIndicator', [ 'Upload PDF', 'Configure', 'Preview', 'Download' ]); // Load saved config (if within 24 hours) const savedConfig = loadPdfConfig(); if (savedConfig) { applyPdfConfig(savedConfig); showToast('Config Restored', 'Previous settings loaded', 'info', 2000); } // Auto-save config on input changes const configInputs = ['remove_pages', 'unit', 'top', 'bottom', 'left', 'right', 'watermark_text', 'watermark_size', 'watermark_rotate', 'output_name']; configInputs.forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('change', savePdfConfig); el.addEventListener('input', savePdfConfig); } }); // Show empty state in preview showPreviewEmpty(elements.previewBefore.querySelector('.preview-frame')); showPreviewEmpty(elements.previewAfter.querySelector('.preview-frame')); // Initialize drag & drop if (elements.dropZone && elements.fileInput) { initDropZone(elements.dropZone, elements.fileInput, { accept: 'application/pdf', maxSize: 100 * 1024 * 1024, onFile: (file) => { if (file) { showToast('File Selected', file.name, 'success'); elements.urlInput.value = ''; pdfStepIndicator.setStep(1); // Move to Configure step } } }); } // URL input change - update step elements.urlInput.addEventListener('input', () => { if (elements.urlInput.value.trim()) { pdfStepIndicator.setStep(1); } clearFieldError(elements.urlInput.closest('.form-group')); }); // Preview tab switching elements.previewTabs.forEach(tab => { tab.addEventListener('click', () => { if (isSplitView) return; const target = tab.dataset.preview; switchPreview(target, elements.previewTabs); }); }); // Split view toggle elements.btnSplitView.addEventListener('click', () => { isSplitView = !isSplitView; elements.btnSplitView.classList.toggle('active', isSplitView); elements.splitView.classList.toggle('active', isSplitView); document.getElementById('preview-before').style.display = isSplitView ? 'none' : ''; document.getElementById('preview-after').style.display = isSplitView ? 'none' : ''; if (!isSplitView) { const activeTab = document.querySelector('.preview-tab.active'); switchPreview(activeTab?.dataset.preview || 'before', elements.previewTabs); } // Sync frames if (pdfBeforeUrl) { elements.splitFrameBefore.src = pdfBeforeUrl; } if (pdfAfterUrl) { elements.splitFrameAfter.src = pdfAfterUrl; } }); // Fullscreen toggle elements.btnFullscreen.addEventListener('click', () => { const activeTab = document.querySelector('.preview-tab.active'); const url = activeTab?.dataset.preview === 'after' ? pdfAfterUrl : pdfBeforeUrl; if (url) { elements.fullscreenFrame.src = url; elements.fullscreenOverlay.classList.add('active'); } else { showToast('No Preview', 'Load a PDF first', 'info'); } }); elements.fullscreenClose.addEventListener('click', () => { elements.fullscreenOverlay.classList.remove('active'); elements.fullscreenFrame.src = ''; }); // Close fullscreen on ESC document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && elements.fullscreenOverlay.classList.contains('active')) { elements.fullscreenOverlay.classList.remove('active'); elements.fullscreenFrame.src = ''; } }); // Button handlers elements.btnBefore.addEventListener('click', () => handlePreviewOriginal(elements)); elements.btnAfter.addEventListener('click', () => handleProcessPdf(elements)); // Clear config button const btnClearConfig = document.getElementById('btnClearConfig'); if (btnClearConfig) { btnClearConfig.addEventListener('click', clearPdfConfig); } // URL validation elements.urlInput.addEventListener('blur', () => { const value = elements.urlInput.value.trim(); if (value) validateUrl(elements.urlInput); }); elements.urlInput.addEventListener('input', () => { clearFieldError(elements.urlInput.closest('.form-group')); }); // Keyboard shortcuts registerShortcut('ctrl+p', () => { if (document.getElementById('page-pdf').classList.contains('active')) { elements.btnBefore.click(); } }); registerShortcut('ctrl+enter', () => { if (document.getElementById('page-pdf').classList.contains('active')) { elements.btnAfter.click(); } }); } /** * Watermark Removal Feature (separate page) */ function initWatermarkRemoval() { const dropZone = document.getElementById('wmDropZone'); const fileInput = document.getElementById('wm_file'); const urlInput = document.getElementById('wm_url'); const btnRemove = document.getElementById('btnRemoveWatermark'); const btnPreview = document.getElementById('btnWmPreview'); const intensitySlider = document.getElementById('wm_intensity'); const intensityValue = document.getElementById('wm_intensity_value'); const progress = document.getElementById('wmProgress'); // Initialize drag & drop if (dropZone && fileInput) { initDropZone(dropZone, fileInput, { accept: 'application/pdf', maxSize: 100 * 1024 * 1024, onFile: (file) => { if (file) { showToast('File Selected', file.name, 'success'); if (urlInput) urlInput.value = ''; } } }); } // Update intensity display if (intensitySlider && intensityValue) { intensitySlider.addEventListener('input', () => { intensityValue.textContent = intensitySlider.value; }); } if (btnPreview) { btnPreview.addEventListener('click', handleWatermarkPreview); } if (btnRemove) { btnRemove.addEventListener('click', handleRemoveWatermark); } } async function handleWatermarkPreview() { const urlInput = document.getElementById('wm_url'); const fileInput = document.getElementById('wm_file'); const btn = document.getElementById('btnWmPreview'); const previewCard = document.getElementById('wmPreviewCard'); const originalImg = document.getElementById('wmPreviewOriginal'); const processedImg = document.getElementById('wmPreviewProcessed'); const hasUrl = urlInput && urlInput.value.trim(); const hasFile = fileInput && fileInput.files.length > 0; if (!hasUrl && !hasFile) { showToast('No PDF', 'Upload a file or enter a URL first', 'error'); return; } setButtonLoading(btn, true, 'Loading...'); try { const fd = new FormData(); if (hasUrl) fd.append('url', urlInput.value.trim()); if (hasFile) fd.append('file', fileInput.files[0]); fd.append('page', '0'); fd.append('method', document.getElementById('wm_method').value || 'inpaint'); fd.append('intensity', document.getElementById('wm_intensity').value || '50'); const res = await fetch('/api/watermark-preview', { method: 'POST', body: fd }); if (!res.ok) { let err = { detail: 'Request failed' }; try { err = await res.json(); } catch {} throw new Error(err.detail || 'Request failed'); } const data = await res.json(); // Show preview images originalImg.src = 'data:image/png;base64,' + data.original; processedImg.src = 'data:image/png;base64,' + data.processed; previewCard.style.display = 'block'; // Scroll to preview previewCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); showToast('Preview Ready', 'Compare original vs processed', 'success', 2000); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); } } async function handleRemoveWatermark() { const urlInput = document.getElementById('wm_url'); const fileInput = document.getElementById('wm_file'); const btn = document.getElementById('btnRemoveWatermark'); const progress = document.getElementById('wmProgress'); const hasUrl = urlInput && urlInput.value.trim(); const hasFile = fileInput && fileInput.files.length > 0; if (!hasUrl && !hasFile) { showToast('No PDF', 'Upload a file or enter a URL first', 'error'); return; } setButtonLoading(btn, true, 'Processing...'); showProgress(progress, true, true); try { const fd = new FormData(); if (hasUrl) fd.append('url', urlInput.value.trim()); if (hasFile) fd.append('file', fileInput.files[0]); fd.append('output_name', document.getElementById('wm_output_name').value || 'cleaned.pdf'); fd.append('watermark_text', document.getElementById('wm_text').value || 'Educated Nepal'); fd.append('method', document.getElementById('wm_method').value || 'inpaint'); fd.append('intensity', document.getElementById('wm_intensity').value || '50'); fd.append('dpi', document.getElementById('wm_dpi').value || '150'); fd.append('quality', document.getElementById('wm_quality').value || '85'); const res = await fetch('/api/remove-watermark', { method: 'POST', body: fd }); if (!res.ok) { let err = { detail: 'Request failed' }; try { err = await res.json(); } catch {} throw new Error(err.detail || 'Request failed'); } const blob = await res.blob(); const filename = document.getElementById('wm_output_name').value || 'cleaned.pdf'; // Download const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename.endsWith('.pdf') ? filename : filename + '.pdf'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); showSuccessAnimation('Watermark Removed!', `${filename} has been downloaded`); // Add to recent files if (typeof addToRecentFiles === 'function') { addToRecentFiles(filename, 'watermark-removal'); } } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } function switchPreview(name, tabs) { tabs.forEach(t => t.classList.toggle('active', t.dataset.preview === name)); document.getElementById('preview-before').classList.toggle('active', name === 'before'); document.getElementById('preview-after').classList.toggle('active', name === 'after'); } function validatePdfForm() { const urlInput = document.getElementById('pdf_url'); const fileInput = document.getElementById('pdf_file'); const hasUrl = urlInput.value.trim(); const hasFile = fileInput.files.length > 0; if (!hasUrl && !hasFile) { showToast('No PDF', 'Upload a file or enter a URL', 'error'); return false; } if (hasUrl && !validateUrl(urlInput)) { return false; } return true; } function buildPdfFormData(includeProcessOptions) { const fd = new FormData(); const url = document.getElementById('pdf_url').value.trim(); const file = document.getElementById('pdf_file').files[0]; if (url) fd.append('url', url); if (file) fd.append('file', file); fd.append('output_name', document.getElementById('output_name').value.trim() || 'cropped.pdf'); if (includeProcessOptions) { fd.append('remove_pages', document.getElementById('remove_pages').value.trim()); fd.append('unit', document.getElementById('unit').value); fd.append('top', document.getElementById('top').value || '0'); fd.append('bottom', document.getElementById('bottom').value || '0'); fd.append('left', document.getElementById('left').value || '0'); fd.append('right', document.getElementById('right').value || '0'); fd.append('watermark_text', document.getElementById('watermark_text').value || ''); fd.append('watermark_size', document.getElementById('watermark_size').value || '36'); fd.append('watermark_rotate', document.getElementById('watermark_rotate').value || '45'); } return fd; } async function postForBlob(endpoint, formData) { const res = await fetch(endpoint, { method: 'POST', body: formData }); if (!res.ok) { let err = { detail: 'Request failed' }; try { err = await res.json(); } catch {} throw new Error(err.detail || 'Request failed'); } return await res.blob(); } async function handlePreviewOriginal(elements) { if (!validatePdfForm()) return; setButtonLoading(elements.btnBefore, true, 'Loading...'); elements.btnAfter.disabled = true; showProgress(elements.progress, true, true); pdfStepIndicator.setStep(2); // Preview step try { const blob = await postForBlob('/api/fetch', buildPdfFormData(false)); if (pdfBeforeUrl) URL.revokeObjectURL(pdfBeforeUrl); pdfBeforeUrl = URL.createObjectURL(blob); // Clear empty state and show iframe elements.previewBefore.querySelector('.preview-frame').innerHTML = ''; elements.frameBefore = document.getElementById('frameBefore'); elements.frameBefore.src = pdfBeforeUrl; elements.splitFrameBefore.src = pdfBeforeUrl; if (!isSplitView) { switchPreview('before', elements.previewTabs); } showToast('Preview Ready', 'Original PDF loaded', 'success'); } catch (e) { showToast('Error', e.message, 'error'); pdfStepIndicator.setStep(1); // Back to configure } finally { setButtonLoading(elements.btnBefore, false); elements.btnAfter.disabled = false; showProgress(elements.progress, false); } } async function handleProcessPdf(elements) { if (!validatePdfForm()) return; setButtonLoading(elements.btnAfter, true, 'Processing...'); elements.btnBefore.disabled = true; showProgress(elements.progress, true, true); pdfStepIndicator.setStep(3); // Download step try { const outName = document.getElementById('output_name').value.trim() || 'cropped.pdf'; const filename = outName.toLowerCase().endsWith('.pdf') ? outName : outName + '.pdf'; const blob = await postForBlob('/api/process', buildPdfFormData(true)); if (pdfAfterUrl) URL.revokeObjectURL(pdfAfterUrl); pdfAfterUrl = URL.createObjectURL(blob); // Clear empty state and show iframe elements.previewAfter.querySelector('.preview-frame').innerHTML = ''; elements.frameAfter = document.getElementById('frameAfter'); elements.frameAfter.src = pdfAfterUrl; elements.splitFrameAfter.src = pdfAfterUrl; // Download const a = document.createElement('a'); a.href = pdfAfterUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); if (!isSplitView) { switchPreview('after', elements.previewTabs); } pdfStepIndicator.complete(); // Show success animation showSuccessAnimation('PDF Processed!', `${filename} has been downloaded`); } catch (e) { showToast('Error', e.message, 'error'); pdfStepIndicator.setStep(2); } finally { setButtonLoading(elements.btnAfter, false); elements.btnBefore.disabled = false; showProgress(elements.progress, false); } }