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