diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index 574b64f..56d0cd3 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -38,6 +38,7 @@ export const singlePdfLoadTools = [ 'posterize', 'remove-blank-pages', 'add-attachments', + 'sanitize-pdf', ]; export const simpleTools = [ diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index b5bb628..fd9e5a6 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -412,6 +412,12 @@ export const categories = [ icon: 'lock', subtitle: 'Add a password to protect your PDF.', }, + { + id: 'sanitize-pdf', + name: 'Sanitize PDF', + icon: 'shield-alert', + subtitle: 'Remove metadata, annotations, scripts, and more.', + }, { id: 'decrypt', name: 'Decrypt PDF', diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 9d6f250..3c04413 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -269,9 +269,9 @@ async function handleSinglePdfUpload(toolId, file) { if ((child as Element).nodeType !== 1) continue; let key = (child as Element).tagName; - const elementChildren = Array.from((child as Element).children).filter( - (c) => c.nodeType === 1 - ); + const elementChildren = Array.from( + (child as Element).children + ).filter((c) => c.nodeType === 1); if (key === 'rdf:li') { appendXmpNodes(child, ulElement, indentLevel); diff --git a/src/js/logic/flatten.ts b/src/js/logic/flatten.ts index 16e4c80..17f58ed 100644 --- a/src/js/logic/flatten.ts +++ b/src/js/logic/flatten.ts @@ -2,6 +2,11 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +export function flattenFormsInDoc(pdfDoc) { + const form = pdfDoc.getForm(); + form.flatten(); +} + export async function flatten() { if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); @@ -9,8 +14,7 @@ export async function flatten() { } showLoader('Flattening PDF...'); try { - const form = state.pdfDoc.getForm(); - form.flatten(); + flattenFormsInDoc(state.pdfDoc); const flattenedBytes = await state.pdfDoc.save(); downloadFile( diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index 4ba504b..261c41e 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -63,6 +63,7 @@ import { import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { linearizePdf } from './linearize.js'; import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js'; +import { sanitizePdf } from './sanitize-pdf.js'; export const toolLogic = { merge: { process: merge, setup: setupMergeTool }, @@ -135,4 +136,5 @@ export const toolLogic = { process: addAttachments, setup: setupAddAttachmentsTool, }, + 'sanitize-pdf': sanitizePdf, }; diff --git a/src/js/logic/remove-annotations.ts b/src/js/logic/remove-annotations.ts index 5aa425e..e079f8f 100644 --- a/src/js/logic/remove-annotations.ts +++ b/src/js/logic/remove-annotations.ts @@ -28,6 +28,48 @@ export function setupRemoveAnnotationsTool() { }); } +export function removeAnnotationsFromDoc( + pdfDoc, + pageIndices = null, + annotationTypes = null +) { + const pages = pdfDoc.getPages(); + const targetPages = + pageIndices || Array.from({ length: pages.length }, (_, i) => i); + + for (const pageIndex of targetPages) { + const page = pages[pageIndex]; + const annotRefs = page.node.Annots()?.asArray() || []; + + if (!annotationTypes) { + if (annotRefs.length > 0) { + page.node.delete(PDFName.of('Annots')); + } + } else { + const annotsToKeep = []; + + for (const ref of annotRefs) { + const annot = pdfDoc.context.lookup(ref); + const subtype = annot + .get(PDFName.of('Subtype')) + ?.toString() + .substring(1); + + if (!subtype || !annotationTypes.has(subtype)) { + annotsToKeep.push(ref); + } + } + + if (annotsToKeep.length > 0) { + const newAnnotsArray = pdfDoc.context.obj(annotsToKeep); + page.node.set(PDFName.of('Annots'), newAnnotsArray); + } else { + page.node.delete(PDFName.of('Annots')); + } + } + } +} + export async function removeAnnotations() { showLoader('Removing annotations...'); try { @@ -80,34 +122,7 @@ export async function removeAnnotations() { if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.'); - const pages = state.pdfDoc.getPages(); - - for (const pageIndex of targetPageIndices) { - const page = pages[pageIndex]; - const annotRefs = page.node.Annots()?.asArray() || []; - - const annotsToKeep = []; - - for (const ref of annotRefs) { - const annot = state.pdfDoc.context.lookup(ref); - const subtype = annot - .get(PDFName.of('Subtype')) - ?.toString() - .substring(1); - - // If the subtype is NOT in the list to remove, add it to our new array - if (!subtype || !typesToRemove.has(subtype)) { - annotsToKeep.push(ref); - } - } - - if (annotsToKeep.length > 0) { - const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep); - page.node.set(PDFName.of('Annots'), newAnnotsArray); - } else { - page.node.delete(PDFName.of('Annots')); - } - } + removeAnnotationsFromDoc(state.pdfDoc, targetPageIndices, typesToRemove); const newPdfBytes = await state.pdfDoc.save(); downloadFile( diff --git a/src/js/logic/remove-metadata.ts b/src/js/logic/remove-metadata.ts index a4f140f..3fbbc4e 100644 --- a/src/js/logic/remove-metadata.ts +++ b/src/js/logic/remove-metadata.ts @@ -1,23 +1,54 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +import { PDFName } from 'pdf-lib'; + +export function removeMetadataFromDoc(pdfDoc) { + const infoDict = pdfDoc.getInfoDict(); + const allKeys = infoDict.keys(); + allKeys.forEach((key: any) => { + infoDict.delete(key); + }); + + pdfDoc.setTitle(''); + pdfDoc.setAuthor(''); + pdfDoc.setSubject(''); + pdfDoc.setKeywords([]); + pdfDoc.setCreator(''); + pdfDoc.setProducer(''); + + try { + const catalogDict = pdfDoc.catalog.dict; + if (catalogDict.has(PDFName.of('Metadata'))) { + catalogDict.delete(PDFName.of('Metadata')); + } + } catch (e) { + console.warn('Could not remove XMP metadata:', e.message); + } + + try { + const context = pdfDoc.context; + if (context.trailerInfo) { + delete context.trailerInfo.ID; + } + } catch (e) { + console.warn('Could not remove document IDs:', e.message); + } + + try { + const catalogDict = pdfDoc.catalog.dict; + if (catalogDict.has(PDFName.of('PieceInfo'))) { + catalogDict.delete(PDFName.of('PieceInfo')); + } + } catch (e) { + console.warn('Could not remove PieceInfo:', e.message); + } +} export async function removeMetadata() { showLoader('Removing all metadata...'); try { - const infoDict = state.pdfDoc.getInfoDict(); - - const allKeys = infoDict.keys(); - allKeys.forEach((key: any) => { - infoDict.delete(key); - }); - - state.pdfDoc.setTitle(''); - state.pdfDoc.setAuthor(''); - state.pdfDoc.setSubject(''); - state.pdfDoc.setKeywords([]); - state.pdfDoc.setCreator(''); - state.pdfDoc.setProducer(''); + removeMetadataFromDoc(state.pdfDoc); const newPdfBytes = await state.pdfDoc.save(); downloadFile( diff --git a/src/js/logic/sanitize-pdf.ts b/src/js/logic/sanitize-pdf.ts new file mode 100644 index 0000000..9ef1dfb --- /dev/null +++ b/src/js/logic/sanitize-pdf.ts @@ -0,0 +1,419 @@ +import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { downloadFile } from '../utils/helpers.js'; +import { state } from '../state.js'; +import { removeMetadataFromDoc } from './remove-metadata.js'; +import { removeAnnotationsFromDoc } from './remove-annotations.js'; +import { flattenFormsInDoc } from './flatten.js'; +import { PDFName } from 'pdf-lib'; + +export async function sanitizePdf() { + if (!state.pdfDoc) { + showAlert('Error', 'No PDF document loaded.'); + return; + } + + showLoader('Sanitizing PDF...'); + try { + const pdfDoc = state.pdfDoc; + + const shouldFlattenForms = ( + document.getElementById('flatten-forms') as HTMLInputElement + ).checked; + const shouldRemoveMetadata = ( + document.getElementById('remove-metadata') as HTMLInputElement + ).checked; + const shouldRemoveAnnotations = ( + document.getElementById('remove-annotations') as HTMLInputElement + ).checked; + const shouldRemoveJavascript = ( + document.getElementById('remove-javascript') as HTMLInputElement + ).checked; + const shouldRemoveEmbeddedFiles = ( + document.getElementById('remove-embedded-files') as HTMLInputElement + ).checked; + const shouldRemoveLayers = ( + document.getElementById('remove-layers') as HTMLInputElement + ).checked; + const shouldRemoveLinks = ( + document.getElementById('remove-links') as HTMLInputElement + ).checked; + const shouldRemoveStructureTree = ( + document.getElementById('remove-structure-tree') as HTMLInputElement + ).checked; + const shouldRemoveMarkInfo = ( + document.getElementById('remove-markinfo') as HTMLInputElement + ).checked; + const shouldRemoveFonts = ( + document.getElementById('remove-fonts') as HTMLInputElement + ).checked; + + let changesMade = false; + + if (shouldFlattenForms) { + try { + flattenFormsInDoc(pdfDoc); + changesMade = true; + } catch (e) { + console.warn(`Could not flatten forms: ${e.message}`); + try { + const catalogDict = pdfDoc.catalog.dict; + if (catalogDict.has(PDFName.of('AcroForm'))) { + catalogDict.delete(PDFName.of('AcroForm')); + changesMade = true; + } + } catch (removeError) { + console.warn('Could not remove AcroForm:', removeError.message); + } + } + } + + if (shouldRemoveMetadata) { + removeMetadataFromDoc(pdfDoc); + changesMade = true; + } + + if (shouldRemoveAnnotations) { + removeAnnotationsFromDoc(pdfDoc); + changesMade = true; + } + + if (shouldRemoveJavascript) { + try { + if (pdfDoc.javaScripts && pdfDoc.javaScripts.length > 0) { + pdfDoc.javaScripts = []; + changesMade = true; + } + + const catalogDict = pdfDoc.catalog.dict; + + const namesRef = catalogDict.get(PDFName.of('Names')); + if (namesRef) { + try { + const namesDict = pdfDoc.context.lookup(namesRef); + if (namesDict.has(PDFName.of('JavaScript'))) { + namesDict.delete(PDFName.of('JavaScript')); + changesMade = true; + } + } catch (e) { + console.warn('Could not access Names/JavaScript:', e.message); + } + } + + if (catalogDict.has(PDFName.of('OpenAction'))) { + catalogDict.delete(PDFName.of('OpenAction')); + changesMade = true; + } + + if (catalogDict.has(PDFName.of('AA'))) { + catalogDict.delete(PDFName.of('AA')); + changesMade = true; + } + + const pages = pdfDoc.getPages(); + for (const page of pages) { + try { + const pageDict = page.node; + if (pageDict.has(PDFName.of('AA'))) { + pageDict.delete(PDFName.of('AA')); + changesMade = true; + } + } catch (e) { + console.warn('Could not remove page actions:', e.message); + } + } + } catch (e) { + console.warn(`Could not remove JavaScript: ${e.message}`); + } + } + + if (shouldRemoveEmbeddedFiles) { + try { + const catalogDict = pdfDoc.catalog.dict; + + const namesRef = catalogDict.get(PDFName.of('Names')); + if (namesRef) { + try { + const namesDict = pdfDoc.context.lookup(namesRef); + if (namesDict.has(PDFName.of('EmbeddedFiles'))) { + namesDict.delete(PDFName.of('EmbeddedFiles')); + changesMade = true; + } + } catch (e) { + console.warn('Could not access Names/EmbeddedFiles:', e.message); + } + } + + if (catalogDict.has(PDFName.of('EmbeddedFiles'))) { + catalogDict.delete(PDFName.of('EmbeddedFiles')); + changesMade = true; + } + + const pages = pdfDoc.getPages(); + for (const page of pages) { + try { + const annotRefs = page.node.Annots()?.asArray() || []; + const annotsToKeep = []; + + for (const ref of annotRefs) { + try { + const annot = pdfDoc.context.lookup(ref); + const subtype = annot + .get(PDFName.of('Subtype')) + ?.toString() + .substring(1); + + if (subtype !== 'FileAttachment') { + annotsToKeep.push(ref); + } else { + changesMade = true; + } + } catch (e) { + annotsToKeep.push(ref); + } + } + + if (annotsToKeep.length !== annotRefs.length) { + if (annotsToKeep.length > 0) { + const newAnnotsArray = pdfDoc.context.obj(annotsToKeep); + page.node.set(PDFName.of('Annots'), newAnnotsArray); + } else { + page.node.delete(PDFName.of('Annots')); + } + } + } catch (pageError) { + console.warn( + `Could not process page for attachments: ${pageError.message}` + ); + } + } + + if (pdfDoc.embeddedFiles && pdfDoc.embeddedFiles.length > 0) { + pdfDoc.embeddedFiles = []; + changesMade = true; + } + + if (catalogDict.has(PDFName.of('Collection'))) { + catalogDict.delete(PDFName.of('Collection')); + changesMade = true; + } + } catch (e) { + console.warn(`Could not remove embedded files: ${e.message}`); + } + } + + if (shouldRemoveLayers) { + try { + const catalogDict = pdfDoc.catalog.dict; + + if (catalogDict.has(PDFName.of('OCProperties'))) { + catalogDict.delete(PDFName.of('OCProperties')); + changesMade = true; + } + + const pages = pdfDoc.getPages(); + for (const page of pages) { + try { + const pageDict = page.node; + + if (pageDict.has(PDFName.of('OCProperties'))) { + pageDict.delete(PDFName.of('OCProperties')); + changesMade = true; + } + + const resourcesRef = pageDict.get(PDFName.of('Resources')); + if (resourcesRef) { + try { + const resourcesDict = pdfDoc.context.lookup(resourcesRef); + if (resourcesDict.has(PDFName.of('Properties'))) { + resourcesDict.delete(PDFName.of('Properties')); + changesMade = true; + } + } catch (e) { + console.warn('Could not access Resources:', e.message); + } + } + } catch (e) { + console.warn('Could not remove page layers:', e.message); + } + } + } catch (e) { + console.warn(`Could not remove layers: ${e.message}`); + } + } + + if (shouldRemoveLinks) { + try { + const pages = pdfDoc.getPages(); + + for (const page of pages) { + try { + const annotRefs = page.node.Annots()?.asArray() || []; + const annotsToKeep = []; + + for (const ref of annotRefs) { + try { + const annot = pdfDoc.context.lookup(ref); + const subtype = annot + .get(PDFName.of('Subtype')) + ?.toString() + .substring(1); + + let hasExternalLink = false; + + if (subtype === 'Link') { + const action = annot.get(PDFName.of('A')); + if (action) { + try { + const actionDict = pdfDoc.context.lookup(action); + const actionType = actionDict + .get(PDFName.of('S')) + ?.toString() + .substring(1); + + if (actionType === 'URI' || actionType === 'Launch') { + hasExternalLink = true; + changesMade = true; + } + } catch (e) { + // Keep if we can't determine + } + } + } + + if (!hasExternalLink) { + annotsToKeep.push(ref); + } + } catch (e) { + // Keep annotation if we can't read it + annotsToKeep.push(ref); + } + } + + if (annotsToKeep.length !== annotRefs.length) { + if (annotsToKeep.length > 0) { + const newAnnotsArray = pdfDoc.context.obj(annotsToKeep); + page.node.set(PDFName.of('Annots'), newAnnotsArray); + } else { + page.node.delete(PDFName.of('Annots')); + } + } + } catch (pageError) { + console.warn( + `Could not process page for links: ${pageError.message}` + ); + } + } + } catch (e) { + console.warn(`Could not remove links: ${e.message}`); + } + } + + if (shouldRemoveStructureTree) { + try { + const catalogDict = pdfDoc.catalog.dict; + + if (catalogDict.has(PDFName.of('StructTreeRoot'))) { + catalogDict.delete(PDFName.of('StructTreeRoot')); + changesMade = true; + } + + const pages = pdfDoc.getPages(); + for (const page of pages) { + try { + const pageDict = page.node; + if (pageDict.has(PDFName.of('StructParents'))) { + pageDict.delete(PDFName.of('StructParents')); + changesMade = true; + } + } catch (e) { + console.warn('Could not remove page StructParents:', e.message); + } + } + + if (catalogDict.has(PDFName.of('ParentTree'))) { + catalogDict.delete(PDFName.of('ParentTree')); + changesMade = true; + } + } catch (e) { + console.warn(`Could not remove structure tree: ${e.message}`); + } + } + + if (shouldRemoveMarkInfo) { + try { + const catalogDict = pdfDoc.catalog.dict; + + if (catalogDict.has(PDFName.of('MarkInfo'))) { + catalogDict.delete(PDFName.of('MarkInfo')); + changesMade = true; + } + + if (catalogDict.has(PDFName.of('Marked'))) { + catalogDict.delete(PDFName.of('Marked')); + changesMade = true; + } + } catch (e) { + console.warn(`Could not remove MarkInfo: ${e.message}`); + } + } + + if (shouldRemoveFonts) { + try { + const pages = pdfDoc.getPages(); + + for (const page of pages) { + try { + const pageDict = page.node; + const resourcesRef = pageDict.get(PDFName.of('Resources')); + + if (resourcesRef) { + try { + const resourcesDict = pdfDoc.context.lookup(resourcesRef); + + if (resourcesDict.has(PDFName.of('Font'))) { + resourcesDict.delete(PDFName.of('Font')); + changesMade = true; + } + } catch (e) { + console.warn( + 'Could not access Resources for fonts:', + e.message + ); + } + } + } catch (e) { + console.warn('Could not remove page fonts:', e.message); + } + } + + if (pdfDoc.fonts && pdfDoc.fonts.length > 0) { + pdfDoc.fonts = []; + changesMade = true; + } + } catch (e) { + console.warn(`Could not remove fonts: ${e.message}`); + } + } + + if (!changesMade) { + showAlert( + 'No Changes', + 'No items were selected for removal or none were found in the PDF.' + ); + hideLoader(); + return; + } + + const sanitizedPdfBytes = await pdfDoc.save(); + downloadFile( + new Blob([sanitizedPdfBytes], { type: 'application/pdf' }), + 'sanitized.pdf' + ); + showAlert('Success', 'PDF has been sanitized and downloaded.'); + } catch (e) { + console.error('Sanitization Error:', e); + showAlert('Error', `An error occurred during sanitization: ${e.message}`); + } finally { + hideLoader(); + } +} diff --git a/src/js/ui.ts b/src/js/ui.ts index d85ff7a..09159b4 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -6,98 +6,98 @@ 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'), + 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'); + 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'); + 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'); - }); + 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'); - }); - } + 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: {}, + sortableInstances: {}, }; function initializeOrganizeSortable(containerId: any) { - const container = document.getElementById(containerId); - if (!container) return; + const container = document.getElementById(containerId); + if (!container) return; - if (thumbnailState.sortableInstances[containerId]) { - thumbnailState.sortableInstances[containerId].destroy(); - } + 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'; - }, - }); + 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'; + }, + }); } /** @@ -106,103 +106,103 @@ function initializeOrganizeSortable(containerId: any) { * @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; + const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator'; + const container = document.getElementById(containerId); + if (!container) return; - container.innerHTML = ''; - showLoader('Rendering page previews...'); + 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; + 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; + 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 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 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'; + const img = document.createElement('img'); + img.src = canvas.toDataURL(); + img.className = 'max-w-full max-h-full object-contain'; - imgContainer.appendChild(img); + imgContainer.appendChild(img); - if (toolId === 'organize') { - wrapper.className = 'page-thumbnail relative group'; - wrapper.appendChild(imgContainer); + 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 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); - }); + 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); + 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 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 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)`; - }); + 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); + controlsDiv.append(pageNumSpan, rotateBtn); + wrapper.appendChild(controlsDiv); + } + + container.appendChild(wrapper); + createIcons({ icons }); } - container.appendChild(wrapper); - createIcons({ icons }); - } + if (toolId === 'organize') { + initializeOrganizeSortable(containerId); + } - if (toolId === 'organize') { - initializeOrganizeSortable(containerId); - } - - hideLoader(); + hideLoader(); }; /** @@ -211,36 +211,36 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { * @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'; + 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 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); + 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); - }); - } + 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 + // @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 ` + return `
@@ -251,8 +251,7 @@ const createFileInputHTML = (options = {}) => {
- ${ - showControls + ${showControls ? ` `, - split: () => ` + split: () => `

Split PDF

Extract pages from a PDF using various methods.

${createFileInputHTML()} @@ -378,7 +377,7 @@ export const toolTemplates = {
`, - encrypt: () => ` + encrypt: () => `

Encrypt PDF

Upload a PDF to create a new, password-protected version.

${createFileInputHTML()} @@ -396,7 +395,7 @@ export const toolTemplates = { `, - decrypt: () => ` + decrypt: () => `

Decrypt PDF

Upload an encrypted PDF and provide its password to create an unlocked version.

${createFileInputHTML()} @@ -410,7 +409,7 @@ export const toolTemplates = { `, - organize: () => ` + organize: () => `

Organize PDF

Reorder, rotate, or delete pages. Drag and drop pages to reorder them.

${createFileInputHTML()} @@ -419,7 +418,7 @@ export const toolTemplates = { `, - rotate: () => ` + rotate: () => `

Rotate PDF

Rotate all or specific pages in a PDF document.

${createFileInputHTML()} @@ -444,7 +443,7 @@ export const toolTemplates = { `, - 'add-page-numbers': () => ` + 'add-page-numbers': () => `

Add Page Numbers

Add customizable page numbers to your PDF file.

${createFileInputHTML()} @@ -479,7 +478,7 @@ export const toolTemplates = { `, - 'pdf-to-jpg': () => ` + 'pdf-to-jpg': () => `

PDF to JPG

Convert each page of a PDF file into a high-quality JPG image.

${createFileInputHTML()} @@ -489,14 +488,14 @@ export const toolTemplates = { `, - 'jpg-to-pdf': () => ` + 'jpg-to-pdf': () => `

JPG to PDF

Convert one or more JPG images into a single PDF file.

${createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true })}
`, - 'scan-to-pdf': () => ` + 'scan-to-pdf': () => `

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/*' })} @@ -504,7 +503,7 @@ export const toolTemplates = { `, - crop: () => ` + crop: () => `

Crop PDF

Click and drag to select a crop area on any page. You can set different crop areas for each page.

${createFileInputHTML()} @@ -529,7 +528,7 @@ export const toolTemplates = { `, - compress: () => ` + compress: () => `

Compress PDF

Reduce file size by choosing the compression method that best suits your document.

${createFileInputHTML()} @@ -559,14 +558,14 @@ export const toolTemplates = { `, - 'pdf-to-greyscale': () => ` + 'pdf-to-greyscale': () => `

PDF to Greyscale

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': () => ` + 'pdf-to-zip': () => `

Combine PDFs into ZIP

Select multiple PDF files to download them together in a single ZIP archive.

${createFileInputHTML({ multiple: true, showControls: true })} @@ -574,7 +573,7 @@ export const toolTemplates = { `, - 'edit-metadata': () => ` + 'edit-metadata': () => `

Edit PDF Metadata

Modify the core metadata fields of your PDF. Leave a field blank to clear it.

@@ -637,21 +636,21 @@ export const toolTemplates = { `, - 'remove-metadata': () => ` + 'remove-metadata': () => `

Remove PDF Metadata

Completely remove identifying metadata from your PDF.

${createFileInputHTML()}
`, - flatten: () => ` + flatten: () => `

Flatten PDF

Make PDF forms and annotations non-editable by flattening them.

${createFileInputHTML()}
`, - 'pdf-to-png': () => ` + 'pdf-to-png': () => `

PDF to PNG

Convert each page of a PDF file into a high-quality PNG image.

${createFileInputHTML()} @@ -661,14 +660,14 @@ export const toolTemplates = { `, - 'png-to-pdf': () => ` + 'png-to-pdf': () => `

PNG to PDF

Convert one or more PNG images into a single PDF file.

${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })}
`, - 'pdf-to-webp': () => ` + 'pdf-to-webp': () => `

PDF to WebP

Convert each page of a PDF file into a modern WebP image.

${createFileInputHTML()} @@ -678,14 +677,14 @@ export const toolTemplates = { `, - 'webp-to-pdf': () => ` + 'webp-to-pdf': () => `

WebP to PDF

Convert one or more WebP images into a single PDF file.

${createFileInputHTML({ multiple: true, accept: 'image/webp', showControls: true })}
`, - edit: () => ` + edit: () => `

PDF Studio

An all-in-one PDF workspace where you can annotate, draw, highlight, redact, add comments and shapes, take screenshots, and view PDFs.

${createFileInputHTML()} @@ -694,7 +693,7 @@ export const toolTemplates = {
`, - 'delete-pages': () => ` + 'delete-pages': () => `

Delete Pages

Remove specific pages or ranges of pages from your PDF file.

${createFileInputHTML()} @@ -706,7 +705,7 @@ export const toolTemplates = { `, - 'add-blank-page': () => ` + 'add-blank-page': () => `

Add Blank Pages

Insert one or more blank pages at a specific position in your document.

${createFileInputHTML()} @@ -720,7 +719,7 @@ export const toolTemplates = { `, - 'extract-pages': () => ` + 'extract-pages': () => `

Extract Pages

Extract specific pages from a PDF into separate files. Your files will download in a ZIP archive.

${createFileInputHTML()} @@ -733,7 +732,7 @@ export const toolTemplates = { `, - 'add-watermark': () => ` + 'add-watermark': () => `

Add Watermark

Apply a text or image watermark to every page of your PDF document.

${createFileInputHTML()} @@ -797,7 +796,7 @@ export const toolTemplates = { `, - 'add-header-footer': () => ` + 'add-header-footer': () => `

Add Header & Footer

Add custom text to the top and bottom margins of every page.

${createFileInputHTML()} @@ -855,7 +854,7 @@ export const toolTemplates = { `, - 'image-to-pdf': () => ` + 'image-to-pdf': () => `

Image to PDF Converter

Combine multiple images into a single PDF. Drag and drop to reorder.

${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })} @@ -863,7 +862,7 @@ export const toolTemplates = { `, - 'change-permissions': () => ` + 'change-permissions': () => `

Change PDF Permissions

Unlock a PDF and re-save it with new passwords and permissions.

${createFileInputHTML()} @@ -911,7 +910,7 @@ export const toolTemplates = { `, - 'pdf-to-markdown': () => ` + 'pdf-to-markdown': () => `

PDF to Markdown

Convert a PDF's text content into a structured Markdown file.

${createFileInputHTML({ accept: '.pdf' })} @@ -921,7 +920,7 @@ export const toolTemplates = { `, - 'txt-to-pdf': () => ` + 'txt-to-pdf': () => `

Text to PDF

Type or paste your text below and convert it to a PDF with custom formatting.

@@ -952,28 +951,28 @@ export const toolTemplates = { `, - 'invert-colors': () => ` + 'invert-colors': () => `

Invert PDF Colors

Convert your PDF to a "dark mode" by inverting its colors. This works best on simple text and image documents.

${createFileInputHTML()}
`, - 'view-metadata': () => ` + 'view-metadata': () => `

View PDF Metadata

Upload a PDF to view its internal properties, such as Title, Author, and Creation Date.

${createFileInputHTML()}
`, - 'reverse-pages': () => ` + 'reverse-pages': () => `

Reverse PDF Pages

Flip the order of all pages in your document, making the last page the first.

${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
`, - 'md-to-pdf': () => ` + 'md-to-pdf': () => `

Markdown 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.

@@ -1006,42 +1005,42 @@ export const toolTemplates = {
`, - 'svg-to-pdf': () => ` + 'svg-to-pdf': () => `

SVG to PDF

Convert one or more SVG vector images into a single PDF file.

${createFileInputHTML({ multiple: true, accept: 'image/svg+xml', showControls: true })}
`, - 'bmp-to-pdf': () => ` + 'bmp-to-pdf': () => `

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': () => ` + 'heic-to-pdf': () => `

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': () => ` + 'tiff-to-pdf': () => `

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': () => ` + 'pdf-to-bmp': () => `

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': () => ` + 'pdf-to-tiff': () => `

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()} @@ -1049,7 +1048,7 @@ export const toolTemplates = { `, - 'split-in-half': () => ` + 'split-in-half': () => `

Split Pages in Half

Choose a method to divide every page of your document into two separate pages.

${createFileInputHTML()} @@ -1065,7 +1064,7 @@ export const toolTemplates = { `, - 'page-dimensions': () => ` + 'page-dimensions': () => `

Analyze Page Dimensions

Upload a PDF to see the precise dimensions, standard size, and orientation of every page.

${createFileInputHTML()} @@ -1098,7 +1097,7 @@ export const toolTemplates = { `, - 'n-up': () => ` + 'n-up': () => `

N-Up Page Arrangement

Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.

${createFileInputHTML()} @@ -1161,7 +1160,7 @@ export const toolTemplates = { `, - 'duplicate-organize': () => ` + 'duplicate-organize': () => `

Page Manager

Drag pages to reorder them. Use the icon to duplicate a page or the icon to delete it.

${createFileInputHTML()} @@ -1174,7 +1173,7 @@ export const toolTemplates = { `, - 'combine-single-page': () => ` + 'combine-single-page': () => `

Combine to a Single Page

Stitch all pages of your PDF together vertically to create one continuous, scrollable page.

${createFileInputHTML()} @@ -1201,7 +1200,7 @@ export const toolTemplates = { `, - 'fix-dimensions': () => ` + 'fix-dimensions': () => `

Standardize Page Dimensions

Convert all pages in your PDF to a uniform size. Choose a standard format or define a custom dimension.

${createFileInputHTML()} @@ -1277,7 +1276,7 @@ export const toolTemplates = { `, - 'change-background-color': () => ` + 'change-background-color': () => `

Change Background Color

Select a new background color for every page of your PDF.

${createFileInputHTML()} @@ -1289,7 +1288,7 @@ export const toolTemplates = { `, - 'change-text-color': () => ` + 'change-text-color': () => `

Change Text Color

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()} @@ -1313,7 +1312,7 @@ export const toolTemplates = { `, - 'compare-pdfs': () => ` + 'compare-pdfs': () => `

Compare PDFs

Upload two files to visually compare them using either an overlay or a side-by-side view.

@@ -1364,7 +1363,7 @@ export const toolTemplates = { `, - 'ocr-pdf': () => ` + 'ocr-pdf': () => `

OCR PDF

Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.

@@ -1388,15 +1387,15 @@ export const toolTemplates = {
${Object.entries(tesseractLanguages) - .map( - ([code, name]) => ` + .map( + ([code, name]) => ` ` - ) - .join('')} + ) + .join('')}

Selected: None

@@ -1476,7 +1475,7 @@ export const toolTemplates = { `, - 'word-to-pdf': () => ` + 'word-to-pdf': () => `

Word to PDF Converter

Upload a .docx file to convert it into a high-quality PDF with selectable text. Complex layouts may not be perfectly preserved.

@@ -1495,7 +1494,7 @@ export const toolTemplates = { `, - 'sign-pdf': () => ` + 'sign-pdf': () => `

Sign PDF

Create your signature, select it, then click on the document to place. You can drag to move placed signatures.

${createFileInputHTML()} @@ -1586,7 +1585,7 @@ export const toolTemplates = { `, - 'remove-annotations': () => ` + 'remove-annotations': () => `

Remove Annotations

Select the types of annotations to remove from all pages or a specific range.

${createFileInputHTML()} @@ -1645,7 +1644,7 @@ export const toolTemplates = { `, - cropper: () => ` + cropper: () => `

PDF Cropper

Upload a PDF to visually crop one or more pages. This tool offers a live preview and two distinct cropping modes.

@@ -1689,7 +1688,7 @@ export const toolTemplates = { `, - 'form-filler': () => ` + 'form-filler': () => `

PDF Form Filler

Upload a PDF to fill in existing form fields. The PDF view on the right will update as you type.

${createFileInputHTML()} @@ -1736,7 +1735,7 @@ export const toolTemplates = { `, - posterize: () => ` + posterize: () => `

Posterize PDF

Split pages into multiple smaller sheets to print as a poster. Navigate the preview and see the grid update based on your settings.

${createFileInputHTML()} @@ -1836,7 +1835,7 @@ export const toolTemplates = { `, - 'remove-blank-pages': () => ` + 'remove-blank-pages': () => `

Remove Blank Pages

Automatically detect and remove blank or nearly blank pages from your PDF. Adjust the sensitivity to control what is considered "blank".

${createFileInputHTML()} @@ -1861,7 +1860,7 @@ export const toolTemplates = { `, - 'alternate-merge': () => ` + 'alternate-merge': () => `

Alternate & Mix Pages

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 })} @@ -1879,14 +1878,14 @@ export const toolTemplates = { `, - linearize: () => ` + linearize: () => `

Linearize PDFs (Fast Web View)

Optimize multiple PDFs for faster loading over the web. Files will be downloaded in a ZIP archive.

${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
`, - 'add-attachments': () => ` + 'add-attachments': () => `

Add Attachments to PDF

First, upload the PDF document you want to add files to.

${createFileInputHTML({ accept: 'application/pdf' })} @@ -1910,4 +1909,72 @@ export const toolTemplates = { `, + + 'sanitize-pdf': () => ` +

Sanitize PDF

+

Remove potentially sensitive or unnecessary information from your PDF before sharing. Select the items you want to remove.

+ ${createFileInputHTML()} +
+ + +`, };