From e0e5d5024043718f388a7e5248cc966e64425f28 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Wed, 22 Oct 2025 13:15:22 +0530 Subject: [PATCH] feat(pdf-tools): add sanitize-pdf tool for comprehensive document cleaning Implement a new PDF sanitization tool that allows users to remove various potentially sensitive elements from PDFs including metadata, annotations, JavaScript, embedded files, and more. The tool provides configurable options through checkboxes to selectively remove different types of content while preserving the core document structure. Extract reusable utility functions from existing tools (remove-metadata, remove-annotations, flatten) to support the new sanitization feature. The tool handles edge cases gracefully and provides feedback when no changes are made. --- src/js/config/pdf-tools.ts | 1 + src/js/config/tools.ts | 6 + src/js/handlers/fileHandler.ts | 6 +- src/js/logic/flatten.ts | 8 +- src/js/logic/index.ts | 2 + src/js/logic/remove-annotations.ts | 71 ++-- src/js/logic/remove-metadata.ts | 57 ++- src/js/logic/sanitize-pdf.ts | 419 ++++++++++++++++++++++ src/js/ui.ts | 545 ++++++++++++++++------------- 9 files changed, 830 insertions(+), 285 deletions(-) create mode 100644 src/js/logic/sanitize-pdf.ts 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()} +
+ + +`, };