Files
Stirling-PDF/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
T
James BruntonandGitHub 971321fb19 Fix printing on Mac desktop (#5920)
# Description of Changes
Fix #5164 

As I mentioned on the bug
https://github.com/Stirling-Tools/Stirling-PDF/issues/5164#issuecomment-4045170827,
it's impossible to print on Mac currently because
`iframe.contentWindow?.print()` silently does nothing in Tauri on Mac,
but [it seems unlikely that this will be
fixed](https://github.com/tauri-apps/tauri/issues/13451#issuecomment-4048075861).

Instead, I've linked directly to the Mac `PDFKit` framework in Rust to
use its printing functionality instead of Safari's. I believe that
`PDFKit` is what `Preview.app` is using and the print UI that it
generates seems to perform identically, so this should solve the issue
on Mac. Hopefully one day the TS iframe print API will be fixed and
we'll be able to get rid of this code, or [there'll be an official Tauri
plugin for printing which we can use
instead](https://github.com/tauri-apps/plugins-workspace/issues/293).

This implementation should be entirely Mac-specific. Windows & Linux
will continue to use their TS printing (which comes from EmbedPDF)
unless we have a good reason to change them to use a native solution as
well.
2026-03-16 10:49:45 +00:00

804 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useMemo, useState } from 'react';
import { createPluginRegistration } from '@embedpdf/core';
import type { PluginRegistry } from '@embedpdf/core';
import { EmbedPDF } from '@embedpdf/core/react';
import { usePdfiumEngine } from '@embedpdf/engines/react';
import { PrivateContent } from '@app/components/shared/PrivateContent';
// Import the essential plugins
import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react';
import { Scroller, ScrollPluginPackage } from '@embedpdf/plugin-scroll/react';
import { DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/react';
import { RenderPluginPackage } from '@embedpdf/plugin-render/react';
import { ZoomPluginPackage, ZoomMode } from '@embedpdf/plugin-zoom/react';
import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react';
import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react';
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
import { PanPluginPackage } from '@embedpdf/plugin-pan/react';
import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react';
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
import { ExportPluginPackage } from '@embedpdf/plugin-export/react';
import { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark/react';
import { AttachmentPluginPackage } from '@embedpdf/plugin-attachment/react';
import { PrintPluginPackage } from '@embedpdf/plugin-print/react';
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react';
import type { AnnotationTool, AnnotationEvent } from '@embedpdf/plugin-annotation';
import { PdfAnnotationSubtype } from '@embedpdf/models';
import type { PdfAnnotationObject, Rect } from '@embedpdf/models';
import { RedactionPluginPackage, RedactionLayer } from '@embedpdf/plugin-redaction/react';
import { CustomSearchLayer } from '@app/components/viewer/CustomSearchLayer';
import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge';
import ToolLoadingFallback from '@app/components/tools/ToolLoadingFallback';
import { Center, Stack, Text } from '@mantine/core';
import { ScrollAPIBridge } from '@app/components/viewer/ScrollAPIBridge';
import { SelectionAPIBridge } from '@app/components/viewer/SelectionAPIBridge';
import { PanAPIBridge } from '@app/components/viewer/PanAPIBridge';
import { SpreadAPIBridge } from '@app/components/viewer/SpreadAPIBridge';
import { SearchAPIBridge } from '@app/components/viewer/SearchAPIBridge';
import { ThumbnailAPIBridge } from '@app/components/viewer/ThumbnailAPIBridge';
import { RotateAPIBridge } from '@app/components/viewer/RotateAPIBridge';
import { SignatureAPIBridge } from '@app/components/viewer/SignatureAPIBridge';
import { AnnotationAPIBridge } from '@app/components/viewer/AnnotationAPIBridge';
import { HistoryAPIBridge } from '@app/components/viewer/HistoryAPIBridge';
import type { SignatureAPI, AnnotationAPI, HistoryAPI } from '@app/components/viewer/viewerTypes';
import { ExportAPIBridge } from '@app/components/viewer/ExportAPIBridge';
import { BookmarkAPIBridge } from '@app/components/viewer/BookmarkAPIBridge';
import { AttachmentAPIBridge } from '@app/components/viewer/AttachmentAPIBridge';
import { PrintAPIBridge } from '@app/components/viewer/PrintAPIBridge';
import { isPdfFile } from '@app/utils/fileUtils';
import { useTranslation } from 'react-i18next';
import { LinkLayer } from '@app/components/viewer/LinkLayer';
import { TextSelectionHandler } from '@app/components/viewer/TextSelectionHandler';
import { RedactionSelectionMenu } from '@app/components/viewer/RedactionSelectionMenu';
import { AnnotationSelectionMenu } from '@app/components/viewer/AnnotationSelectionMenu';
import { RedactionPendingTracker, RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker';
import { RedactionAPIBridge } from '@app/components/viewer/RedactionAPIBridge';
import { DocumentPermissionsAPIBridge } from '@app/components/viewer/DocumentPermissionsAPIBridge';
import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrapper';
import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentContext';
import { absoluteWithBasePath } from '@app/constants/app';
import { FormFieldOverlay } from '@app/tools/formFill/FormFieldOverlay';
interface LocalEmbedPDFProps {
file?: File | Blob;
url?: string | null;
fileName?: string;
enableAnnotations?: boolean;
enableRedaction?: boolean;
enableFormFill?: boolean;
isManualRedactionMode?: boolean;
showBakedAnnotations?: boolean;
onSignatureAdded?: (annotation: PdfAnnotationObject) => void;
signatureApiRef?: React.RefObject<SignatureAPI>;
annotationApiRef?: React.RefObject<AnnotationAPI>;
historyApiRef?: React.RefObject<HistoryAPI>;
redactionTrackerRef?: React.RefObject<RedactionPendingTrackerAPI>;
/** File identity passed through to FormFieldOverlay for stale-field guards */
fileId?: string | null;
}
export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId }: LocalEmbedPDFProps) {
const { t } = useTranslation();
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: Rect}>>([]);
// Convert File to URL if needed
useEffect(() => {
if (file) {
const objectUrl = URL.createObjectURL(file);
setPdfUrl(objectUrl);
return () => URL.revokeObjectURL(objectUrl);
} else if (url) {
setPdfUrl(url);
}
}, [file, url]);
// Create plugins configuration
const plugins = useMemo(() => {
if (!pdfUrl) return [];
// Calculate 3.5rem in pixels dynamically based on root font size
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const viewportGap = rootFontSize * 3.5;
// Determine export filename - use provided fileName, or extract from file/url
let exportFileName = 'document.pdf';
if (fileName) {
exportFileName = fileName;
} else if (file && 'name' in file) {
exportFileName = file.name;
} else if (url) {
const urlPath = url.split('/').pop() || 'document.pdf';
exportFileName = urlPath.split('?')[0]; // Remove query params
}
return [
createPluginRegistration(DocumentManagerPluginPackage, {
initialDocuments: [{
url: pdfUrl,
name: exportFileName,
}],
}),
createPluginRegistration(ViewportPluginPackage, {
viewportGap,
}),
createPluginRegistration(ScrollPluginPackage),
createPluginRegistration(RenderPluginPackage, {
withForms: !enableFormFill,
withAnnotations: !enableAnnotations, // Show baked annotations only when annotation layer is OFF; live layer visibility is controlled via CSS
}),
// Register interaction manager (required for zoom and selection features)
createPluginRegistration(InteractionManagerPluginPackage),
// Register selection plugin (depends on InteractionManager)
createPluginRegistration(SelectionPluginPackage),
// Register history plugin for undo/redo (recommended for annotations)
// Always register for reading existing annotations
createPluginRegistration(HistoryPluginPackage),
// Register annotation plugin (depends on InteractionManager, Selection, History)
// Always register for reading existing annotations like links
createPluginRegistration(AnnotationPluginPackage, {
annotationAuthor: 'Digital Signature',
autoCommit: true,
deactivateToolAfterCreate: false,
selectAfterCreate: true,
}),
// Register redaction plugin (depends on InteractionManager, Selection, History)
// Always register for redaction functionality
createPluginRegistration(RedactionPluginPackage, {
useAnnotationMode: true,
drawBlackBoxes: false,
}),
// Register pan plugin (depends on Viewport, InteractionManager)
createPluginRegistration(PanPluginPackage, {
defaultMode: 'mobile', // Try mobile mode which might be more permissive
}),
// Register pan plugin (depends on Viewport, InteractionManager) - keep disabled to prevent drag panning
createPluginRegistration(PanPluginPackage, {}),
// Register zoom plugin with configuration
createPluginRegistration(ZoomPluginPackage, {
defaultZoomLevel: ZoomMode.FitWidth, // Start with FitWidth, will be adjusted in ZoomAPIBridge
minZoom: 0.2,
maxZoom: 5.0,
}),
// Register tiling plugin (depends on Render, Scroll, Viewport)
createPluginRegistration(TilingPluginPackage, {
tileSize: 768,
overlapPx: 5,
extraRings: 1,
}),
// Register spread plugin for dual page layout
createPluginRegistration(SpreadPluginPackage, {
defaultSpreadMode: SpreadMode.None, // Start with single page view
}),
// Register search plugin for text search
createPluginRegistration(SearchPluginPackage),
// Register thumbnail plugin for page thumbnails
createPluginRegistration(ThumbnailPluginPackage),
// Register bookmark plugin for PDF outline support
createPluginRegistration(BookmarkPluginPackage),
// Register attachment plugin for PDF attachments support
createPluginRegistration(AttachmentPluginPackage),
// Register rotate plugin
createPluginRegistration(RotatePluginPackage),
// Register export plugin for downloading PDFs
createPluginRegistration(ExportPluginPackage, {
defaultFileName: exportFileName,
}),
// Register print plugin for printing PDFs
createPluginRegistration(PrintPluginPackage),
];
}, [pdfUrl, enableAnnotations, fileName, file, url]);
// Initialize the engine with the React hook - use local WASM for offline support
const { engine, isLoading, error } = usePdfiumEngine({
wasmUrl: absoluteWithBasePath('/pdfium/pdfium.wasm'),
});
// Early return if no file or URL provided
if (!file && !url) {
return (
<Center h="100%" w="100%">
<Stack align="center" gap="md">
<div style={{ fontSize: '24px' }}>📄</div>
<Text c="dimmed" size="sm">
No PDF provided
</Text>
</Stack>
</Center>
);
}
// Check if the file is actually a PDF
if (file && !isPdfFile(file)) {
const fileName = 'name' in file ? file.name : t('viewer.unknownFile');
return (
<Center h="100%" w="100%">
<Stack align="center" gap="md">
<div style={{ fontSize: '48px' }}>📄</div>
<Text size="lg" fw={600} c="dimmed">
{t('viewer.cannotPreviewFile')}
</Text>
<Text c="dimmed" size="sm" style={{ textAlign: 'center', maxWidth: '400px' }}>
{t('viewer.onlyPdfSupported')}
</Text>
<PrivateContent>
<Text c="dimmed" size="xs" style={{ fontFamily: 'monospace' }}>
{fileName}
</Text>
</PrivateContent>
</Stack>
</Center>
);
}
if (isLoading || !engine || !pdfUrl) {
return <ToolLoadingFallback toolName="PDF Engine" />;
}
if (error) {
return (
<Center h="100%" w="100%">
<Stack align="center" gap="md">
<div style={{ fontSize: '24px' }}></div>
<Text c="red" size="sm" style={{ textAlign: 'center' }}>
Error loading PDF engine: {error.message}
</Text>
</Stack>
</Center>
);
}
// Wrap your UI with the <EmbedPDF> provider
return (
<PrivateContent>
<div
style={{
height: '100%',
width: '100%',
position: 'relative',
flex: 1,
minHeight: 0,
minWidth: 0,
}}>
<EmbedPDF
engine={engine}
plugins={plugins}
onInitialized={async (registry: PluginRegistry) => {
// v2.0: Use registry.getPlugin() to access plugin APIs
const annotationPlugin = registry.getPlugin('annotation');
if (!annotationPlugin || !annotationPlugin.provides) return;
const annotationApi = annotationPlugin.provides();
if (!annotationApi) return;
if (enableAnnotations) {
// LooseAnnotationTool bypasses strict Partial<T> defaults typing from the library —
// EmbedPDF accepts extra runtime properties (borderWidth, textColor, finishOnDoubleClick,
// etc.) that aren't reflected in the TypeScript model types.
type LooseAnnotationTool = {
id: string;
name: string;
interaction?: { exclusive: boolean; cursor: string; textSelection?: boolean; isRotatable?: boolean };
matchScore?: (annotation: PdfAnnotationObject) => number;
defaults?: Record<string, unknown>;
clickBehavior?: Record<string, unknown>;
behavior?: { deactivateToolAfterCreate?: boolean; selectAfterCreate?: boolean };
};
const ensureTool = (tool: LooseAnnotationTool) => {
const existing = annotationApi.getTool?.(tool.id);
if (!existing) {
annotationApi.addTool(tool as unknown as AnnotationTool);
}
};
ensureTool({
id: 'highlight',
name: 'Highlight',
interaction: { exclusive: true, cursor: 'text', textSelection: true },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.HIGHLIGHT,
strokeColor: '#ffd54f',
color: '#ffd54f',
opacity: 0.6,
},
behavior: {
deactivateToolAfterCreate: false,
selectAfterCreate: true,
},
});
ensureTool({
id: 'underline',
name: 'Underline',
interaction: { exclusive: true, cursor: 'text', textSelection: true },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.UNDERLINE,
strokeColor: '#ffb300',
color: '#ffb300',
opacity: 1,
},
behavior: {
deactivateToolAfterCreate: false,
selectAfterCreate: true,
},
});
ensureTool({
id: 'strikeout',
name: 'Strikeout',
interaction: { exclusive: true, cursor: 'text', textSelection: true },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.STRIKEOUT,
strokeColor: '#e53935',
color: '#e53935',
opacity: 1,
},
behavior: {
deactivateToolAfterCreate: false,
selectAfterCreate: true,
},
});
ensureTool({
id: 'squiggly',
name: 'Squiggly',
interaction: { exclusive: true, cursor: 'text', textSelection: true },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.SQUIGGLY,
strokeColor: '#00acc1',
color: '#00acc1',
opacity: 1,
},
behavior: {
deactivateToolAfterCreate: false,
selectAfterCreate: true,
},
});
ensureTool({
id: 'ink',
name: 'Pen',
interaction: { exclusive: true, cursor: 'crosshair' },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.INK,
strokeColor: '#1f2933',
color: '#1f2933',
opacity: 1,
borderWidth: 2,
lineWidth: 2,
strokeWidth: 2,
},
behavior: {
deactivateToolAfterCreate: false,
selectAfterCreate: true,
},
});
ensureTool({
id: 'inkHighlighter',
name: 'Ink Highlighter',
interaction: { exclusive: true, cursor: 'crosshair' },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.INK && (annotation.strokeColor === '#ffd54f' || annotation.color === '#ffd54f') ? 8 : 0),
defaults: {
type: PdfAnnotationSubtype.INK,
strokeColor: '#ffd54f',
color: '#ffd54f',
opacity: 0.5,
borderWidth: 6,
lineWidth: 6,
strokeWidth: 6,
},
behavior: {
deactivateToolAfterCreate: false,
selectAfterCreate: true,
},
});
ensureTool({
id: 'square',
name: 'Square',
interaction: { exclusive: true, cursor: 'crosshair' },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.SQUARE,
color: '#0000ff', // fill color (blue)
strokeColor: '#cf5b5b', // border color (reddish pink)
opacity: 0.5,
borderWidth: 1,
strokeWidth: 1,
lineWidth: 1,
},
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: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.CIRCLE,
color: '#0000ff', // fill color (blue)
strokeColor: '#cf5b5b', // border color (reddish pink)
opacity: 0.5,
borderWidth: 1,
strokeWidth: 1,
lineWidth: 1,
},
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: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.LINE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.LINE,
color: '#1565c0',
opacity: 1,
borderWidth: 2,
strokeWidth: 2,
lineWidth: 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: PdfAnnotationObject) => {
if (annotation.type !== PdfAnnotationSubtype.LINE) return 0;
// EmbedPDF stores endStyle/lineEndingStyles at runtime; library types use lineEndings
const ann = annotation as PdfAnnotationObject & { endStyle?: string; lineEndingStyles?: { end?: string } };
return (ann.endStyle === 'ClosedArrow' || ann.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: PdfAnnotationObject) => (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: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.POLYGON,
color: '#0000ff', // fill color (blue)
strokeColor: '#cf5b5b', // border color (reddish pink)
opacity: 0.5,
borderWidth: 1,
},
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', isRotatable: false },
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.FREETEXT,
textColor: '#111111',
fontSize: 14,
fontFamily: 'Helvetica',
opacity: 1,
interiorColor: '#fffef7',
contents: 'Text',
},
behavior: {
deactivateToolAfterCreate: true,
selectAfterCreate: true,
},
});
ensureTool({
id: 'note',
name: 'Note',
interaction: { exclusive: true, cursor: 'pointer', isRotatable: false },
matchScore: (annotation: PdfAnnotationObject) => (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: true,
selectAfterCreate: true,
},
});
ensureTool({
id: 'stamp',
name: 'Image Stamp',
interaction: { exclusive: false, cursor: 'copy' },
matchScore: (annotation: PdfAnnotationObject) => (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' },
matchScore: () => 0,
defaults: {
type: PdfAnnotationSubtype.STAMP,
},
});
ensureTool({
id: 'signatureInk',
name: 'Signature Draw',
interaction: { exclusive: true, cursor: 'crosshair' },
matchScore: () => 0,
defaults: {
type: PdfAnnotationSubtype.INK,
strokeColor: '#000000',
color: '#000000',
opacity: 1.0,
borderWidth: 2,
},
});
annotationApi.onAnnotationEvent((event: AnnotationEvent) => {
if (event.type === 'create' && event.committed) {
setAnnotations(prev => [...prev, {
id: event.annotation.id,
pageIndex: event.pageIndex,
rect: event.annotation.rect
}]);
if (onSignatureAdded) {
onSignatureAdded(event.annotation);
}
} else if (event.type === 'delete' && event.committed) {
setAnnotations(prev => prev.filter(ann => ann.id !== event.annotation.id));
}
});
}
}}
>
<ActiveDocumentProvider>
<ZoomAPIBridge />
<ScrollAPIBridge />
<SelectionAPIBridge />
<PanAPIBridge />
<SpreadAPIBridge />
<SearchAPIBridge />
<ThumbnailAPIBridge />
<RotateAPIBridge />
{(enableAnnotations || enableRedaction || isManualRedactionMode) && <HistoryAPIBridge ref={historyApiRef} />}
{/* Always render RedactionAPIBridge when in manual redaction mode so buttons can switch from annotation mode */}
{(enableRedaction || isManualRedactionMode) && <RedactionAPIBridge />}
{/* Always render SignatureAPIBridge so annotation tools (draw) can be activated even when starting in redaction mode */}
{(enableAnnotations || enableRedaction || isManualRedactionMode) && <SignatureAPIBridge ref={signatureApiRef} />}
{(enableRedaction || isManualRedactionMode) && <RedactionPendingTracker ref={redactionTrackerRef} />}
{enableAnnotations && <AnnotationAPIBridge ref={annotationApiRef} />}
<ExportAPIBridge />
<BookmarkAPIBridge />
<AttachmentAPIBridge />
<PrintAPIBridge
file={file}
url={pdfUrl}
fileName={fileName}
/>
<DocumentPermissionsAPIBridge />
<DocumentReadyWrapper
fallback={
<Center style={{ height: '100%', width: '100%' }}>
<ToolLoadingFallback />
</Center>
}
>
{(documentId) => (
<GlobalPointerProvider documentId={documentId}>
<Viewport
documentId={documentId}
style={{
backgroundColor: 'var(--bg-background)',
height: '100%',
width: '100%',
maxHeight: '100%',
maxWidth: '100%',
overflow: 'auto',
position: 'relative',
flex: 1,
minHeight: 0,
minWidth: 0,
contain: 'strict',
}}
>
<Scroller
documentId={documentId}
renderPage={({ width, height, pageIndex }) => {
return (
<Rotate key={`${documentId}-${pageIndex}`} documentId={documentId} pageIndex={pageIndex}>
<PagePointerProvider documentId={documentId} pageIndex={pageIndex}>
<div
data-page-index={pageIndex}
data-page-width={width}
data-page-height={height}
style={{
width,
height,
position: 'relative',
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}
onDragOver={(e) => e.preventDefault()}
>
<TilingLayer documentId={documentId} pageIndex={pageIndex} />
<CustomSearchLayer documentId={documentId} pageIndex={pageIndex} />
<div className="pdf-selection-layer" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
<SelectionLayer documentId={documentId} pageIndex={pageIndex} background="var(--pdf-selection-bg)" />
</div>
<TextSelectionHandler documentId={documentId} pageIndex={pageIndex} />
{/* FormFieldOverlay for interactive form filling */}
{enableFormFill && (
<FormFieldOverlay
documentId={documentId}
pageIndex={pageIndex}
pageWidth={width}
pageHeight={height}
fileId={fileId}
/>
)}
{/* AnnotationLayer for annotation editing and annotation-based redactions */}
{(enableAnnotations || enableRedaction) && (
<AnnotationLayer
documentId={documentId}
pageIndex={pageIndex}
selectionOutline={{ color: "#007ACC" }}
selectionMenu={(props) => <AnnotationSelectionMenu {...props} />}
style={!showBakedAnnotations ? { opacity: 0, pointerEvents: 'none' } : undefined}
/>
)}
{enableRedaction && (
<RedactionLayer
documentId={documentId}
pageIndex={pageIndex}
selectionMenu={(props) => <RedactionSelectionMenu {...props} />}
/>
)}
{/* LinkLayer uses EmbedPDF annotation state for link rendering */}
<LinkLayer
documentId={documentId}
pageIndex={pageIndex}
/>
</div>
</PagePointerProvider>
</Rotate>
);
}}
/>
</Viewport>
</GlobalPointerProvider>
)}
</DocumentReadyWrapper>
</ActiveDocumentProvider>
</EmbedPDF>
</div>
</PrivateContent>
);
}