Every tool you need to work with PDFs in one place

Free, secure, and easy to use. Merge, split, compress, convert, edit, sign, and more, right in your browser.

View All Tools
Advertisement 728x90

All PDF Tools

About One Page Tools

One Page Tools was created with a simple mission: to provide a comprehensive, free, and secure suite of PDF utilities that anyone can use, anywhere, without needing to download or install software. In today's digital world, PDFs are ubiquitous, but managing them can be a hassle. We believe that powerful tools should be accessible to everyone.

Why Online PDF Tools Are Essential

Whether you're a student, a professional, or just someone managing personal documents, you've likely faced challenges with PDF files. You might need to combine multiple reports into a single file, extract a specific chapter from an ebook, reduce a large file size for emailing, or sign a contract electronically. Our tools solve these common problems directly in your browser.

Our Core Tool Highlights

  • Manage & Organize: Tools like Merge PDF, Split PDF, and Organize PDF give you full control over your document's structure. Combine, separate, reorder, or delete pages with ease.
  • Optimize & Compress: Our Compress PDF tool intelligently reduces file size while maintaining visual quality, making your files easier to share and store.
  • Convert To & From PDF: Seamlessly convert common file formats. Turn your Word, PowerPoint, Excel, and JPG files into professional-quality PDFs, or convert your PDFs into other formats.
  • Edit & Sign: The Edit PDF tool allows you to add text, shapes, and drawings directly onto your PDF pages. With Sign PDF, you can create and apply a legally-binding electronic signature in seconds.
  • Secure Your Documents: Add a password with Protect PDF or remove restrictions with Unlock PDF. Add a Watermark to protect your intellectual property.

Your Privacy is Our Priority

Unlike other online services, One Page Tools processes all your files directly on your computer (client-side). Your files are never uploaded to our servers. This means your data remains 100% private and secure. What happens in your browser, stays in your browser.

