import { useImperativeHandle, forwardRef, useCallback } from 'react'; import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react'; import { PdfAnnotationSubtype, PdfAnnotationIcon } from '@embedpdf/models'; import type { AnnotationToolId, AnnotationToolOptions, AnnotationAPI, AnnotationEvent, AnnotationPatch, AnnotationRect, AnnotationSelection, } from '@app/components/viewer/viewerTypes'; import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady'; type NoteIcon = NonNullable; type AnnotationDefaults = | { type: | PdfAnnotationSubtype.HIGHLIGHT | PdfAnnotationSubtype.UNDERLINE | PdfAnnotationSubtype.STRIKEOUT | PdfAnnotationSubtype.SQUIGGLY; strokeColor: string; color?: string; opacity: number; customData?: Record; } | { type: PdfAnnotationSubtype.INK; strokeColor: string; color?: string; opacity?: number; borderWidth?: number; strokeWidth?: number; lineWidth?: number; customData?: Record; } | { type: PdfAnnotationSubtype.FREETEXT; fontColor?: string; fontSize?: number; fontFamily?: string; textAlign?: number; opacity?: number; color?: string; interiorColor?: string; borderWidth?: number; contents?: string; icon?: PdfAnnotationIcon; customData?: Record; } | { type: PdfAnnotationSubtype.SQUARE | PdfAnnotationSubtype.CIRCLE | PdfAnnotationSubtype.POLYGON; color: string; strokeColor: string; opacity: number; fillOpacity: number; strokeOpacity: number; borderWidth: number; strokeWidth: number; lineWidth: number; customData?: Record; } | { type: PdfAnnotationSubtype.LINE | PdfAnnotationSubtype.POLYLINE; color: string; strokeColor?: string; opacity: number; borderWidth?: number; strokeWidth?: number; lineWidth?: number; startStyle?: string; endStyle?: string; lineEndingStyles?: { start: string; end: string }; customData?: Record; } | { type: PdfAnnotationSubtype.STAMP; imageSrc?: string; imageSize?: { width: number; height: number }; customData?: Record; } | null; type AnnotationApiSurface = { setActiveTool: (toolId: AnnotationToolId | null) => void; getActiveTool?: () => { id: AnnotationToolId } | null; setToolDefaults?: (toolId: AnnotationToolId, defaults: AnnotationDefaults) => void; getSelectedAnnotation?: () => AnnotationSelection | null; deselectAnnotation?: () => void; updateAnnotation?: (pageIndex: number, annotationId: string, patch: AnnotationPatch) => void; deleteAnnotation?: (pageIndex: number, annotationId: string) => void; deleteAnnotations?: (annotations: Array<{ pageIndex: number; id: string }>) => void; createAnnotation?: (pageIndex: number, annotation: Record) => void; getSelectedAnnotations?: () => AnnotationSelection[]; onAnnotationEvent?: (listener: (event: AnnotationEvent) => void) => void | (() => void); purgeAnnotation?: (pageIndex: number, annotationId: string) => void; /** v2.7.0: move annotation without regenerating its appearance stream */ moveAnnotation?: (pageIndex: number, annotationId: string, newRect: AnnotationRect) => void; }; type ToolDefaultsBuilder = (options?: AnnotationToolOptions) => AnnotationDefaults; const NOTE_ICON_MAP: Record = { Comment: PdfAnnotationIcon.Comment, Key: PdfAnnotationIcon.Key, Note: PdfAnnotationIcon.Note, Help: PdfAnnotationIcon.Help, NewParagraph: PdfAnnotationIcon.NewParagraph, Paragraph: PdfAnnotationIcon.Paragraph, Insert: PdfAnnotationIcon.Insert, }; const DEFAULTS = { highlight: '#ffd54f', underline: '#ffb300', strikeout: '#e53935', squiggly: '#00acc1', ink: '#1f2933', inkHighlighter: '#ffd54f', text: '#111111', note: '#ffd54f', shapeFill: '#0000ff', shapeStroke: '#cf5b5b', shapeOpacity: 0.5, }; const withCustomData = (options?: AnnotationToolOptions) => options?.customData ? { customData: options.customData } : {}; const getIconEnum = (icon?: NoteIcon) => NOTE_ICON_MAP[icon ?? 'Comment'] ?? PdfAnnotationIcon.Comment; const buildStampDefaults: ToolDefaultsBuilder = (options) => ({ type: PdfAnnotationSubtype.STAMP, ...(options?.imageSrc ? { imageSrc: options.imageSrc } : {}), ...(options?.imageSize ? { imageSize: options.imageSize } : {}), ...withCustomData(options), }); const buildInkDefaults = (options?: AnnotationToolOptions, opacityOverride?: number): AnnotationDefaults => { const colorValue = options?.color ?? (opacityOverride ? DEFAULTS.inkHighlighter : DEFAULTS.ink); return { type: PdfAnnotationSubtype.INK, strokeColor: colorValue, color: colorValue, opacity: options?.opacity ?? opacityOverride ?? 1, borderWidth: options?.thickness ?? (opacityOverride ? 6 : 2), strokeWidth: options?.thickness ?? (opacityOverride ? 6 : 2), lineWidth: options?.thickness ?? (opacityOverride ? 6 : 2), ...withCustomData(options), }; }; const TOOL_DEFAULT_BUILDERS: Record = { select: () => null, highlight: (options) => { const colorValue = options?.color ?? DEFAULTS.highlight; return { type: PdfAnnotationSubtype.HIGHLIGHT, strokeColor: colorValue, color: colorValue, opacity: options?.opacity ?? 0.6, ...withCustomData(options), }; }, underline: (options) => { const colorValue = options?.color ?? DEFAULTS.underline; return { type: PdfAnnotationSubtype.UNDERLINE, strokeColor: colorValue, color: colorValue, opacity: options?.opacity ?? 1, ...withCustomData(options), }; }, strikeout: (options) => { const colorValue = options?.color ?? DEFAULTS.strikeout; return { type: PdfAnnotationSubtype.STRIKEOUT, strokeColor: colorValue, color: colorValue, opacity: options?.opacity ?? 1, ...withCustomData(options), }; }, squiggly: (options) => { const colorValue = options?.color ?? DEFAULTS.squiggly; return { type: PdfAnnotationSubtype.SQUIGGLY, strokeColor: colorValue, color: colorValue, opacity: options?.opacity ?? 1, ...withCustomData(options), }; }, ink: (options) => buildInkDefaults(options), inkHighlighter: (options) => buildInkDefaults(options, options?.opacity ?? 0.6), text: (options) => ({ type: PdfAnnotationSubtype.FREETEXT, fontColor: options?.color ?? DEFAULTS.text, fontSize: options?.fontSize ?? 14, fontFamily: options?.fontFamily ?? 'Helvetica', textAlign: options?.textAlign ?? 0, opacity: options?.opacity ?? 1, borderWidth: options?.thickness ?? 1, ...(options?.fillColor ? { color: options.fillColor, interiorColor: options.fillColor } : {}), ...withCustomData(options), }), note: (options) => { const bgColor = options?.fillColor ?? DEFAULTS.note; const fontColor = options?.color ?? DEFAULTS.text; return { type: PdfAnnotationSubtype.FREETEXT, fontColor, fontFamily: options?.fontFamily ?? 'Helvetica', textAlign: options?.textAlign ?? 0, fontSize: options?.fontSize ?? 12, opacity: options?.opacity ?? 1, color: bgColor, interiorColor: bgColor, borderWidth: options?.thickness ?? 0, contents: options?.contents ?? 'Note', icon: getIconEnum(options?.icon), ...withCustomData(options), }; }, square: (options) => ({ type: PdfAnnotationSubtype.SQUARE, color: options?.color ?? DEFAULTS.shapeFill, strokeColor: options?.strokeColor ?? DEFAULTS.shapeStroke, opacity: options?.opacity ?? DEFAULTS.shapeOpacity, fillOpacity: options?.fillOpacity ?? DEFAULTS.shapeOpacity, strokeOpacity: options?.strokeOpacity ?? DEFAULTS.shapeOpacity, borderWidth: options?.borderWidth ?? 1, strokeWidth: options?.borderWidth ?? 1, lineWidth: options?.borderWidth ?? 1, ...withCustomData(options), }), circle: (options) => ({ type: PdfAnnotationSubtype.CIRCLE, color: options?.color ?? DEFAULTS.shapeFill, strokeColor: options?.strokeColor ?? DEFAULTS.shapeStroke, opacity: options?.opacity ?? DEFAULTS.shapeOpacity, fillOpacity: options?.fillOpacity ?? DEFAULTS.shapeOpacity, strokeOpacity: options?.strokeOpacity ?? DEFAULTS.shapeOpacity, borderWidth: options?.borderWidth ?? 1, strokeWidth: options?.borderWidth ?? 1, lineWidth: options?.borderWidth ?? 1, ...withCustomData(options), }), line: (options) => ({ type: PdfAnnotationSubtype.LINE, color: options?.color ?? '#1565c0', strokeColor: options?.color ?? '#1565c0', opacity: options?.opacity ?? 1, borderWidth: options?.borderWidth ?? 2, strokeWidth: options?.borderWidth ?? 2, lineWidth: options?.borderWidth ?? 2, ...withCustomData(options), }), lineArrow: (options) => ({ type: PdfAnnotationSubtype.LINE, color: options?.color ?? '#1565c0', strokeColor: options?.color ?? '#1565c0', opacity: options?.opacity ?? 1, borderWidth: options?.borderWidth ?? 2, strokeWidth: options?.borderWidth ?? 2, lineWidth: options?.borderWidth ?? 2, startStyle: 'None', endStyle: 'ClosedArrow', lineEndingStyles: { start: 'None', end: 'ClosedArrow' }, ...withCustomData(options), }), polyline: (options) => ({ type: PdfAnnotationSubtype.POLYLINE, color: options?.color ?? '#1565c0', opacity: options?.opacity ?? 1, borderWidth: options?.borderWidth ?? 2, ...withCustomData(options), }), polygon: (options) => ({ type: PdfAnnotationSubtype.POLYGON, color: options?.color ?? DEFAULTS.shapeFill, strokeColor: options?.strokeColor ?? DEFAULTS.shapeStroke, opacity: options?.opacity ?? DEFAULTS.shapeOpacity, fillOpacity: options?.fillOpacity ?? DEFAULTS.shapeOpacity, strokeOpacity: options?.strokeOpacity ?? DEFAULTS.shapeOpacity, borderWidth: options?.borderWidth ?? 1, strokeWidth: options?.borderWidth ?? 1, lineWidth: options?.borderWidth ?? 1, ...withCustomData(options), }), stamp: buildStampDefaults, signatureStamp: buildStampDefaults, signatureInk: (options) => buildInkDefaults(options), }; export const AnnotationAPIBridge = forwardRef(function AnnotationAPIBridge(_props, ref) { // Use the provided annotation API just like SignatureAPIBridge/HistoryAPIBridge const { provides: annotationApi } = useAnnotationCapability(); const documentReady = useDocumentReady(); const buildAnnotationDefaults = useCallback( (toolId: AnnotationToolId, options?: AnnotationToolOptions) => TOOL_DEFAULT_BUILDERS[toolId]?.(options) ?? null, [] ); const configureAnnotationTool = useCallback( (toolId: AnnotationToolId, options?: AnnotationToolOptions) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; if (!api?.setActiveTool) return; const defaults = buildAnnotationDefaults(toolId, options); // Reset tool first, then activate (like SignatureAPIBridge does) api.setActiveTool(null); api.setActiveTool(toolId === 'select' ? null : toolId); // Verify tool was activated before setting defaults (like SignatureAPIBridge does) const activeTool = api.getActiveTool?.(); if (activeTool && activeTool.id === toolId && defaults) { api.setToolDefaults?.(toolId, defaults); } }, [annotationApi, buildAnnotationDefaults] ); useImperativeHandle( ref, () => ({ activateAnnotationTool: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => { configureAnnotationTool(toolId, options); }, isReady: () => !!annotationApi && documentReady, setAnnotationStyle: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => { const defaults = buildAnnotationDefaults(toolId, options); const api = annotationApi as unknown as AnnotationApiSurface | undefined; if (defaults && api?.setToolDefaults) { api.setToolDefaults(toolId, defaults); } }, getSelectedAnnotation: () => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; if (!api?.getSelectedAnnotation) { return null; } try { return api.getSelectedAnnotation(); } catch (error) { // Some EmbedPDF builds expose getSelectedAnnotation with an internal // `this`/state dependency (e.g. reading `selectedUid` from undefined). // If that happens, fail gracefully and treat it as "no selection" // instead of crashing the entire annotations tool. // Only log unexpected errors - "No active document" is a common expected state during init const errorMessage = error instanceof Error ? error.message : String(error); if (!errorMessage.includes('No active document')) { console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error); } return null; } }, deselectAnnotation: () => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.deselectAnnotation?.(); }, updateAnnotation: (pageIndex: number, annotationId: string, patch: AnnotationPatch) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.updateAnnotation?.(pageIndex, annotationId, patch); }, deactivateTools: () => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.setActiveTool?.(null); }, onAnnotationEvent: (listener: (event: AnnotationEvent) => void) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; if (api?.onAnnotationEvent) { return api.onAnnotationEvent(listener); } return undefined; }, getActiveTool: () => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; return api?.getActiveTool?.() ?? null; }, deleteAnnotation: (pageIndex: number, annotationId: string) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.deleteAnnotation?.(pageIndex, annotationId); }, deleteAnnotations: (annotations: Array<{ pageIndex: number; id: string }>) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.deleteAnnotations?.(annotations); }, createAnnotation: (pageIndex: number, annotation: Record) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.createAnnotation?.(pageIndex, annotation); }, getSelectedAnnotations: () => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; return api?.getSelectedAnnotations?.() ?? []; }, purgeAnnotation: (pageIndex: number, annotationId: string) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.purgeAnnotation?.(pageIndex, annotationId); }, moveAnnotation: (pageIndex: number, annotationId: string, newRect: AnnotationRect) => { const api = annotationApi as unknown as AnnotationApiSurface | undefined; api?.moveAnnotation?.(pageIndex, annotationId, newRect); }, }), [annotationApi, configureAnnotationTool, buildAnnotationDefaults] ); return null; });