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