`, async process(files, options) { App.showLoader('Compressing PDF...'); const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, { updateMetadata: false, ignoreEncryption: true }); const imageObjects = pdfDoc.context.enumerateIndirectObjects().filter(obj => obj[1].dict?.get(PDFLib.PDFName.of('Subtype')) === PDFLib.PDFName.of('Image')); for (const ref of imageObjects) { const image = pdfDoc.context.lookup(ref); if (image.dict.get(PDFLib.PDFName.of('Filter')) !== PDFLib.PDFName.of('DCTDecode')) continue; // only compress JPEGs for now const imageBytes = image.getContents(); const embeddedImage = await pdfDoc.embedJpg(imageBytes, parseFloat(options.quality)); image.dict.set(PDFLib.PDFName.of('Width'), PDFLib.PDFNumber.of(embeddedImage.width)); image.dict.set(PDFLib.PDFName.of('Height'), PDFLib.PDFNumber.of(embeddedImage.height)); } const compressedBytes = await pdfDoc.save(); App.createDownloadLink(compressedBytes, 'compressed.pdf', 'application/pdf'); } }, 'pdf-to-word': { title: 'PDF to Word (.txt)', desc: 'Convert PDF to an editable text file.', icon: '📄➡️W', fileType: '.pdf', multiple: false, async process(files) { App.showLoader('Extracting text...'); const pdfBytes = await files[0].arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise; let textContent = ''; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const text = await page.getTextContent(); textContent += text.items.map(s => s.str).join(' ') + '\n\n'; } const blob = new Blob([textContent], { type: 'text/plain' }); App.createDownloadLink(blob, 'converted.txt', 'text/plain'); } }, 'pdf-to-ppt': { title: 'PDF to PowerPoint', desc: 'Convert each PDF page into a PPTX slide.', icon: '📄➡️P', fileType: '.pdf', multiple: false, async process(files) { App.showLoader('Converting to PowerPoint...'); const pptx = new PptxGenJS(); const pdfBytes = await files[0].arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise; for (let i = 1; i <= pdf.numPages; i++) { App.showLoader(`Processing page ${i} of ${pdf.numPages}`); const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.createElement('canvas'); canvas.height = viewport.height; canvas.width = viewport.width; const context = canvas.getContext('2d'); await page.render({ canvasContext: context, viewport: viewport }).promise; const slide = pptx.addSlide(); slide.addImage({ data: canvas.toDataURL('image/png'), x: 0, y: 0, w: '100%', h: '100%' }); } const pptxBlob = await pptx.write('blob'); App.createDownloadLink(pptxBlob, 'converted.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'); } }, 'word-to-pdf': { title: 'Word to PDF', desc: 'Convert DOCX files to PDF.', icon: 'W➡️📄', fileType: '.docx', multiple: false, async process(files) { App.showLoader('Converting Word to PDF...'); const arrayBuffer = await files[0].arrayBuffer(); const result = await mammoth.convertToHtml({ arrayBuffer }); const element = document.createElement('div'); element.style.padding = '2cm'; element.innerHTML = result.value; await html2pdf().from(element).set({ filename: 'word.pdf' }).save(); } }, 'excel-to-pdf': { title: 'Excel to PDF', desc: 'Convert XLSX files to PDF.', icon: 'X➡️📄', fileType: '.xlsx', multiple: false, async process(files) { App.showLoader('Converting Excel to PDF...'); const data = await files[0].arrayBuffer(); const workbook = XLSX.read(data); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const html = XLSX.utils.sheet_to_html(worksheet); const element = document.createElement('div'); element.innerHTML = `${html}`; await html2pdf().set({ margin: 10, filename: 'excel.pdf', html2canvas: { scale: 2 }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'landscape' } }).from(element).save(); } }, 'powerpoint-to-pdf': { title: 'PowerPoint to PDF', desc: 'Convert PPTX to PDF (as images).', icon: 'P➡️📄', fileType: '.pptx', multiple: false, process: () => App.showError('Direct PPTX to PDF conversion is complex. Recommended: Save as PDF in PowerPoint, then use our other tools.') }, 'edit-pdf': { title: 'Edit PDF', desc: 'Add text, shapes, and drawings to a PDF.', icon: '✏️', fileType: '.pdf', multiple: false, isInteractive: true, onFileSelect: async (file) => { App.resetPdfEditor(); const pdfBytes = await file.arrayBuffer(); App.pdfEditor.pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true }); const optionsHtml = `
1 / ${App.pdfEditor.pdfDoc.getPageCount()}
`; App.toolOptions.innerHTML = optionsHtml; App.toolOptions.style.display = 'block'; const canvasEl = document.getElementById('pdf-edit-canvas'); App.pdfEditor.fabricCanvas = new fabric.Canvas(canvasEl); await App.renderPdfPageToCanvas(1); // Attach listeners document.getElementById('prev-page-btn').onclick = App.goToPrevPage; document.getElementById('next-page-btn').onclick = App.goToNextPage; document.getElementById('add-text-btn').onclick = () => App.pdfEditor.fabricCanvas.add(new fabric.IText('Your Text', { left: 50, top: 100, fontSize: 20, fill: 'red' })); document.getElementById('add-rect-btn').onclick = () => App.pdfEditor.fabricCanvas.add(new fabric.Rect({ left: 100, top: 100, width: 200, height: 100, fill: 'transparent', stroke: 'red', strokeWidth: 2 })); document.getElementById('draw-mode-btn').onclick = (e) => { App.pdfEditor.fabricCanvas.isDrawingMode = !App.pdfEditor.fabricCanvas.isDrawingMode; e.target.textContent = App.pdfEditor.fabricCanvas.isDrawingMode ? 'Stop Drawing' : 'Draw'; e.target.style.backgroundColor = App.pdfEditor.fabricCanvas.isDrawingMode ? '#c22520' : ''; }; }, async process() { App.showLoader('Applying edits...'); // Save current page state before processing App.pdfEditor.pageStates[App.pdfEditor.currentPage] = App.pdfEditor.fabricCanvas.toJSON(); const { PDFDocument, rgb } = PDFLib; const pages = App.pdfEditor.pdfDoc.getPages(); for (let i = 0; i < pages.length; i++) { const pageNum = i + 1; if (App.pdfEditor.pageStates[pageNum]) { App.showLoader(`Applying to page ${pageNum}/${pages.length}...`); const tempCanvas = new fabric.Canvas(null); const { width, height } = pages[i].getSize(); tempCanvas.setDimensions({ width, height }); await new Promise(resolve => tempCanvas.loadFromJSON(App.pdfEditor.pageStates[pageNum], resolve)); const overlayPngBytes = await new Promise(resolve => { const dataUrl = tempCanvas.toDataURL({ format: 'png' }); fetch(dataUrl).then(res => res.arrayBuffer()).then(buffer => resolve(new Uint8Array(buffer))); }); const overlayImage = await App.pdfEditor.pdfDoc.embedPng(overlayPngBytes); pages[i].drawImage(overlayImage, { x: 0, y: 0, width, height }); } } const pdfBytes = await App.pdfEditor.pdfDoc.save(); App.createDownloadLink(pdfBytes, 'edited.pdf', 'application/pdf'); } }, 'sign-pdf': { title: 'Sign PDF', desc: 'Draw your signature and apply it to a PDF.', icon: '✒️', fileType: '.pdf', multiple: false, isInteractive: true, onFileSelect: () => { const optionsHtml = `

