diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml
index a425ad3d5..d91fc3563 100644
--- a/frontend/public/locales/en-GB/translation.toml
+++ b/frontend/public/locales/en-GB/translation.toml
@@ -708,6 +708,11 @@ tags = "signature,autograph"
title = "Sign"
desc = "Adds signature to PDF by drawing, text or image"
+[home.annotate]
+tags = "annotate,highlight,draw"
+title = "Annotate"
+desc = "Highlight, draw, add notes and shapes in the viewer"
+
[home.flatten]
tags = "simplify,remove,interactive"
title = "Flatten"
@@ -3904,6 +3909,21 @@ draw = "Draw"
save = "Save"
saveChanges = "Save Changes"
+[annotation]
+title = "Annotate"
+highlight = "Highlight"
+pen = "Pen"
+text = "Text box"
+note = "Note"
+rectangle = "Rectangle"
+ellipse = "Ellipse"
+select = "Select"
+exit = "Exit annotation mode"
+strokeWidth = "Width"
+opacity = "Opacity"
+fontSize = "Font size"
+chooseColor = "Choose colour"
+
[search]
title = "Search PDF"
placeholder = "Enter search term..."
diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx
index cd95b90b9..391d595c0 100644
--- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx
+++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx
@@ -1,16 +1,15 @@
-import React, { useState, useEffect } from 'react';
-import { ActionIcon, Popover } from '@mantine/core';
+import React, { useEffect } from 'react';
+import { ActionIcon } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
import { Tooltip } from '@app/components/shared/Tooltip';
import { ViewerContext } from '@app/contexts/ViewerContext';
import { useSignature } from '@app/contexts/SignatureContext';
-import { ColorSwatchButton, ColorPicker } from '@app/components/annotation/shared/ColorPicker';
import { useFileState, useFileContext } from '@app/contexts/FileContext';
import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils';
import { createProcessedFile } from '@app/contexts/file/fileActions';
import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext';
-import { useNavigationState } from '@app/contexts/NavigationContext';
+import { useNavigation, useNavigationState } from '@app/contexts/NavigationContext';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
@@ -23,16 +22,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
const { t } = useTranslation();
const { sidebarRefs } = useSidebarContext();
const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs);
- const [selectedColor, setSelectedColor] = useState('#000000');
- const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
- const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false);
-
// Viewer context for PDF controls - safely handle when not available
const viewerContext = React.useContext(ViewerContext);
// Signature context for accessing drawing API
const { signatureApiRef, isPlacementMode } = useSignature();
+ const { setToolAndWorkbench } = useNavigation();
+
// File state for save functionality
const { state, selectors } = useFileState();
const { actions: fileActions } = useFileContext();
@@ -41,6 +38,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
// Check if we're in sign mode
const { selectedTool } = useNavigationState();
const isSignMode = selectedTool === 'sign';
+ const isAnnotateMode = selectedTool === 'annotate';
+
+ // When leaving viewer, turn off annotation overlay
+ useEffect(() => {
+ if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
+ viewerContext.setAnnotationMode(false);
+ }
+ }, [currentView, viewerContext]);
// Turn off annotation mode when switching away from viewer
useEffect(() => {
@@ -75,89 +80,19 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
- {/* Annotation Mode Toggle with Drawing Controls */}
- {viewerContext?.isAnnotationMode ? (
- // When active: Show color picker on hover
-
setIsHoverColorPickerOpen(true)}
- onMouseLeave={() => setIsHoverColorPickerOpen(false)}
- style={{ display: 'inline-flex' }}
+ {/* Launch Annotate tool in the left panel */}
+
+ setToolAndWorkbench('annotate', 'viewer')}
+ disabled={disabled}
+ aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
>
- setIsHoverColorPickerOpen(false)}
- position="left"
- withArrow
- shadow="md"
- offset={8}
- >
-
- {
- viewerContext?.toggleAnnotationMode();
- setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off
- // Deactivate drawing tool when exiting annotation mode
- if (signatureApiRef?.current) {
- try {
- signatureApiRef.current.deactivateTools();
- } catch (error) {
- console.log('Signature API not ready:', error);
- }
- }
- }}
- disabled={disabled}
- aria-label="Drawing mode active"
- >
-
-
-
-
-
-
-
Drawing Color
-
{
- setIsHoverColorPickerOpen(false); // Close hover picker
- setIsColorPickerOpen(true); // Open main color picker modal
- }}
- />
-
-
-
-
-
- ) : (
- // When inactive: Show "Draw" tooltip
-
- {
- viewerContext?.toggleAnnotationMode();
- // Activate ink drawing tool when entering annotation mode
- if (signatureApiRef?.current && currentView === 'viewer') {
- try {
- signatureApiRef.current.activateDrawMode();
- signatureApiRef.current.updateDrawSettings(selectedColor, 2);
- } catch (error) {
- console.log('Signature API not ready:', error);
- }
- }
- }}
- disabled={disabled}
- aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
- >
-
-
-
- )}
+
+
+
{/* Save PDF with Annotations */}
@@ -213,25 +148,6 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
-
- {/* Color Picker Modal */}
- setIsColorPickerOpen(false)}
- selectedColor={selectedColor}
- onColorChange={(color) => {
- setSelectedColor(color);
- // Update drawing tool color if annotation mode is active
- if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') {
- try {
- signatureApiRef.current.updateDrawSettings(color, 2);
- } catch (error) {
- console.log('Unable to update drawing settings:', error);
- }
- }
- }}
- title="Choose Drawing Color"
- />
>
);
}
diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx
index 9ca8bac0a..6aa613812 100644
--- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx
+++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx
@@ -84,9 +84,10 @@ const EmbedPdfViewerContent = ({
const { selectedTool } = useNavigationState();
// Tools that use the stamp/signature placement system with hover preview
const isSignatureMode = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage';
+ const isAnnotateTool = selectedTool === 'annotate';
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
- const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
+ const shouldEnableAnnotations = isSignatureMode || isAnnotateTool || isAnnotationMode || isAnnotationsVisible;
const isPlacementOverlayActive = Boolean(
isSignatureMode && shouldEnableAnnotations && isPlacementMode && signatureConfig
);
diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
index 3e5fa5f1c..9a93a225a 100644
--- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
+++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
@@ -121,9 +121,9 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
selectAfterCreate: true,
}),
- // Register pan plugin (depends on Viewport, InteractionManager)
+ // Register pan plugin (depends on Viewport, InteractionManager) - keep disabled to prevent drag panning
createPluginRegistration(PanPluginPackage, {
- defaultMode: 'mobile', // Try mobile mode which might be more permissive
+ defaultMode: 'disabled',
}),
// Register zoom plugin with configuration
@@ -248,7 +248,305 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
if (!annotationApi) return;
if (enableAnnotations) {
- annotationApi.addTool({
+ const ensureTool = (tool: any) => {
+ const existing = annotationApi.getTool?.(tool.id);
+ if (!existing) {
+ annotationApi.addTool(tool);
+ }
+ };
+
+ ensureTool({
+ id: 'highlight',
+ name: 'Highlight',
+ interaction: { exclusive: true, cursor: 'text', textSelection: true },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.HIGHLIGHT,
+ color: '#ffd54f',
+ opacity: 0.6,
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'underline',
+ name: 'Underline',
+ interaction: { exclusive: true, cursor: 'text', textSelection: true },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.UNDERLINE,
+ color: '#ffb300',
+ opacity: 1,
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'strikeout',
+ name: 'Strikeout',
+ interaction: { exclusive: true, cursor: 'text', textSelection: true },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.STRIKEOUT,
+ color: '#e53935',
+ opacity: 1,
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'squiggly',
+ name: 'Squiggly',
+ interaction: { exclusive: true, cursor: 'text', textSelection: true },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.SQUIGGLY,
+ color: '#00acc1',
+ opacity: 1,
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'ink',
+ name: 'Pen',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.INK,
+ color: '#1f2933',
+ opacity: 1,
+ borderWidth: 2,
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'inkHighlighter',
+ name: 'Ink Highlighter',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.INK && annotation.color === '#ffd54f' ? 8 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.INK,
+ color: '#ffd54f',
+ opacity: 0.5,
+ borderWidth: 6,
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'square',
+ name: 'Square',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.SQUARE,
+ color: '#1565c0',
+ interiorColor: '#e3f2fd',
+ opacity: 0.35,
+ borderWidth: 2,
+ },
+ clickBehavior: {
+ enabled: true,
+ defaultSize: { width: 120, height: 90 },
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'circle',
+ name: 'Circle',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.CIRCLE,
+ color: '#1565c0',
+ interiorColor: '#e3f2fd',
+ opacity: 0.35,
+ borderWidth: 2,
+ },
+ clickBehavior: {
+ enabled: true,
+ defaultSize: { width: 100, height: 100 },
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'line',
+ name: 'Line',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.LINE ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.LINE,
+ color: '#1565c0',
+ opacity: 1,
+ borderWidth: 2,
+ },
+ clickBehavior: {
+ enabled: true,
+ defaultLength: 120,
+ defaultAngle: 0,
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'lineArrow',
+ name: 'Arrow',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.LINE && (annotation.endStyle === 'ClosedArrow' || annotation.lineEndingStyles?.end === 'ClosedArrow') ? 9 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.LINE,
+ color: '#1565c0',
+ opacity: 1,
+ borderWidth: 2,
+ startStyle: 'None',
+ endStyle: 'ClosedArrow',
+ lineEndingStyles: { start: 'None', end: 'ClosedArrow' },
+ },
+ clickBehavior: {
+ enabled: true,
+ defaultLength: 120,
+ defaultAngle: 0,
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'polyline',
+ name: 'Polyline',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.POLYLINE ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.POLYLINE,
+ color: '#1565c0',
+ opacity: 1,
+ borderWidth: 2,
+ },
+ clickBehavior: {
+ enabled: true,
+ finishOnDoubleClick: true,
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'polygon',
+ name: 'Polygon',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.POLYGON,
+ color: '#1565c0',
+ interiorColor: '#e3f2fd',
+ opacity: 0.35,
+ borderWidth: 2,
+ },
+ clickBehavior: {
+ enabled: true,
+ finishOnDoubleClick: true,
+ defaultSize: { width: 140, height: 100 },
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'text',
+ name: 'Text',
+ interaction: { exclusive: true, cursor: 'text' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 10 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.FREETEXT,
+ textColor: '#111111',
+ fontSize: 14,
+ fontFamily: 'Helvetica',
+ opacity: 1,
+ interiorColor: '#fffef7',
+ contents: 'Text',
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'note',
+ name: 'Note',
+ interaction: { exclusive: true, cursor: 'pointer' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 8 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.FREETEXT,
+ textColor: '#1b1b1b',
+ color: '#ffa000',
+ interiorColor: '#fff8e1',
+ opacity: 1,
+ contents: 'Note',
+ fontSize: 12,
+ },
+ clickBehavior: {
+ enabled: true,
+ defaultSize: { width: 160, height: 100 },
+ },
+ behavior: {
+ deactivateToolAfterCreate: false,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
+ id: 'stamp',
+ name: 'Image Stamp',
+ interaction: { exclusive: false, cursor: 'copy' },
+ matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.STAMP ? 5 : 0),
+ defaults: {
+ type: PdfAnnotationSubtype.STAMP,
+ },
+ behavior: {
+ deactivateToolAfterCreate: true,
+ selectAfterCreate: true,
+ },
+ });
+
+ ensureTool({
id: 'signatureStamp',
name: 'Digital Signature',
interaction: { exclusive: false, cursor: 'copy' },
@@ -258,7 +556,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
},
});
- annotationApi.addTool({
+ ensureTool({
id: 'signatureInk',
name: 'Signature Draw',
interaction: { exclusive: true, cursor: 'crosshair' },
diff --git a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx
index 92d436e5d..050b0c10d 100644
--- a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx
+++ b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx
@@ -2,7 +2,7 @@ import { useImperativeHandle, forwardRef, useEffect, useCallback, useRef, useSta
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
import { useSignature } from '@app/contexts/SignatureContext';
-import type { SignatureAPI } from '@app/components/viewer/viewerTypes';
+import type { AnnotationToolId, AnnotationToolOptions, SignatureAPI } from '@app/components/viewer/viewerTypes';
import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters';
import { useViewer } from '@app/contexts/ViewerContext';
@@ -199,12 +199,161 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI
}
}, [annotationApi, signatureConfig, placementPreviewSize, applyStampDefaults, cssToPdfSize]);
+ const buildAnnotationDefaults = useCallback(
+ (toolId: AnnotationToolId, options?: AnnotationToolOptions) => {
+ switch (toolId) {
+ case 'highlight':
+ return {
+ type: PdfAnnotationSubtype.HIGHLIGHT,
+ color: options?.color ?? '#ffd54f',
+ opacity: options?.opacity ?? 0.6,
+ };
+ case 'underline':
+ return {
+ type: PdfAnnotationSubtype.UNDERLINE,
+ color: options?.color ?? '#ffb300',
+ opacity: options?.opacity ?? 1,
+ };
+ case 'strikeout':
+ return {
+ type: PdfAnnotationSubtype.STRIKEOUT,
+ color: options?.color ?? '#e53935',
+ opacity: options?.opacity ?? 1,
+ };
+ case 'squiggly':
+ return {
+ type: PdfAnnotationSubtype.SQUIGGLY,
+ color: options?.color ?? '#00acc1',
+ opacity: options?.opacity ?? 1,
+ };
+ case 'ink':
+ return {
+ type: PdfAnnotationSubtype.INK,
+ color: options?.color ?? '#1f2933',
+ opacity: options?.opacity ?? 1,
+ borderWidth: options?.thickness ?? 2,
+ lineWidth: options?.thickness ?? 2,
+ strokeWidth: options?.thickness ?? 2,
+ };
+ case 'inkHighlighter':
+ return {
+ type: PdfAnnotationSubtype.INK,
+ color: options?.color ?? '#ffd54f',
+ opacity: options?.opacity ?? 0.5,
+ borderWidth: options?.thickness ?? 6,
+ lineWidth: options?.thickness ?? 6,
+ strokeWidth: options?.thickness ?? 6,
+ };
+ case 'text':
+ return {
+ type: PdfAnnotationSubtype.FREETEXT,
+ textColor: options?.color ?? '#111111',
+ fontSize: options?.fontSize ?? 14,
+ fontFamily: options?.fontFamily ?? 'Helvetica',
+ opacity: options?.opacity ?? 1,
+ interiorColor: options?.fillColor ?? '#fffef7',
+ borderWidth: options?.thickness ?? 1,
+ };
+ case 'note':
+ return {
+ type: PdfAnnotationSubtype.FREETEXT,
+ textColor: options?.color ?? '#1b1b1b',
+ color: options?.color ?? '#ffa000',
+ interiorColor: options?.fillColor ?? '#fff8e1',
+ opacity: options?.opacity ?? 1,
+ fontSize: options?.fontSize ?? 12,
+ contents: 'Note',
+ };
+ case 'square':
+ return {
+ type: PdfAnnotationSubtype.SQUARE,
+ color: options?.color ?? '#1565c0',
+ interiorColor: options?.fillColor ?? '#e3f2fd',
+ opacity: options?.opacity ?? 0.35,
+ borderWidth: options?.thickness ?? 2,
+ };
+ case 'circle':
+ return {
+ type: PdfAnnotationSubtype.CIRCLE,
+ color: options?.color ?? '#1565c0',
+ interiorColor: options?.fillColor ?? '#e3f2fd',
+ opacity: options?.opacity ?? 0.35,
+ borderWidth: options?.thickness ?? 2,
+ };
+ case 'line':
+ return {
+ type: PdfAnnotationSubtype.LINE,
+ color: options?.color ?? '#1565c0',
+ opacity: options?.opacity ?? 1,
+ borderWidth: options?.thickness ?? 2,
+ };
+ case 'lineArrow':
+ return {
+ type: PdfAnnotationSubtype.LINE,
+ color: options?.color ?? '#1565c0',
+ opacity: options?.opacity ?? 1,
+ borderWidth: options?.thickness ?? 2,
+ startStyle: 'None',
+ endStyle: 'ClosedArrow',
+ lineEndingStyles: { start: 'None', end: 'ClosedArrow' },
+ };
+ case 'polyline':
+ return {
+ type: PdfAnnotationSubtype.POLYLINE,
+ color: options?.color ?? '#1565c0',
+ opacity: options?.opacity ?? 1,
+ borderWidth: options?.thickness ?? 2,
+ };
+ case 'polygon':
+ return {
+ type: PdfAnnotationSubtype.POLYGON,
+ color: options?.color ?? '#1565c0',
+ interiorColor: options?.fillColor ?? '#e3f2fd',
+ opacity: options?.opacity ?? 0.35,
+ borderWidth: options?.thickness ?? 2,
+ };
+ case 'stamp':
+ return {
+ type: PdfAnnotationSubtype.STAMP,
+ };
+ case 'select':
+ default:
+ return null;
+ }
+ },
+ []
+ );
+
+ const configureAnnotationTool = useCallback(
+ (toolId: AnnotationToolId, options?: AnnotationToolOptions) => {
+ if (!annotationApi) return;
+
+ const defaults = buildAnnotationDefaults(toolId, options);
+ const api = annotationApi as any;
+
+ if (defaults) {
+ api.setToolDefaults?.(toolId, defaults);
+ }
+
+ api.setActiveTool?.(toolId === 'select' ? null : toolId);
+ },
+ [annotationApi, buildAnnotationDefaults]
+ );
+
// Enable keyboard deletion of selected annotations
useEffect(() => {
// Always enable delete key when we have annotation API and are in sign mode
if (!annotationApi || (isPlacementMode === undefined)) return;
const handleKeyDown = (event: KeyboardEvent) => {
+ // Skip delete/backspace while a text input/textarea is focused (e.g., editing textbox)
+ const target = event.target as HTMLElement | null;
+ const tag = target?.tagName?.toLowerCase();
+ const editable = target?.getAttribute?.('contenteditable');
+ if (tag === 'input' || tag === 'textarea' || editable === 'true') {
+ return;
+ }
+
if (event.key === 'Delete' || event.key === 'Backspace') {
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
@@ -371,7 +520,23 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI
return [];
}
},
- }), [annotationApi, signatureConfig, placementPreviewSize, applyStampDefaults]);
+ activateAnnotationTool: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => {
+ configureAnnotationTool(toolId, options);
+ },
+ setAnnotationStyle: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => {
+ const defaults = buildAnnotationDefaults(toolId, options);
+ const api = annotationApi as any;
+ if (defaults && api?.setToolDefaults) {
+ api.setToolDefaults(toolId, defaults);
+ }
+ },
+ getSelectedAnnotation: () => {
+ return annotationApi?.getSelectedAnnotation?.() ?? null;
+ },
+ updateAnnotation: (pageIndex: number, annotationId: string, patch: Partial) => {
+ annotationApi?.updateAnnotation?.(pageIndex, annotationId, patch);
+ },
+ }), [annotationApi, signatureConfig, placementPreviewSize, applyStampDefaults, configureAnnotationTool, buildAnnotationDefaults]);
useEffect(() => {
if (!annotationApi?.onAnnotationEvent) {
diff --git a/frontend/src/core/components/viewer/viewerTypes.ts b/frontend/src/core/components/viewer/viewerTypes.ts
index 3d61377c7..088d94050 100644
--- a/frontend/src/core/components/viewer/viewerTypes.ts
+++ b/frontend/src/core/components/viewer/viewerTypes.ts
@@ -14,6 +14,10 @@ export interface SignatureAPI {
updateDrawSettings: (color: string, size: number) => void;
deactivateTools: () => void;
getPageAnnotations: (pageIndex: number) => Promise;
+ activateAnnotationTool?: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => void;
+ setAnnotationStyle?: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => void;
+ getSelectedAnnotation?: () => any | null;
+ updateAnnotation?: (pageIndex: number, annotationId: string, patch: Partial) => void;
}
export interface HistoryAPI {
@@ -23,3 +27,33 @@ export interface HistoryAPI {
canRedo: () => boolean;
subscribe?: (listener: () => void) => () => void;
}
+
+export type AnnotationToolId =
+ | 'select'
+ | 'highlight'
+ | 'underline'
+ | 'strikeout'
+ | 'squiggly'
+ | 'ink'
+ | 'inkHighlighter'
+ | 'text'
+ | 'note'
+ | 'square'
+ | 'circle'
+ | 'line'
+ | 'lineArrow'
+ | 'polyline'
+ | 'polygon'
+ | 'stamp'
+ | 'signatureStamp'
+ | 'signatureInk';
+
+export interface AnnotationToolOptions {
+ color?: string;
+ fillColor?: string;
+ opacity?: number;
+ thickness?: number;
+ fontSize?: number;
+ fontFamily?: string;
+ imageSrc?: string;
+}
diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx
index cda99dc40..9faff8c54 100644
--- a/frontend/src/core/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx
@@ -48,6 +48,7 @@ import Crop from "@app/tools/Crop";
import Sign from "@app/tools/Sign";
import AddText from "@app/tools/AddText";
import AddImage from "@app/tools/AddImage";
+import Annotate from "@app/tools/Annotate";
import { compressOperationConfig } from "@app/hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from "@app/hooks/tools/split/useSplitOperation";
import { addPasswordOperationConfig } from "@app/hooks/tools/addPassword/useAddPasswordOperation";
@@ -226,6 +227,19 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
synonyms: getSynonyms(t, 'addImage'),
supportsAutomate: false,
},
+ annotate: {
+ icon: ,
+ name: t('home.annotate.title', 'Annotate'),
+ component: Annotate,
+ description: t('home.annotate.desc', 'Highlight, draw, add notes, and shapes directly in the viewer'),
+ categoryId: ToolCategoryId.STANDARD_TOOLS,
+ subcategoryId: SubcategoryId.EDIT,
+ workbench: 'viewer',
+ operationConfig: signOperationConfig,
+ automationSettings: null,
+ synonyms: getSynonyms(t, 'annotate'),
+ supportsAutomate: false,
+ },
// Document Security
diff --git a/frontend/src/core/tools/Annotate.tsx b/frontend/src/core/tools/Annotate.tsx
new file mode 100644
index 000000000..66116c858
--- /dev/null
+++ b/frontend/src/core/tools/Annotate.tsx
@@ -0,0 +1,589 @@
+import { useEffect, useMemo, useState, useContext, useCallback, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert, Text, Group, ActionIcon, Stack, Divider, Slider, Box, Tooltip as MantineTooltip, Button, TextInput, NumberInput } from '@mantine/core';
+
+import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
+import { useNavigation } from '@app/contexts/NavigationContext';
+import { useFileSelection, useFileContext } from '@app/contexts/FileContext';
+import { BaseToolProps } from '@app/types/tool';
+import { useSignature } from '@app/contexts/SignatureContext';
+import { ViewerContext } from '@app/contexts/ViewerContext';
+import { ColorPicker, ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker';
+import LocalIcon from '@app/components/shared/LocalIcon';
+import type { AnnotationToolId } from '@app/components/viewer/viewerTypes';
+
+const Annotate = (_props: BaseToolProps) => {
+ const { t } = useTranslation();
+ const { setToolAndWorkbench } = useNavigation();
+ const { selectedFiles } = useFileSelection();
+ const { selectors } = useFileContext();
+ const { signatureApiRef } = useSignature();
+ const viewerContext = useContext(ViewerContext);
+
+ const [activeTool, setActiveTool] = useState('highlight');
+ const [inkColor, setInkColor] = useState('#1f2933');
+ const [inkWidth, setInkWidth] = useState(2);
+ const [highlightColor, setHighlightColor] = useState('#ffd54f');
+ const [highlightOpacity, setHighlightOpacity] = useState(60);
+ const [underlineColor, setUnderlineColor] = useState('#ffb300');
+ const [strikeoutColor, setStrikeoutColor] = useState('#e53935');
+ const [squigglyColor, setSquigglyColor] = useState('#00acc1');
+ const [textColor, setTextColor] = useState('#111111');
+ const [textSize, setTextSize] = useState(14);
+ const [shapeStrokeColor, setShapeStrokeColor] = useState('#1565c0');
+ const [shapeFillColor, setShapeFillColor] = useState('#e3f2fd');
+ const [shapeOpacity, setShapeOpacity] = useState(35);
+ const [shapeThickness, setShapeThickness] = useState(2);
+ const [colorPickerTarget, setColorPickerTarget] = useState<'ink' | 'highlight' | 'underline' | 'strikeout' | 'squiggly' | 'text' | 'shapeStroke' | 'shapeFill' | null>(null);
+ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
+ const [selectedAnn, setSelectedAnn] = useState(null);
+ const [selectedAnnId, setSelectedAnnId] = useState(null);
+ const [selectedTextDraft, setSelectedTextDraft] = useState('');
+ const [selectedFontSize, setSelectedFontSize] = useState(14);
+ const selectedUpdateTimer = useRef | null>(null);
+ const stampInputRef = useRef(null);
+
+ const buildToolOptions = useCallback((toolId: AnnotationToolId) => {
+ switch (toolId) {
+ case 'ink':
+ return { color: inkColor, thickness: inkWidth };
+ case 'inkHighlighter':
+ return { color: highlightColor, opacity: highlightOpacity / 100, thickness: 6 };
+ case 'highlight':
+ return { color: highlightColor, opacity: highlightOpacity / 100 };
+ case 'underline':
+ return { color: underlineColor, opacity: 1 };
+ case 'strikeout':
+ return { color: strikeoutColor, opacity: 1 };
+ case 'squiggly':
+ return { color: squigglyColor, opacity: 1 };
+ case 'text':
+ return { color: textColor, fontSize: textSize };
+ case 'note':
+ return { color: textColor };
+ case 'square':
+ case 'circle':
+ case 'polygon':
+ return {
+ color: shapeStrokeColor,
+ interiorColor: shapeFillColor,
+ opacity: shapeOpacity / 100,
+ borderWidth: shapeThickness,
+ };
+ case 'line':
+ case 'polyline':
+ case 'lineArrow':
+ return {
+ color: shapeStrokeColor,
+ opacity: shapeOpacity / 100,
+ borderWidth: shapeThickness,
+ };
+ default:
+ return {};
+ }
+ }, [highlightColor, highlightOpacity, inkColor, inkWidth, underlineColor, strikeoutColor, squigglyColor, textColor, textSize, shapeStrokeColor, shapeFillColor, shapeOpacity, shapeThickness]);
+
+ useEffect(() => {
+ setToolAndWorkbench('annotate', 'viewer');
+ }, [setToolAndWorkbench]);
+
+ useEffect(() => {
+ if (!viewerContext) return;
+ if (viewerContext.isAnnotationMode) return;
+
+ viewerContext.setAnnotationMode(true);
+ signatureApiRef?.current?.activateAnnotationTool?.(activeTool, buildToolOptions(activeTool));
+ }, [viewerContext?.isAnnotationMode, signatureApiRef, activeTool, buildToolOptions]);
+
+ const activateAnnotationTool = (toolId: AnnotationToolId) => {
+ viewerContext?.setAnnotationMode(true);
+ setActiveTool(toolId);
+ const options = buildToolOptions(toolId);
+ signatureApiRef?.current?.activateAnnotationTool?.(toolId, options);
+
+ if (toolId === 'stamp') {
+ // Use existing add image flow for stamp assets
+ if (stampInputRef.current) {
+ stampInputRef.current.click();
+ }
+ }
+ };
+
+ useEffect(() => {
+ // push style updates to EmbedPDF when sliders/colors change
+ signatureApiRef?.current?.setAnnotationStyle?.(activeTool, buildToolOptions(activeTool));
+ }, [activeTool, buildToolOptions, signatureApiRef]);
+
+ // Allow exiting multi-point tools with Escape (e.g., polyline)
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key !== 'Escape') return;
+ if (['polyline', 'polygon'].includes(activeTool)) {
+ signatureApiRef?.current?.setAnnotationStyle?.(activeTool, buildToolOptions(activeTool));
+ signatureApiRef?.current?.activateAnnotationTool?.(null as any);
+ setTimeout(() => {
+ signatureApiRef?.current?.activateAnnotationTool?.(activeTool, buildToolOptions(activeTool));
+ }, 50);
+ }
+ };
+ window.addEventListener('keydown', handler);
+ return () => window.removeEventListener('keydown', handler);
+ }, [activeTool, buildToolOptions, signatureApiRef]);
+
+ // Poll selected annotation to allow editing existing highlights/text
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const ann = signatureApiRef?.current?.getSelectedAnnotation?.();
+ const annId = ann?.object?.id ?? null;
+ setSelectedAnn(ann || null);
+ // Only reset drafts when selection changes
+ if (annId !== selectedAnnId) {
+ setSelectedAnnId(annId);
+ if (ann?.object?.contents !== undefined) {
+ setSelectedTextDraft(ann.object.contents ?? '');
+ }
+ if (ann?.object?.fontSize !== undefined) {
+ setSelectedFontSize(ann.object.fontSize ?? 14);
+ }
+ }
+ }, 150);
+ return () => clearInterval(interval);
+ }, [signatureApiRef, selectedAnnId]);
+
+ const annotationTools: { id: AnnotationToolId; label: string; icon: string }[] = [
+ { id: 'highlight', label: t('annotation.highlight', 'Highlight'), icon: 'highlight' },
+ { id: 'underline', label: t('annotation.underline', 'Underline'), icon: 'format-underlined' },
+ { id: 'strikeout', label: t('annotation.strikeout', 'Strikeout'), icon: 'strikethrough-s' },
+ { id: 'squiggly', label: t('annotation.squiggly', 'Squiggly'), icon: 'show-chart' },
+ { id: 'ink', label: t('annotation.pen', 'Pen'), icon: 'edit' },
+ { id: 'inkHighlighter', label: t('annotation.inkHighlighter', 'Ink Highlighter'), icon: 'brush' },
+ { id: 'text', label: t('annotation.text', 'Text box'), icon: 'text-fields' },
+ { id: 'note', label: t('annotation.note', 'Note'), icon: 'sticky-note-2' },
+ { id: 'square', label: t('annotation.square', 'Square'), icon: 'crop-square' },
+ { id: 'circle', label: t('annotation.circle', 'Circle'), icon: 'radio-button-unchecked' },
+ { id: 'line', label: t('annotation.line', 'Line'), icon: 'show-chart' },
+ { id: 'lineArrow', label: t('annotation.arrow', 'Arrow'), icon: 'trending-flat' },
+ { id: 'polyline', label: t('annotation.polyline', 'Polyline'), icon: 'polyline' },
+ { id: 'polygon', label: t('annotation.polygon', 'Polygon'), icon: 'change-history' },
+ { id: 'stamp', label: t('annotation.stamp', 'Stamp'), icon: 'image' },
+ ];
+
+ const activeColor =
+ colorPickerTarget === 'ink'
+ ? inkColor
+ : colorPickerTarget === 'highlight' || colorPickerTarget === 'inkHighlighter'
+ ? highlightColor
+ : colorPickerTarget === 'underline'
+ ? underlineColor
+ : colorPickerTarget === 'strikeout'
+ ? strikeoutColor
+ : colorPickerTarget === 'squiggly'
+ ? squigglyColor
+ : colorPickerTarget === 'shapeStroke'
+ ? shapeStrokeColor
+ : colorPickerTarget === 'shapeFill'
+ ? shapeFillColor
+ : textColor;
+
+ const steps = useMemo(() => {
+ if (selectedFiles.length === 0) return [];
+
+ const toolButtons = (
+
+ {annotationTools.map((tool) => (
+
+ activateAnnotationTool(tool.id)}
+ aria-label={tool.label}
+ >
+
+
+
+ ))}
+
+ );
+
+ const controls = (
+
+
+ {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ try {
+ const dataUrl: string = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+ // push into stamp defaults and activate stamp tool
+ signatureApiRef?.current?.setAnnotationStyle?.('stamp', { imageSrc: dataUrl as string });
+ signatureApiRef?.current?.activateAnnotationTool?.('stamp', { imageSrc: dataUrl as string });
+ setActiveTool('stamp');
+ } catch (err) {
+ console.error('Failed to load stamp image', err);
+ } finally {
+ e.target.value = '';
+ }
+ }}
+ />
+ {
+ const target =
+ activeTool === 'ink'
+ ? 'ink'
+ : activeTool === 'highlight' || activeTool === 'inkHighlighter'
+ ? 'highlight'
+ : activeTool === 'underline'
+ ? 'underline'
+ : activeTool === 'strikeout'
+ ? 'strikeout'
+ : activeTool === 'squiggly'
+ ? 'squiggly'
+ : ['square', 'circle', 'line', 'lineArrow', 'polyline', 'polygon'].includes(activeTool)
+ ? 'shapeStroke'
+ : 'text';
+ setColorPickerTarget(target);
+ setIsColorPickerOpen(true);
+ }}
+ />
+ {['square', 'circle', 'polygon'].includes(activeTool) && (
+ {
+ setColorPickerTarget('shapeFill');
+ setIsColorPickerOpen(true);
+ }}
+ />
+ )}
+ {activeTool === 'ink' && (
+ <>
+ {t('annotation.strokeWidth', 'Width')}
+
+ >
+ )}
+ {(activeTool === 'highlight' || activeTool === 'inkHighlighter') && (
+ <>
+ {t('annotation.opacity', 'Opacity')}
+
+ >
+ )}
+ {activeTool === 'text' && (
+ <>
+ {t('annotation.fontSize', 'Font size')}
+
+ >
+ )}
+ {['square', 'circle', 'line', 'lineArrow', 'polyline', 'polygon'].includes(activeTool) && (
+ <>
+ {t('annotation.opacity', 'Opacity')}
+
+ {t('annotation.strokeWidth', 'Stroke')}
+
+ >
+ )}
+
+
+
+ {t('annotation.tipPlace', 'Click anywhere on the PDF to place highlights, drawings, notes, or text.')}
+
+ {selectedAnn && (
+
+ {t('annotation.editSelected', 'Edit selected annotation')}
+ {(selectedAnn.object?.type === 9 || selectedAnn.object?.type === 1 || selectedAnn.object?.type === 3 || selectedAnn.object?.type === 15) && (
+ <>
+ {t('annotation.opacity', 'Opacity')}
+ {
+ signatureApiRef?.current?.updateAnnotation?.(
+ selectedAnn.object?.pageIndex ?? 0,
+ selectedAnn.object?.id,
+ { opacity: value / 100 }
+ );
+ }}
+ />
+ >
+ )}
+ {(selectedAnn.object?.type === 9 || selectedAnn.object?.type === 15 || selectedAnn.object?.type === 3 || selectedAnn.object?.type === 1) && (
+ {
+ setColorPickerTarget('highlight');
+ setIsColorPickerOpen(true);
+ }}
+ />
+ )}
+ {(selectedAnn.object?.type === 3 || selectedAnn.object?.type === 1) && (
+ <>
+ {
+ const val = e.currentTarget.value;
+ setSelectedTextDraft(val);
+ if (selectedUpdateTimer.current) {
+ clearTimeout(selectedUpdateTimer.current);
+ }
+ selectedUpdateTimer.current = setTimeout(() => {
+ signatureApiRef?.current?.updateAnnotation?.(
+ selectedAnn.object?.pageIndex ?? 0,
+ selectedAnn.object?.id,
+ { contents: val, textColor: selectedAnn.object?.textColor ?? textColor }
+ );
+ }, 120);
+ }}
+ />
+ {selectedAnn.object?.type === 3 && (
+ {
+ const size = typeof val === 'number' ? val : 14;
+ setSelectedFontSize(size);
+ signatureApiRef?.current?.updateAnnotation?.(
+ selectedAnn.object?.pageIndex ?? 0,
+ selectedAnn.object?.id,
+ { fontSize: size }
+ );
+ }}
+ />
+ )}
+ >
+ )}
+ {['4','5','7','8','12','15'].includes(String(selectedAnn.object?.type)) && (
+ <>
+ {t('annotation.opacity', 'Opacity')}
+ {
+ signatureApiRef?.current?.updateAnnotation?.(
+ selectedAnn.object?.pageIndex ?? 0,
+ selectedAnn.object?.id,
+ { opacity: value / 100 }
+ );
+ }}
+ />
+ {t('annotation.strokeWidth', 'Stroke')}
+ {
+ signatureApiRef?.current?.updateAnnotation?.(
+ selectedAnn.object?.pageIndex ?? 0,
+ selectedAnn.object?.id,
+ { borderWidth: value }
+ );
+ setShapeThickness(value);
+ }}
+ />
+
+ {
+ setColorPickerTarget('shapeStroke');
+ setIsColorPickerOpen(true);
+ }}
+ />
+ {['4','5','7','8','12'].includes(String(selectedAnn.object?.type)) && (
+ {
+ setColorPickerTarget('shapeFill');
+ setIsColorPickerOpen(true);
+ }}
+ />
+ )}
+
+ >
+ )}
+
+ )}
+ }
+ onClick={async () => {
+ try {
+ const pdfArrayBuffer = await viewerContext?.exportActions?.saveAsCopy?.();
+ if (!pdfArrayBuffer) return;
+ const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
+ const fileName = selectors.getFiles()[0]?.name || 'annotated.pdf';
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = fileName;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ } catch (error) {
+ console.error('Failed to save annotated PDF', error);
+ }
+ }}
+ >
+ {t('rightRail.save', 'Save')}
+
+ setIsColorPickerOpen(false)}
+ selectedColor={activeColor}
+ onColorChange={(color) => {
+ if (colorPickerTarget === 'ink') {
+ setInkColor(color);
+ if (activeTool === 'ink') {
+ signatureApiRef?.current?.setAnnotationStyle?.('ink', buildToolOptions('ink'));
+ }
+ if (selectedAnn?.object?.type === 15) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, { color });
+ }
+ } else if (colorPickerTarget === 'highlight') {
+ setHighlightColor(color);
+ if (activeTool === 'highlight' || activeTool === 'inkHighlighter') {
+ signatureApiRef?.current?.setAnnotationStyle?.(activeTool, buildToolOptions(activeTool));
+ }
+ if (selectedAnn?.object?.type === 9 || selectedAnn?.object?.type === 15) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, { color });
+ }
+ } else if (colorPickerTarget === 'underline') {
+ setUnderlineColor(color);
+ signatureApiRef?.current?.setAnnotationStyle?.('underline', buildToolOptions('underline'));
+ if (selectedAnn?.object?.id) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, { color });
+ }
+ } else if (colorPickerTarget === 'strikeout') {
+ setStrikeoutColor(color);
+ signatureApiRef?.current?.setAnnotationStyle?.('strikeout', buildToolOptions('strikeout'));
+ if (selectedAnn?.object?.id) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, { color });
+ }
+ } else if (colorPickerTarget === 'squiggly') {
+ setSquigglyColor(color);
+ signatureApiRef?.current?.setAnnotationStyle?.('squiggly', buildToolOptions('squiggly'));
+ if (selectedAnn?.object?.id) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, { color });
+ }
+ } else {
+ setTextColor(color);
+ if (activeTool === 'text') {
+ signatureApiRef?.current?.setAnnotationStyle?.('text', buildToolOptions('text'));
+ }
+ if (selectedAnn?.object?.type === 3 || selectedAnn?.object?.type === 1) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, {
+ textColor: color,
+ color,
+ });
+ }
+ }
+ if (colorPickerTarget === 'shapeStroke' && ['square', 'circle', 'line', 'lineArrow', 'polyline', 'polygon'].includes(activeTool)) {
+ setShapeStrokeColor(color);
+ signatureApiRef?.current?.setAnnotationStyle?.(activeTool, buildToolOptions(activeTool));
+ if (selectedAnn?.object?.id) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, {
+ color,
+ strokeColor: color,
+ borderWidth: shapeThickness,
+ });
+ }
+ }
+ if (colorPickerTarget === 'shapeFill' && ['square', 'circle', 'polygon'].includes(activeTool)) {
+ setShapeFillColor(color);
+ signatureApiRef?.current?.setAnnotationStyle?.(activeTool, buildToolOptions(activeTool));
+ if (selectedAnn && (selectedAnn.object?.interiorColor !== undefined || ['4','5','7','8','12'].includes(String(selectedAnn.object?.type)))) {
+ signatureApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, {
+ interiorColor: color,
+ fillColor: color,
+ borderWidth: shapeThickness,
+ });
+ }
+ }
+ }}
+ title={t('annotation.chooseColor', 'Choose color')}
+ />
+
+ );
+
+ return [
+ {
+ title: t('annotation.title', 'Annotate'),
+ isCollapsed: false,
+ onCollapsedClick: undefined,
+ content: (
+
+
+
+ {t('annotation.desc', 'Use highlight, pen, text, and notes. Changes stay liveāno flattening required.')}
+
+
+
+ {t('annotation.title', 'Annotate')}
+ {toolButtons}
+
+ {controls}
+
+ ),
+ },
+ ];
+ }, [
+ activeTool,
+ annotationTools,
+ highlightColor,
+ highlightOpacity,
+ inkColor,
+ inkWidth,
+ selectedFiles.length,
+ t,
+ selectedAnn,
+ textColor,
+ textSize,
+ viewerContext?.exportActions,
+ selectors,
+ ]);
+
+ return createToolFlow({
+ files: {
+ selectedFiles,
+ isCollapsed: false,
+ },
+ steps,
+ review: {
+ isVisible: false,
+ operation: { files: [], downloadUrl: null },
+ title: '',
+ onFileClick: () => {},
+ onUndo: () => {},
+ },
+ forceStepNumbers: true,
+ });
+};
+
+export default Annotate;
diff --git a/frontend/src/core/types/toolId.ts b/frontend/src/core/types/toolId.ts
index 23b03a871..9a8dee3ba 100644
--- a/frontend/src/core/types/toolId.ts
+++ b/frontend/src/core/types/toolId.ts
@@ -25,6 +25,7 @@ export const CORE_REGULAR_TOOL_IDS = [
'ocr',
'addImage',
'rotate',
+ 'annotate',
'scannerImageSplit',
'editTableOfContents',
'scannerEffect',