([]);
+ const [isNavigating, setIsNavigating] = useState(false);
+
+ const processedLinks = useMemo(() => {
+ return links.map(link => ({
+ ...link,
+ scaledRect: {
+ left: link.rect.origin.x * scale,
+ top: link.rect.origin.y * scale,
+ width: link.rect.size.width * scale,
+ height: link.rect.size.height * scale,
+ }
+ }));
+ }, [links, scale]);
+
+ useEffect(() => {
+ const fetchLinks = async () => {
+ if (!annotation) return;
+
+ try {
+ // Use the annotation API's built-in filtering if available
+ const pageAnnotations = await annotation
+ .getPageAnnotations({
+ pageIndex,
+ // Try to filter for link annotations (type 2) if the API supports it
+ ...(annotation.getPageAnnotations.length > 1 ? { types: [2] } : {})
+ })
+ .toPromise();
+
+ // Filter for link annotations (type 2 is LINK in PDF spec) as fallback
+ const linkAnnotations = pageAnnotations.filter(
+ (ann: any) => ann.type === 2
+ ) as LinkAnnotation[];
+
+ if (linkAnnotations.length > 0) {
+ setLinks(linkAnnotations);
+ return;
+ }
+ } catch (error) {
+ console.error('[LinkLayer] Failed to fetch links from annotation API:', error);
+ }
+
+ if (pdfDocument) {
+ try {
+ // Try different methods to get link annotations
+ let pdfLinks: any[] = [];
+
+ if (pdfDocument.getPageAnnotations && typeof pdfDocument.getPageAnnotations === 'function') {
+ pdfLinks = await pdfDocument.getPageAnnotations(pageIndex);
+ } else if (pdfDocument.getAnnotations && typeof pdfDocument.getAnnotations === 'function') {
+ const allAnnotations = await pdfDocument.getAnnotations();
+ pdfLinks = allAnnotations.filter((ann: any) => ann.pageIndex === pageIndex && ann.type === 2);
+ } else if (pdfDocument.pages && pdfDocument.pages[pageIndex]) {
+ const page = pdfDocument.pages[pageIndex];
+ if (page.getAnnotations && typeof page.getAnnotations === 'function') {
+ pdfLinks = await page.getAnnotations();
+ }
+ }
+
+ const convertedLinks = pdfLinks.map((ann: any) => ({
+ id: ann.id || `pdf-link-${pageIndex}-${Math.random()}`,
+ type: ann.type || 2,
+ rect: ann.rect || ann,
+ target: ann.target || ann.action
+ })) as LinkAnnotation[];
+
+ setLinks(convertedLinks);
+ } catch (error) {
+ console.warn('[LinkLayer] Failed to get annotations from PDF document:', error);
+ }
+ } else {
+ console.warn('[LinkLayer] No annotation API or PDF document available');
+ }
+ };
+
+ fetchLinks();
+ }, [annotation, pageIndex, pdfDocument]);
+
+ const handleLinkClick = useCallback(async (link: LinkAnnotation) => {
+ if (isNavigating) return; // Prevent multiple simultaneous navigations
+
+ try {
+ setIsNavigating(true);
+
+ if (onLinkClick) {
+ onLinkClick(link.target);
+ return;
+ }
+
+ if (isInternalLink(link)) {
+ const targetPage = link.target?.destination?.pageIndex ??
+ link.target?.action?.destination?.pageIndex;
+ if (targetPage !== undefined && scroll) {
+ await scroll.scrollToPage({
+ pageNumber: targetPage + 1, // PDF pages are 1-indexed
+ behavior: 'smooth',
+ });
+ }
+ } else if (isExternalLink(link)) {
+ const uri = link.target?.uri ?? link.target?.action?.uri;
+ if (uri) {
+ window.open(uri, '_blank', 'noopener,noreferrer');
+ }
+ } else {
+ throw new Error(`Unsupported link type: ${link.target?.type}`);
+ }
+ } catch (error) {
+ console.error('[LinkLayer] Navigation failed:', error);
+ } finally {
+ setIsNavigating(false);
+ }
+ }, [isNavigating, onLinkClick, scroll]);
+
+ return (
+
+ {processedLinks.map((link) => {
+ const { id } = link;
+ const { left, top, width, height } = link.scaledRect;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
index 2de2a2e58..adef8c4fb 100644
--- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
+++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
@@ -43,6 +43,7 @@ import { ExportAPIBridge } from '@app/components/viewer/ExportAPIBridge';
import { BookmarkAPIBridge } from '@app/components/viewer/BookmarkAPIBridge';
import { isPdfFile } from '@app/utils/fileUtils';
import { useTranslation } from 'react-i18next';
+import { LinkLayer } from '@app/components/viewer/LinkLayer';
interface LocalEmbedPDFProps {
file?: File | Blob;
@@ -103,15 +104,17 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
createPluginRegistration(SelectionPluginPackage),
// Register history plugin for undo/redo (recommended for annotations)
- ...(enableAnnotations ? [createPluginRegistration(HistoryPluginPackage)] : []),
+ // Always register for reading existing annotations
+ createPluginRegistration(HistoryPluginPackage),
// Register annotation plugin (depends on InteractionManager, Selection, History)
- ...(enableAnnotations ? [createPluginRegistration(AnnotationPluginPackage, {
+ // Always register for reading existing annotations like links
+ createPluginRegistration(AnnotationPluginPackage, {
annotationAuthor: 'Digital Signature',
autoCommit: true,
deactivateToolAfterCreate: false,
selectAfterCreate: true,
- })] : []),
+ }),
// Register pan plugin (depends on Viewport, InteractionManager)
createPluginRegistration(PanPluginPackage, {
@@ -229,68 +232,62 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
{
+ onInitialized={async (registry) => {
const annotationPlugin = registry.getPlugin('annotation');
if (!annotationPlugin || !annotationPlugin.provides) return;
const annotationApi = annotationPlugin.provides();
if (!annotationApi) return;
- // Add custom signature stamp tool for image signatures
- annotationApi.addTool({
- id: 'signatureStamp',
- name: 'Digital Signature',
- interaction: { exclusive: false, cursor: 'copy' },
- matchScore: () => 0,
- defaults: {
- type: PdfAnnotationSubtype.STAMP,
- // Image will be set dynamically when signature is created
- },
- });
+ if (enableAnnotations) {
+ annotationApi.addTool({
+ id: 'signatureStamp',
+ name: 'Digital Signature',
+ interaction: { exclusive: false, cursor: 'copy' },
+ matchScore: () => 0,
+ defaults: {
+ type: PdfAnnotationSubtype.STAMP,
+ },
+ });
- // Add custom ink signature tool for drawn signatures
- annotationApi.addTool({
- id: 'signatureInk',
- name: 'Signature Draw',
- interaction: { exclusive: true, cursor: 'crosshair' },
- matchScore: () => 0,
- defaults: {
- type: PdfAnnotationSubtype.INK,
- color: '#000000',
- opacity: 1.0,
- borderWidth: 2,
- },
- });
+ annotationApi.addTool({
+ id: 'signatureInk',
+ name: 'Signature Draw',
+ interaction: { exclusive: true, cursor: 'crosshair' },
+ matchScore: () => 0,
+ defaults: {
+ type: PdfAnnotationSubtype.INK,
+ color: '#000000',
+ opacity: 1.0,
+ borderWidth: 2,
+ },
+ });
- // Listen for annotation events to track annotations and notify parent
- annotationApi.onAnnotationEvent((event: any) => {
- if (event.type === 'create' && event.committed) {
- // Add to annotations list
- setAnnotations(prev => [...prev, {
- id: event.annotation.id,
- pageIndex: event.pageIndex,
- rect: event.annotation.rect
- }]);
+ annotationApi.onAnnotationEvent((event: any) => {
+ if (event.type === 'create' && event.committed) {
+ setAnnotations(prev => [...prev, {
+ id: event.annotation.id,
+ pageIndex: event.pageIndex,
+ rect: event.annotation.rect
+ }]);
- // Notify parent if callback provided
- if (onSignatureAdded) {
- onSignatureAdded(event.annotation);
+ if (onSignatureAdded) {
+ onSignatureAdded(event.annotation);
+ }
+ } else if (event.type === 'delete' && event.committed) {
+ setAnnotations(prev => prev.filter(ann => ann.id !== event.annotation.id));
+ } else if (event.type === 'loaded') {
+ const loadedAnnotations = event.annotations || [];
+ setAnnotations(loadedAnnotations.map((ann: any) => ({
+ id: ann.id,
+ pageIndex: ann.pageIndex || 0,
+ rect: ann.rect
+ })));
}
- } else if (event.type === 'delete' && event.committed) {
- // Remove from annotations list
- setAnnotations(prev => prev.filter(ann => ann.id !== event.annotation.id));
- } else if (event.type === 'loaded') {
- // Handle initial load of annotations
- const loadedAnnotations = event.annotations || [];
- setAnnotations(loadedAnnotations.map((ann: any) => ({
- id: ann.id,
- pageIndex: ann.pageIndex || 0,
- rect: ann.rect
- })));
- }
- });
- } : undefined}
+ });
+ }
+ }}
>
@@ -352,6 +349,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
{/* Selection layer for text interaction */}
+
+ {/* Link layer for clickable PDF links */}
+
+
{/* Annotation layer for signatures (only when enabled) */}
{enableAnnotations && (