/** * PDF Tools Module * Images to PDF, Merge PDFs, Split PDF */ // =============== Images to PDF =============== let img2pdfFiles = []; function initImagesToPdf() { const dropZone = document.getElementById('img2pdfDropZone'); const fileInput = document.getElementById('img2pdf_files'); const preview = document.getElementById('img2pdfPreview'); const btnCreate = document.getElementById('btnImg2Pdf'); const btnClear = document.getElementById('btnImg2PdfClear'); const progress = document.getElementById('img2pdfProgress'); if (!dropZone || !fileInput) return; // Click to browse - handle clicks on the drop zone and its children dropZone.addEventListener('click', (e) => { // Don't trigger if clicking on a remove button if (e.target.classList.contains('drop-zone-file-remove')) return; fileInput.click(); }); // Drag & drop ['dragenter', 'dragover'].forEach(e => { dropZone.addEventListener(e, (ev) => { ev.preventDefault(); dropZone.classList.add('drag-over'); }); }); ['dragleave', 'drop'].forEach(e => { dropZone.addEventListener(e, (ev) => { ev.preventDefault(); dropZone.classList.remove('drag-over'); }); }); dropZone.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); addImages(files); }); fileInput.addEventListener('change', () => { addImages(Array.from(fileInput.files)); fileInput.value = ''; }); btnClear.addEventListener('click', () => { img2pdfFiles = []; renderImagePreview(); }); btnCreate.addEventListener('click', () => createPdfFromImages()); function addImages(files) { files.forEach(file => { img2pdfFiles.push(file); }); renderImagePreview(); showToast('Images Added', `${files.length} image(s) added`, 'success', 2000); } function renderImagePreview() { preview.innerHTML = ''; btnCreate.disabled = img2pdfFiles.length === 0; btnClear.disabled = img2pdfFiles.length === 0; img2pdfFiles.forEach((file, index) => { const card = document.createElement('div'); card.className = 'image-card'; card.draggable = true; card.dataset.index = index; const url = URL.createObjectURL(file); card.innerHTML = ` ${file.name}
${file.name}
#${index + 1}
`; // Remove button card.querySelector('.drop-zone-file-remove').addEventListener('click', (e) => { e.stopPropagation(); img2pdfFiles.splice(index, 1); renderImagePreview(); }); // Drag reorder card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', index); card.style.opacity = '0.5'; }); card.addEventListener('dragend', () => { card.style.opacity = '1'; }); card.addEventListener('dragover', (e) => { e.preventDefault(); card.style.borderColor = 'var(--primary)'; }); card.addEventListener('dragleave', () => { card.style.borderColor = ''; }); card.addEventListener('drop', (e) => { e.preventDefault(); card.style.borderColor = ''; const fromIndex = parseInt(e.dataTransfer.getData('text/plain')); const toIndex = index; if (fromIndex !== toIndex) { const item = img2pdfFiles.splice(fromIndex, 1)[0]; img2pdfFiles.splice(toIndex, 0, item); renderImagePreview(); } }); preview.appendChild(card); }); } } async function createPdfFromImages() { if (img2pdfFiles.length === 0) { showToast('No Images', 'Add images first', 'error'); return; } const btn = document.getElementById('btnImg2Pdf'); const progress = document.getElementById('img2pdfProgress'); setButtonLoading(btn, true, 'Creating...'); showProgress(progress, true, true); try { const fd = new FormData(); img2pdfFiles.forEach((file, i) => { fd.append('files', file); }); fd.append('order', img2pdfFiles.map((_, i) => i).join(',')); fd.append('output_name', document.getElementById('img2pdf_output').value || 'images.pdf'); fd.append('page_size', document.getElementById('img2pdf_pagesize').value || 'a4'); const res = await fetch('/api/images-to-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const filename = document.getElementById('img2pdf_output').value || 'images.pdf'; downloadBlob(blob, filename.endsWith('.pdf') ? filename : filename + '.pdf'); showSuccessAnimation('PDF Created!', `${img2pdfFiles.length} images converted`); if (typeof addToRecentFiles === 'function') { addToRecentFiles(filename, 'images-to-pdf'); } } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== Merge PDFs =============== let mergeFiles = []; function initMergePdf() { const dropZone = document.getElementById('mergeDropZone'); const fileInput = document.getElementById('merge_files'); const fileList = document.getElementById('mergeFileList'); const btnMerge = document.getElementById('btnMerge'); const btnClear = document.getElementById('btnMergeClear'); const progress = document.getElementById('mergeProgress'); if (!dropZone || !fileInput) return; // Click to browse - handle clicks on the drop zone and its children dropZone.addEventListener('click', (e) => { if (e.target.classList.contains('drop-zone-file-remove')) return; fileInput.click(); }); ['dragenter', 'dragover'].forEach(e => { dropZone.addEventListener(e, (ev) => { ev.preventDefault(); dropZone.classList.add('drag-over'); }); }); ['dragleave', 'drop'].forEach(e => { dropZone.addEventListener(e, (ev) => { ev.preventDefault(); dropZone.classList.remove('drag-over'); }); }); dropZone.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer.files).filter(f => f.type === 'application/pdf'); addPdfs(files); }); fileInput.addEventListener('change', () => { addPdfs(Array.from(fileInput.files)); fileInput.value = ''; }); btnClear.addEventListener('click', () => { mergeFiles = []; renderMergeList(); }); btnMerge.addEventListener('click', () => mergePdfs()); function addPdfs(files) { files.forEach(file => mergeFiles.push(file)); renderMergeList(); showToast('PDFs Added', `${files.length} file(s) added`, 'success', 2000); } function renderMergeList() { fileList.innerHTML = ''; btnMerge.disabled = mergeFiles.length < 2; btnClear.disabled = mergeFiles.length === 0; mergeFiles.forEach((file, index) => { const item = document.createElement('div'); item.style.cssText = 'display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--border-light); border-radius: 8px; margin-bottom: 8px; cursor: move;'; item.draggable = true; item.dataset.index = index; item.innerHTML = ` ${file.name} #${index + 1} `; item.querySelector('.drop-zone-file-remove').addEventListener('click', (e) => { e.stopPropagation(); mergeFiles.splice(index, 1); renderMergeList(); }); // Drag reorder item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', index); item.style.opacity = '0.5'; }); item.addEventListener('dragend', () => item.style.opacity = '1'); item.addEventListener('dragover', (e) => { e.preventDefault(); item.style.background = 'var(--primary-light)'; }); item.addEventListener('dragleave', () => item.style.background = ''); item.addEventListener('drop', (e) => { e.preventDefault(); item.style.background = ''; const fromIndex = parseInt(e.dataTransfer.getData('text/plain')); if (fromIndex !== index) { const moved = mergeFiles.splice(fromIndex, 1)[0]; mergeFiles.splice(index, 0, moved); renderMergeList(); } }); fileList.appendChild(item); }); } } async function mergePdfs() { if (mergeFiles.length < 2) { showToast('Need More Files', 'Add at least 2 PDFs', 'error'); return; } const btn = document.getElementById('btnMerge'); const progress = document.getElementById('mergeProgress'); setButtonLoading(btn, true, 'Merging...'); showProgress(progress, true, true); try { const fd = new FormData(); mergeFiles.forEach(file => fd.append('files', file)); fd.append('order', mergeFiles.map((_, i) => i).join(',')); fd.append('output_name', document.getElementById('merge_output').value || 'merged.pdf'); const res = await fetch('/api/merge-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const filename = document.getElementById('merge_output').value || 'merged.pdf'; downloadBlob(blob, filename.endsWith('.pdf') ? filename : filename + '.pdf'); showSuccessAnimation('PDFs Merged!', `${mergeFiles.length} files combined`); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== Split PDF =============== let splitFile = null; function initSplitPdf() { const dropZone = document.getElementById('splitDropZone'); const fileInput = document.getElementById('split_file'); const btnSplit = document.getElementById('btnSplit'); const modeSelect = document.getElementById('split_mode'); const pagesInput = document.getElementById('split_pages'); const pagesLabel = document.getElementById('split_pages_label'); const progress = document.getElementById('splitProgress'); if (!dropZone) return; // Initialize drop zone initDropZone(dropZone, fileInput, { maxSize: 100 * 1024 * 1024, onFile: (file) => { splitFile = file; btnSplit.disabled = !file; if (file) { showToast('PDF Selected', file.name, 'success', 2000); } } }); // Mode change - update label modeSelect.addEventListener('change', () => { const mode = modeSelect.value; if (mode === 'all') { pagesLabel.textContent = 'Not needed for this mode'; pagesInput.placeholder = 'Not needed'; pagesInput.disabled = true; } else if (mode === 'range') { pagesLabel.textContent = 'Pages to Extract'; pagesInput.placeholder = 'e.g., 1,3,5-8'; pagesInput.disabled = false; } else if (mode === 'chunks') { pagesLabel.textContent = 'Pages per Chunk'; pagesInput.placeholder = 'e.g., 5'; pagesInput.disabled = false; } }); // Trigger initial state modeSelect.dispatchEvent(new Event('change')); btnSplit.addEventListener('click', () => splitPdf()); } async function splitPdf() { if (!splitFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnSplit'); const progress = document.getElementById('splitProgress'); const mode = document.getElementById('split_mode').value; const pages = document.getElementById('split_pages').value; if ((mode === 'range' || mode === 'chunks') && !pages.trim()) { showToast('Missing Input', 'Enter pages or chunk size', 'error'); return; } setButtonLoading(btn, true, 'Splitting...'); showProgress(progress, true, true); try { const fd = new FormData(); fd.append('file', splitFile); fd.append('mode', mode); fd.append('pages', pages); fd.append('output_name', document.getElementById('split_output').value || 'split'); const res = await fetch('/api/split-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const contentType = res.headers.get('content-type'); const outputName = document.getElementById('split_output').value || 'split'; if (contentType.includes('zip')) { downloadBlob(blob, `${outputName}.zip`); } else { downloadBlob(blob, `${outputName}.pdf`); } showSuccessAnimation('PDF Split!', 'Download started'); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== Utility =============== function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } // =============== PDF to Images =============== let pdf2imgFile = null; function initPdf2Img() { const dropZone = document.getElementById('pdf2imgDropZone'); const fileInput = document.getElementById('pdf2img_file'); const btn = document.getElementById('btnPdf2Img'); if (!dropZone || !fileInput) return; initDropZone(dropZone, fileInput, { maxSize: 100 * 1024 * 1024, onFile: (file) => { pdf2imgFile = file; btn.disabled = !file; if (file) showToast('PDF Selected', file.name, 'success', 2000); } }); btn.addEventListener('click', () => convertPdfToImages()); } async function convertPdfToImages() { if (!pdf2imgFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnPdf2Img'); const progress = document.getElementById('pdf2imgProgress'); setButtonLoading(btn, true, 'Converting...'); showProgress(progress, true, true); try { const fd = new FormData(); fd.append('file', pdf2imgFile); fd.append('format', document.getElementById('pdf2img_format').value); fd.append('dpi', document.getElementById('pdf2img_dpi').value); fd.append('pages', document.getElementById('pdf2img_pages').value); fd.append('output_name', document.getElementById('pdf2img_output').value || 'pages'); const res = await fetch('/api/pdf-to-images', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const outputName = document.getElementById('pdf2img_output').value || 'pages'; downloadBlob(blob, `${outputName}_images.zip`); showSuccessAnimation('Conversion Complete!', 'Images downloaded as ZIP'); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== Compress PDF =============== let compressFile = null; function initCompressPdf() { const dropZone = document.getElementById('compressDropZone'); const fileInput = document.getElementById('compress_file'); const btn = document.getElementById('btnCompress'); const btnPreview = document.getElementById('btnCompressPreview'); const qualitySlider = document.getElementById('compress_quality'); const qualityVal = document.getElementById('compress_quality_val'); if (!dropZone || !fileInput) return; initDropZone(dropZone, fileInput, { maxSize: 100 * 1024 * 1024, onFile: (file) => { compressFile = file; btn.disabled = !file; if (btnPreview) btnPreview.disabled = !file; const resultEl = document.getElementById('compressResult'); const previewEl = document.getElementById('compressPreviewCard'); if (resultEl) resultEl.style.display = 'none'; if (previewEl) previewEl.style.display = 'none'; if (file) { showToast('PDF Selected', file.name, 'success', 2000); // Trigger initial estimate updateCompressPdfEstimate(); } } }); // Quality slider with debounced estimate if (qualitySlider && qualityVal) { let estimateTimeout = null; qualitySlider.addEventListener('input', () => { qualityVal.textContent = qualitySlider.value; // Debounce the estimate call if (estimateTimeout) clearTimeout(estimateTimeout); estimateTimeout = setTimeout(() => { updateCompressPdfEstimate(); }, 300); }); } if (btnPreview) btnPreview.addEventListener('click', () => previewCompressPdf()); btn.addEventListener('click', () => compressPdf()); } async function updateCompressPdfEstimate() { if (!compressFile) return; const qualitySlider = document.getElementById('compress_quality'); if (!qualitySlider) return; const quality = qualitySlider.value; // Create estimate display if it doesn't exist let displayDiv = document.getElementById('compressPdfEstimate'); if (!displayDiv) { displayDiv = document.createElement('div'); displayDiv.id = 'compressPdfEstimate'; displayDiv.style.cssText = 'margin-top: 12px; padding: 12px; background: var(--border-light); border-radius: var(--radius); font-size: 13px;'; const qualityGroup = qualitySlider.closest('.form-group'); if (qualityGroup) { qualityGroup.appendChild(displayDiv); } } displayDiv.innerHTML = 'Estimating file size...'; try { const fd = new FormData(); fd.append('file', compressFile); fd.append('quality', quality); const res = await fetch('/api/estimate/compress-pdf', { method: 'POST', body: fd }); if (!res.ok) { displayDiv.innerHTML = 'Could not estimate'; return; } const data = await res.json(); const origKB = (data.original_size / 1024).toFixed(1); const estKB = (data.estimated_size / 1024).toFixed(1); const origMB = (data.original_size / 1024 / 1024).toFixed(2); const estMB = (data.estimated_size / 1024 / 1024).toFixed(2); const reduction = data.reduction_percent; // Use KB for small files, MB for large const useKB = data.original_size < 1024 * 1024; const origDisplay = useKB ? `${origKB} KB` : `${origMB} MB`; const estDisplay = useKB ? `${estKB} KB` : `${estMB} MB`; displayDiv.innerHTML = `
Original: ${origDisplay} Estimated: ${estDisplay} ${reduction > 0 ? '-' + reduction + '%' : 'Minimal reduction'}
`; } catch (e) { console.error('Estimate error:', e); displayDiv.innerHTML = 'Could not estimate'; } } async function previewCompressPdf() { if (!compressFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnCompressPreview'); const previewCard = document.getElementById('compressPreviewCard'); const originalImg = document.getElementById('compressPreviewOriginal'); const processedImg = document.getElementById('compressPreviewProcessed'); setButtonLoading(btn, true, 'Loading...'); try { const fd = new FormData(); fd.append('file', compressFile); fd.append('quality', document.getElementById('compress_quality').value); const res = await fetch('/api/preview/compress-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } 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 compressed', 'success', 2000); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); } } async function compressPdf() { if (!compressFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnCompress'); const progress = document.getElementById('compressProgress'); const resultDiv = document.getElementById('compressResult'); const statsDiv = document.getElementById('compressStats'); setButtonLoading(btn, true, 'Compressing...'); showProgress(progress, true, true); resultDiv.style.display = 'none'; try { const fd = new FormData(); fd.append('file', compressFile); fd.append('quality', document.getElementById('compress_quality').value); fd.append('output_name', document.getElementById('compress_output').value || 'compressed.pdf'); const res = await fetch('/api/compress-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const filename = document.getElementById('compress_output').value || 'compressed.pdf'; downloadBlob(blob, filename.endsWith('.pdf') ? filename : filename + '.pdf'); // Show compression stats const originalSize = res.headers.get('X-Original-Size'); const compressedSize = res.headers.get('X-Compressed-Size'); const reduction = res.headers.get('X-Reduction-Percent'); if (originalSize && compressedSize) { const origMB = (parseInt(originalSize) / 1024 / 1024).toFixed(2); const compMB = (parseInt(compressedSize) / 1024 / 1024).toFixed(2); statsDiv.textContent = `${origMB} MB → ${compMB} MB (${reduction}% smaller)`; resultDiv.style.display = 'block'; } showSuccessAnimation('PDF Compressed!', `Reduced by ${reduction}%`); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== Rotate PDF =============== let rotateFile = null; function initRotatePdf() { const dropZone = document.getElementById('rotateDropZone'); const fileInput = document.getElementById('rotate_file'); const btn = document.getElementById('btnRotate'); const btnPreview = document.getElementById('btnRotatePreview'); if (!dropZone || !fileInput) return; initDropZone(dropZone, fileInput, { maxSize: 100 * 1024 * 1024, onFile: (file) => { rotateFile = file; btn.disabled = !file; btnPreview.disabled = !file; document.getElementById('rotatePreviewCard').style.display = 'none'; if (file) showToast('PDF Selected', file.name, 'success', 2000); } }); btnPreview.addEventListener('click', () => previewRotatePdf()); btn.addEventListener('click', () => rotatePdf()); } async function previewRotatePdf() { if (!rotateFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnRotatePreview'); const previewCard = document.getElementById('rotatePreviewCard'); const originalImg = document.getElementById('rotatePreviewOriginal'); const processedImg = document.getElementById('rotatePreviewProcessed'); setButtonLoading(btn, true, 'Loading...'); try { const fd = new FormData(); fd.append('file', rotateFile); fd.append('rotation', document.getElementById('rotate_angle').value); const res = await fetch('/api/preview/rotate-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } 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 rotated', 'success', 2000); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); } } async function rotatePdf() { if (!rotateFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnRotate'); const progress = document.getElementById('rotateProgress'); setButtonLoading(btn, true, 'Rotating...'); showProgress(progress, true, true); try { const fd = new FormData(); fd.append('file', rotateFile); fd.append('rotation', document.getElementById('rotate_angle').value); fd.append('pages', document.getElementById('rotate_pages').value); fd.append('output_name', document.getElementById('rotate_output').value || 'rotated.pdf'); const res = await fetch('/api/rotate-pdf', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const filename = document.getElementById('rotate_output').value || 'rotated.pdf'; downloadBlob(blob, filename.endsWith('.pdf') ? filename : filename + '.pdf'); showSuccessAnimation('PDF Rotated!', 'Download started'); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== Page Numbers =============== let pagenumsFile = null; function initPageNums() { const dropZone = document.getElementById('pagenumsDropZone'); const fileInput = document.getElementById('pagenums_file'); const btn = document.getElementById('btnPageNums'); const btnPreview = document.getElementById('btnPageNumsPreview'); if (!dropZone || !fileInput) return; initDropZone(dropZone, fileInput, { maxSize: 100 * 1024 * 1024, onFile: (file) => { pagenumsFile = file; btn.disabled = !file; btnPreview.disabled = !file; document.getElementById('pagenumsPreviewCard').style.display = 'none'; if (file) showToast('PDF Selected', file.name, 'success', 2000); } }); btnPreview.addEventListener('click', () => previewPageNums()); btn.addEventListener('click', () => addPageNumbers()); } async function previewPageNums() { if (!pagenumsFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnPageNumsPreview'); const previewCard = document.getElementById('pagenumsPreviewCard'); const originalImg = document.getElementById('pagenumsPreviewOriginal'); const processedImg = document.getElementById('pagenumsPreviewProcessed'); setButtonLoading(btn, true, 'Loading...'); try { const fd = new FormData(); fd.append('file', pagenumsFile); fd.append('position', document.getElementById('pagenums_position').value); fd.append('format', document.getElementById('pagenums_format').value); fd.append('start_number', document.getElementById('pagenums_start').value); fd.append('font_size', document.getElementById('pagenums_fontsize').value); const res = await fetch('/api/preview/page-numbers', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } 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 numbered', 'success', 2000); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); } } async function addPageNumbers() { if (!pagenumsFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnPageNums'); const progress = document.getElementById('pagenumsProgress'); setButtonLoading(btn, true, 'Processing...'); showProgress(progress, true, true); try { const fd = new FormData(); fd.append('file', pagenumsFile); fd.append('position', document.getElementById('pagenums_position').value); fd.append('format', document.getElementById('pagenums_format').value); fd.append('start_number', document.getElementById('pagenums_start').value); fd.append('font_size', document.getElementById('pagenums_fontsize').value); fd.append('output_name', document.getElementById('pagenums_output').value || 'numbered.pdf'); const res = await fetch('/api/add-page-numbers', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const blob = await res.blob(); const filename = document.getElementById('pagenums_output').value || 'numbered.pdf'; downloadBlob(blob, filename.endsWith('.pdf') ? filename : filename + '.pdf'); showSuccessAnimation('Page Numbers Added!', 'Download started'); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } // =============== PDF OCR =============== let ocrFile = null; let ocrExtractedText = ''; function initPdfOcr() { const dropZone = document.getElementById('ocrDropZone'); const fileInput = document.getElementById('ocr_file'); const btn = document.getElementById('btnOcr'); const copyBtn = document.getElementById('btnOcrCopy'); const notice = document.getElementById('ocrNotice'); if (!dropZone || !fileInput) return; // Check if Tesseract is available fetch('/api/ocr-status') .then(r => r.json()) .then(data => { if (!data.available && notice) { notice.style.display = 'flex'; } }) .catch(() => {}); initDropZone(dropZone, fileInput, { maxSize: 100 * 1024 * 1024, onFile: (file) => { ocrFile = file; btn.disabled = !file; document.getElementById('ocrResult').style.display = 'none'; copyBtn.disabled = true; if (file) showToast('PDF Selected', file.name, 'success', 2000); } }); btn.addEventListener('click', () => extractPdfText()); copyBtn.addEventListener('click', () => copyOcrText()); } async function extractPdfText() { if (!ocrFile) { showToast('No PDF', 'Upload a PDF first', 'error'); return; } const btn = document.getElementById('btnOcr'); const copyBtn = document.getElementById('btnOcrCopy'); const progress = document.getElementById('ocrProgress'); const resultDiv = document.getElementById('ocrResult'); const textArea = document.getElementById('ocrText'); setButtonLoading(btn, true, 'Extracting...'); showProgress(progress, true, true); resultDiv.style.display = 'none'; try { const fd = new FormData(); fd.append('file', ocrFile); fd.append('language', document.getElementById('ocr_language').value); fd.append('pages', document.getElementById('ocr_pages').value); fd.append('dpi', document.getElementById('ocr_dpi').value); fd.append('output_format', 'json'); const res = await fetch('/api/pdf-ocr', { method: 'POST', body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Failed' })); throw new Error(err.detail); } const data = await res.json(); ocrExtractedText = data.text; textArea.value = ocrExtractedText; resultDiv.style.display = 'block'; copyBtn.disabled = false; showToast('Text Extracted', `${data.pages} page(s) processed`, 'success'); } catch (e) { showToast('Error', e.message, 'error'); } finally { setButtonLoading(btn, false); showProgress(progress, false); } } async function copyOcrText() { if (!ocrExtractedText) return; try { await navigator.clipboard.writeText(ocrExtractedText); showToast('Copied!', 'Text copied to clipboard', 'success', 2000); } catch (e) { showToast('Copy Failed', 'Please select and copy manually', 'error'); } } // =============== Initialize All =============== function initPdfTools() { initImagesToPdf(); initMergePdf(); initSplitPdf(); initPdf2Img(); initCompressPdf(); initRotatePdf(); initPageNums(); initPdfOcr(); }