Compare commits

...

3 Commits

Author SHA1 Message Date
Anthony Stirling
464ab0dee0
Merge branch 'V2' into booklet 2025-09-22 14:47:42 +01:00
Anthony Stirling
68ccb0f970 formats and renames 2025-09-22 14:47:28 +01:00
James Brunton
c76edebf0f
Add Crop to V2 (#4471)
# Description of Changes
Add Crop to V2

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-09-22 14:06:20 +01:00
29 changed files with 1324 additions and 368 deletions

View File

@ -18,7 +18,9 @@ public class PDFFile {
@Schema(description = "The input PDF file", format = "binary")
private MultipartFile fileInput;
@Schema(description = "File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
@Schema(
description =
"File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
private String fileId;
@AssertTrue(message = "Either fileInput or fileId must be provided")

View File

@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.common.annotations.api.ConfigApi;
@ -18,7 +17,6 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.ServerCertificateServiceInterface;
@ConfigApi
@RequiredArgsConstructor
@Hidden
public class ConfigController {

View File

@ -427,9 +427,9 @@
"title": "Flatten",
"desc": "Remove all interactive elements and forms from a PDF"
},
"manageSignatures": {
"title": "Sign with Certificate",
"desc": "Add digital signatures to PDF documents using certificates"
"certSign": {
"title": "Sign with Certificate",
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
},
"repair": {
"title": "Repair",
@ -447,14 +447,6 @@
"title": "Compare",
"desc": "Compares and shows the differences between 2 PDF Documents"
},
"certSign": {
"title": "Sign with Certificate",
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
},
"manageSignatures": {
"title": "Manage Signatures",
"desc": "Sign PDFs with certificates using manual or server-managed keys"
},
"removeCertSign": {
"title": "Remove Certificate Sign",
"desc": "Remove certificate signature from PDF"
@ -1191,7 +1183,9 @@
},
"pageSelection": {
"tooltip": {
"header": { "title": "Page Selection Guide" },
"header": {
"title": "Page Selection Guide"
},
"basic": {
"title": "Basic Usage",
"text": "Select specific pages from your PDF document using simple syntax.",
@ -1225,11 +1219,15 @@
"comma": "Comma: , or | — combine selections (e.g., 1-10, 20)",
"not": "NOT: ! or \"not\" — exclude pages (e.g., 3n & not 30)"
},
"examples": { "title": "Examples" }
"examples": {
"title": "Examples"
}
}
},
"bulkSelection": {
"header": { "title": "Page Selection Guide" },
"header": {
"title": "Page Selection Guide"
},
"syntax": {
"title": "Syntax Basics",
"text": "Use numbers, ranges, keywords, and progressions (n starts at 0). Parentheses are supported.",
@ -1781,67 +1779,11 @@
}
},
"certSign": {
"tags": "authenticate,PEM,P12,official,encrypt",
"tags": "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto",
"title": "Certificate Signing",
"header": "Sign a PDF with your certificate (Work in progress)",
"selectPDF": "Select a PDF File for Signing:",
"jksNote": "Note: If your certificate type is not listed below, please convert it to a Java Keystore (.jks) file using the keytool command line tool. Then, choose the .jks file option below.",
"selectKey": "Select Your Private Key File (PKCS#8 format, could be .pem or .der):",
"selectCert": "Select Your Certificate File (X.509 format, could be .pem or .der):",
"selectP12": "Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate):",
"selectJKS": "Select Your Java Keystore File (.jks or .keystore):",
"certType": "Certificate Type",
"password": "Enter Your Keystore or Private Key Password (If Any):",
"showSig": "Show Signature",
"reason": "Reason",
"location": "Location",
"name": "Name",
"showLogo": "Show Logo",
"submit": "Sign PDF",
"files": {
"placeholder": "Select PDF files to sign with certificates"
},
"signMode": {
"stepTitle": "Sign Mode"
},
"certTypeStep": {
"stepTitle": "Certificate Format"
},
"certFiles": {
"stepTitle": "Certificate Files"
},
"appearance": {
"stepTitle": "Signature Appearance",
"title": "Signature Appearance",
"invisible": "Invisible",
"visible": "Visible",
"options": {
"title": "Signature Details"
}
},
"sign": {
"submit": "Sign PDF",
"results": "Signed PDF"
},
"error": {
"failed": "An error occurred whilst signing the PDF."
},
"choosePrivateKey": "Choose Private Key File",
"chooseCertificate": "Choose Certificate File",
"chooseP12File": "Choose PKCS12 File",
"choosePfxFile": "Choose PFX File",
"chooseJksFile": "Choose JKS File",
"passwordOptional": "Leave empty if no password",
"serverCertMessage": "Using server certificate - no files or password required",
"pageNumber": "Page Number",
"logoTitle": "Logo",
"noLogo": "No Logo"
},
"manageSignatures": {
"tags": "sign,certificate,PEM,PKCS12,JKS,server,manual,auto",
"title": "Manage Signatures",
"desc": "Sign PDFs with certificates using manual or server-managed keys",
"filenamePrefix": "signed",
"signMode": {
"stepTitle": "Sign Mode",
"tooltip": {
"header": {
"title": "About PDF Signatures"
@ -1866,6 +1808,72 @@
}
}
},
"certTypeStep": {
"stepTitle": "Certificate Format"
},
"certFiles": {
"stepTitle": "Certificate Files"
},
"appearance": {
"stepTitle": "Signature Appearance",
"tooltip": {
"header": {
"title": "About Signature Appearance"
},
"invisible": {
"title": "Invisible Signatures",
"text": "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance.",
"bullet1": "Provides security without visual changes",
"bullet2": "Meets legal requirements for digital signing",
"bullet3": "Doesn't affect document layout or design"
},
"visible": {
"title": "Visible Signatures",
"text": "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed.",
"bullet1": "Shows signer name and date on the document",
"bullet2": "Can include reason and location for signing",
"bullet3": "Choose which page to place the signature",
"bullet4": "Optional logo can be included"
}
}
},
"sign": {
"submit": "Sign PDF",
"results": "Signed PDF"
},
"error": {
"failed": "An error occurred whilst processing signatures."
},
"tooltip": {
"header": {
"title": "About Managing Signatures"
},
"overview": {
"title": "What can this tool do?",
"text": "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing.",
"bullet1": "Check existing signatures and their validity",
"bullet2": "View detailed information about signers and certificates",
"bullet3": "Add new digital signatures to secure your documents",
"bullet4": "Multiple files supported with easy navigation"
},
"validation": {
"title": "Checking Signatures",
"text": "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing.",
"bullet1": "Shows if signatures are valid or invalid",
"bullet2": "Displays signer information and signing date",
"bullet3": "Checks if the document was modified after signing",
"bullet4": "Can use custom certificates for verification"
},
"signing": {
"title": "Adding Signatures",
"text": "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only.",
"bullet1": "Supports PEM, PKCS12, JKS, and server certificate formats",
"bullet2": "Option to show or hide signature on the PDF",
"bullet3": "Add reason, location, and signer name",
"bullet4": "Choose which page to place visible signatures",
"bullet5": "Use server certificate for simple 'Sign with Stirling-PDF' option"
}
},
"certType": {
"tooltip": {
"header": {
@ -1878,7 +1886,7 @@
"which": {
"title": "Which option should I use?",
"text": "Choose the format that matches your certificate file:",
"bullet1": "PKCS12 (.p12) one combined file (most common)",
"bullet1": "PKCS#12 (.p12 / .pfx) one combined file (most common)",
"bullet2": "PFX (.pfx) Microsoft's version of PKCS12",
"bullet3": "PEM separate private-key and certificate .pem files",
"bullet4": "JKS Java .jks keystore for dev / CI-CD workflows"
@ -2093,7 +2101,36 @@
"tags": "trim,shrink,edit,shape",
"title": "Crop",
"header": "Crop PDF",
"submit": "Submit"
"submit": "Apply Crop",
"noFileSelected": "Select a PDF file to begin cropping",
"preview": {
"title": "Crop Area Selection"
},
"reset": "Reset to full PDF",
"coordinates": {
"title": "Position and Size",
"x": "X Position",
"y": "Y Position",
"width": "Width",
"height": "Height"
},
"error": {
"invalidArea": "Crop area extends beyond PDF boundaries",
"failed": "Failed to crop PDF"
},
"steps": {
"selectArea": "Select Crop Area"
},
"tooltip": {
"title": "How to Crop PDFs",
"description": "Select the area to crop from your PDF by dragging and resizing the blue overlay on the thumbnail.",
"drag": "Drag the overlay to move the crop area",
"resize": "Drag the corner and edge handles to resize",
"precision": "Use coordinate inputs for precise positioning"
},
"results": {
"title": "Crop Results"
}
},
"autoSplitPDF": {
"tags": "QR-based,separate,scan-segment,organize",
@ -2733,20 +2770,14 @@
"actualSize": "Actual Size"
},
"viewer": {
"noPdfLoaded": "No PDF loaded. Click to upload a PDF.",
"choosePdf": "Choose PDF",
"noPagesToDisplay": "No pages to display.",
"singlePageView": "Single Page View",
"dualPageView": "Dual Page View",
"hideSidebars": "Hide Sidebars",
"showSidebars": "Show Sidebars",
"zoomOut": "Zoom out",
"zoomIn": "Zoom in",
"firstPage": "First Page",
"lastPage": "Last Page",
"previousPage": "Previous Page",
"nextPage": "Next Page",
"pageNavigation": "Page Navigation",
"currentPage": "Current Page",
"totalPages": "Total Pages"
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"singlePageView": "Single Page View",
"dualPageView": "Dual Page View"
},
"rightRail": {
"closeSelected": "Close Selected Files",
@ -3130,156 +3161,5 @@
"processImages": "Process Images",
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
}
},
"manageSignatures": {
"title": "Sign with Certificate",
"filenamePrefix": "signed",
"files": {
"placeholder": "Select PDF files to sign with certificates"
},
"fileStatus": {
"stepTitle": "File Status"
},
"fileNavigation": "File {{current}} of {{total}}",
"hasSignatures": "Contains {{count}} signature(s)",
"noSignatures": "No signatures detected",
"signed": "Signed",
"certType": {
"stepTitle": "Certificate Type",
"tooltip": {
"header": {
"title": "About Certificate Types"
},
"what": {
"title": "What's a certificate?",
"text": "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload."
},
"which": {
"title": "Which option should I use?",
"text": "Choose the format that matches your certificate file:",
"bullet1": "PKCS#12 (.p12 / .pfx) one combined file (most common)",
"bullet2": "PEM separate private-key and certificate .pem files",
"bullet3": "JKS Java .jks keystore for dev / CI-CD workflows"
},
"convert": {
"title": "Key not listed?",
"text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS."
}
}
},
"certFiles": {
"stepTitle": "Certificate Files"
},
"appearance": {
"stepTitle": "Signature Appearance",
"title": "Signature Appearance",
"invisible": "Invisible",
"visible": "Visible",
"options": {
"title": "Signature Details"
},
"tooltip": {
"header": {
"title": "About Signature Appearance"
},
"invisible": {
"title": "Invisible Signatures",
"text": "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance.",
"bullet1": "Provides security without visual changes",
"bullet2": "Meets legal requirements for digital signing",
"bullet3": "Doesn't affect document layout or design"
},
"visible": {
"title": "Visible Signatures",
"text": "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed.",
"bullet1": "Shows signer name and date on the document",
"bullet2": "Can include reason and location for signing",
"bullet3": "Choose which page to place the signature",
"bullet4": "Optional logo can be included"
}
}
},
"mode": {
"title": "Action",
"validate": "Check for Signatures",
"viewEdit": "View/Edit Signatures",
"sign": "Add New Signature"
},
"validation": {
"title": "Validation Options",
"customCert": "Custom Certificate (Optional)",
"customCert.desc": "Upload a custom certificate for validation"
},
"signing": {
"title": "Certificate Settings",
"certType": "Certificate Type",
"choosePrivateKey": "Choose Private Key File",
"chooseCertificate": "Choose Certificate File",
"chooseP12File": "Choose PKCS12 File",
"chooseJksFile": "Choose JKS File",
"password": "Certificate Password",
"passwordOptional": "Leave empty if no password",
"showSignature": "Show visible signature on PDF",
"reason": "Reason for Signing",
"location": "Location",
"name": "Signer Name",
"pageNumber": "Page Number",
"logoTitle": "Logo",
"noLogo": "No Logo",
"showLogo": "Show Logo"
},
"validate": {
"submit": "Validate Signatures",
"results": "Signature Validation Results"
},
"sign": {
"submit": "Sign PDF",
"results": "Signed PDF"
},
"results": {
"title": "Signature Results"
},
"error": {
"failed": "An error occurred whilst processing signatures."
},
"tooltip": {
"header": {
"title": "About Managing Signatures"
},
"overview": {
"title": "What can this tool do?",
"text": "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing.",
"bullet1": "Check existing signatures and their validity",
"bullet2": "View detailed information about signers and certificates",
"bullet3": "Add new digital signatures to secure your documents",
"bullet4": "Multiple files supported with easy navigation"
},
"validation": {
"title": "Checking Signatures",
"text": "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing.",
"bullet1": "Shows if signatures are valid or invalid",
"bullet2": "Displays signer information and signing date",
"bullet3": "Checks if the document was modified after signing",
"bullet4": "Can use custom certificates for verification"
},
"signing": {
"title": "Adding Signatures",
"text": "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only.",
"bullet1": "Supports PEM, PKCS12, and JKS certificate formats",
"bullet2": "Option to show or hide signature on the PDF",
"bullet3": "Add reason, location, and signer name",
"bullet4": "Choose which page to place visible signatures"
}
}
},
"viewer": {
"firstPage": "First Page",
"lastPage": "Last Page",
"previousPage": "Previous Page",
"nextPage": "Next Page",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"singlePageView": "Single Page View",
"dualPageView": "Dual Page View"
}
}

View File

@ -1,11 +1,11 @@
import { Stack, Text, TextInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
import FileUploadButton from "../../shared/FileUploadButton";
interface CertificateFilesSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
disabled?: boolean;
}

View File

@ -1,9 +1,9 @@
import { Stack, Button } from "@mantine/core";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
interface CertificateFormatSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
disabled?: boolean;
}

View File

@ -1,10 +1,10 @@
import { Stack, Button } from "@mantine/core";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
import { useAppConfig } from "../../../hooks/useAppConfig";
interface CertificateTypeSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
disabled?: boolean;
}

View File

@ -1,10 +1,10 @@
import { Stack, Text, Button, TextInput, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
interface SignatureAppearanceSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
parameters: CertSignParameters;
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
disabled?: boolean;
}

View File

@ -0,0 +1,300 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { Box, useMantineTheme, MantineTheme } from '@mantine/core';
import {
PDFBounds,
Rectangle,
domToPDFCoordinates,
pdfToDOMCoordinates,
constrainDOMRectToThumbnail,
isPointInThumbnail
} from '../../../utils/cropCoordinates';
import { type ResizeHandle } from '../../../constants/cropConstants';
interface CropAreaSelectorProps {
/** PDF bounds for coordinate conversion */
pdfBounds: PDFBounds;
/** Current crop area in PDF coordinates */
cropArea: Rectangle;
/** Callback when crop area changes */
onCropAreaChange: (cropArea: Rectangle) => void;
/** Whether the selector is disabled */
disabled?: boolean;
/** Child content (typically the PDF thumbnail) */
children: React.ReactNode;
}
const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
pdfBounds,
cropArea,
onCropAreaChange,
disabled = false,
children
}) => {
const theme = useMantineTheme();
const containerRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState<ResizeHandle>(null);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const [initialCropArea, setInitialCropArea] = useState<Rectangle>(cropArea);
// Convert PDF crop area to DOM coordinates for display
const domRect = pdfToDOMCoordinates(cropArea, pdfBounds);
// Handle mouse down on overlay (start dragging or resizing)
const handleOverlayMouseDown = useCallback((e: React.MouseEvent) => {
if (disabled || !containerRef.current) return;
e.preventDefault();
e.stopPropagation();
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check if we're clicking on a resize handle first (higher priority)
const handle = getResizeHandle(x, y, domRect);
if (handle) {
setIsResizing(handle);
setInitialCropArea(cropArea);
setIsDragging(false); // Ensure we're not dragging when resizing
} else if (isPointInCropArea(x, y, domRect)) {
// Only allow dragging if we're not on a resize handle
setIsDragging(true);
setIsResizing(null); // Ensure we're not resizing when dragging
setDragStart({ x: x - domRect.x, y: y - domRect.y });
}
}, [disabled, cropArea, domRect]);
// Handle mouse down on container (start new selection)
const handleContainerMouseDown = useCallback((e: React.MouseEvent) => {
if (disabled || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Only start new selection if clicking within thumbnail area
if (!isPointInThumbnail(x, y, pdfBounds)) return;
e.preventDefault();
e.stopPropagation();
// Start new crop selection
const newDomRect: Rectangle = { x, y, width: 20, height: 20 };
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
onCropAreaChange(newCropArea);
setIsResizing('se'); // Start resizing from the southeast corner
setInitialCropArea(newCropArea);
}, [disabled, pdfBounds, onCropAreaChange]);
// Handle mouse move
const handleMouseMove = useCallback((e: MouseEvent) => {
if (disabled || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (isDragging) {
// Dragging the entire crop area
const newX = x - dragStart.x;
const newY = y - dragStart.y;
const newDomRect: Rectangle = {
x: newX,
y: newY,
width: domRect.width,
height: domRect.height
};
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
onCropAreaChange(newCropArea);
} else if (isResizing) {
// Resizing the crop area
const newDomRect = calculateResizedRect(isResizing, domRect, x, y);
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
onCropAreaChange(newCropArea);
}
}, [disabled, isDragging, isResizing, dragStart, domRect, initialCropArea, pdfBounds, onCropAreaChange]);
// Handle mouse up
const handleMouseUp = useCallback(() => {
setIsDragging(false);
setIsResizing(null);
}, []);
// Add global mouse event listeners
useEffect(() => {
if (isDragging || isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
return (
<Box
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: '100%',
cursor: 'crosshair',
userSelect: 'none'
}}
onMouseDown={handleContainerMouseDown}
>
{/* PDF Thumbnail Content */}
{children}
{/* Crop Area Overlay */}
{!disabled && (
<Box
ref={overlayRef}
style={{
position: 'absolute',
left: domRect.x,
top: domRect.y,
width: domRect.width,
height: domRect.height,
border: `2px solid ${theme.other.crop.overlayBorder}`,
backgroundColor: theme.other.crop.overlayBackground,
cursor: 'move',
pointerEvents: 'auto'
}}
onMouseDown={handleOverlayMouseDown}
>
{/* Resize Handles */}
{renderResizeHandles(disabled, theme)}
</Box>
)}
</Box>
);
};
// Helper functions
function getResizeHandle(x: number, y: number, domRect: Rectangle): ResizeHandle {
const handleSize = 8;
const tolerance = handleSize;
// Corner handles (check these first, they have priority)
if (isNear(x, domRect.x, tolerance) && isNear(y, domRect.y, tolerance)) return 'nw';
if (isNear(x, domRect.x + domRect.width, tolerance) && isNear(y, domRect.y, tolerance)) return 'ne';
if (isNear(x, domRect.x, tolerance) && isNear(y, domRect.y + domRect.height, tolerance)) return 'sw';
if (isNear(x, domRect.x + domRect.width, tolerance) && isNear(y, domRect.y + domRect.height, tolerance)) return 'se';
// Edge handles (only if not in corner area)
if (isNear(x, domRect.x + domRect.width / 2, tolerance) && isNear(y, domRect.y, tolerance)) return 'n';
if (isNear(x, domRect.x + domRect.width, tolerance) && isNear(y, domRect.y + domRect.height / 2, tolerance)) return 'e';
if (isNear(x, domRect.x + domRect.width / 2, tolerance) && isNear(y, domRect.y + domRect.height, tolerance)) return 's';
if (isNear(x, domRect.x, tolerance) && isNear(y, domRect.y + domRect.height / 2, tolerance)) return 'w';
return null;
}
function isNear(a: number, b: number, tolerance: number): boolean {
return Math.abs(a - b) <= tolerance;
}
function isPointInCropArea(x: number, y: number, domRect: Rectangle): boolean {
return x >= domRect.x && x <= domRect.x + domRect.width &&
y >= domRect.y && y <= domRect.y + domRect.height;
}
function calculateResizedRect(
handle: ResizeHandle,
currentRect: Rectangle,
mouseX: number,
mouseY: number,
): Rectangle {
let { x, y, width, height } = currentRect;
switch (handle) {
case 'nw':
width += x - mouseX;
height += y - mouseY;
x = mouseX;
y = mouseY;
break;
case 'ne':
width = mouseX - x;
height += y - mouseY;
y = mouseY;
break;
case 'sw':
width += x - mouseX;
height = mouseY - y;
x = mouseX;
break;
case 'se':
width = mouseX - x;
height = mouseY - y;
break;
case 'n':
height += y - mouseY;
y = mouseY;
break;
case 'e':
width = mouseX - x;
break;
case 's':
height = mouseY - y;
break;
case 'w':
width += x - mouseX;
x = mouseX;
break;
}
// Enforce minimum size
width = Math.max(10, width);
height = Math.max(10, height);
return { x, y, width, height };
}
function renderResizeHandles(disabled: boolean, theme: MantineTheme) {
if (disabled) return null;
const handleSize = 8;
const handleStyle = {
position: 'absolute' as const,
width: handleSize,
height: handleSize,
backgroundColor: theme.other.crop.handleColor,
border: `1px solid ${theme.other.crop.handleBorder}`,
borderRadius: '2px',
pointerEvents: 'auto' as const
};
return (
<>
{/* Corner handles */}
<Box style={{ ...handleStyle, left: -handleSize/2, top: -handleSize/2, cursor: 'nw-resize' }} />
<Box style={{ ...handleStyle, right: -handleSize/2, top: -handleSize/2, cursor: 'ne-resize' }} />
<Box style={{ ...handleStyle, left: -handleSize/2, bottom: -handleSize/2, cursor: 'sw-resize' }} />
<Box style={{ ...handleStyle, right: -handleSize/2, bottom: -handleSize/2, cursor: 'se-resize' }} />
{/* Edge handles */}
<Box style={{ ...handleStyle, left: '50%', marginLeft: -handleSize/2, top: -handleSize/2, cursor: 'n-resize' }} />
<Box style={{ ...handleStyle, right: -handleSize/2, top: '50%', marginTop: -handleSize/2, cursor: 'e-resize' }} />
<Box style={{ ...handleStyle, left: '50%', marginLeft: -handleSize/2, bottom: -handleSize/2, cursor: 's-resize' }} />
<Box style={{ ...handleStyle, left: -handleSize/2, top: '50%', marginTop: -handleSize/2, cursor: 'w-resize' }} />
</>
);
}
export default CropAreaSelector;

View File

@ -0,0 +1,262 @@
import { useMemo, useState, useEffect } from "react";
import { Stack, Text, Box, Group, NumberInput, ActionIcon, Center, Alert } from "@mantine/core";
import { useTranslation } from "react-i18next";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
import CropAreaSelector from "./CropAreaSelector";
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
import {
calculatePDFBounds,
PDFBounds,
Rectangle
} from "../../../utils/cropCoordinates";
import { pdfWorkerManager } from "../../../services/pdfWorkerManager";
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
interface CropSettingsProps {
parameters: CropParametersHook;
disabled?: boolean;
}
const CONTAINER_SIZE = 250; // Fit within actual pane width
const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
const { t } = useTranslation();
const { selectedFiles, selectedFileStubs } = useSelectedFiles();
// Get the first selected file for preview
const selectedStub = useMemo(() => {
return selectedFileStubs.length > 0 ? selectedFileStubs[0] : null;
}, [selectedFileStubs]);
// Get the first selected file for PDF processing
const selectedFile = useMemo(() => {
return selectedFiles.length > 0 ? selectedFiles[0] : null;
}, [selectedFiles]);
// Get thumbnail for the selected file
const [thumbnail, setThumbnail] = useState<string | null>(null);
const [pdfBounds, setPdfBounds] = useState<PDFBounds | null>(null);
useEffect(() => {
const loadPDFDimensions = async () => {
if (!selectedStub || !selectedFile) {
setPdfBounds(null);
setThumbnail(null);
return;
}
setThumbnail(selectedStub.thumbnailUrl || null);
try {
// Get PDF dimensions from the actual file
const arrayBuffer = await selectedFile.arrayBuffer();
// Load PDF to get actual dimensions
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, {
disableAutoFetch: true,
disableStream: true,
stopAtErrors: false
});
const firstPage = await pdf.getPage(1);
const viewport = firstPage.getViewport({ scale: 1 });
const pdfWidth = viewport.width;
const pdfHeight = viewport.height;
const bounds = calculatePDFBounds(pdfWidth, pdfHeight, CONTAINER_SIZE, CONTAINER_SIZE);
setPdfBounds(bounds);
// Initialize crop area to full PDF if parameters are still default
if (parameters.parameters.cropArea === DEFAULT_CROP_AREA) {
parameters.resetToFullPDF(bounds);
}
// Cleanup PDF
pdfWorkerManager.destroyDocument(pdf);
} catch (error) {
console.error('Failed to load PDF dimensions:', error);
// Fallback to A4 dimensions if PDF loading fails
const bounds = calculatePDFBounds(PAGE_SIZES.A4.width, PAGE_SIZES.A4.height, CONTAINER_SIZE, CONTAINER_SIZE);
setPdfBounds(bounds);
if (parameters.parameters.cropArea.width === PAGE_SIZES.A4.width && parameters.parameters.cropArea.height === PAGE_SIZES.A4.height) {
parameters.resetToFullPDF(bounds);
}
}
};
loadPDFDimensions();
}, [selectedStub, selectedFile, parameters]);
// Current crop area
const cropArea = parameters.getCropArea();
// Handle crop area changes from the selector
const handleCropAreaChange = (newCropArea: Rectangle) => {
if (pdfBounds) {
parameters.setCropArea(newCropArea, pdfBounds);
}
};
// Handle manual coordinate input changes
const handleCoordinateChange = (field: keyof Rectangle, value: number | string) => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
if (isNaN(numValue)) return;
const newCropArea = { ...cropArea, [field]: numValue };
if (pdfBounds) {
parameters.setCropArea(newCropArea, pdfBounds);
}
};
// Reset to full PDF
const handleReset = () => {
if (pdfBounds) {
parameters.resetToFullPDF(pdfBounds);
}
};
if (!selectedStub || !pdfBounds) {
return (
<Center style={{ height: '200px' }}>
<Text color="dimmed">
{t("crop.noFileSelected", "Select a PDF file to begin cropping")}
</Text>
</Center>
);
}
const isCropValid = parameters.isCropAreaValid(pdfBounds);
const isFullCrop = parameters.isFullPDFCrop(pdfBounds);
return (
<Stack gap="md">
{/* PDF Preview with Crop Selector */}
<Stack gap="xs">
<Group justify="space-between" align="center">
<Text size="sm" fw={500}>
{t("crop.preview.title", "Crop Area Selection")}
</Text>
<ActionIcon
variant="outline"
onClick={handleReset}
disabled={disabled || isFullCrop}
title={t("crop.reset", "Reset to full PDF")}
aria-label={t("crop.reset", "Reset to full PDF")}
>
<RestartAltIcon style={{ fontSize: '1rem' }} />
</ActionIcon>
</Group>
<Center>
<Box
style={{
width: CONTAINER_SIZE,
height: CONTAINER_SIZE,
border: '1px solid var(--mantine-color-gray-3)',
borderRadius: '8px',
backgroundColor: 'var(--mantine-color-gray-0)',
overflow: 'hidden',
position: 'relative'
}}
>
<CropAreaSelector
pdfBounds={pdfBounds}
cropArea={cropArea}
onCropAreaChange={handleCropAreaChange}
disabled={disabled}
>
<DocumentThumbnail
file={selectedStub}
thumbnail={thumbnail}
style={{
width: pdfBounds.thumbnailWidth,
height: pdfBounds.thumbnailHeight,
position: 'absolute',
left: pdfBounds.offsetX,
top: pdfBounds.offsetY
}}
/>
</CropAreaSelector>
</Box>
</Center>
</Stack>
{/* Manual Coordinate Input */}
<Stack gap="xs">
<Text size="sm" fw={500}>
{t("crop.coordinates.title", "Position and Size")}
</Text>
<Group grow>
<NumberInput
label={t("crop.coordinates.x", "X Position")}
value={Math.round(cropArea.x * 10) / 10}
onChange={(value) => handleCoordinateChange('x', value)}
disabled={disabled}
min={0}
max={pdfBounds.actualWidth}
step={0.1}
decimalScale={1}
size="xs"
/>
<NumberInput
label={t("crop.coordinates.y", "Y Position")}
value={Math.round(cropArea.y * 10) / 10}
onChange={(value) => handleCoordinateChange('y', value)}
disabled={disabled}
min={0}
max={pdfBounds.actualHeight}
step={0.1}
decimalScale={1}
size="xs"
/>
</Group>
<Group grow>
<NumberInput
label={t("crop.coordinates.width", "Width")}
value={Math.round(cropArea.width * 10) / 10}
onChange={(value) => handleCoordinateChange('width', value)}
disabled={disabled}
min={0.1}
max={pdfBounds.actualWidth}
step={0.1}
decimalScale={1}
size="xs"
/>
<NumberInput
label={t("crop.coordinates.height", "Height")}
value={Math.round(cropArea.height * 10) / 10}
onChange={(value) => handleCoordinateChange('height', value)}
disabled={disabled}
min={0.1}
max={pdfBounds.actualHeight}
step={0.1}
decimalScale={1}
size="xs"
/>
</Group>
{/* Validation Alert */}
{!isCropValid && (
<Alert color="red" variant="light">
<Text size="xs">
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
</Text>
</Alert>
)}
</Stack>
</Stack>
);
};
export default CropSettings;

View File

@ -0,0 +1,45 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useCertSignTooltips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("certSign.tooltip.header.title", "About Managing Signatures")
},
tips: [
{
title: t("certSign.tooltip.overview.title", "What can this tool do?"),
description: t("certSign.tooltip.overview.text", "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing."),
bullets: [
t("certSign.tooltip.overview.bullet1", "Check existing signatures and their validity"),
t("certSign.tooltip.overview.bullet2", "View detailed information about signers and certificates"),
t("certSign.tooltip.overview.bullet3", "Add new digital signatures to secure your documents"),
t("certSign.tooltip.overview.bullet4", "Multiple files supported with easy navigation")
]
},
{
title: t("certSign.tooltip.validation.title", "Checking Signatures"),
description: t("certSign.tooltip.validation.text", "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing."),
bullets: [
t("certSign.tooltip.validation.bullet1", "Shows if signatures are valid or invalid"),
t("certSign.tooltip.validation.bullet2", "Displays signer information and signing date"),
t("certSign.tooltip.validation.bullet3", "Checks if the document was modified after signing"),
t("certSign.tooltip.validation.bullet4", "Can use custom certificates for verification")
]
},
{
title: t("certSign.tooltip.signing.title", "Adding Signatures"),
description: t("certSign.tooltip.signing.text", "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only."),
bullets: [
t("certSign.tooltip.signing.bullet1", "Supports PEM, PKCS12, JKS, and server certificate formats"),
t("certSign.tooltip.signing.bullet2", "Option to show or hide signature on the PDF"),
t("certSign.tooltip.signing.bullet3", "Add reason, location, and signer name"),
t("certSign.tooltip.signing.bullet4", "Choose which page to place visible signatures"),
t("certSign.tooltip.signing.bullet5", "Use server certificate for simple 'Sign with Stirling-PDF' option")
]
}
]
};
};