Draw your signature in the box below:

`; App.toolOptions.innerHTML = optionsHtml; App.toolOptions.style.display = 'block'; const canvasEl = document.getElementById('signature-canvas'); const wrapper = canvasEl.parentElement; canvasEl.width = wrapper.clientWidth; canvasEl.height = wrapper.clientHeight; const sigCanvas = new fabric.Canvas(canvasEl, { isDrawingMode: true }); sigCanvas.freeDrawingBrush.color = "black"; sigCanvas.freeDrawingBrush.width = 3; window.sigCanvas = sigCanvas; // Make it accessible for processing document.getElementById('clear-signature-btn').onclick = () => sigCanvas.clear(); }, async process(files) { App.showLoader('Applying signature...'); if (window.sigCanvas.isEmpty()) throw new Error("Signature is empty."); const signatureImage = window.sigCanvas.toDataURL({ format: 'png' }); const signatureBytes = await fetch(signatureImage).then(res => res.arrayBuffer()); const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true }); const signaturePng = await pdfDoc.embedPng(signatureBytes); const firstPage = pdfDoc.getPages()[0]; const { width, height } = firstPage.getSize(); const sigDims = signaturePng.scale(0.3); firstPage.drawImage(signaturePng, { x: width - sigDims.width - 50, y: 50, width: sigDims.width, height: sigDims.height, }); const pdfSignedBytes = await pdfDoc.save(); App.createDownloadLink(pdfSignedBytes, 'signed.pdf', 'application/pdf'); } }, 'watermark-pdf': { title: 'Watermark PDF', desc: 'Add a text watermark to every page of your PDF.', icon: '💧', fileType: '.pdf', multiple: false, options: () => ` `, async process(files, options) { App.showLoader('Adding watermark...'); const { PDFDocument, rgb, StandardFonts, degrees } = PDFLib; const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true }); const helveticaFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const pages = pdfDoc.getPages(); for (const page of pages) { const { width, height } = page.getSize(); page.drawText(options.watermark_text, { x: width / 2 - 150, y: height / 2, font: helveticaFont, size: 50, color: rgb(0.75, 0.75, 0.75), opacity: parseFloat(options.watermark_opacity), rotate: degrees(45), }); } const watermarkedBytes = await pdfDoc.save(); App.createDownloadLink(watermarkedBytes, 'watermarked.pdf', 'application/pdf'); } }, 'rotate-pdf': { title: 'Rotate PDF', desc: 'Rotate all pages in a PDF document.', icon: '🔄', fileType: '.pdf', multiple: false, options: () => ` `, async process(files, options) { App.showLoader('Rotating PDF...'); const { PDFDocument, RotationTypes } = PDFLib; const angle = parseInt(options.rotation_angle, 10); const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true }); const pages = pdfDoc.getPages(); let rotationType; if(angle === 90) rotationType = RotationTypes.Degrees90; else if(angle === 180) rotationType = RotationTypes.Degrees180; else if(angle === 270) rotationType = RotationTypes.Degrees270; pages.forEach(page => page.setRotation(rotationType)); const rotatedBytes = await pdfDoc.save(); App.createDownloadLink(rotatedBytes, 'rotated.pdf', 'application/pdf'); } }, 'pdf-to-jpg': { title: 'PDF to JPG', desc: 'Convert each PDF page to a JPG image.', icon: '📄➡️🖼️', fileType: '.pdf', multiple: false, async process(files) { App.showLoader('Converting to JPG...'); const pdfBytes = await files[0].arrayBuffer(); const pdf = await pdfjsLib.getDocument({data: pdfBytes}).promise; const zip = new JSZip(); for (let i = 1; i <= pdf.numPages; i++) { App.showLoader(`Processing page ${i}/${pdf.numPages}`); const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.createElement('canvas'); canvas.height = viewport.height; canvas.width = viewport.width; const context = canvas.getContext('2d'); await page.render({ canvasContext: context, viewport: viewport }).promise; const blob = await new Promise(res => canvas.toBlob(res, 'image/jpeg')); zip.file(`page_${i}.jpg`, blob); } const zipBlob = await zip.generateAsync({type:"blob"}); App.createDownloadLink(zipBlob, 'jpg_images.zip', 'application/zip'); } }, 'jpg-to-pdf': { title: 'JPG to PDF', desc: 'Convert JPG images to a single PDF.', icon: '🖼️➡️📄', fileType: '.jpg, .jpeg', multiple: true, async process(files) { App.showLoader('Converting to PDF...'); const pdfDoc = await PDFLib.PDFDocument.create(); for (const file of files) { const imgBytes = await file.arrayBuffer(); const image = await pdfDoc.embedJpg(imgBytes); const page = pdfDoc.addPage([image.width, image.height]); page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height }); } const pdfBytes = await pdfDoc.save(); App.createDownloadLink(pdfBytes, 'converted.pdf', 'application/pdf'); } }, 'png-to-pdf': { title: 'PNG to PDF', desc: 'Convert PNG images to a single PDF.', icon: '🖼️➡️📄', fileType: '.png', multiple: true, async process(files) { App.showLoader('Converting to PDF...'); const pdfDoc = await PDFLib.PDFDocument.create(); for (const file of files) { const imgBytes = await file.arrayBuffer(); const image = await pdfDoc.embedPng(imgBytes); const page = pdfDoc.addPage([image.width, image.height]); page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height }); } const pdfBytes = await pdfDoc.save(); App.createDownloadLink(pdfBytes, 'converted.pdf', 'application/pdf'); } }, 'html-to-pdf': { title: 'HTML to PDF', desc: 'Convert HTML files to PDF.', icon: '🌐➡️📄', fileType: '.html,.htm', multiple: false, async process(files) { App.showLoader('Converting HTML to PDF...'); const htmlContent = await files[0].text(); await html2pdf().from(htmlContent).set({ filename: 'html.pdf' }).save(); } }, 'unlock-pdf': { title: 'Unlock PDF', desc: 'Remove password from a PDF (if known).', icon: '🔓', fileType: '.pdf', multiple: false, options: () => ``, async process(files, options) { App.showLoader('Unlocking PDF...'); const pdfBytes = await files[0].arrayBuffer(); const loadOptions = { ignoreEncryption: !options.pdf_password }; if (options.pdf_password) loadOptions.password = options.pdf_password; const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, loadOptions); const unlockedBytes = await pdfDoc.save(); App.createDownloadLink(unlockedBytes, 'unlocked.pdf', 'application/pdf'); } }, 'protect-pdf': { title: 'Protect PDF', desc: 'Add a password to protect your PDF.', icon: '🔒', fileType: '.pdf', multiple: false, options: () => ``, async process(files, options) { App.showLoader('Protecting PDF...'); const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true }); await pdfDoc.save({ userPassword: options.pdf_password, ownerPassword: options.pdf_password }); const protectedBytes = await pdfDoc.save(); App.createDownloadLink(protectedBytes, 'protected.pdf', 'application/pdf'); } }, 'organize-pdf': { title: 'Organize PDF', desc: 'Reorder or delete pages in your PDF.', icon: '📊', fileType: '.pdf', multiple: false, new: true, process: () => App.showError('This tool is coming soon!') }, 'repair-pdf': { title: 'Repair PDF', desc: 'Attempt to repair a corrupted PDF file.', icon: '🛠️', fileType: '.pdf', multiple: false, async process(files) { App.showLoader('Repairing PDF...'); const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true, updateMetadata: false }); const repairedBytes = await pdfDoc.save(); App.createDownloadLink(repairedBytes, 'repaired.pdf', 'application/pdf'); } }, 'add-page-numbers': { title: 'Page Numbers', desc: 'Add page numbers to your PDF.', icon: '#️⃣', fileType: '.pdf', multiple: false, async process(files) { App.showLoader('Adding page numbers...'); const { PDFDocument, rgb, StandardFonts } = PDFLib; const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true }); const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); const pages = pdfDoc.getPages(); for (let i = 0; i < pages.length; i++) { const page = pages[i]; const { width } = page.getSize(); page.drawText(`${i + 1} / ${pages.length}`, { x: width - 70, y: 20, font: helveticaFont, size: 12, color: rgb(0, 0, 0) }); } const numberedBytes = await pdfDoc.save(); App.createDownloadLink(numberedBytes, 'numbered.pdf', 'application/pdf'); } }, 'ocr-pdf': { title: 'OCR PDF', desc: 'Recognize text in scanned PDFs.', icon: '🔍', fileType: '.pdf', multiple: false, new: true, process: () => App.showError('This advanced tool is coming soon!')}, 'pdf-to-grayscale': { title: 'PDF to Grayscale', desc: 'Convert PDF to black and white.', icon: '🎨', fileType: '.pdf', multiple: false, new: true, process: () => App.showError('This tool is coming soon!') }, 'flatten-pdf': { title: 'Flatten PDF', desc: 'Make annotations and forms uneditable.', icon: '📰', fileType: '.pdf', multiple: false, new: true, async process(files) { App.showLoader('Flattening PDF...'); const pdfBytes = await files[0].arrayBuffer(); const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true }); pdfDoc.getForm().flatten(); const flattenedBytes = await pdfDoc.save(); App.createDownloadLink(flattenedBytes, 'flattened.pdf', 'application/pdf'); } }, 'pdf-to-png': { title: 'PDF to PNG', desc: 'Convert PDF pages to PNG images.', icon: '📄➡️🖼️', fileType: '.pdf', multiple: false, new: true, async process(files) { App.showLoader('Converting to PNG...'); const pdfBytes = await files[0].arrayBuffer(); const pdf = await pdfjsLib.getDocument({data: pdfBytes}).promise; const zip = new JSZip(); for (let i = 1; i <= pdf.numPages; i++) { App.showLoader(`Processing page ${i}/${pdf.numPages}`); const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2.0 }); // Higher scale for better quality PNG const canvas = document.createElement('canvas'); canvas.height = viewport.height; canvas.width = viewport.width; const context = canvas.getContext('2d'); await page.render({ canvasContext: context, viewport: viewport }).promise; const blob = await new Promise(res => canvas.toBlob(res, 'image/png')); zip.file(`page_${i}.png`, blob); } const zipBlob = await zip.generateAsync({type:"blob"}); App.createDownloadLink(zipBlob, 'png_images.zip', 'application/zip'); } }, 'pdf-to-excel': { title: 'PDF to Excel (.txt)', desc: 'Extract data from PDF to a text file.', icon: '📄➡️X', fileType: '.pdf', multiple: false, process: toolImplementations['pdf-to-word'].process }, }; /*************************************************** * INITIALIZATION ***************************************************/ const init = () => { generateToolCards(); setupEventListeners(); setupRevealAnimations(); }; /*************************************************** * UI & EVENT HANDLERS ***************************************************/ const generateToolCards = () => { // FIX: Dynamically generate cards for ALL tools in the implementation object // instead of using a hardcoded, incomplete list. App.toolsGrid.innerHTML = Object.keys(toolImplementations).map(id => { const tool = toolImplementations[id]; return `
${tool.new ? 'NEW!' : ''}
${tool.icon}

