import { resetState } from './state.js'; import { formatBytes } from './utils/helpers.js'; import { tesseractLanguages } from './config/tesseract-languages.js'; import { icons, createIcons } from 'lucide'; import Sortable from 'sortablejs'; // Centralizing DOM element selection export const dom = { gridView: document.getElementById('grid-view'), toolGrid: document.getElementById('tool-grid'), toolInterface: document.getElementById('tool-interface'), toolContent: document.getElementById('tool-content'), backToGridBtn: document.getElementById('back-to-grid'), loaderModal: document.getElementById('loader-modal'), loaderText: document.getElementById('loader-text'), alertModal: document.getElementById('alert-modal'), alertTitle: document.getElementById('alert-title'), alertMessage: document.getElementById('alert-message'), alertOkBtn: document.getElementById('alert-ok'), heroSection: document.getElementById('hero-section'), featuresSection: document.getElementById('features-section'), toolsHeader: document.getElementById('tools-header'), dividers: document.querySelectorAll('.section-divider'), hideSections: document.querySelectorAll('.hide-section'), }; export const showLoader = (text = 'Processing...') => { dom.loaderText.textContent = text; dom.loaderModal.classList.remove('hidden'); }; export const hideLoader = () => dom.loaderModal.classList.add('hidden'); export const showAlert = (title: any, message: any) => { dom.alertTitle.textContent = title; dom.alertMessage.textContent = message; dom.alertModal.classList.remove('hidden'); }; export const hideAlert = () => dom.alertModal.classList.add('hidden'); export const switchView = (view: any) => { if (view === 'grid') { dom.gridView.classList.remove('hidden'); dom.toolInterface.classList.add('hidden'); // show hero and features and header dom.heroSection.classList.remove('hidden'); dom.featuresSection.classList.remove('hidden'); dom.toolsHeader.classList.remove('hidden'); // show dividers dom.dividers.forEach((divider) => { divider.classList.remove('hidden'); }); // show hideSections dom.hideSections.forEach((section) => { section.classList.remove('hidden'); }); resetState(); } else { dom.gridView.classList.add('hidden'); dom.toolInterface.classList.remove('hidden'); dom.featuresSection.classList.add('hidden'); dom.heroSection.classList.add('hidden'); dom.toolsHeader.classList.add('hidden'); dom.dividers.forEach((divider) => { divider.classList.add('hidden'); }); dom.hideSections.forEach((section) => { section.classList.add('hidden'); }); } }; const thumbnailState = { sortableInstances: {}, }; function initializeOrganizeSortable(containerId: any) { const container = document.getElementById(containerId); if (!container) return; if (thumbnailState.sortableInstances[containerId]) { thumbnailState.sortableInstances[containerId].destroy(); } thumbnailState.sortableInstances[containerId] = Sortable.create(container, { animation: 150, ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', filter: '.delete-page-btn', preventOnFilter: true, onStart: function (evt: any) { evt.item.style.opacity = '0.5'; }, onEnd: function (evt: any) { evt.item.style.opacity = '1'; }, }); } /** * Renders page thumbnails for tools like 'Organize' and 'Rotate'. * @param {string} toolId The ID of the active tool. * @param {object} pdfDoc The loaded pdf-lib document instance. */ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator'; const container = document.getElementById(containerId); if (!container) return; container.innerHTML = ''; showLoader('Rendering page previews...'); const pdfData = await pdfDoc.save(); // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 0.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 wrapper = document.createElement('div'); wrapper.className = 'page-thumbnail relative group'; // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. wrapper.dataset.pageIndex = i - 1; const imgContainer = document.createElement('div'); imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600'; const img = document.createElement('img'); img.src = canvas.toDataURL(); img.className = 'max-w-full max-h-full object-contain'; imgContainer.appendChild(img); if (toolId === 'organize') { wrapper.className = 'page-thumbnail relative group'; wrapper.appendChild(imgContainer); const pageNumSpan = document.createElement('span'); pageNumSpan.className = 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; pageNumSpan.textContent = i.toString(); const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center'; deleteBtn.innerHTML = '×'; deleteBtn.addEventListener('click', (e) => { (e.currentTarget as HTMLElement).parentElement.remove(); initializeOrganizeSortable(containerId); }); wrapper.append(pageNumSpan, deleteBtn); } else if (toolId === 'rotate') { wrapper.className = 'page-rotator-item flex flex-col items-center gap-2'; wrapper.dataset.rotation = '0'; img.classList.add('transition-transform', 'duration-300'); wrapper.appendChild(imgContainer); const controlsDiv = document.createElement('div'); controlsDiv.className = 'flex items-center justify-center gap-3 w-full'; const pageNumSpan = document.createElement('span'); pageNumSpan.className = 'font-medium text-sm text-white'; pageNumSpan.textContent = i.toString(); const rotateBtn = document.createElement('button'); rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full'; rotateBtn.title = 'Rotate 90°'; rotateBtn.innerHTML = ''; rotateBtn.addEventListener('click', (e) => { e.stopPropagation(); const card = (e.currentTarget as HTMLElement).closest( '.page-rotator-item' ) as HTMLElement; const imgEl = card.querySelector('img'); let currentRotation = parseInt(card.dataset.rotation); currentRotation = (currentRotation + 90) % 360; card.dataset.rotation = currentRotation.toString(); imgEl.style.transform = `rotate(${currentRotation}deg)`; }); controlsDiv.append(pageNumSpan, rotateBtn); wrapper.appendChild(controlsDiv); } container.appendChild(wrapper); createIcons({ icons }); } if (toolId === 'organize') { initializeOrganizeSortable(containerId); } hideLoader(); }; /** * Renders a list of uploaded files in the specified container. * @param {HTMLElement} container The DOM element to render the list into. * @param {File[]} files The array of file objects. */ export const renderFileDisplay = (container: any, files: any) => { container.textContent = ''; if (files.length > 0) { files.forEach((file: any) => { const fileDiv = document.createElement('div'); fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; const nameSpan = document.createElement('span'); nameSpan.className = 'truncate font-medium text-gray-200'; nameSpan.textContent = file.name; const sizeSpan = document.createElement('span'); sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'; sizeSpan.textContent = formatBytes(file.size); fileDiv.append(nameSpan, sizeSpan); container.appendChild(fileDiv); }); } }; const createFileInputHTML = (options = {}) => { // @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'. const multiple = options.multiple ? 'multiple' : ''; // @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'. const acceptedFiles = options.accept || 'application/pdf'; // @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message const showControls = options.showControls || false; // NEW: Add this parameter return `
Click to select a file or drag and drop
${multiple ? 'PDFs or Images' : 'A single PDF file'}
Your files never leave your device.
Combine whole files, or select specific pages to merge into a new document.
${createFileInputHTML({ multiple: true, showControls: true })}How it works:
How it works:
Extract pages from a PDF using various methods.
${createFileInputHTML()}How it works:
Total Pages:
How it works:
This will create a new PDF containing only the even or only the odd pages from your original document.
How it works:
Click on the page thumbnails below to select them. Click again to deselect. All selected pages will be extracted.
How it works:
This mode will create a separate PDF file for every single page in your document and download them together in one ZIP archive.
Upload a PDF to create a new, password-protected version.
${createFileInputHTML()}To create the secure file, each page is converted into an image. This means you won't be able to select text or click links in the final encrypted PDF.
Upload an encrypted PDF and provide its password to create an unlocked version.
${createFileInputHTML()}Reorder, rotate, or delete pages. Drag and drop pages to reorder them.
${createFileInputHTML()} `, rotate: () => `Rotate all or specific pages in a PDF document.
${createFileInputHTML()}Add customizable page numbers to your PDF file.
${createFileInputHTML()}Convert each page of a PDF file into a high-quality JPG image.
${createFileInputHTML()}Click "Download All as ZIP" to get images for all pages.
Convert one or more JPG images into a single PDF file.
${createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true })} `, 'scan-to-pdf': () => `Use your device's camera to scan documents and save them as a PDF. On desktop, this will open a file picker.
${createFileInputHTML({ accept: 'image/*' })} `, crop: () => `Click and drag to select a crop area on any page. You can set different crop areas for each page.
${createFileInputHTML()}Reduce file size by choosing the compression method that best suits your document.
${createFileInputHTML()}Choose 'Vector' for text based PDFs, or 'Photon' for scanned documents and complex images.
Convert all pages of a PDF to greyscale. This is done by rendering each page, applying a filter, and rebuilding the PDF.
${createFileInputHTML()} `, 'pdf-to-zip': () => `Select multiple PDF files to download them together in a single ZIP archive.
${createFileInputHTML({ multiple: true, showControls: true })} `, 'edit-metadata': () => `Modify the core metadata fields of your PDF. Leave a field blank to clear it.
pdf-lib library, which may update the Producer, CreationDate, and ModDate fields due to its default behavior on upload. To accurately view a file's final metadata after editing, or just normal viewing, please use our View Metadata tool.
Note: Custom fields are not supported by all PDF readers.
Completely remove identifying metadata from your PDF.
${createFileInputHTML()} `, flatten: () => `Make PDF forms and annotations non-editable by flattening them.
${createFileInputHTML()} `, 'pdf-to-png': () => `Convert each page of a PDF file into a high-quality PNG image.
${createFileInputHTML()}Your file is ready. Click the button to download a ZIP file containing all PNG images.
Convert one or more PNG images into a single PDF file.
${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })} `, 'pdf-to-webp': () => `Convert each page of a PDF file into a modern WebP image.
${createFileInputHTML()}Your file is ready. Click the button to download a ZIP file containing all WebP images.
Convert one or more WebP images into a single PDF file.
${createFileInputHTML({ multiple: true, accept: 'image/webp', showControls: true })} `, edit: () => `An all-in-one PDF workspace where you can annotate, draw, highlight, redact, add comments and shapes, take screenshots, and view PDFs.
${createFileInputHTML()}Remove specific pages or ranges of pages from your PDF file.
${createFileInputHTML()}Total Pages:
Insert a new blank page at a specific position in your document.
${createFileInputHTML()}Total Pages:
Extract specific pages from a PDF into separate files. Your files will download in a ZIP archive.
${createFileInputHTML()}Total Pages:
Apply a text or image watermark to every page of your PDF document.
${createFileInputHTML()}Add custom text to the top and bottom margins of every page.
${createFileInputHTML()} `, 'image-to-pdf': () => `Combine multiple images into a single PDF. Drag and drop to reorder.
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })}Unlock a PDF and re-save it with new passwords and permissions.
${createFileInputHTML()}Convert a PDF's text content into a structured Markdown file.
${createFileInputHTML({ accept: '.pdf' })}Note: This is a text-focused conversion. Tables and images will not be included.
Type or paste your text below and convert it to a PDF with custom formatting.
Convert your PDF to a "dark mode" by inverting its colors. This works best on simple text and image documents.
${createFileInputHTML()} `, 'view-metadata': () => `Upload a PDF to view its internal properties, such as Title, Author, and Creation Date.
${createFileInputHTML()} `, 'reverse-pages': () => `Flip the order of all pages in your document, making the last page the first.
${createFileInputHTML()} `, 'md-to-pdf': () => `Write in Markdown, select your formatting options, and get a high-quality, multi-page PDF.
Note: Images linked from the web (e.g., https://...) require an internet connection to be rendered.
Convert one or more SVG vector images into a single PDF file.
${createFileInputHTML({ multiple: true, accept: 'image/svg+xml', showControls: true })} `, 'bmp-to-pdf': () => `Convert one or more BMP images into a single PDF file.
${createFileInputHTML({ multiple: true, accept: 'image/bmp', showControls: true })} `, 'heic-to-pdf': () => `Convert one or more HEIC (High Efficiency) images from your iPhone or camera into a single PDF file.
${createFileInputHTML({ multiple: true, accept: '.heic,.heif', showControls: true })} `, 'tiff-to-pdf': () => `Convert one or more single or multi-page TIFF images into a single PDF file.
${createFileInputHTML({ multiple: true, accept: 'image/tiff', showControls: true })} `, 'pdf-to-bmp': () => `Convert each page of a PDF file into a BMP image. Your files will be downloaded in a ZIP archive.
${createFileInputHTML()} `, 'pdf-to-tiff': () => `Convert each page of a PDF file into a high-quality TIFF image. Your files will be downloaded in a ZIP archive.
${createFileInputHTML()} `, 'split-in-half': () => `Choose a method to divide every page of your document into two separate pages.
${createFileInputHTML()}Upload a PDF to see the precise dimensions, standard size, and orientation of every page.
${createFileInputHTML()}| Page # | Dimensions (W x H) | Standard Size | Orientation |
|---|
Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.
${createFileInputHTML()}Drag pages to reorder them. Use the icon to duplicate a page or the icon to delete it.
${createFileInputHTML()}Stitch all pages of your PDF together vertically to create one continuous, scrollable page.
${createFileInputHTML()}Convert all pages in your PDF to a uniform size. Choose a standard format or define a custom dimension.
${createFileInputHTML()}Select a new background color for every page of your PDF.
${createFileInputHTML()}Change the color of dark text in your PDF. This process converts pages to images, so text will not be selectable in the final file.
${createFileInputHTML()}Upload two files to visually compare them using either an overlay or a side-by-side view.
Upload Original PDF
Upload Revised PDF
Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.
${createFileInputHTML()}Selected: None
Initializing...
Your searchable PDF is ready. You can also copy or download the extracted text below.
Upload a .docx file to convert it into a high-quality PDF with selectable text. Complex layouts may not be perfectly preserved.
Click to select a file or drag and drop
A single .docx file
Create your signature, select it, then click on the document to place. You can drag to move placed signatures.
${createFileInputHTML()}Your saved signatures will appear here. Click one to select it.
Select the types of annotations to remove from all pages or a specific range.
${createFileInputHTML()}Total Pages:
Upload a PDF to visually crop one or more pages. This tool offers a live preview and two distinct cropping modes.
${createFileInputHTML()}How it works:
Upload a PDF to fill in existing form fields. The PDF view on the right will update as you type.
${createFileInputHTML()}Upload a file to see form fields here.
Split pages into multiple smaller sheets to print as a poster. Navigate the preview and see the grid update based on your settings.
${createFileInputHTML()}Total pages: 0
Automatically detect and remove blank or nearly blank pages from your PDF. Adjust the sensitivity to control what is considered "blank".
${createFileInputHTML()}Higher sensitivity requires pages to be more "blank" to be removed.
Combine pages from 2 or more documents, alternating between them. Drag the files to set the mixing order (e.g., Page 1 from Doc A, Page 1 from Doc B, Page 2 from Doc A, Page 2 from Doc B, etc.).
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}How it works: