mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Add React-based remove annotations tool (#4504)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b35447934e
commit
c7e0ea5b5b
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
icon={<LocalIcon icon="info-rounded" width="1.2rem" height="1.2rem" />}
|
||||
title={t('removeAnnotations.info.title', 'About Remove Annotations')}
|
||||
color="blue"
|
||||
variant="light"
|
||||
>
|
||||
<Text size="sm">
|
||||
{t('removeAnnotations.info.description',
|
||||
'This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents.'
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveAnnotationsSettings;
|
@ -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: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: {
|
||||
|
@ -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<File[]> => {
|
||||
// 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<RemoveAnnotationsParameters>({
|
||||
...removeAnnotationsOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('removeAnnotations.error.failed', 'An error occurred while removing annotations from the PDF.'))
|
||||
});
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import { useBaseParameters } from '../shared/useBaseParameters';
|
||||
|
||||
export type RemoveAnnotationsParameters = Record<string, never>
|
||||
|
||||
export const defaultParameters: RemoveAnnotationsParameters = {
|
||||
};
|
||||
|
||||
export const useRemoveAnnotationsParameters = () => {
|
||||
return useBaseParameters<RemoveAnnotationsParameters>({
|
||||
defaultParameters,
|
||||
endpointName: 'remove-annotations', // Not used for client-side processing, but required by base hook
|
||||
validateFn: () => true, // No parameters to validate
|
||||
});
|
||||
};
|
49
frontend/src/tools/RemoveAnnotations.tsx
Normal file
49
frontend/src/tools/RemoveAnnotations.tsx
Normal file
@ -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: <RemoveAnnotationsSettings />,
|
||||
},
|
||||
],
|
||||
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;
|
Loading…
Reference in New Issue
Block a user