pdftools / static /js /pdf-tools.js
Shivakafle038's picture
PDF Tools Web App - compress, convert, watermark removal
32a841c
/**
* 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 = `
<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>
`;
// 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 = `
<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();
});
// 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 = '<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;
// 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 = `
<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');
// 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();
}