${tool.title}

${tool.desc}

`; }).join(''); }; const setupEventListeners = () => { window.addEventListener('scroll', () => App.header.classList.toggle('scrolled', window.scrollY > 50)); App.hamburger.addEventListener('click', () => { App.hamburger.classList.toggle('active'); App.mobileNav.classList.toggle('active'); }); App.mobileNavLinks.forEach(link => link.addEventListener('click', () => { App.hamburger.classList.remove('active'); App.mobileNav.classList.remove('active'); })); App.toolsGrid.addEventListener('click', (e) => { const card = e.target.closest('.tool-card'); if (card) openModal(card.dataset.toolId); }); App.closeModalBtn.addEventListener('click', closeModal); App.modal.addEventListener('click', (e) => { if (e.target === App.modal) closeModal(); }); App.dropArea.addEventListener('click', () => App.fileInput.click()); ['dragover', 'dragenter'].forEach(eventName => { App.dropArea.addEventListener(eventName, e => { e.preventDefault(); App.dropArea.classList.add('dragover'); }); }); ['dragleave', 'drop'].forEach(eventName => { App.dropArea.addEventListener(eventName, e => { e.preventDefault(); App.dropArea.classList.remove('dragover'); }); }); App.dropArea.addEventListener('drop', (e) => handleFiles(e.dataTransfer.files)); App.fileInput.addEventListener('change', (e) => handleFiles(e.target.files)); App.processBtn.addEventListener('click', handleProcessClick); }; const setupRevealAnimations = () => { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) entry.target.classList.add('active'); }); }, { threshold: 0.1 }); document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); }; /*************************************************** * MODAL LOGIC ***************************************************/ const openModal = (toolId) => { App.currentTool = toolImplementations[toolId]; if (!App.currentTool) return; if (App.currentTool.process && App.currentTool.process.toString().includes("coming soon")) { alert('This tool is coming soon!'); return; } resetModal(); App.modalTitle.textContent = App.currentTool.title; App.fileInput.accept = App.currentTool.fileType || '*/*'; App.fileInput.multiple = App.currentTool.multiple || false; if (App.currentTool.options) { App.toolOptions.innerHTML = App.currentTool.options(); App.toolOptions.style.display = 'block'; } App.modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; }; const closeModal = () => { App.modal.style.display = 'none'; document.body.style.overflow = 'auto'; resetModal(); }; const resetModal = () => { App.uploadedFiles = []; updateFileList(); App.toolOptions.innerHTML = ''; App.toolOptions.style.display = 'none'; App.outputArea.innerHTML = ''; App.processBtn.disabled = true; App.fileInput.value = ''; resetPdfEditor(); }; const resetPdfEditor = () => { if (App.pdfEditor.fabricCanvas) App.pdfEditor.fabricCanvas.dispose(); App.pdfEditor = { pdfDoc: null, currentPage: 1, fabricCanvas: null, pageStates: {} }; if (window.sigCanvas) window.sigCanvas.dispose(); }; const handleFiles = (files) => { const tool = App.currentTool; const newFiles = Array.from(files); if (!tool.multiple) App.uploadedFiles = []; // Replace existing file const allowedTypes = (tool.fileType || '*/*').split(',').map(t => t.trim().toLowerCase()); const validFiles = newFiles.filter(file => { const extension = '.' + file.name.split('.').pop().toLowerCase(); return allowedTypes.includes(extension) || allowedTypes.includes('*/*'); }); if (validFiles.length !== newFiles.length) { showError(`Invalid file type. Please upload ${tool.fileType} files.`); } App.uploadedFiles.push(...validFiles); updateFileList(); if (App.uploadedFiles.length > 0) { App.processBtn.disabled = !!App.currentTool.isInteractive; if (App.currentTool.onFileSelect) { App.showLoader("Preparing tool..."); App.currentTool.onFileSelect(App.uploadedFiles[0]) .then(() => { App.processBtn.disabled = false; }) .catch(e => showError(`Error preparing tool: ${e.message}`)) .finally(App.hideLoader); } } }; const updateFileList = () => { App.fileList.innerHTML = App.uploadedFiles.map((file, index) => `
${file.name}
`).join(''); App.fileList.querySelectorAll('.remove-file-btn').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index, 10); App.uploadedFiles.splice(index, 1); updateFileList(); if (App.uploadedFiles.length === 0) resetModal(); }); }); App.processBtn.disabled = App.uploadedFiles.length === 0; }; const handleProcessClick = async () => { if (App.uploadedFiles.length === 0) return; const options = {}; App.toolOptions.querySelectorAll('input, select').forEach(input => { options[input.id.replace(/-/g, '_')] = input.value; }); try { await App.currentTool.process(App.uploadedFiles, options); } catch (error) { console.error('Processing error:', error); showError(error.message || 'An unexpected error occurred.'); } finally { hideLoader(); } }; /*************************************************** * PDF EDITOR SPECIFIC LOGIC ***************************************************/ const renderPdfPageToCanvas = async (pageNum) => { const { fabricCanvas, pdfDoc } = App.pdfEditor; if (!pdfDoc || !fabricCanvas) return; const pageBytes = await pdfDoc.save(); const pdf = await pdfjsLib.getDocument({ data: pageBytes }).promise; const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: 1.5 }); fabricCanvas.setDimensions({ width: viewport.width, height: viewport.height }); const bgCanvas = document.createElement('canvas'); bgCanvas.width = viewport.width; bgCanvas.height = viewport.height; await page.render({ canvasContext: bgCanvas.getContext('2d'), viewport }).promise; fabricCanvas.setBackgroundImage(new fabric.Image(bgCanvas), fabricCanvas.renderAll.bind(fabricCanvas)); if (App.pdfEditor.pageStates[pageNum]) { fabricCanvas.loadFromJSON(App.pdfEditor.pageStates[pageNum]); } else { fabricCanvas.clear(); } fabricCanvas.renderAll(); }; const updatePageAndRender = async (newPageNum) => { if(App.pdfEditor.fabricCanvas) App.pdfEditor.pageStates[App.pdfEditor.currentPage] = App.pdfEditor.fabricCanvas.toJSON(); App.pdfEditor.currentPage = newPageNum; await renderPdfPageToCanvas(newPageNum); updatePageIndicator(); }; const goToPrevPage = async () => { if (App.pdfEditor.currentPage > 1) await updatePageAndRender(App.pdfEditor.currentPage - 1); }; const goToNextPage = async () => { if (App.pdfEditor.currentPage < App.pdfEditor.pdfDoc.getPageCount()) await updatePageAndRender(App.pdfEditor.currentPage + 1); }; const updatePageIndicator = () => { const totalPages = App.pdfEditor.pdfDoc.getPageCount(); document.getElementById('page-indicator').textContent = `${App.pdfEditor.currentPage} / ${totalPages}`; document.getElementById('prev-page-btn').disabled = App.pdfEditor.currentPage === 1; document.getElementById('next-page-btn').disabled = App.pdfEditor.currentPage === totalPages; }; /*************************************************** * UTILITY FUNCTIONS ***************************************************/ const showLoader = (text = 'Processing...') => { App.loaderText.textContent = text; App.loader.style.display = 'flex'; }; const hideLoader = () => App.loader.style.display = 'none'; const showError = (message) => alert(`Error: ${message}`); const createDownloadLink = (data, filename, type) => { const blob = data instanceof Blob ? data : new Blob([data], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.className = 'download-link'; a.textContent = `Download ${filename}`; App.outputArea.innerHTML = ''; App.outputArea.appendChild(a); }; init(); });