@kitsy/pdf 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +16 -0
  4. package/README.md +256 -2
  5. package/THIRD_PARTY_NOTICES.md +48 -0
  6. package/dist/chunk-2K5X7L5J.js +43 -0
  7. package/dist/chunk-2K5X7L5J.js.map +1 -0
  8. package/dist/chunk-4EAYBJ6G.js +362 -0
  9. package/dist/chunk-4EAYBJ6G.js.map +1 -0
  10. package/dist/chunk-4LBJTDV5.js +165 -0
  11. package/dist/chunk-4LBJTDV5.js.map +1 -0
  12. package/dist/chunk-4ZEZ2GLD.js +353 -0
  13. package/dist/chunk-4ZEZ2GLD.js.map +1 -0
  14. package/dist/chunk-5OIMR74G.js +273 -0
  15. package/dist/chunk-5OIMR74G.js.map +1 -0
  16. package/dist/chunk-6NFNAKWA.js +257 -0
  17. package/dist/chunk-6NFNAKWA.js.map +1 -0
  18. package/dist/chunk-7MTEMQLL.js +47 -0
  19. package/dist/chunk-7MTEMQLL.js.map +1 -0
  20. package/dist/chunk-7V4SWTUK.js +83 -0
  21. package/dist/chunk-7V4SWTUK.js.map +1 -0
  22. package/dist/chunk-7XH7MFZ4.js +295 -0
  23. package/dist/chunk-7XH7MFZ4.js.map +1 -0
  24. package/dist/chunk-BK3MYSUS.js +61 -0
  25. package/dist/chunk-BK3MYSUS.js.map +1 -0
  26. package/dist/chunk-BLHNKR3S.js +35 -0
  27. package/dist/chunk-BLHNKR3S.js.map +1 -0
  28. package/dist/chunk-CCJ43SIX.js +342 -0
  29. package/dist/chunk-CCJ43SIX.js.map +1 -0
  30. package/dist/chunk-CNEDWATL.js +210 -0
  31. package/dist/chunk-CNEDWATL.js.map +1 -0
  32. package/dist/chunk-CYTWDJE2.js +152 -0
  33. package/dist/chunk-CYTWDJE2.js.map +1 -0
  34. package/dist/chunk-DKVOICYZ.js +45 -0
  35. package/dist/chunk-DKVOICYZ.js.map +1 -0
  36. package/dist/chunk-DPEJU4P6.js +79 -0
  37. package/dist/chunk-DPEJU4P6.js.map +1 -0
  38. package/dist/chunk-DQPABZTU.js +22 -0
  39. package/dist/chunk-DQPABZTU.js.map +1 -0
  40. package/dist/chunk-EKHEY3LI.js +46 -0
  41. package/dist/chunk-EKHEY3LI.js.map +1 -0
  42. package/dist/chunk-FIEYAKPI.js +421 -0
  43. package/dist/chunk-FIEYAKPI.js.map +1 -0
  44. package/dist/chunk-G467LFKC.js +191 -0
  45. package/dist/chunk-G467LFKC.js.map +1 -0
  46. package/dist/chunk-GLYWSVSB.js +250 -0
  47. package/dist/chunk-GLYWSVSB.js.map +1 -0
  48. package/dist/chunk-GXBIMUO7.js +135 -0
  49. package/dist/chunk-GXBIMUO7.js.map +1 -0
  50. package/dist/chunk-H6EERRCR.js +180 -0
  51. package/dist/chunk-H6EERRCR.js.map +1 -0
  52. package/dist/chunk-HDTM3LXD.js +35 -0
  53. package/dist/chunk-HDTM3LXD.js.map +1 -0
  54. package/dist/chunk-IGMX56C3.js +180 -0
  55. package/dist/chunk-IGMX56C3.js.map +1 -0
  56. package/dist/chunk-J4IRICCJ.js +74 -0
  57. package/dist/chunk-J4IRICCJ.js.map +1 -0
  58. package/dist/chunk-JXKQ7S6W.js +331 -0
  59. package/dist/chunk-JXKQ7S6W.js.map +1 -0
  60. package/dist/chunk-KY6DXS6S.js +34 -0
  61. package/dist/chunk-KY6DXS6S.js.map +1 -0
  62. package/dist/chunk-NOFGCYGS.js +22 -0
  63. package/dist/chunk-NOFGCYGS.js.map +1 -0
  64. package/dist/chunk-OSHUJRKD.js +131 -0
  65. package/dist/chunk-OSHUJRKD.js.map +1 -0
  66. package/dist/chunk-PMH7OWPF.js +40 -0
  67. package/dist/chunk-PMH7OWPF.js.map +1 -0
  68. package/dist/chunk-PRBKSX3A.js +299 -0
  69. package/dist/chunk-PRBKSX3A.js.map +1 -0
  70. package/dist/chunk-SVPUX7AM.js +48 -0
  71. package/dist/chunk-SVPUX7AM.js.map +1 -0
  72. package/dist/chunk-TR6HJ4NO.js +659 -0
  73. package/dist/chunk-TR6HJ4NO.js.map +1 -0
  74. package/dist/chunk-TV3E4YUF.js +69 -0
  75. package/dist/chunk-TV3E4YUF.js.map +1 -0
  76. package/dist/chunk-VCTN77K2.js +112 -0
  77. package/dist/chunk-VCTN77K2.js.map +1 -0
  78. package/dist/chunk-WEOPDD3P.js +120 -0
  79. package/dist/chunk-WEOPDD3P.js.map +1 -0
  80. package/dist/chunk-ZBZ7OC6I.js +228 -0
  81. package/dist/chunk-ZBZ7OC6I.js.map +1 -0
  82. package/dist/config-CJYo46X9.d.ts +19 -0
  83. package/dist/index.d.ts +517 -0
  84. package/dist/index.js +23 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/react/certificate-sign.d.ts +5 -0
  87. package/dist/react/certificate-sign.js +14 -0
  88. package/dist/react/certificate-sign.js.map +1 -0
  89. package/dist/react/compress.d.ts +5 -0
  90. package/dist/react/compress.js +8 -0
  91. package/dist/react/compress.js.map +1 -0
  92. package/dist/react/fill-form.d.ts +5 -0
  93. package/dist/react/fill-form.js +8 -0
  94. package/dist/react/fill-form.js.map +1 -0
  95. package/dist/react/images-to-pdf.d.ts +5 -0
  96. package/dist/react/images-to-pdf.js +7 -0
  97. package/dist/react/images-to-pdf.js.map +1 -0
  98. package/dist/react/merge.d.ts +5 -0
  99. package/dist/react/merge.js +8 -0
  100. package/dist/react/merge.js.map +1 -0
  101. package/dist/react/metadata.d.ts +5 -0
  102. package/dist/react/metadata.js +8 -0
  103. package/dist/react/metadata.js.map +1 -0
  104. package/dist/react/organize.d.ts +5 -0
  105. package/dist/react/organize.js +12 -0
  106. package/dist/react/organize.js.map +1 -0
  107. package/dist/react/pdf-to-images.d.ts +5 -0
  108. package/dist/react/pdf-to-images.js +13 -0
  109. package/dist/react/pdf-to-images.js.map +1 -0
  110. package/dist/react/protect.d.ts +5 -0
  111. package/dist/react/protect.js +8 -0
  112. package/dist/react/protect.js.map +1 -0
  113. package/dist/react/sign.d.ts +5 -0
  114. package/dist/react/sign.js +13 -0
  115. package/dist/react/sign.js.map +1 -0
  116. package/dist/react/split.d.ts +5 -0
  117. package/dist/react/split.js +11 -0
  118. package/dist/react/split.js.map +1 -0
  119. package/dist/react/verify-signatures.d.ts +5 -0
  120. package/dist/react/verify-signatures.js +8 -0
  121. package/dist/react/verify-signatures.js.map +1 -0
  122. package/dist/react/watermark.d.ts +5 -0
  123. package/dist/react/watermark.js +10 -0
  124. package/dist/react/watermark.js.map +1 -0
  125. package/dist/react.d.ts +40 -0
  126. package/dist/react.js +40 -0
  127. package/dist/react.js.map +1 -0
  128. package/dist/styles.css +2 -0
  129. package/dist/vite.d.ts +5 -0
  130. package/dist/vite.js +9 -0
  131. package/dist/vite.js.map +1 -0
  132. package/package.json +163 -4
