| |
| |
| |
| |
|
|
| |
|
|
| 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; |
|
|
| |
| 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.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 = ` |
| <img class="image-thumb" src="${url}" alt="${file.name}" /> |
| <div class="image-meta"> |
| <div class="image-filename">${file.name}</div> |
| <div class="image-info"> |
| <span class="image-badge">#${index + 1}</span> |
| <button class="drop-zone-file-remove" data-index="${index}">×</button> |
| </div> |
| </div> |
| `; |
|
|
| |
| card.querySelector('.drop-zone-file-remove').addEventListener('click', (e) => { |
| e.stopPropagation(); |
| img2pdfFiles.splice(index, 1); |
| renderImagePreview(); |
| }); |
|
|
| |
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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; |
|
|
| |
| 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 = ` |
| <svg width="20" height="20" fill="none" stroke="var(--primary)" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> |
| </svg> |
| <span style="flex: 1; font-size: 13px; font-weight: 500;">${file.name}</span> |
| <span style="font-size: 11px; color: var(--text-muted);">#${index + 1}</span> |
| <button class="drop-zone-file-remove" data-index="${index}" style="padding: 4px 8px;">×</button> |
| `; |
|
|
| item.querySelector('.drop-zone-file-remove').addEventListener('click', (e) => { |
| e.stopPropagation(); |
| mergeFiles.splice(index, 1); |
| renderMergeList(); |
| }); |
|
|
| |
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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; |
|
|
| |
| initDropZone(dropZone, fileInput, { |
| maxSize: 100 * 1024 * 1024, |
| onFile: (file) => { |
| splitFile = file; |
| btnSplit.disabled = !file; |
| if (file) { |
| showToast('PDF Selected', file.name, 'success', 2000); |
| } |
| } |
| }); |
|
|
| |
| 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; |
| } |
| }); |
|
|
| |
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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); |
| } |
|
|
|
|
| |
|
|
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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); |
| |
| updateCompressPdfEstimate(); |
| } |
| } |
| }); |
|
|
| |
| if (qualitySlider && qualityVal) { |
| let estimateTimeout = null; |
| qualitySlider.addEventListener('input', () => { |
| qualityVal.textContent = qualitySlider.value; |
| |
| 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; |
| |
| |
| 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 = '<span style="color: var(--text-muted);">Estimating file size...</span>'; |
| |
| 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 = '<span style="color: var(--text-muted);">Could not estimate</span>'; |
| 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; |
| |
| |
| const useKB = data.original_size < 1024 * 1024; |
| const origDisplay = useKB ? `${origKB} KB` : `${origMB} MB`; |
| const estDisplay = useKB ? `${estKB} KB` : `${estMB} MB`; |
| |
| displayDiv.innerHTML = ` |
| <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;"> |
| <span><strong>Original:</strong> ${origDisplay}</span> |
| <span style="color: var(--primary);">→</span> |
| <span><strong>Estimated:</strong> ${estDisplay}</span> |
| <span style="color: ${reduction > 0 ? 'var(--success)' : 'var(--text-muted)'}; font-weight: 600;"> |
| ${reduction > 0 ? '-' + reduction + '%' : 'Minimal reduction'} |
| </span> |
| </div> |
| `; |
| } catch (e) { |
| console.error('Estimate error:', e); |
| displayDiv.innerHTML = '<span style="color: var(--text-muted);">Could not estimate</span>'; |
| } |
| } |
|
|
| 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'); |
|
|
| |
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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); |
| } |
| } |
|
|
|
|
| |
|
|
| 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; |
|
|
| |
| 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'); |
| } |
| } |
|
|
|
|
| |
|
|
| function initPdfTools() { |
| initImagesToPdf(); |
| initMergePdf(); |
| initSplitPdf(); |
| initPdf2Img(); |
| initCompressPdf(); |
| initRotatePdf(); |
| initPageNums(); |
| initPdfOcr(); |
| } |
|
|