View File

@ -6,26 +6,26 @@ export const useCertificateTypeTips = (): TooltipContent => {
return {
header: {
title: t("manageSignatures.certType.tooltip.header.title", "About Certificate Types")
title: t("certSign.certType.tooltip.header.title", "About Certificate Types")
},
tips: [
{
title: t("manageSignatures.certType.tooltip.what.title", "What's a certificate?"),
description: t("manageSignatures.certType.tooltip.what.text", "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload.")
title: t("certSign.certType.tooltip.what.title", "What's a certificate?"),
description: t("certSign.certType.tooltip.what.text", "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload.")
},
{
title: t("manageSignatures.certType.tooltip.which.title", "Which option should I use?"),
description: t("manageSignatures.certType.tooltip.which.text", "Choose the format that matches your certificate file:"),
title: t("certSign.certType.tooltip.which.title", "Which option should I use?"),
description: t("certSign.certType.tooltip.which.text", "Choose the format that matches your certificate file:"),
bullets: [
t("manageSignatures.certType.tooltip.which.bullet1", "PKCS12 (.p12) one combined file (most common)"),
t("manageSignatures.certType.tooltip.which.bullet2", "PFX (.pfx) Microsoft's version of PKCS12"),
t("manageSignatures.certType.tooltip.which.bullet3", "PEM separate private-key and certificate .pem files"),
t("manageSignatures.certType.tooltip.which.bullet4", "JKS Java .jks keystore for dev / CI-CD workflows")
t("certSign.certType.tooltip.which.bullet1", "PKCS12 (.p12) one combined file (most common)"),
t("certSign.certType.tooltip.which.bullet2", "PFX (.pfx) Microsoft's version of PKCS12"),
t("certSign.certType.tooltip.which.bullet3", "PEM separate private-key and certificate .pem files"),
t("certSign.certType.tooltip.which.bullet4", "JKS Java .jks keystore for dev / CI-CD workflows")
]
},
{
title: t("manageSignatures.certType.tooltip.convert.title", "Key not listed?"),
description: t("manageSignatures.certType.tooltip.convert.text", "Convert your file to a Java keystore (.jks) with keytool, then pick JKS.")
title: t("certSign.certType.tooltip.convert.title", "Key not listed?"),
description: t("certSign.certType.tooltip.convert.text", "Convert your file to a Java keystore (.jks) with keytool, then pick JKS.")
}
]
};

View File

@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next';
export function useCropTooltips() {
const { t } = useTranslation();
return {
header: {
title: t("crop.tooltip.title", "How to Crop PDFs")
},
tips: [
{
description: t("crop.tooltip.description", "Select the area to crop from your PDF by dragging and resizing the blue overlay on the thumbnail."),
bullets: [
t("crop.tooltip.drag", "Drag the overlay to move the crop area"),
t("crop.tooltip.resize", "Drag the corner and edge handles to resize"),
t("crop.tooltip.precision", "Use coordinate inputs for precise positioning"),
]
}
]
};
}

View File

@ -1,45 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useManageSignaturesTooltips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("manageSignatures.tooltip.header.title", "About Managing Signatures")
},
tips: [
{
title: t("manageSignatures.tooltip.overview.title", "What can this tool do?"),
description: t("manageSignatures.tooltip.overview.text", "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing."),
bullets: [
t("manageSignatures.tooltip.overview.bullet1", "Check existing signatures and their validity"),
t("manageSignatures.tooltip.overview.bullet2", "View detailed information about signers and certificates"),
t("manageSignatures.tooltip.overview.bullet3", "Add new digital signatures to secure your documents"),
t("manageSignatures.tooltip.overview.bullet4", "Multiple files supported with easy navigation")
]
},
{
title: t("manageSignatures.tooltip.validation.title", "Checking Signatures"),
description: t("manageSignatures.tooltip.validation.text", "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing."),
bullets: [
t("manageSignatures.tooltip.validation.bullet1", "Shows if signatures are valid or invalid"),
t("manageSignatures.tooltip.validation.bullet2", "Displays signer information and signing date"),
t("manageSignatures.tooltip.validation.bullet3", "Checks if the document was modified after signing"),
t("manageSignatures.tooltip.validation.bullet4", "Can use custom certificates for verification")
]
},
{
title: t("manageSignatures.tooltip.signing.title", "Adding Signatures"),
description: t("manageSignatures.tooltip.signing.text", "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only."),
bullets: [
t("manageSignatures.tooltip.signing.bullet1", "Supports PEM, PKCS12, JKS, and server certificate formats"),
t("manageSignatures.tooltip.signing.bullet2", "Option to show or hide signature on the PDF"),
t("manageSignatures.tooltip.signing.bullet3", "Add reason, location, and signer name"),
t("manageSignatures.tooltip.signing.bullet4", "Choose which page to place visible signatures"),
t("manageSignatures.tooltip.signing.bullet5", "Use server certificate for simple 'Sign with Stirling-PDF' option")
]
}
]
};
};

