This commit is contained in:
Reece 2025-12-06 11:31:05 +00:00
parent 9f54df290d
commit 346a05748e
12 changed files with 1146 additions and 291 deletions

View File

@ -19,9 +19,10 @@ public class ReactRoutingController {
@Value("${server.servlet.context-path:/}")
private String contextPath;
@GetMapping(value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request)
throws IOException {
@GetMapping(
value = {"/", "/index.html"},
produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request) throws IOException {
ClassPathResource resource = new ClassPathResource("static/index.html");
try (InputStream inputStream = resource.getInputStream()) {
@ -47,8 +48,7 @@ public class ReactRoutingController {
@GetMapping(
"/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}")
public ResponseEntity<String> forwardRootPaths(HttpServletRequest request)
throws IOException {
public ResponseEntity<String> forwardRootPaths(HttpServletRequest request) throws IOException {
return serveIndexHtml(request);
}

View File

@ -331,7 +331,9 @@ public class SecurityConfiguration {
formLogin ->
formLogin
.loginPage("/login") // Redirect here when unauthenticated
.loginProcessingUrl("/perform_login") // Process form posts here (not /login)
.loginProcessingUrl(
"/perform_login") // Process form posts here (not
// /login)
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService,

View File

@ -3911,6 +3911,7 @@ saveChanges = "Save Changes"
[annotation]
title = "Annotate"
desc = "Use highlight, pen, text, and notes. Changes stay live—no flattening required."
highlight = "Highlight"
pen = "Pen"
text = "Text box"
@ -3921,8 +3922,39 @@ select = "Select"
exit = "Exit annotation mode"
strokeWidth = "Width"
opacity = "Opacity"
strokeOpacity = "Stroke Opacity"
fillOpacity = "Fill Opacity"
fontSize = "Font size"
chooseColor = "Choose colour"
color = "Colour"
strokeColor = "Stroke Colour"
fillColor = "Fill Colour"
underline = "Underline"
strikeout = "Strikeout"
squiggly = "Squiggly"
inkHighlighter = "Ink Highlighter"
square = "Square"
circle = "Circle"
polygon = "Polygon"
line = "Line"
stamp = "Add Image"
textMarkup = "Text Markup"
drawing = "Drawing"
shapes = "Shapes"
notesStamps = "Notes & Stamps"
settings = "Settings"
borderOn = "Border: On"
borderOff = "Border: Off"
editInk = "Edit Pen"
editLine = "Edit Line"
editNote = "Edit Note"
editText = "Edit Text Box"
editTextMarkup = "Edit Text Markup"
editSelected = "Edit Annotation"
editSquare = "Edit Square"
editCircle = "Edit Circle"
editPolygon = "Edit Polygon"
unsupportedType = "This annotation type is not fully supported for editing."
[search]
title = "Search PDF"

View File

@ -12,6 +12,7 @@ import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions } from
import { RightRailProvider } from "@app/contexts/RightRailContext";
import { ViewerProvider } from "@app/contexts/ViewerContext";
import { SignatureProvider } from "@app/contexts/SignatureContext";
import { AnnotationProvider } from "@app/contexts/AnnotationContext";
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
import { PageEditorProvider } from "@app/contexts/PageEditorContext";
@ -93,13 +94,15 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
<ViewerProvider>
<PageEditorProvider>
<SignatureProvider>
<RightRailProvider>
<TourOrchestrationProvider>
<AdminTourOrchestrationProvider>
{children}
</AdminTourOrchestrationProvider>
</TourOrchestrationProvider>
</RightRailProvider>
<AnnotationProvider>
<RightRailProvider>
<TourOrchestrationProvider>
<AdminTourOrchestrationProvider>
{children}
</AdminTourOrchestrationProvider>
</TourOrchestrationProvider>
</RightRailProvider>
</AnnotationProvider>
</SignatureProvider>
</PageEditorProvider>
</ViewerProvider>

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch } from '@mantine/core';
import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch, Slider, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
interface ColorPickerProps {
@ -8,6 +8,10 @@ interface ColorPickerProps {
selectedColor: string;
onColorChange: (color: string) => void;
title?: string;
opacity?: number;
onOpacityChange?: (opacity: number) => void;
showOpacity?: boolean;
opacityLabel?: string;
}
export const ColorPicker: React.FC<ColorPickerProps> = ({
@ -15,10 +19,15 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
onClose,
selectedColor,
onColorChange,
title
title,
opacity,
onOpacityChange,
showOpacity = false,
opacityLabel,
}) => {
const { t } = useTranslation();
const resolvedTitle = title ?? t('colorPicker.title', 'Choose colour');
const resolvedOpacityLabel = opacityLabel ?? t('annotation.opacity', 'Opacity');
return (
<Modal
@ -38,6 +47,23 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
size="lg"
fullWidth
/>
{showOpacity && onOpacityChange && opacity !== undefined && (
<Stack gap="xs">
<Text size="sm" fw={500}>{resolvedOpacityLabel}</Text>
<Slider
min={10}
max={100}
value={opacity}
onChange={onOpacityChange}
marks={[
{ value: 25, label: '25%' },
{ value: 50, label: '50%' },
{ value: 75, label: '75%' },
{ value: 100, label: '100%' },
]}
/>
</Stack>
)}
<Group justify="flex-end">
<Button onClick={onClose}>
{t('common.done', 'Done')}

View File

@ -0,0 +1,196 @@
import { useImperativeHandle, forwardRef, useCallback } from 'react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { PdfAnnotationSubtype } from '@embedpdf/models';
import type { AnnotationToolId, AnnotationToolOptions, AnnotationAPI } from '@app/components/viewer/viewerTypes';
export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function AnnotationAPIBridge(_props, ref) {
const annotationApi = useAnnotationCapability();
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',
borderWidth: options?.thickness ?? 2,
strokeWidth: options?.thickness ?? 2,
lineWidth: options?.thickness ?? 2,
};
case 'inkHighlighter':
return {
type: PdfAnnotationSubtype.INK,
color: options?.color ?? '#ffd54f',
opacity: options?.opacity ?? 0.6,
borderWidth: options?.thickness ?? 6,
strokeWidth: options?.thickness ?? 6,
lineWidth: 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 ?? '#0000ff',
strokeColor: options?.strokeColor ?? '#cf5b5b',
opacity: options?.opacity ?? 0.5,
fillOpacity: options?.fillOpacity ?? 0.5,
strokeOpacity: options?.strokeOpacity ?? 0.5,
borderWidth: options?.borderWidth ?? 1,
strokeWidth: options?.borderWidth ?? 1,
lineWidth: options?.borderWidth ?? 1,
};
case 'circle':
return {
type: PdfAnnotationSubtype.CIRCLE,
color: options?.color ?? '#0000ff',
strokeColor: options?.strokeColor ?? '#cf5b5b',
opacity: options?.opacity ?? 0.5,
fillOpacity: options?.fillOpacity ?? 0.5,
strokeOpacity: options?.strokeOpacity ?? 0.5,
borderWidth: options?.borderWidth ?? 1,
strokeWidth: options?.borderWidth ?? 1,
lineWidth: options?.borderWidth ?? 1,
};
case 'line':
return {
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,
};
case 'lineArrow':
return {
type: PdfAnnotationSubtype.LINE,
color: options?.color ?? '#1565c0',
strokeColor: options?.color ?? '#1565c0',
opacity: options?.opacity ?? 1,
borderWidth: options?.borderWidth ?? 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?.borderWidth ?? 2,
};
case 'polygon':
return {
type: PdfAnnotationSubtype.POLYGON,
color: options?.color ?? '#0000ff',
strokeColor: options?.strokeColor ?? '#cf5b5b',
opacity: options?.opacity ?? 0.5,
fillOpacity: options?.fillOpacity ?? 0.5,
strokeOpacity: options?.strokeOpacity ?? 0.5,
borderWidth: options?.borderWidth ?? 1,
strokeWidth: options?.borderWidth ?? 1,
lineWidth: options?.borderWidth ?? 1,
};
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]
);
useImperativeHandle(ref, () => ({
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: () => {
const api = annotationApi as any;
return api?.getSelectedAnnotation?.() ?? null;
},
deselectAnnotation: () => {
const api = annotationApi as any;
api?.deselectAnnotation?.();
},
updateAnnotation: (pageIndex: number, annotationId: string, patch: Partial<any>) => {
const api = annotationApi as any;
api?.updateAnnotation?.(pageIndex, annotationId, patch);
},
deactivateTools: () => {
if (!annotationApi) return;
const api = annotationApi as any;
api?.setActiveTool?.(null);
},
}), [annotationApi, configureAnnotationTool, buildAnnotationDefaults]);
return null;
});

View File

@ -11,6 +11,7 @@ import { ThumbnailSidebar } from '@app/components/viewer/ThumbnailSidebar';
import { BookmarkSidebar } from '@app/components/viewer/BookmarkSidebar';
import { useNavigationGuard, useNavigationState } from '@app/contexts/NavigationContext';
import { useSignature } from '@app/contexts/SignatureContext';
import { useAnnotation } from '@app/contexts/AnnotationContext';
import { createStirlingFilesAndStubs } from '@app/services/fileStubHelpers';
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
import { isStirlingFile } from '@app/types/fileContext';
@ -67,8 +68,9 @@ const EmbedPdfViewerContent = ({
}
}, [rotationState.rotation]);
// Get signature context
// Get signature and annotation contexts
const { signatureApiRef, historyApiRef, signatureConfig, isPlacementMode } = useSignature();
const { annotationApiRef } = useAnnotation();
// Get current file from FileContext
const { selectors, state } = useFileState();
@ -324,6 +326,7 @@ const EmbedPdfViewerContent = ({
url={effectiveFile.url}
enableAnnotations={shouldEnableAnnotations}
signatureApiRef={signatureApiRef as React.RefObject<any>}
annotationApiRef={annotationApiRef as React.RefObject<any>}
historyApiRef={historyApiRef as React.RefObject<any>}
onSignatureAdded={() => {
// Handle signature added - for debugging, enable console logs as needed

View File

@ -38,8 +38,9 @@ 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, HistoryAPI } from '@app/components/viewer/viewerTypes';
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 { PrintAPIBridge } from '@app/components/viewer/PrintAPIBridge';
@ -53,10 +54,11 @@ interface LocalEmbedPDFProps {
enableAnnotations?: boolean;
onSignatureAdded?: (annotation: any) => void;
signatureApiRef?: React.RefObject<SignatureAPI>;
annotationApiRef?: React.RefObject<AnnotationAPI>;
historyApiRef?: React.RefObject<HistoryAPI>;
}
export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef }: LocalEmbedPDFProps) {
const { t } = useTranslation();
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
@ -329,6 +331,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
color: '#1f2933',
opacity: 1,
borderWidth: 2,
lineWidth: 2,
strokeWidth: 2,
},
behavior: {
deactivateToolAfterCreate: false,
@ -346,6 +350,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
color: '#ffd54f',
opacity: 0.5,
borderWidth: 6,
lineWidth: 6,
strokeWidth: 6,
},
behavior: {
deactivateToolAfterCreate: false,
@ -360,10 +366,12 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.SQUARE,
color: '#1565c0',
interiorColor: '#e3f2fd',
opacity: 0.35,
borderWidth: 2,
color: '#0000ff', // fill color (blue)
strokeColor: '#cf5b5b', // border color (reddish pink)
opacity: 0.5,
borderWidth: 1,
strokeWidth: 1,
lineWidth: 1,
},
clickBehavior: {
enabled: true,
@ -382,10 +390,12 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.CIRCLE,
color: '#1565c0',
interiorColor: '#e3f2fd',
opacity: 0.35,
borderWidth: 2,
color: '#0000ff', // fill color (blue)
strokeColor: '#cf5b5b', // border color (reddish pink)
opacity: 0.5,
borderWidth: 1,
strokeWidth: 1,
lineWidth: 1,
},
clickBehavior: {
enabled: true,
@ -407,6 +417,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
color: '#1565c0',
opacity: 1,
borderWidth: 2,
strokeWidth: 2,
lineWidth: 2,
},
clickBehavior: {
enabled: true,
@ -472,10 +484,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.POLYGON,
color: '#1565c0',
interiorColor: '#e3f2fd',
opacity: 0.35,
borderWidth: 2,
color: '#0000ff', // fill color (blue)
strokeColor: '#cf5b5b', // border color (reddish pink)
opacity: 0.5,
borderWidth: 1,
},
clickBehavior: {
enabled: true,
@ -604,6 +616,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
<ThumbnailAPIBridge />
<RotateAPIBridge />
{enableAnnotations && <SignatureAPIBridge ref={signatureApiRef} />}
{enableAnnotations && <AnnotationAPIBridge ref={annotationApiRef} />}
{enableAnnotations && <HistoryAPIBridge ref={historyApiRef} />}
<ExportAPIBridge />
<BookmarkAPIBridge />

View File

@ -533,6 +533,10 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
getSelectedAnnotation: () => {
return annotationApi?.getSelectedAnnotation?.() ?? null;
},
deselectAnnotation: () => {
const api = annotationApi as any;
api?.deselectAnnotation?.();
},
updateAnnotation: (pageIndex: number, annotationId: string, patch: Partial<any>) => {
annotationApi?.updateAnnotation?.(pageIndex, annotationId, patch);
},

View File

@ -17,9 +17,19 @@ export interface SignatureAPI {
activateAnnotationTool?: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => void;
setAnnotationStyle?: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => void;
getSelectedAnnotation?: () => any | null;
deselectAnnotation?: () => void;
updateAnnotation?: (pageIndex: number, annotationId: string, patch: Partial<any>) => void;
}
export interface AnnotationAPI {
activateAnnotationTool: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => void;
setAnnotationStyle: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => void;
getSelectedAnnotation: () => any | null;
deselectAnnotation: () => void;
updateAnnotation: (pageIndex: number, annotationId: string, patch: Partial<any>) => void;
deactivateTools: () => void;
}
export interface HistoryAPI {
undo: () => void;
redo: () => void;
@ -52,6 +62,8 @@ export interface AnnotationToolOptions {
color?: string;
fillColor?: string;
opacity?: number;
strokeOpacity?: number;
fillOpacity?: number;
thickness?: number;
fontSize?: number;
fontFamily?: string;

View File

@ -0,0 +1,26 @@
import React, { createContext, useContext, ReactNode, useRef } from 'react';
import type { AnnotationAPI } from '@app/components/viewer/viewerTypes';
interface AnnotationContextValue {
annotationApiRef: React.RefObject<AnnotationAPI | null>;
}
const AnnotationContext = createContext<AnnotationContextValue | undefined>(undefined);
export const AnnotationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const annotationApiRef = useRef<AnnotationAPI>(null);
const value: AnnotationContextValue = {
annotationApiRef,
};
return <AnnotationContext.Provider value={value}>{children}</AnnotationContext.Provider>;
};
export const useAnnotation = (): AnnotationContextValue => {
const context = useContext(AnnotationContext);
if (!context) {
throw new Error('useAnnotation must be used within an AnnotationProvider');
}
return context;
};

File diff suppressed because it is too large Load Diff