| |
| |
| |
|
|
| let pdfBeforeUrl = null; |
| let pdfAfterUrl = null; |
| let isSplitView = false; |
| let pdfStepIndicator = null; |
|
|
| |
| 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); |
| |
| |
| 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') |
| }; |
|
|
| |
| pdfStepIndicator = new StepIndicator('pdfStepIndicator', [ |
| 'Upload PDF', |
| 'Configure', |
| 'Preview', |
| 'Download' |
| ]); |
|
|
| |
| const savedConfig = loadPdfConfig(); |
| if (savedConfig) { |
| applyPdfConfig(savedConfig); |
| showToast('Config Restored', 'Previous settings loaded', 'info', 2000); |
| } |
|
|
| |
| 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); |
| } |
| }); |
|
|
| |
| showPreviewEmpty(elements.previewBefore.querySelector('.preview-frame')); |
| showPreviewEmpty(elements.previewAfter.querySelector('.preview-frame')); |
|
|
| |
| 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); |
| } |
| } |
| }); |
| } |
|
|
| |
| elements.urlInput.addEventListener('input', () => { |
| if (elements.urlInput.value.trim()) { |
| pdfStepIndicator.setStep(1); |
| } |
| clearFieldError(elements.urlInput.closest('.form-group')); |
| }); |
|
|
| |
| elements.previewTabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| if (isSplitView) return; |
| const target = tab.dataset.preview; |
| switchPreview(target, elements.previewTabs); |
| }); |
| }); |
|
|
| |
| 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); |
| } |
| |
| |
| if (pdfBeforeUrl) { |
| elements.splitFrameBefore.src = pdfBeforeUrl; |
| } |
| if (pdfAfterUrl) { |
| elements.splitFrameAfter.src = pdfAfterUrl; |
| } |
| }); |
|
|
| |
| 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 = ''; |
| }); |
|
|
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape' && elements.fullscreenOverlay.classList.contains('active')) { |
| elements.fullscreenOverlay.classList.remove('active'); |
| elements.fullscreenFrame.src = ''; |
| } |
| }); |
|
|
| |
| elements.btnBefore.addEventListener('click', () => handlePreviewOriginal(elements)); |
| elements.btnAfter.addEventListener('click', () => handleProcessPdf(elements)); |
| |
| |
| const btnClearConfig = document.getElementById('btnClearConfig'); |
| if (btnClearConfig) { |
| btnClearConfig.addEventListener('click', clearPdfConfig); |
| } |
|
|
| |
| 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')); |
| }); |
|
|
| |
| 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(); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| 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'); |
|
|
| |
| 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 = ''; |
| } |
| } |
| }); |
| } |
|
|
| |
| 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(); |
| |
| |
| originalImg.src = 'data:image/png;base64,' + data.original; |
| processedImg.src = 'data:image/png;base64,' + data.processed; |
| previewCard.style.display = 'block'; |
| |
| |
| 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'; |
|
|
| |
| 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`); |
| |
| |
| 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); |
|
|
| try { |
| const blob = await postForBlob('/api/fetch', buildPdfFormData(false)); |
| |
| if (pdfBeforeUrl) URL.revokeObjectURL(pdfBeforeUrl); |
| pdfBeforeUrl = URL.createObjectURL(blob); |
| |
| |
| elements.previewBefore.querySelector('.preview-frame').innerHTML = '<iframe id="frameBefore"></iframe>'; |
| 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); |
| } 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); |
|
|
| 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); |
| |
| |
| elements.previewAfter.querySelector('.preview-frame').innerHTML = '<iframe id="frameAfter"></iframe>'; |
| elements.frameAfter = document.getElementById('frameAfter'); |
| elements.frameAfter.src = pdfAfterUrl; |
| elements.splitFrameAfter.src = pdfAfterUrl; |
|
|
| |
| 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(); |
| |
| |
| 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); |
| } |
| } |
|
|