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",
|
"tags": "comments,highlight,notes,markup,remove",
|
||||||
"title": "Remove Annotations",
|
"title": "Remove Annotations",
|
||||||
"header": "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": {
|
"compare": {
|
||||||
"tags": "differentiate,contrast,changes,analysis",
|
"tags": "differentiate,contrast,changes,analysis",
|
||||||
|
@ -1243,7 +1243,17 @@
|
|||||||
"tags": "comments,highlight,notes,markup,remove",
|
"tags": "comments,highlight,notes,markup,remove",
|
||||||
"title": "Remove Annotations",
|
"title": "Remove Annotations",
|
||||||
"header": "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": {
|
"compare": {
|
||||||
"tags": "differentiate,contrast,changes,analysis",
|
"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 { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||||
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||||
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
||||||
|
import { removeAnnotationsOperationConfig } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
|
||||||
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
||||||
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
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 ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
||||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||||
import CropSettings from "../components/tools/crop/CropSettings";
|
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 PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings"
|
||||||
import ExtractImages from "../tools/ExtractImages";
|
import ExtractImages from "../tools/ExtractImages";
|
||||||
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
||||||
@ -521,10 +524,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
removeAnnotations: {
|
removeAnnotations: {
|
||||||
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.removeAnnotations.title", "Remove Annotations"),
|
name: t("home.removeAnnotations.title", "Remove Annotations"),
|
||||||
component: null,
|
component: RemoveAnnotations,
|
||||||
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
|
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.REMOVAL,
|
subcategoryId: SubcategoryId.REMOVAL,
|
||||||
|
maxFiles: -1,
|
||||||
|
operationConfig: removeAnnotationsOperationConfig,
|
||||||
|
settingsComponent: RemoveAnnotationsSettings,
|
||||||
synonyms: getSynonyms(t, "removeAnnotations")
|
synonyms: getSynonyms(t, "removeAnnotations")
|
||||||
},
|
},
|
||||||
removeImage: {
|
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