feat: add Sign PDF tool (draw/type/upload signatures, place on a PDF) (#370)

Draw, type, or upload a signature and place resizable/rotatable copies across PDF pages; output flattened server-side with PyMuPDF. Visual electronic signature, not cryptographic. New interactive-sign display mode (pdf.js + Konva) and a custom docs-pool route.
This commit is contained in:
SnapOtter
2026-06-29 11:01:25 +08:00
committed by GitHub
parent 1c202c6ef0
commit 0cdd560ac4
51 changed files with 2223 additions and 10 deletions
+25
View File
@@ -36,6 +36,7 @@ import { CropCanvas } from "@/components/tools/crop-canvas";
import type { EraserCanvasRef } from "@/components/tools/eraser-canvas";
import { EraserCanvas } from "@/components/tools/eraser-canvas";
import type { PreviewTransform } from "@/components/tools/rotate-settings";
import { SignCanvas, type SignCanvasRef } from "@/components/tools/sign-canvas";
import { useTranslation } from "@/contexts/i18n-context";
import { useAuth } from "@/hooks/use-auth";
import { useMobile } from "@/hooks/use-mobile";
@@ -359,6 +360,11 @@ export function ToolPage() {
// Center of the painted mask as a 0-100 percentage — used to init the slider at the right spot
const [eraserSliderInitPos, setEraserSliderInitPos] = useState<number | null>(null);
// Sign state
const signCanvasRef = useRef<SignCanvasRef | null>(null);
const [signHasSelection, setSignHasSelection] = useState(false);
const [signPlacementCount, setSignPlacementCount] = useState(0);
// Page-level drag overlay state
const [isDraggingOver, setIsDraggingOver] = useState(false);
const dragCounter = useRef(0);
@@ -651,6 +657,14 @@ export function ToolPage() {
maskedFileCount: eraserMaskedCount,
}
: undefined,
signProps:
displayMode === "interactive-sign"
? {
canvasRef: signCanvasRef,
hasSelection: signHasSelection,
placementCount: signPlacementCount,
}
: undefined,
};
const ToolSettings = registryEntry.Settings;
@@ -843,6 +857,17 @@ export function ToolPage() {
);
}
if (displayMode === "interactive-sign" && hasFile && originalBlobUrl) {
return (
<SignCanvas
ref={signCanvasRef}
fileUrl={originalBlobUrl}
onSelectionChange={setSignHasSelection}
onCountChange={setSignPlacementCount}
/>
);
}
if (displayMode === "interactive-split" && hasFile && originalBlobUrl) {
if (registryEntry?.ResultsPanel) {
const Panel = registryEntry.ResultsPanel;