diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index c801c72ee..a633b9c0b 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -1851,7 +1851,17 @@
"tags": "comments,highlight,notes,markup,remove",
"title": "Remove Annotations",
"header": "Remove Annotations",
- "submit": "Remove"
+ "submit": "Remove",
+ "settings": {
+ "title": "Settings"
+ },
+ "info": {
+ "title": "About Remove Annotations",
+ "description": "This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents."
+ },
+ "error": {
+ "failed": "An error occurred while removing annotations from the PDF."
+ }
},
"compare": {
"tags": "differentiate,contrast,changes,analysis",
diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json
index 9cd801a66..d20cca6ae 100644
--- a/frontend/public/locales/en-US/translation.json
+++ b/frontend/public/locales/en-US/translation.json
@@ -1243,7 +1243,17 @@
"tags": "comments,highlight,notes,markup,remove",
"title": "Remove Annotations",
"header": "Remove Annotations",
- "submit": "Remove"
+ "submit": "Remove",
+ "settings": {
+ "title": "Settings"
+ },
+ "info": {
+ "title": "About Remove Annotations",
+ "description": "This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents."
+ },
+ "error": {
+ "failed": "An error occurred while removing annotations from the PDF."
+ }
},
"compare": {
"tags": "differentiate,contrast,changes,analysis",
diff --git a/frontend/src/components/tools/removeAnnotations/RemoveAnnotationsSettings.tsx b/frontend/src/components/tools/removeAnnotations/RemoveAnnotationsSettings.tsx
new file mode 100644
index 000000000..e2618ad42
--- /dev/null
+++ b/frontend/src/components/tools/removeAnnotations/RemoveAnnotationsSettings.tsx
@@ -0,0 +1,26 @@
+import { useTranslation } from 'react-i18next';
+import { Stack, Text, Alert } from '@mantine/core';
+import LocalIcon from '../../shared/LocalIcon';
+
+const RemoveAnnotationsSettings = () => {
+ const { t } = useTranslation();
+
+ return (
+
+ }
+ title={t('removeAnnotations.info.title', 'About Remove Annotations')}
+ color="blue"
+ variant="light"
+ >
+
+ {t('removeAnnotations.info.description',
+ 'This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents.'
+ )}
+
+
+
+ );
+};
+
+export default RemoveAnnotationsSettings;
\ No newline at end of file
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index 6442249c1..c8c29821e 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -56,6 +56,7 @@ import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation"
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
+import { removeAnnotationsOperationConfig } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
import CompressSettings from "../components/tools/compress/CompressSettings";
@@ -86,6 +87,8 @@ import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustP
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
import CropSettings from "../components/tools/crop/CropSettings";
+import RemoveAnnotations from "../tools/RemoveAnnotations";
+import RemoveAnnotationsSettings from "../components/tools/removeAnnotations/RemoveAnnotationsSettings";
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings"
import ExtractImages from "../tools/ExtractImages";
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
@@ -521,10 +524,13 @@ export function useFlatToolRegistry(): ToolRegistry {
removeAnnotations: {
icon: ,
name: t("home.removeAnnotations.title", "Remove Annotations"),
- component: null,
+ component: RemoveAnnotations,
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
+ maxFiles: -1,
+ operationConfig: removeAnnotationsOperationConfig,
+ settingsComponent: RemoveAnnotationsSettings,
synonyms: getSynonyms(t, "removeAnnotations")
},
removeImage: {
diff --git a/frontend/src/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts b/frontend/src/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts
new file mode 100644
index 000000000..d82c6c2bb
--- /dev/null
+++ b/frontend/src/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts
@@ -0,0 +1,99 @@
+import { useTranslation } from 'react-i18next';
+import { useToolOperation, ToolType } from '../shared/useToolOperation';
+import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
+import { RemoveAnnotationsParameters, defaultParameters } from './useRemoveAnnotationsParameters';
+
+// Client-side PDF processing using PDF-lib
+const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParameters, files: File[]): Promise => {
+ // Dynamic import of PDF-lib for client-side processing
+ const { PDFDocument, PDFName, PDFRef, PDFDict } = await import('pdf-lib');
+
+ const processedFiles: File[] = [];
+
+ for (const file of files) {
+ try {
+ // Load the PDF
+ const fileArrayBuffer = await file.arrayBuffer();
+ const pdfBytesIn = new Uint8Array(fileArrayBuffer);
+ const pdfDoc = await PDFDocument.load(pdfBytesIn, { ignoreEncryption: true });
+ const ctx = pdfDoc.context;
+
+ const pages = pdfDoc.getPages();
+ for (let i = 0; i < pages.length; i++) {
+ const page = pages[i];
+
+ // Annots() returns PDFArray | undefined
+ const annots = page.node.Annots();
+ if (!annots || annots.size() === 0) continue;
+
+ // Delete each annotation object (they are usually PDFRef)
+ for (let j = annots.size() - 1; j >= 0; j--) {
+ try {
+ const entry = annots.get(j);
+ if (entry instanceof PDFRef) {
+ ctx.delete(entry);
+ } else if (entry instanceof PDFDict) {
+ // In practice, Annots array should contain refs; if not, just remove the array linkage.
+ // (We avoid poking internal maps to find a ref for the dict.)
+ }
+ } catch (err) {
+ console.warn(`Failed to remove annotation ${j} on page ${i + 1}:`, err);
+ }
+ }
+
+ // Remove the Annots key entirely
+ try {
+ if (page.node.has(PDFName.of('Annots'))) {
+ page.node.delete(PDFName.of('Annots'));
+ }
+ } catch (err) {
+ console.warn(`Failed to delete /Annots on page ${i + 1}:`, err);
+ }
+ }
+
+ // Optional: if removing ALL annotations across the doc, strip AcroForm to avoid dangling widget refs
+ try {
+ const catalog = pdfDoc.context.lookup(pdfDoc.context.trailerInfo.Root);
+ if (catalog && 'has' in catalog && 'delete' in catalog) {
+ const catalogDict = catalog as any;
+ if (catalogDict.has(PDFName.of('AcroForm'))) {
+ catalogDict.delete(PDFName.of('AcroForm'));
+ }
+ }
+ } catch (err) {
+ console.warn('Failed to remove /AcroForm:', err);
+ }
+
+ // Save returns Uint8Array — safe for Blob
+ const outBytes = await pdfDoc.save();
+ const outBlob = new Blob([new Uint8Array(outBytes)], { type: 'application/pdf' });
+
+ // Create new file with original name
+ const processedFile = new File([outBlob], file.name, { type: 'application/pdf' });
+
+ processedFiles.push(processedFile);
+ } catch (error) {
+ console.error('Error processing file:', file.name, error);
+ throw new Error(`Failed to process ${file.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }
+
+ return processedFiles;
+};
+
+// Static configuration object
+export const removeAnnotationsOperationConfig = {
+ toolType: ToolType.custom,
+ operationType: 'removeAnnotations',
+ customProcessor: removeAnnotationsProcessor,
+ defaultParameters,
+} as const;
+
+export const useRemoveAnnotationsOperation = () => {
+ const { t } = useTranslation();
+
+ return useToolOperation({
+ ...removeAnnotationsOperationConfig,
+ getErrorMessage: createStandardErrorHandler(t('removeAnnotations.error.failed', 'An error occurred while removing annotations from the PDF.'))
+ });
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters.ts b/frontend/src/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters.ts
new file mode 100644
index 000000000..b62e1da08
--- /dev/null
+++ b/frontend/src/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters.ts
@@ -0,0 +1,14 @@
+import { useBaseParameters } from '../shared/useBaseParameters';
+
+export type RemoveAnnotationsParameters = Record
+
+export const defaultParameters: RemoveAnnotationsParameters = {
+};
+
+export const useRemoveAnnotationsParameters = () => {
+ return useBaseParameters({
+ defaultParameters,
+ endpointName: 'remove-annotations', // Not used for client-side processing, but required by base hook
+ validateFn: () => true, // No parameters to validate
+ });
+};
\ No newline at end of file
diff --git a/frontend/src/tools/RemoveAnnotations.tsx b/frontend/src/tools/RemoveAnnotations.tsx
new file mode 100644
index 000000000..3c1518fcf
--- /dev/null
+++ b/frontend/src/tools/RemoveAnnotations.tsx
@@ -0,0 +1,49 @@
+import { useTranslation } from "react-i18next";
+import { createToolFlow } from "../components/tools/shared/createToolFlow";
+import RemoveAnnotationsSettings from "../components/tools/removeAnnotations/RemoveAnnotationsSettings";
+import { useRemoveAnnotationsParameters } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsParameters";
+import { useRemoveAnnotationsOperation } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
+import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
+import { BaseToolProps, ToolComponent } from "../types/tool";
+
+const RemoveAnnotations = (props: BaseToolProps) => {
+ const { t } = useTranslation();
+
+ const base = useBaseTool(
+ 'removeAnnotations',
+ useRemoveAnnotationsParameters,
+ useRemoveAnnotationsOperation,
+ props
+ );
+
+ return createToolFlow({
+ files: {
+ selectedFiles: base.selectedFiles,
+ isCollapsed: base.hasResults,
+ },
+ steps: [
+ {
+ title: t("removeAnnotations.settings.title", "Settings"),
+ isCollapsed: base.settingsCollapsed,
+ onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
+ content: ,
+ },
+ ],
+ executeButton: {
+ text: t("removeAnnotations.submit", "Remove Annotations"),
+ isVisible: !base.hasResults,
+ loadingText: t("loading", "Processing..."),
+ onClick: base.handleExecute,
+ disabled: !base.params.validateParameters() || !base.hasFiles,
+ },
+ review: {
+ isVisible: base.hasResults,
+ operation: base.operation,
+ title: t("removeAnnotations.title", "Annotations Removed"),
+ onFileClick: base.handleThumbnailClick,
+ onUndo: base.handleUndo,
+ },
+ });
+};
+
+export default RemoveAnnotations as ToolComponent;
\ No newline at end of file