View File

@ -6,30 +6,30 @@ export const useSignModeTips = (): TooltipContent => {
return {
header: {
title: t("manageSignatures.signMode.tooltip.header.title", "About PDF Signatures")
title: t("certSign.signMode.tooltip.header.title", "About PDF Signatures")
},
tips: [
{
title: t("manageSignatures.signMode.tooltip.overview.title", "How signatures work"),
description: t("manageSignatures.signMode.tooltip.overview.text", "Both modes seal the document (any edits are flagged as tampering) and record who/when/how for auditing. Viewer trust depends on the certificate chain.")
title: t("certSign.signMode.tooltip.overview.title", "How signatures work"),
description: t("certSign.signMode.tooltip.overview.text", "Both modes seal the document (any edits are flagged as tampering) and record who/when/how for auditing. Viewer trust depends on the certificate chain.")
},
{
title: t("manageSignatures.signMode.tooltip.manual.title", "Manual - Bring your certificate"),
description: t("manageSignatures.signMode.tooltip.manual.text", "Use your own certificate files for brand-aligned identity. Can display <b>Trusted</b> when your CA/chain is recognized."),
title: t("certSign.signMode.tooltip.manual.title", "Manual - Bring your certificate"),
description: t("certSign.signMode.tooltip.manual.text", "Use your own certificate files for brand-aligned identity. Can display <b>Trusted</b> when your CA/chain is recognized."),
bullets: [
t("manageSignatures.signMode.tooltip.manual.use", "Use for: customer-facing, legal, compliance.")
t("certSign.signMode.tooltip.manual.use", "Use for: customer-facing, legal, compliance.")
]
},
{
title: t("manageSignatures.signMode.tooltip.auto.title", "Auto - Zero-setup, instant system seal"),
description: t("manageSignatures.signMode.tooltip.auto.text", "Signs with a server <b>self-signed</b> certificate. Same <b>tamper-evident seal</b> and <b>audit trail</b>; typically shows <b>Unverified</b> in viewers."),
title: t("certSign.signMode.tooltip.auto.title", "Auto - Zero-setup, instant system seal"),
description: t("certSign.signMode.tooltip.auto.text", "Signs with a server <b>self-signed</b> certificate. Same <b>tamper-evident seal</b> and <b>audit trail</b>; typically shows <b>Unverified</b> in viewers."),
bullets: [
t("manageSignatures.signMode.tooltip.auto.use", "Use when: you need speed and consistent internal identity across reviews and records.")
t("certSign.signMode.tooltip.auto.use", "Use when: you need speed and consistent internal identity across reviews and records.")
]
},
{
title: t("manageSignatures.signMode.tooltip.rule.title", "Rule of thumb"),
description: t("manageSignatures.signMode.tooltip.rule.text", "Need recipient <b>Trusted</b> status? <b>Manual</b>. Need a fast, tamper-evident seal and audit trail with no setup? <b>Auto</b>.")
title: t("certSign.signMode.tooltip.rule.title", "Rule of thumb"),
description: t("certSign.signMode.tooltip.rule.text", "Need recipient <b>Trusted</b> status? <b>Manual</b>. Need a fast, tamper-evident seal and audit trail with no setup? <b>Auto</b>.")
}
]
};

View File

@ -6,26 +6,26 @@ export const useSignatureAppearanceTips = (): TooltipContent => {
return {
header: {
title: t("manageSignatures.appearance.tooltip.header.title", "About Signature Appearance")
title: t("certSign.appearance.tooltip.header.title", "About Signature Appearance")
},
tips: [
{
title: t("manageSignatures.appearance.tooltip.invisible.title", "Invisible Signatures"),
description: t("manageSignatures.appearance.tooltip.invisible.text", "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance."),
title: t("certSign.appearance.tooltip.invisible.title", "Invisible Signatures"),
description: t("certSign.appearance.tooltip.invisible.text", "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance."),
bullets: [
t("manageSignatures.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"),
t("manageSignatures.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"),
t("manageSignatures.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design")
t("certSign.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"),
t("certSign.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"),
t("certSign.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design")
]
},
{
title: t("manageSignatures.appearance.tooltip.visible.title", "Visible Signatures"),
description: t("manageSignatures.appearance.tooltip.visible.text", "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed."),
title: t("certSign.appearance.tooltip.visible.title", "Visible Signatures"),
description: t("certSign.appearance.tooltip.visible.text", "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed."),
bullets: [
t("manageSignatures.appearance.tooltip.visible.bullet1", "Shows signer name and date on the document"),
t("manageSignatures.appearance.tooltip.visible.bullet2", "Can include reason and location for signing"),
t("manageSignatures.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"),
t("manageSignatures.appearance.tooltip.visible.bullet4", "Optional logo can be included")
t("certSign.appearance.tooltip.visible.bullet1", "Shows signer name and date on the document"),
t("certSign.appearance.tooltip.visible.bullet2", "Can include reason and location for signing"),
t("certSign.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"),
t("certSign.appearance.tooltip.visible.bullet4", "Optional logo can be included")
]
}
]

View File

@ -0,0 +1,12 @@
import { PAGE_SIZES } from "./pageSizeConstants";
// Default crop area (covers entire page)
export const DEFAULT_CROP_AREA = {
x: 0,
y: 0,
width: PAGE_SIZES.A4.width,
height: PAGE_SIZES.A4.height,
} as const;
export type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null;

View File

@ -0,0 +1,8 @@
// Default PDF page sizes in points (1 point = 1/72 inch)
export const PAGE_SIZES = {
A4: { width: 595, height: 842 },
LETTER: { width: 612, height: 792 },
A3: { width: 842, height: 1191 },
A5: { width: 420, height: 595 },
LEGAL: { width: 612, height: 1008 },
} as const;

View File

@ -19,11 +19,12 @@ import AutoRename from "../tools/AutoRename";
import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
import ManageSignatures from "../tools/ManageSignatures";
import CertSign from "../tools/CertSign";
import BookletImposition from "../tools/BookletImposition";
import Flatten from "../tools/Flatten";
import Rotate from "../tools/Rotate";
import ChangeMetadata from "../tools/ChangeMetadata";
import Crop from "../tools/Crop";
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
@ -37,7 +38,7 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
import { certSignOperationConfig } from "../hooks/tools/certSign/useCertSignOperation";
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
@ -45,6 +46,7 @@ import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperati
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
import CompressSettings from "../components/tools/compress/CompressSettings";
import SplitSettings from "../components/tools/split/SplitSettings";
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
@ -56,7 +58,7 @@ import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/Add
import OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
@ -68,6 +70,7 @@ import MergeSettings from '../components/tools/merge/MergeSettings';
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
import CropSettings from "../components/tools/crop/CropSettings";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -160,16 +163,16 @@ export function useFlatToolRegistry(): ToolRegistry {
const allTools: ToolRegistry = {
// Signing
manageSignatures: {
certSign: {
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.certSign.title", "Certificate Sign"),
component: ManageSignatures,
component: CertSign,
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
maxFiles: -1,
endpoints: ["cert-sign"],
operationConfig: manageSignaturesOperationConfig,
operationConfig: certSignOperationConfig,
settingsComponent: CertificateTypeSettings,
},
sign: {
@ -322,10 +325,14 @@ export function useFlatToolRegistry(): ToolRegistry {
crop: {
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.crop.title", "Crop PDF"),
component: null,
component: Crop,
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["crop"],
operationConfig: cropOperationConfig,
settingsComponent: CropSettings,
},
rotate: {
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,

View File

@ -1,10 +1,10 @@
import { useTranslation } from 'react-i18next';
import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { ManageSignaturesParameters, defaultParameters } from './useManageSignaturesParameters';
import { CertSignParameters, defaultParameters } from './useCertSignParameters';
// Build form data for signing
export const buildManageSignaturesFormData = (parameters: ManageSignaturesParameters, file: File): FormData => {
export const buildCertSignFormData = (parameters: CertSignParameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
@ -52,20 +52,20 @@ export const buildManageSignaturesFormData = (parameters: ManageSignaturesParame
};
// Static configuration object
export const manageSignaturesOperationConfig = {
export const certSignOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildManageSignaturesFormData,
operationType: 'manageSignatures',
buildFormData: buildCertSignFormData,
operationType: 'certSign',
endpoint: '/api/v1/security/cert-sign',
multiFileEndpoint: false,
defaultParameters,
} as const;
export const useManageSignaturesOperation = () => {
export const useCertSignOperation = () => {
const { t } = useTranslation();
return useToolOperation<ManageSignaturesParameters>({
...manageSignaturesOperationConfig,
getErrorMessage: createStandardErrorHandler(t('manageSignatures.error.failed', 'An error occurred while processing signatures.'))
return useToolOperation<CertSignParameters>({
...certSignOperationConfig,
getErrorMessage: createStandardErrorHandler(t('certSign.error.failed', 'An error occurred while processing signatures.'))
});
};

View File

@ -1,7 +1,7 @@
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface ManageSignaturesParameters extends BaseParameters {
export interface CertSignParameters extends BaseParameters {
// Sign mode selection
signMode: 'MANUAL' | 'AUTO';
// Certificate signing options (only for manual mode)
@ -21,7 +21,7 @@ export interface ManageSignaturesParameters extends BaseParameters {
showLogo: boolean;
}
export const defaultParameters: ManageSignaturesParameters = {
export const defaultParameters: CertSignParameters = {
signMode: 'MANUAL',
certType: '',
password: '',
@ -33,12 +33,12 @@ export const defaultParameters: ManageSignaturesParameters = {
showLogo: true,
};
export type ManageSignaturesParametersHook = BaseParametersHook<ManageSignaturesParameters>;
export type CertSignParametersHook = BaseParametersHook<CertSignParameters>;
export const useManageSignaturesParameters = (): ManageSignaturesParametersHook => {
export const useCertSignParameters = (): CertSignParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'manage-signatures',
endpointName: 'cert-sign',
validateFn: (params) => {
// Auto mode (server certificate) - no additional validation needed
if (params.signMode === 'AUTO') {

View File

@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolType } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { CropParameters, defaultParameters } from './useCropParameters';
// Static configuration that can be used by both the hook and automation executor
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
const formData = new FormData();
formData.append("fileInput", file);
const cropArea = parameters.cropArea;
// Backend expects precise float values for PDF coordinates
formData.append("x", cropArea.x.toString());
formData.append("y", cropArea.y.toString());
formData.append("width", cropArea.width.toString());
formData.append("height", cropArea.height.toString());
return formData;
};
// Static configuration object
export const cropOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildCropFormData,
operationType: 'crop',
endpoint: '/api/v1/general/crop',
defaultParameters,
} as const;
export const useCropOperation = () => {
const { t } = useTranslation();
return useToolOperation<CropParameters>({
...cropOperationConfig,
getErrorMessage: createStandardErrorHandler(
t('crop.error.failed', 'An error occurred while cropping the PDF.')
)
});
};

View File

@ -0,0 +1,141 @@
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
import { useCallback } from 'react';
import { Rectangle, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea, isRectangle } from '../../../utils/cropCoordinates';
import { DEFAULT_CROP_AREA } from '../../../constants/cropConstants';
export interface CropParameters extends BaseParameters {
cropArea: Rectangle;
}
export const defaultParameters: CropParameters = {
cropArea: DEFAULT_CROP_AREA,
};
export type CropParametersHook = BaseParametersHook<CropParameters> & {
/** Set crop area with PDF bounds validation */
setCropArea: (cropArea: Rectangle, pdfBounds?: PDFBounds) => void;
/** Get current crop area as CropArea object */
getCropArea: () => Rectangle;
/** Reset to full PDF dimensions */
resetToFullPDF: (pdfBounds: PDFBounds) => void;
/** Check if current crop area is valid for the PDF */
isCropAreaValid: (pdfBounds?: PDFBounds) => boolean;
/** Check if crop area covers the entire PDF */
isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean;
/** Update crop area with constraints applied */
updateCropAreaConstrained: (cropArea: Partial<Rectangle>, pdfBounds?: PDFBounds) => void;
};
export const useCropParameters = (): CropParametersHook => {
const baseHook = useBaseParameters({
defaultParameters,
endpointName: 'crop',
validateFn: (params) => {
const rect = params.cropArea;
// Basic validation - coordinates and dimensions must be positive
return rect.x >= 0 &&
rect.y >= 0 &&
rect.width > 0 &&
rect.height > 0;
},
});
// Get current crop area as CropArea object
const getCropArea = useCallback((): Rectangle => {
return baseHook.parameters.cropArea;
}, [baseHook.parameters]);
// Set crop area with optional PDF bounds validation
const setCropArea = useCallback((cropArea: Rectangle, pdfBounds?: PDFBounds) => {
let finalCropArea = roundCropArea(cropArea);
// Apply PDF bounds constraints if provided
if (pdfBounds) {
finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds);
}
baseHook.updateParameter('cropArea', finalCropArea);
}, [baseHook]);
// Reset to cover entire PDF
const resetToFullPDF = useCallback((pdfBounds: PDFBounds) => {
const fullCropArea = createFullPDFCropArea(pdfBounds);
setCropArea(fullCropArea);
}, [setCropArea]);
// Check if current crop area is valid for the given PDF bounds
const isCropAreaValid = useCallback((pdfBounds?: PDFBounds): boolean => {
const cropArea = getCropArea();
// Basic validation
if (cropArea.x < 0 || cropArea.y < 0 || cropArea.width <= 0 || cropArea.height <= 0) {
return false;
}
// PDF bounds validation if provided
if (pdfBounds) {
const tolerance = 0.01; // Small tolerance for floating point precision
return cropArea.x + cropArea.width <= pdfBounds.actualWidth + tolerance &&
cropArea.y + cropArea.height <= pdfBounds.actualHeight + tolerance;
}
return true;
}, [getCropArea]);
// Check if crop area covers the entire PDF
const isFullPDFCrop = useCallback((pdfBounds?: PDFBounds): boolean => {
if (!pdfBounds) return false;
const cropArea = getCropArea();
const tolerance = 0.5; // Allow 0.5 point tolerance for floating point precision
return Math.abs(cropArea.x) < tolerance &&
Math.abs(cropArea.y) < tolerance &&
Math.abs(cropArea.width - pdfBounds.actualWidth) < tolerance &&
Math.abs(cropArea.height - pdfBounds.actualHeight) < tolerance;
}, [getCropArea]);
// Update crop area with constraints applied
const updateCropAreaConstrained = useCallback((
partialCropArea: Partial<Rectangle>,
pdfBounds?: PDFBounds
) => {
const currentCropArea = getCropArea();
const newCropArea = { ...currentCropArea, ...partialCropArea };
setCropArea(newCropArea, pdfBounds);
}, [getCropArea, setCropArea]);
// Enhanced validation that considers PDF bounds
const validateParameters = useCallback((pdfBounds?: PDFBounds): boolean => {
return baseHook.validateParameters() && isCropAreaValid(pdfBounds);
}, [baseHook, isCropAreaValid]);
// Override updateParameter to ensure positive values
const updateParameter = useCallback(<K extends keyof CropParameters>(
parameter: K,
value: CropParameters[K]
) => {
if(isRectangle(value)) {
value.x = Math.max(0.1, value.x); // Minimum 0.1 point
value.x = Math.max(0.1, value.y); // Minimum 0.1 point
value.width = Math.max(0, value.width); // Minimum 0 point
value.height = Math.max(0, value.height); // Minimum 0 point
}
baseHook.updateParameter(parameter, value);
}, [baseHook]);
return {
...baseHook,
updateParameter,
validateParameters: () => validateParameters(),
setCropArea,
getCropArea,
resetToFullPDF,
isCropAreaValid,
isFullPDFCrop,
updateCropAreaConstrained,
};
};

View File

@ -64,6 +64,16 @@ export const mantineTheme = createTheme({
xl: 'var(--shadow-xl)',
},
// Custom variables for specific components
other: {
crop: {
overlayBorder: 'var(--color-primary-500)',
overlayBackground: 'rgba(59, 130, 246, 0.1)', // Blue with 10% opacity
handleColor: 'var(--color-primary-500)',
handleBorder: 'var(--bg-surface)',
},
},
// Component customizations
components: {
Button: {

View File

@ -1,24 +1,24 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
import CertificateFormatSettings from "../components/tools/manageSignatures/CertificateFormatSettings";
import CertificateFilesSettings from "../components/tools/manageSignatures/CertificateFilesSettings";
import SignatureAppearanceSettings from "../components/tools/manageSignatures/SignatureAppearanceSettings";
import { useManageSignaturesParameters } from "../hooks/tools/manageSignatures/useManageSignaturesParameters";
import { useManageSignaturesOperation } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
import CertificateFormatSettings from "../components/tools/certSign/CertificateFormatSettings";
import CertificateFilesSettings from "../components/tools/certSign/CertificateFilesSettings";
import SignatureAppearanceSettings from "../components/tools/certSign/SignatureAppearanceSettings";
import { useCertSignParameters } from "../hooks/tools/certSign/useCertSignParameters";
import { useCertSignOperation } from "../hooks/tools/certSign/useCertSignOperation";
import { useCertificateTypeTips } from "../components/tooltips/useCertificateTypeTips";
import { useSignatureAppearanceTips } from "../components/tooltips/useSignatureAppearanceTips";
import { useSignModeTips } from "../components/tooltips/useSignModeTips";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
const ManageSignatures = (props: BaseToolProps) => {
const CertSign = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'manageSignatures',
useManageSignaturesParameters,
useManageSignaturesOperation,
'certSign',
useCertSignParameters,
useCertSignOperation,
props
);
@ -126,6 +126,6 @@ const ManageSignatures = (props: BaseToolProps) => {
};
// Static method to get the operation hook for automation
ManageSignatures.tool = () => useManageSignaturesOperation;
CertSign.tool = () => useCertSignOperation;
export default ManageSignatures as ToolComponent;
export default CertSign as ToolComponent;

View File

@ -0,0 +1,59 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import CropSettings from "../components/tools/crop/CropSettings";
import { useCropParameters } from "../hooks/tools/crop/useCropParameters";
import { useCropOperation } from "../hooks/tools/crop/useCropOperation";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { useCropTooltips } from "../components/tooltips/useCropTooltips";
import { BaseToolProps, ToolComponent } from "../types/tool";
const Crop = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'crop',
useCropParameters,
useCropOperation,
props
);
const tooltips = useCropTooltips();
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
minFiles: 1,
},
steps: [
{
title: t("crop.steps.selectArea", "Select Crop Area"),
isCollapsed: !base.hasFiles, // Collapsed until files selected
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
tooltip: tooltips,
content: (
<CropSettings
parameters={base.params}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("crop.submit", "Apply Crop"),
loadingText: t("loading"),
onClick: base.handleExecute,
isVisible: !base.hasResults,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("crop.results.title", "Crop Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Crop as ToolComponent;

View File

@ -56,7 +56,6 @@ const TOOL_IDS = [
'devSsoGuide',
'devAirgapped',
'bookletImposition',
'manageSignatures',
] as const;
// Tool identity - what PDF operation we're performing (type-safe)

View File

@ -0,0 +1,218 @@
/**
* Utility functions for crop coordinate conversion and PDF bounds handling
*/
export interface PDFBounds {
/** PDF width in points (actual PDF dimensions) */
actualWidth: number;
/** PDF height in points (actual PDF dimensions) */
actualHeight: number;
/** Thumbnail display width in pixels */
thumbnailWidth: number;
/** Thumbnail display height in pixels */
thumbnailHeight: number;
/** Horizontal offset for centering thumbnail in container */
offsetX: number;
/** Vertical offset for centering thumbnail in container */
offsetY: number;
/** Scale factor: thumbnailSize / actualSize */
scale: number;
}
export interface Rectangle {
/** X coordinate */
x: number;
/** Y coordinate */
y: number;
/** Width */
width: number;
/** Height */
height: number;
}
/** Runtime type guard */
export function isRectangle(value: unknown): value is Rectangle {
if (value === null || typeof value !== "object") return false;
const r = value as Record<string, unknown>;
const isNum = (n: unknown): n is number =>
typeof n === "number" && Number.isFinite(n);
return (
isNum(r.x) &&
isNum(r.y) &&
isNum(r.width) &&
isNum(r.height) &&
r.width >= 0 &&
r.height >= 0
);
}
/**
* Calculate PDF bounds for coordinate conversion based on thumbnail dimensions
*/
export const calculatePDFBounds = (
actualPDFWidth: number,
actualPDFHeight: number,
containerWidth: number,
containerHeight: number
): PDFBounds => {
// Calculate scale to fit PDF within container while maintaining aspect ratio
const scaleX = containerWidth / actualPDFWidth;
const scaleY = containerHeight / actualPDFHeight;
const scale = Math.min(scaleX, scaleY);
// Calculate actual thumbnail display size
const thumbnailWidth = actualPDFWidth * scale;
const thumbnailHeight = actualPDFHeight * scale;
// Calculate centering offsets - these represent where the thumbnail is positioned within the container
const offsetX = (containerWidth - thumbnailWidth) / 2;
const offsetY = (containerHeight - thumbnailHeight) / 2;
return {
actualWidth: actualPDFWidth,
actualHeight: actualPDFHeight,
thumbnailWidth,
thumbnailHeight,
offsetX,
offsetY,
scale
};
};
/**
* Convert DOM coordinates (relative to container) to PDF coordinates
* Handles coordinate system conversion (DOM uses top-left, PDF uses bottom-left origin)
*/
export const domToPDFCoordinates = (
domRect: Rectangle,
pdfBounds: PDFBounds
): Rectangle => {
// Convert DOM coordinates to thumbnail-relative coordinates
const thumbX = domRect.x - pdfBounds.offsetX;
const thumbY = domRect.y - pdfBounds.offsetY;
// Convert to PDF coordinates (scale and flip Y-axis)
const pdfX = thumbX / pdfBounds.scale;
const pdfY = pdfBounds.actualHeight - ((thumbY + domRect.height) / pdfBounds.scale);
const pdfWidth = domRect.width / pdfBounds.scale;
const pdfHeight = domRect.height / pdfBounds.scale;
return {
x: pdfX,
y: pdfY,
width: pdfWidth,
height: pdfHeight
};
};
/**
* Convert PDF coordinates to DOM coordinates (relative to container)
*/
export const pdfToDOMCoordinates = (
cropArea: Rectangle,
pdfBounds: PDFBounds
): Rectangle => {
// Convert PDF coordinates to thumbnail coordinates (scale and flip Y-axis)
const thumbX = cropArea.x * pdfBounds.scale;
const thumbY = (pdfBounds.actualHeight - cropArea.y - cropArea.height) * pdfBounds.scale;
const thumbWidth = cropArea.width * pdfBounds.scale;
const thumbHeight = cropArea.height * pdfBounds.scale;
// Add container offsets to get DOM coordinates
return {
x: thumbX + pdfBounds.offsetX,
y: thumbY + pdfBounds.offsetY,
width: thumbWidth,
height: thumbHeight
};
};
/**
* Constrain a crop area to stay within PDF bounds
*/
export const constrainCropAreaToPDF = (
cropArea: Rectangle,
pdfBounds: PDFBounds
): Rectangle => {
// Ensure crop area doesn't extend beyond PDF boundaries
const maxX = Math.max(0, pdfBounds.actualWidth - cropArea.width);
const maxY = Math.max(0, pdfBounds.actualHeight - cropArea.height);
return {
x: Math.max(0, Math.min(cropArea.x, maxX)),
y: Math.max(0, Math.min(cropArea.y, maxY)),
width: Math.min(cropArea.width, pdfBounds.actualWidth - Math.max(0, cropArea.x)),
height: Math.min(cropArea.height, pdfBounds.actualHeight - Math.max(0, cropArea.y))
};
};
/**
* Constrain DOM coordinates to stay within thumbnail bounds
*/
export const constrainDOMRectToThumbnail = (
domRect: Rectangle,
pdfBounds: PDFBounds
): Rectangle => {
const thumbnailLeft = pdfBounds.offsetX;
const thumbnailTop = pdfBounds.offsetY;
const thumbnailRight = pdfBounds.offsetX + pdfBounds.thumbnailWidth;
const thumbnailBottom = pdfBounds.offsetY + pdfBounds.thumbnailHeight;
// Constrain position
const maxX = Math.max(thumbnailLeft, thumbnailRight - domRect.width);
const maxY = Math.max(thumbnailTop, thumbnailBottom - domRect.height);
const constrainedX = Math.max(thumbnailLeft, Math.min(domRect.x, maxX));
const constrainedY = Math.max(thumbnailTop, Math.min(domRect.y, maxY));
// Constrain size to fit within thumbnail bounds from current position
const maxWidth = thumbnailRight - constrainedX;
const maxHeight = thumbnailBottom - constrainedY;
return {
x: constrainedX,
y: constrainedY,
width: Math.min(domRect.width, maxWidth),
height: Math.min(domRect.height, maxHeight)
};
};
/**
* Check if a point is within the thumbnail area (not just the container)
*/
export const isPointInThumbnail = (
x: number,
y: number,
pdfBounds: PDFBounds
): boolean => {
return x >= pdfBounds.offsetX &&
x <= pdfBounds.offsetX + pdfBounds.thumbnailWidth &&
y >= pdfBounds.offsetY &&
y <= pdfBounds.offsetY + pdfBounds.thumbnailHeight;
};
/**
* Create a default crop area that covers the entire PDF
*/
export const createFullPDFCropArea = (pdfBounds: PDFBounds): Rectangle => {
return {
x: 0,
y: 0,
width: pdfBounds.actualWidth,
height: pdfBounds.actualHeight
};
};
/**
* Round crop coordinates to reasonable precision (0.1 point)
*/
export const roundCropArea = (cropArea: Rectangle): Rectangle => {
return {
x: Math.round(cropArea.x * 10) / 10,
y: Math.round(cropArea.y * 10) / 10,
width: Math.round(cropArea.width * 10) / 10,
height: Math.round(cropArea.height * 10) / 10
};
};

View File

@ -31,7 +31,7 @@ export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
'/unlock-pdf-forms': 'unlockPDFForms',
'/remove-certificate-sign': 'removeCertSign',
'/remove-cert-sign': 'removeCertSign',
'/cert-sign': 'manageSignatures',
'/manage-signatures': 'manageSignatures',
'/cert-sign': 'certSign',
'/manage-signatures': 'certSign',
'/booklet-imposition': 'bookletImposition',
};

BIN
testing/crop_test.pdf Normal file

Binary file not shown.