@@ -0,0 +1,659 @@
1
+ import { useRenderDocument } from './chunk-J4IRICCJ.js';
2
+ import { opaqueBounds, whiteToTransparent } from './chunk-KY6DXS6S.js';
3
+ import { applySignatures } from './chunk-SVPUX7AM.js';
4
+ import { renderPageToCanvas, canvasToBlob } from './chunk-DPEJU4P6.js';
5
+ import { useSinglePdf } from './chunk-BK3MYSUS.js';
6
+ import { PdfToolPage, PdfToolColumns, cardClass, secondaryButtonClass, iconButtonClass, SideTips, SelectedPdfCard, FileDropzone, StatusBanner, primaryButtonClass, BackToPdfTools, fieldLabelClass, inputClass } from './chunk-H6EERRCR.js';
7
+ import { downloadBlob, toBaseName, formatBytes } from './chunk-EKHEY3LI.js';
8
+ import { useState, useRef, useEffect, useCallback } from 'react';
9
+ import '@fontsource/dancing-script/400.css';
10
+ import '@fontsource/great-vibes/400.css';
11
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
+
13
+ var TABS = [
14
+ { id: "draw", label: "Draw", icon: "fa-solid fa-pen-nib" },
15
+ { id: "type", label: "Type", icon: "fa-solid fa-keyboard" },
16
+ { id: "upload", label: "Upload", icon: "fa-solid fa-upload" }
17
+ ];
18
+ var FONTS = [
19
+ { family: "Dancing Script", label: "Handwritten" },
20
+ { family: "Great Vibes", label: "Elegant" },
21
+ { family: "Helvetica, Arial, sans-serif", label: "Plain (dates, initials)" }
22
+ ];
23
+ var INKS = [
24
+ { value: "#0f172a", label: "Black" },
25
+ { value: "#1d4ed8", label: "Blue" }
26
+ ];
27
+ async function trimToPng(canvas, padding = 8) {
28
+ const context = canvas.getContext("2d");
29
+ const { data } = context.getImageData(0, 0, canvas.width, canvas.height);
30
+ const bounds = opaqueBounds(data, canvas.width, canvas.height);
31
+ if (!bounds) return null;
32
+ const x = Math.max(0, bounds.x - padding);
33
+ const y = Math.max(0, bounds.y - padding);
34
+ const width = Math.min(canvas.width - x, bounds.width + padding * 2);
35
+ const height = Math.min(canvas.height - y, bounds.height + padding * 2);
36
+ const out = document.createElement("canvas");
37
+ out.width = width;
38
+ out.height = height;
39
+ out.getContext("2d").drawImage(canvas, x, y, width, height, 0, 0, width, height);
40
+ const blob = await canvasToBlob(out, "image/png");
41
+ return {
42
+ bytes: new Uint8Array(await blob.arrayBuffer()),
43
+ url: URL.createObjectURL(blob),
44
+ aspect: width / height
45
+ };
46
+ }
47
+ function DrawPad({ ink, onReady }) {
48
+ const canvasRef = useRef(null);
49
+ const drawing = useRef(false);
50
+ const last = useRef(null);
51
+ const [hasInk, setHasInk] = useState(false);
52
+ useEffect(() => {
53
+ const canvas = canvasRef.current;
54
+ const ratio = Math.min(3, window.devicePixelRatio || 1);
55
+ canvas.width = canvas.clientWidth * ratio;
56
+ canvas.height = canvas.clientHeight * ratio;
57
+ const context = canvas.getContext("2d");
58
+ context.scale(ratio, ratio);
59
+ context.lineCap = "round";
60
+ context.lineJoin = "round";
61
+ }, []);
62
+ const point = (event) => {
63
+ const rect = canvasRef.current.getBoundingClientRect();
64
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
65
+ };
66
+ const start = (event) => {
67
+ event.preventDefault();
68
+ canvasRef.current.setPointerCapture(event.pointerId);
69
+ drawing.current = true;
70
+ last.current = point(event);
71
+ };
72
+ const moveTo = (event) => {
73
+ if (!drawing.current) return;
74
+ const context = canvasRef.current.getContext("2d");
75
+ const next = point(event);
76
+ const distance = Math.hypot(next.x - last.current.x, next.y - last.current.y);
77
+ context.strokeStyle = ink;
78
+ context.lineWidth = Math.max(1.6, 3.2 - distance / 18);
79
+ context.beginPath();
80
+ context.moveTo(last.current.x, last.current.y);
81
+ context.lineTo(next.x, next.y);
82
+ context.stroke();
83
+ last.current = next;
84
+ setHasInk(true);
85
+ };
86
+ const stop = () => {
87
+ drawing.current = false;
88
+ };
89
+ const clear = () => {
90
+ const canvas = canvasRef.current;
91
+ canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
92
+ setHasInk(false);
93
+ };
94
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ /* @__PURE__ */ jsx(
96
+ "canvas",
97
+ {
98
+ ref: canvasRef,
99
+ onPointerDown: start,
100
+ onPointerMove: moveTo,
101
+ onPointerUp: stop,
102
+ onPointerCancel: stop,
103
+ "aria-label": "Signature drawing area",
104
+ className: "h-40 w-full touch-none rounded-xl border border-dashed border-slate-300 bg-white dark:border-slate-600"
105
+ }
106
+ ),
107
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
108
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => onReady(canvasRef.current), disabled: !hasInk, className: primaryButtonClass, children: [
109
+ /* @__PURE__ */ jsx("i", { className: "fa-solid fa-check" }),
110
+ "Use signature"
111
+ ] }),
112
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: clear, disabled: !hasInk, className: secondaryButtonClass, children: [
113
+ /* @__PURE__ */ jsx("i", { className: "fa-solid fa-eraser" }),
114
+ "Clear"
115
+ ] })
116
+ ] })
117
+ ] });
118
+ }
119
+ function TypePad({ ink, onReady }) {
120
+ const [text, setText] = useState("");
121
+ const [font, setFont] = useState(FONTS[0].family);
122
+ const render = async () => {
123
+ const size = 120;
124
+ const cssFont = `${size}px ${font.includes(",") ? font : `"${font}"`}`;
125
+ await document.fonts.load(cssFont, text);
126
+ const canvas = document.createElement("canvas");
127
+ const context = canvas.getContext("2d");
128
+ context.font = cssFont;
129
+ const width = Math.ceil(context.measureText(text).width) + size;
130
+ canvas.width = width;
131
+ canvas.height = Math.ceil(size * 1.8);
132
+ context.font = cssFont;
133
+ context.fillStyle = ink;
134
+ context.textBaseline = "middle";
135
+ context.fillText(text, size / 2, canvas.height / 2);
136
+ onReady(canvas);
137
+ };
138
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
139
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
140
+ /* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
141
+ "Your name or text",
142
+ /* @__PURE__ */ jsx(
143
+ "input",
144
+ {
145
+ type: "text",
146
+ value: text,
147
+ maxLength: 60,
148
+ onChange: (event) => setText(event.target.value),
149
+ placeholder: "Jane Doe",
150
+ className: `mt-2 ${inputClass}`
151
+ }
152
+ )
153
+ ] }),
154
+ /* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
155
+ "Style",
156
+ /* @__PURE__ */ jsx("select", { value: font, onChange: (event) => setFont(event.target.value), className: `mt-2 ${inputClass}`, children: FONTS.map((option) => /* @__PURE__ */ jsx("option", { value: option.family, children: option.label }, option.family)) })
157
+ ] })
158
+ ] }),
159
+ /* @__PURE__ */ jsx(
160
+ "div",
161
+ {
162
+ className: "mt-3 flex h-24 items-center justify-center overflow-hidden rounded-xl border border-dashed border-slate-300 bg-white px-4 text-4xl dark:border-slate-600",
163
+ style: { fontFamily: font.includes(",") ? font : `"${font}"`, color: ink },
164
+ children: text || /* @__PURE__ */ jsx("span", { className: "text-base text-slate-400", children: "Preview" })
165
+ }
166
+ ),
167
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: render, disabled: !text.trim(), className: `mt-3 ${primaryButtonClass}`, children: [
168
+ /* @__PURE__ */ jsx("i", { className: "fa-solid fa-check" }),
169
+ "Use signature"
170
+ ] })
171
+ ] });
172
+ }
173
+ function UploadPad({ onReady, onError }) {
174
+ const [removeBackground, setRemoveBackground] = useState(true);
175
+ const inputRef = useRef(null);
176
+ const handleFile = async (file) => {
177
+ if (!file) return;
178
+ try {
179
+ const bitmap = await createImageBitmap(file);
180
+ const scale = Math.min(1, 1200 / bitmap.width);
181
+ const canvas = document.createElement("canvas");
182
+ canvas.width = Math.round(bitmap.width * scale);
183
+ canvas.height = Math.round(bitmap.height * scale);
184
+ const context = canvas.getContext("2d");
185
+ context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
186
+ bitmap.close?.();
187
+ if (removeBackground) {
188
+ const image = context.getImageData(0, 0, canvas.width, canvas.height);
189
+ whiteToTransparent(image.data);
190
+ context.putImageData(image, 0, 0);
191
+ }
192
+ onReady(canvas);
193
+ } catch {
194
+ onError("That image could not be read. Try a PNG or JPG.");
195
+ }
196
+ };
197
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx(
199
+ "input",
200
+ {
201
+ ref: inputRef,
202
+ type: "file",
203
+ accept: "image/png,image/jpeg,image/webp",
204
+ className: "hidden",
205
+ onChange: (event) => {
206
+ handleFile(event.target.files?.[0]);
207
+ event.target.value = "";
208
+ }
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => inputRef.current?.click(), className: secondaryButtonClass, children: [
212
+ /* @__PURE__ */ jsx("i", { className: "fa-solid fa-image" }),
213
+ "Choose signature image"
214
+ ] }),
215
+ /* @__PURE__ */ jsxs("label", { className: "mt-3 flex items-center gap-3 text-sm text-slate-700 dark:text-slate-300", children: [
216
+ /* @__PURE__ */ jsx(
217
+ "input",
218
+ {
219
+ type: "checkbox",
220
+ checked: removeBackground,
221
+ onChange: (event) => setRemoveBackground(event.target.checked),
222
+ className: "h-4 w-4 accent-[#1F8CFF]"
223
+ }
224
+ ),
225
+ "Remove white background (for photos or scans of paper)"
226
+ ] })
227
+ ] });
228
+ }
229
+ function SignatureCreator({ onCreate, onError }) {
230
+ const [tab, setTab] = useState("draw");
231
+ const [ink, setInk] = useState(INKS[0].value);
232
+ const finish = async (canvas) => {
233
+ const result = await trimToPng(canvas);
234
+ if (!result) {
235
+ onError("The signature is empty.");
236
+ return;
237
+ }
238
+ onCreate(result);
239
+ };
240
+ return /* @__PURE__ */ jsxs("div", { children: [
241
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
242
+ /* @__PURE__ */ jsx("div", { role: "tablist", className: "inline-flex rounded-xl border border-slate-200 bg-slate-50 p-1 dark:border-slate-700 dark:bg-slate-900", children: TABS.map((option) => /* @__PURE__ */ jsxs(
243
+ "button",
244
+ {
245
+ type: "button",
246
+ role: "tab",
247
+ "aria-selected": tab === option.id,
248
+ onClick: () => setTab(option.id),
249
+ className: `inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.12em] transition ${tab === option.id ? "bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-slate-100" : "text-slate-500 hover:text-slate-800 dark:text-slate-400"}`,
250
+ children: [
251
+ /* @__PURE__ */ jsx("i", { className: option.icon }),
252
+ option.label
253
+ ]
254
+ },
255
+ option.id
256
+ )) }),
257
+ tab !== "upload" ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", "aria-label": "Ink colour", children: INKS.map((option) => /* @__PURE__ */ jsx(
258
+ "button",
259
+ {
260
+ type: "button",
261
+ onClick: () => setInk(option.value),
262
+ "aria-label": `${option.label} ink`,
263
+ "aria-pressed": ink === option.value,
264
+ className: `h-6 w-6 rounded-full border-2 ${ink === option.value ? "border-[#1F8CFF]" : "border-transparent"}`,
265
+ style: { backgroundColor: option.value }
266
+ },
267
+ option.value
268
+ )) }) : null
269
+ ] }),
270
+ /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
271
+ tab === "draw" ? /* @__PURE__ */ jsx(DrawPad, { ink, onReady: finish }) : null,
272
+ tab === "type" ? /* @__PURE__ */ jsx(TypePad, { ink, onReady: finish }) : null,
273
+ tab === "upload" ? /* @__PURE__ */ jsx(UploadPad, { onReady: finish, onError }) : null
274
+ ] })
275
+ ] });
276
+ }
277
+ var PREVIEW_WIDTH = 900;
278
+ var nextId = 0;
279
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
280
+ function usePagePreview(doc, pageIndex) {
281
+ const [preview, setPreview] = useState(null);
282
+ useEffect(() => {
283
+ if (!doc) {
284
+ setPreview(null);
285
+ return void 0;
286
+ }
287
+ let cancelled = false;
288
+ let url = null;
289
+ (async () => {
290
+ const page = await doc.getPage(pageIndex + 1);
291
+ const base = page.getViewport({ scale: 1 });
292
+ const ratio = Math.min(2, window.devicePixelRatio || 1);
293
+ const render = await renderPageToCanvas(doc, pageIndex + 1, {
294
+ scale: PREVIEW_WIDTH / base.width * ratio
295
+ });
296
+ const blob = await canvasToBlob(render.canvas, "image/jpeg", 0.85);
297
+ if (cancelled) return;
298
+ url = URL.createObjectURL(blob);
299
+ setPreview({ url, width: render.width, height: render.height });
300
+ })().catch(() => {
301
+ });
302
+ return () => {
303
+ cancelled = true;
304
+ if (url) URL.revokeObjectURL(url);
305
+ };
306
+ }, [doc, pageIndex]);
307
+ return preview;
308
+ }
309
+ function PlacementBox({ placement, signature, pageAspect, selected, onSelect, onChange, onRemove, containerRef }) {
310
+ const startDrag = (event, mode) => {
311
+ event.preventDefault();
312
+ event.stopPropagation();
313
+ onSelect();
314
+ const rect = containerRef.current.getBoundingClientRect();
315
+ const origin = { x: event.clientX, y: event.clientY, placement };
316
+ const handleMove = (moveEvent) => {
317
+ const dx = (moveEvent.clientX - origin.x) / rect.width;
318
+ const dy = (moveEvent.clientY - origin.y) / rect.height;
319
+ const start = origin.placement;
320
+ if (mode === "move") {
321
+ onChange({
322
+ x: clamp(start.x + dx, 0, 1 - start.width),
323
+ y: clamp(start.y + dy, 0, 1 - start.height)
324
+ });
325
+ } else {
326
+ const width = clamp(start.width + dx, 0.04, 1 - start.x);
327
+ const height = width * pageAspect / signature.aspect;
328
+ if (start.y + height <= 1) onChange({ width, height });
329
+ }
330
+ };
331
+ const handleUp = () => {
332
+ window.removeEventListener("pointermove", handleMove);
333
+ window.removeEventListener("pointerup", handleUp);
334
+ };
335
+ window.addEventListener("pointermove", handleMove);
336
+ window.addEventListener("pointerup", handleUp);
337
+ };
338
+ const nudge = (event) => {
339
+ const step = event.shiftKey ? 0.02 : 5e-3;
340
+ const moves = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] };
341
+ if (event.key === "Delete" || event.key === "Backspace") {
342
+ event.preventDefault();
343
+ onRemove();
344
+ return;
345
+ }
346
+ const move = moves[event.key];
347
+ if (!move) return;
348
+ event.preventDefault();
349
+ onChange({
350
+ x: clamp(placement.x + move[0], 0, 1 - placement.width),
351
+ y: clamp(placement.y + move[1], 0, 1 - placement.height)
352
+ });
353
+ };
354
+ return /* @__PURE__ */ jsxs(
355
+ "div",
356
+ {
357
+ role: "button",
358
+ tabIndex: 0,
359
+ "aria-label": "Placed signature. Drag to move, use arrow keys to nudge, Delete to remove.",
360
+ onPointerDown: (event) => startDrag(event, "move"),
361
+ onKeyDown: nudge,
362
+ onFocus: onSelect,
363
+ className: `absolute cursor-move touch-none select-none outline-none ${selected ? "ring-2 ring-[#1F8CFF]" : "ring-1 ring-[#1F8CFF]/40 hover:ring-[#1F8CFF]"}`,
364
+ style: {
365
+ left: `${placement.x * 100}%`,
366
+ top: `${placement.y * 100}%`,
367
+ width: `${placement.width * 100}%`,
368
+ height: `${placement.height * 100}%`
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ jsx("img", { src: signature.url, alt: "", draggable: false, className: "pointer-events-none h-full w-full" }),
372
+ selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
373
+ /* @__PURE__ */ jsx(
374
+ "button",
375
+ {
376
+ type: "button",
377
+ onPointerDown: (event) => event.stopPropagation(),
378
+ onClick: onRemove,
379
+ "aria-label": "Remove signature",
380
+ className: "absolute -right-3 -top-3 inline-flex h-6 w-6 items-center justify-center rounded-full bg-rose-600 text-[10px] text-white shadow",
381
+ children: /* @__PURE__ */ jsx("i", { className: "fa-solid fa-xmark" })
382
+ }
383
+ ),
384
+ /* @__PURE__ */ jsx(
385
+ "span",
386
+ {
387
+ onPointerDown: (event) => startDrag(event, "resize"),
388
+ "aria-hidden": "true",
389
+ className: "absolute -bottom-2 -right-2 h-4 w-4 cursor-nwse-resize rounded-sm border-2 border-white bg-[#1F8CFF] shadow"
390
+ }
391
+ )
392
+ ] }) : null
393
+ ]
394
+ }
395
+ );
396
+ }
397
+ function SignPdf() {
398
+ const [signatures, setSignatures] = useState([]);
399
+ const [placements, setPlacements] = useState([]);
400
+ const [selectedId, setSelectedId] = useState(null);
401
+ const [pageIndex, setPageIndex] = useState(0);
402
+ const [isSaving, setIsSaving] = useState(false);
403
+ const [status, setStatus] = useState({
404
+ tone: "info",
405
+ text: "Add a PDF, create your signature, then place it on the page."
406
+ });
407
+ const containerRef = useRef(null);
408
+ const signaturesRef = useRef(signatures);
409
+ signaturesRef.current = signatures;
410
+ useEffect(() => {
411
+ document.title = "Sign PDF | Apps";
412
+ return () => signaturesRef.current.forEach((signature) => URL.revokeObjectURL(signature.url));
413
+ }, []);
414
+ const onError = useCallback((text) => setStatus({ tone: "error", text }), []);
415
+ const onLoaded = useCallback(({ file, pageCount }) => {
416
+ setPlacements([]);
417
+ setPageIndex(0);
418
+ setStatus({
419
+ tone: "success",
420
+ text: `${file.name} loaded with ${pageCount} page${pageCount === 1 ? "" : "s"}. Create a signature to place it.`
421
+ });
422
+ }, []);
423
+ const pdf = useSinglePdf({ onError, onLoaded });
424
+ const { doc, error: renderError } = useRenderDocument(pdf.bytes);
425
+ const preview = usePagePreview(doc, pageIndex);
426
+ const pageAspect = preview ? preview.width / preview.height : 1 / 1.414;
427
+ const placeSignature = (signature, onPage = pageIndex) => {
428
+ const width = 0.3;
429
+ const height = Math.min(width * pageAspect / signature.aspect, 1);
430
+ const step = placements.filter((placement2) => placement2.pageIndex === onPage).length % 6;
431
+ const placement = {
432
+ id: `place-${++nextId}`,
433
+ signatureId: signature.id,
434
+ pageIndex: onPage,
435
+ x: clamp(0.6 - width / 2 - step * 0.04, 0, 1 - width),
436
+ y: clamp(0.8 - height / 2 - step * 0.08, 0, 1 - height),
437
+ width,
438
+ height
439
+ };
440
+ setPlacements((current) => [...current, placement]);
441
+ setSelectedId(placement.id);
442
+ };
443
+ const addSignature = (created) => {
444
+ const signature = { id: `sig-${++nextId}`, ...created };
445
+ setSignatures((current) => [...current, signature]);
446
+ if (pdf.pageCount) {
447
+ placeSignature(signature);
448
+ setStatus({ tone: "success", text: "Signature placed. Drag it into position, or resize it from the corner." });
449
+ }
450
+ };
451
+ const updatePlacement = (id, patch) => {
452
+ setPlacements((current) => current.map((placement) => placement.id === id ? { ...placement, ...patch } : placement));
453
+ };
454
+ const removePlacement = (id) => {
455
+ setPlacements((current) => current.filter((placement) => placement.id !== id));
456
+ setSelectedId(null);
457
+ };
458
+ const removeSignature = (id) => {
459
+ setSignatures((current) => {
460
+ const removed = current.find((signature) => signature.id === id);
461
+ if (removed) URL.revokeObjectURL(removed.url);
462
+ return current.filter((signature) => signature.id !== id);
463
+ });
464
+ setPlacements((current) => current.filter((placement) => placement.signatureId !== id));
465
+ };
466
+ const clearFile = () => {
467
+ pdf.clear();
468
+ setPlacements([]);
469
+ };
470
+ const handleSave = async () => {
471
+ if (!pdf.bytes || !placements.length) return;
472
+ setIsSaving(true);
473
+ setStatus({ tone: "info", text: "Signing your PDF..." });
474
+ try {
475
+ const byId2 = new Map(signatures.map((signature) => [signature.id, signature]));
476
+ const bytes = await applySignatures(
477
+ pdf.bytes,
478
+ placements.map((placement) => ({ ...placement, image: byId2.get(placement.signatureId).bytes }))
479
+ );
480
+ const blob = new Blob([bytes], { type: "application/pdf" });
481
+ downloadBlob(blob, `${toBaseName(pdf.file.name, "document")}-signed.pdf`);
482
+ setStatus({
483
+ tone: "success",
484
+ text: `Signed with ${placements.length} signature${placements.length === 1 ? "" : "s"} (${formatBytes(blob.size)}). Your download has started.`
485
+ });
486
+ } catch (error) {
487
+ setStatus({ tone: "error", text: `Signing failed: ${error?.message || "Please try again."}` });
488
+ } finally {
489
+ setIsSaving(false);
490
+ }
491
+ };
492
+ const busy = isSaving || pdf.isReading;
493
+ const pagePlacements = placements.filter((placement) => placement.pageIndex === pageIndex);
494
+ const byId = new Map(signatures.map((signature) => [signature.id, signature]));
495
+ return /* @__PURE__ */ jsx(
496
+ PdfToolPage,
497
+ {
498
+ title: "Sign PDF",
499
+ description: "Draw, type, or upload your signature and place it anywhere on the page. Runs in your browser; your document and signature never leave your device.",
500
+ stats: [
501
+ { value: pdf.pageCount || "-", label: "Pages" },
502
+ { value: placements.length || "-", label: "Placed" }
503
+ ],
504
+ children: /* @__PURE__ */ jsx(
505
+ PdfToolColumns,
506
+ {
507
+ main: /* @__PURE__ */ jsxs(Fragment, { children: [
508
+ pdf.file ? /* @__PURE__ */ jsx(
509
+ SelectedPdfCard,
510
+ {
511
+ signed: pdf.signed,
512
+ file: pdf.file,
513
+ pageCount: pdf.pageCount,
514
+ onRemove: clearFile,
515
+ disabled: isSaving
516
+ }
517
+ ) : /* @__PURE__ */ jsx(
518
+ FileDropzone,
519
+ {
520
+ accept: ".pdf,application/pdf",
521
+ disabled: busy,
522
+ onFiles: pdf.selectFiles,
523
+ title: "Drag and drop a PDF here",
524
+ hint: "or click to browse from your device"
525
+ }
526
+ ),
527
+ renderError ? /* @__PURE__ */ jsx("p", { className: "mt-3 text-xs text-rose-700 dark:text-rose-300", children: renderError }) : null,
528
+ pdf.pageCount ? /* @__PURE__ */ jsxs("div", { className: "mt-5", children: [
529
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex flex-wrap items-center justify-between gap-2", children: [
530
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
531
+ /* @__PURE__ */ jsx(
532
+ "button",
533
+ {
534
+ type: "button",
535
+ onClick: () => setPageIndex((index) => Math.max(0, index - 1)),
536
+ disabled: pageIndex === 0,
537
+ className: iconButtonClass,
538
+ "aria-label": "Previous page",
539
+ children: /* @__PURE__ */ jsx("i", { className: "fa-solid fa-chevron-left" })
540
+ }
541
+ ),
542
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-700 dark:text-slate-300", children: [
543
+ "Page ",
544
+ pageIndex + 1,
545
+ " of ",
546
+ pdf.pageCount
547
+ ] }),
548
+ /* @__PURE__ */ jsx(
549
+ "button",
550
+ {
551
+ type: "button",
552
+ onClick: () => setPageIndex((index) => Math.min(pdf.pageCount - 1, index + 1)),
553
+ disabled: pageIndex === pdf.pageCount - 1,
554
+ className: iconButtonClass,
555
+ "aria-label": "Next page",
556
+ children: /* @__PURE__ */ jsx("i", { className: "fa-solid fa-chevron-right" })
557
+ }
558
+ )
559
+ ] }),
560
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-slate-500 dark:text-slate-400", children: [
561
+ pagePlacements.length,
562
+ " on this page"
563
+ ] })
564
+ ] }),
565
+ /* @__PURE__ */ jsx("div", { className: "flex justify-center rounded-2xl bg-slate-100 p-3 dark:bg-slate-800", children: /* @__PURE__ */ jsxs(
566
+ "div",
567
+ {
568
+ ref: containerRef,
569
+ onPointerDown: () => setSelectedId(null),
570
+ className: "relative w-full max-w-[720px] bg-white shadow-md",
571
+ style: { aspectRatio: preview ? `${preview.width} / ${preview.height}` : "1 / 1.414" },
572
+ children: [
573
+ preview ? /* @__PURE__ */ jsx(
574
+ "img",
575
+ {
576
+ src: preview.url,
577
+ alt: `Page ${pageIndex + 1}`,
578
+ draggable: false,
579
+ className: "pointer-events-none absolute inset-0 h-full w-full select-none"
580
+ }
581
+ ) : /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("i", { className: "fa-solid fa-spinner fa-spin text-slate-400", "aria-label": "Rendering page" }) }),
582
+ pagePlacements.map((placement) => /* @__PURE__ */ jsx(
583
+ PlacementBox,
584
+ {
585
+ placement,
586
+ signature: byId.get(placement.signatureId),
587
+ pageAspect,
588
+ selected: selectedId === placement.id,
589
+ onSelect: () => setSelectedId(placement.id),
590
+ onChange: (patch) => updatePlacement(placement.id, patch),
591
+ onRemove: () => removePlacement(placement.id),
592
+ containerRef
593
+ },
594
+ placement.id
595
+ ))
596
+ ]
597
+ }
598
+ ) })
599
+ ] }) : null,
600
+ /* @__PURE__ */ jsx(StatusBanner, { status }),
601
+ /* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-wrap gap-3", children: [
602
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: handleSave, disabled: busy || !placements.length, className: primaryButtonClass, children: [
603
+ /* @__PURE__ */ jsx("i", { className: `fa-solid ${isSaving ? "fa-spinner fa-spin" : "fa-signature"}` }),
604
+ isSaving ? "Signing..." : "Sign & Download"
605
+ ] }),
606
+ /* @__PURE__ */ jsx(BackToPdfTools, {})
607
+ ] })
608
+ ] }),
609
+ side: /* @__PURE__ */ jsxs(Fragment, { children: [
610
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-slate-900 dark:text-slate-100", children: "Your signature" }),
611
+ /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(SignatureCreator, { onCreate: addSignature, onError }) }),
612
+ signatures.length ? /* @__PURE__ */ jsx("ul", { className: "mt-5 space-y-2", children: signatures.map((signature) => /* @__PURE__ */ jsxs("li", { className: `${cardClass} !p-2`, children: [
613
+ /* @__PURE__ */ jsx("div", { className: "flex h-14 items-center justify-center rounded-lg bg-white", children: /* @__PURE__ */ jsx("img", { src: signature.url, alt: "Saved signature", className: "max-h-full max-w-full" }) }),
614
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex gap-2", children: [
615
+ /* @__PURE__ */ jsxs(
616
+ "button",
617
+ {
618
+ type: "button",
619
+ onClick: () => placeSignature(signature),
620
+ disabled: !pdf.pageCount,
621
+ className: `flex-1 justify-center ${secondaryButtonClass}`,
622
+ children: [
623
+ /* @__PURE__ */ jsx("i", { className: "fa-solid fa-plus" }),
624
+ "Place"
625
+ ]
626
+ }
627
+ ),
628
+ /* @__PURE__ */ jsx(
629
+ "button",
630
+ {
631
+ type: "button",
632
+ onClick: () => removeSignature(signature.id),
633
+ className: iconButtonClass,
634
+ "aria-label": "Delete signature",
635
+ children: /* @__PURE__ */ jsx("i", { className: "fa-solid fa-trash" })
636
+ }
637
+ )
638
+ ] })
639
+ ] }, signature.id)) }) : null,
640
+ /* @__PURE__ */ jsx(
641
+ SideTips,
642
+ {
643
+ tips: [
644
+ "Place the same signature on several pages, or add initials with Type \u2192 Plain.",
645
+ "Select a signature and press Delete to remove it; arrow keys nudge it.",
646
+ "This adds a visible signature image. It is not a certificate-based digital signature."
647
+ ]
648
+ }
649
+ )
650
+ ] })
651
+ }
652
+ )
653
+ }
654
+ );
655
+ }
656
+
657
+ export { SignPdf };
658
+ //# sourceMappingURL=chunk-TR6HJ4NO.js.map
659
+ //# sourceMappingURL=chunk-TR6HJ4NO.js.map