@kitsy/pdf 0.0.0-stage → 0.1.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 +22 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +16 -0
  4. package/README.md +229 -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-4USCGC24.js +304 -0
  13. package/dist/chunk-4USCGC24.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-G467LFKC.js +191 -0
  43. package/dist/chunk-G467LFKC.js.map +1 -0
  44. package/dist/chunk-GLYWSVSB.js +250 -0
  45. package/dist/chunk-GLYWSVSB.js.map +1 -0
  46. package/dist/chunk-GXBIMUO7.js +135 -0
  47. package/dist/chunk-GXBIMUO7.js.map +1 -0
  48. package/dist/chunk-H6EERRCR.js +180 -0
  49. package/dist/chunk-H6EERRCR.js.map +1 -0
  50. package/dist/chunk-HDTM3LXD.js +35 -0
  51. package/dist/chunk-HDTM3LXD.js.map +1 -0
  52. package/dist/chunk-IGMX56C3.js +180 -0
  53. package/dist/chunk-IGMX56C3.js.map +1 -0
  54. package/dist/chunk-J4IRICCJ.js +74 -0
  55. package/dist/chunk-J4IRICCJ.js.map +1 -0
  56. package/dist/chunk-JXKQ7S6W.js +331 -0
  57. package/dist/chunk-JXKQ7S6W.js.map +1 -0
  58. package/dist/chunk-KY6DXS6S.js +34 -0
  59. package/dist/chunk-KY6DXS6S.js.map +1 -0
  60. package/dist/chunk-NOFGCYGS.js +22 -0
  61. package/dist/chunk-NOFGCYGS.js.map +1 -0
  62. package/dist/chunk-OSHUJRKD.js +131 -0
  63. package/dist/chunk-OSHUJRKD.js.map +1 -0
  64. package/dist/chunk-PMH7OWPF.js +40 -0
  65. package/dist/chunk-PMH7OWPF.js.map +1 -0
  66. package/dist/chunk-PRBKSX3A.js +299 -0
  67. package/dist/chunk-PRBKSX3A.js.map +1 -0
  68. package/dist/chunk-SVPUX7AM.js +48 -0
  69. package/dist/chunk-SVPUX7AM.js.map +1 -0
  70. package/dist/chunk-TR6HJ4NO.js +659 -0
  71. package/dist/chunk-TR6HJ4NO.js.map +1 -0
  72. package/dist/chunk-TV3E4YUF.js +69 -0
  73. package/dist/chunk-TV3E4YUF.js.map +1 -0
  74. package/dist/chunk-U2ROTOPF.js +256 -0
  75. package/dist/chunk-U2ROTOPF.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 +487 -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,40 @@
1
+ // src/shared/pageGeometry.ts
2
+ function normalizeRotation(angle) {
3
+ return (Math.round(angle / 90) * 90 % 360 + 360) % 360;
4
+ }
5
+ function visualToUser(page, vx, vy) {
6
+ const { width: w, height: h } = page;
7
+ switch (normalizeRotation(page.rotation)) {
8
+ case 90:
9
+ return { x: w - vy, y: vx, angle: 90 };
10
+ case 180:
11
+ return { x: w - vx, y: h - vy, angle: 180 };
12
+ case 270:
13
+ return { x: vy, y: h - vx, angle: 270 };
14
+ default:
15
+ return { x: vx, y: vy, angle: 0 };
16
+ }
17
+ }
18
+ function visualSize(page) {
19
+ const { x, y, width, height } = page.getCropBox();
20
+ const rotation = normalizeRotation(page.getRotation().angle);
21
+ const swapped = rotation === 90 || rotation === 270;
22
+ return {
23
+ x,
24
+ y,
25
+ width,
26
+ height,
27
+ rotation,
28
+ visualWidth: swapped ? height : width,
29
+ visualHeight: swapped ? width : height
30
+ };
31
+ }
32
+ function textOrigin(page, vx, vy) {
33
+ const geometry = visualSize(page);
34
+ const point = visualToUser(geometry, vx, vy);
35
+ return { x: geometry.x + point.x, y: geometry.y + point.y, angle: point.angle };
36
+ }
37
+
38
+ export { normalizeRotation, textOrigin, visualSize, visualToUser };
39
+ //# sourceMappingURL=chunk-PMH7OWPF.js.map
40
+ //# sourceMappingURL=chunk-PMH7OWPF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/pageGeometry.ts"],"names":[],"mappings":";AAMO,SAAS,kBAAkB,KAAA,EAAe;AAC/C,EAAA,OAAA,CAAU,KAAK,KAAA,CAAM,KAAA,GAAQ,EAAE,CAAA,GAAI,EAAA,GAAM,MAAO,GAAA,IAAO,GAAA;AACzD;AAOO,SAAS,YAAA,CACd,IAAA,EACA,EAAA,EACA,EAAA,EACA;AACA,EAAA,MAAM,EAAE,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,GAAE,GAAI,IAAA;AAChC,EAAA,QAAQ,iBAAA,CAAkB,IAAA,CAAK,QAAQ,CAAA;AAAG,IACxC,KAAK,EAAA;AACH,MAAA,OAAO,EAAE,CAAA,EAAG,CAAA,GAAI,IAAI,CAAA,EAAG,EAAA,EAAI,OAAO,EAAA,EAAG;AAAA,IACvC,KAAK,GAAA;AACH,MAAA,OAAO,EAAE,GAAG,CAAA,GAAI,EAAA,EAAI,GAAG,CAAA,GAAI,EAAA,EAAI,OAAO,GAAA,EAAI;AAAA,IAC5C,KAAK,GAAA;AACH,MAAA,OAAO,EAAE,CAAA,EAAG,EAAA,EAAI,GAAG,CAAA,GAAI,EAAA,EAAI,OAAO,GAAA,EAAI;AAAA,IACxC;AACE,MAAA,OAAO,EAAE,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,OAAO,CAAA,EAAE;AAAA;AAEtC;AAEO,SAAS,WAAW,IAAA,EAAe;AAGxC,EAAA,MAAM,EAAE,CAAA,EAAG,CAAA,EAAG,OAAO,MAAA,EAAO,GAAI,KAAK,UAAA,EAAW;AAChD,EAAA,MAAM,QAAA,GAAW,iBAAA,CAAkB,IAAA,CAAK,WAAA,GAAc,KAAK,CAAA;AAC3D,EAAA,MAAM,OAAA,GAAU,QAAA,KAAa,EAAA,IAAM,QAAA,KAAa,GAAA;AAChD,EAAA,OAAO;AAAA,IACL,CAAA;AAAA,IACA,CAAA;AAAA,IACA,KAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA,EAAa,UAAU,MAAA,GAAS,KAAA;AAAA,IAChC,YAAA,EAAc,UAAU,KAAA,GAAQ;AAAA,GAClC;AACF;AAGO,SAAS,UAAA,CAAW,IAAA,EAAe,EAAA,EAAY,EAAA,EAAY;AAChE,EAAA,MAAM,QAAA,GAAW,WAAW,IAAI,CAAA;AAChC,EAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,QAAA,EAAU,EAAA,EAAI,EAAE,CAAA;AAC3C,EAAA,OAAO,EAAE,CAAA,EAAG,QAAA,CAAS,CAAA,GAAI,KAAA,CAAM,CAAA,EAAG,CAAA,EAAG,QAAA,CAAS,CAAA,GAAI,KAAA,CAAM,CAAA,EAAG,KAAA,EAAO,MAAM,KAAA,EAAM;AAChF","file":"chunk-PMH7OWPF.js","sourcesContent":["import { PDFPage } from \"pdf-lib\";\n\n// On-screen (\"visual\") page geometry: what a viewer shows is the CropBox,\n// turned by the page's /Rotate. Tools position things in visual space and\n// convert to PDF user space here.\n\nexport function normalizeRotation(angle: number) {\n return (((Math.round(angle / 90) * 90) % 360) + 360) % 360;\n}\n\n/**\n * Map a point in the page's on-screen (\"visual\") space to PDF user space,\n * accounting for the page's /Rotate. Returns the user-space point and the\n * extra text rotation needed for text to read upright on screen.\n */\nexport function visualToUser(\n page: { width: number; height: number; rotation: number },\n vx: number,\n vy: number\n) {\n const { width: w, height: h } = page;\n switch (normalizeRotation(page.rotation)) {\n case 90:\n return { x: w - vy, y: vx, angle: 90 };\n case 180:\n return { x: w - vx, y: h - vy, angle: 180 };\n case 270:\n return { x: vy, y: h - vx, angle: 270 };\n default:\n return { x: vx, y: vy, angle: 0 };\n }\n}\n\nexport function visualSize(page: PDFPage) {\n // Viewers show the CropBox (it defaults to the MediaBox), which may be\n // smaller than the page and need not start at the origin.\n const { x, y, width, height } = page.getCropBox();\n const rotation = normalizeRotation(page.getRotation().angle);\n const swapped = rotation === 90 || rotation === 270;\n return {\n x,\n y,\n width,\n height,\n rotation,\n visualWidth: swapped ? height : width,\n visualHeight: swapped ? width : height,\n };\n}\n\n/** Where on-screen point (vx, vy) of the visible page lands in PDF user space. */\nexport function textOrigin(page: PDFPage, vx: number, vy: number) {\n const geometry = visualSize(page);\n const point = visualToUser(geometry, vx, vy);\n return { x: geometry.x + point.x, y: geometry.y + point.y, angle: point.angle };\n}\n"]}
@@ -0,0 +1,299 @@
1
+ import { PasswordInput } from './chunk-2K5X7L5J.js';
2
+ import { isEncrypted, protectPdf, browserQpdf, unlockPdf, PasswordError } from './chunk-OSHUJRKD.js';
3
+ import { PdfToolPage, PdfToolColumns, SideTips, SelectedPdfCard, FileDropzone, cardClass, fieldLabelClass, inputClass, StatusBanner, secondaryButtonClass, primaryButtonClass, BackToPdfTools } from './chunk-H6EERRCR.js';
4
+ import { isPdfFile, downloadBlob, toBaseName, formatBytes } from './chunk-EKHEY3LI.js';
5
+ import { useState, useRef, useEffect } from 'react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+
8
+ var MAX_BYTES = 200 * 1024 * 1024;
9
+ function ProtectPdf() {
10
+ const [file, setFile] = useState(null);
11
+ const [bytes, setBytes] = useState(null);
12
+ const [encrypted, setEncrypted] = useState(false);
13
+ const [mode, setMode] = useState("protect");
14
+ const [userPassword, setUserPassword] = useState("");
15
+ const [confirmPassword, setConfirmPassword] = useState("");
16
+ const [ownerPassword, setOwnerPassword] = useState("");
17
+ const [print, setPrint] = useState("full");
18
+ const [extract, setExtract] = useState(true);
19
+ const [modify, setModify] = useState(true);
20
+ const [unlockPassword, setUnlockPassword] = useState("");
21
+ const [needsPassword, setNeedsPassword] = useState(false);
22
+ const [generatedOwner, setGeneratedOwner] = useState("");
23
+ const [isWorking, setIsWorking] = useState(false);
24
+ const [status, setStatus] = useState({
25
+ tone: "info",
26
+ text: "Add a PDF to protect it with a password, or to remove a password you know."
27
+ });
28
+ const unlockInputRef = useRef(null);
29
+ const requestRef = useRef(0);
30
+ useEffect(() => {
31
+ document.title = "Protect / Unlock PDF | Apps";
32
+ }, []);
33
+ useEffect(() => {
34
+ if (needsPassword && !isWorking) unlockInputRef.current?.focus();
35
+ }, [needsPassword, isWorking, status]);
36
+ const resetSecrets = () => {
37
+ setUserPassword("");
38
+ setConfirmPassword("");
39
+ setOwnerPassword("");
40
+ setUnlockPassword("");
41
+ setGeneratedOwner("");
42
+ setNeedsPassword(false);
43
+ };
44
+ const selectFiles = async (files) => {
45
+ const next = files.find(isPdfFile);
46
+ if (!next) {
47
+ if (files.length) setStatus({ tone: "error", text: "Only PDF files are supported." });
48
+ return;
49
+ }
50
+ if (next.size > MAX_BYTES) {
51
+ setStatus({ tone: "error", text: "This PDF is larger than 200 MB." });
52
+ return;
53
+ }
54
+ const request = ++requestRef.current;
55
+ resetSecrets();
56
+ setFile(next);
57
+ setBytes(null);
58
+ try {
59
+ const data = new Uint8Array(await next.arrayBuffer());
60
+ const locked = await isEncrypted(data);
61
+ if (request !== requestRef.current) return;
62
+ setBytes(data);
63
+ setEncrypted(locked);
64
+ setMode(locked ? "unlock" : "protect");
65
+ setStatus(
66
+ locked ? { tone: "info", text: `${next.name} is protected. Enter its password if it has one, then unlock.` } : { tone: "success", text: `${next.name} loaded. Choose a password and permissions.` }
67
+ );
68
+ } catch (error) {
69
+ if (request !== requestRef.current) return;
70
+ setFile(null);
71
+ setStatus({ tone: "error", text: error?.message || "Could not read this PDF." });
72
+ }
73
+ };
74
+ const clearFile = () => {
75
+ requestRef.current += 1;
76
+ setFile(null);
77
+ setBytes(null);
78
+ setEncrypted(false);
79
+ resetSecrets();
80
+ };
81
+ const mismatch = Boolean(confirmPassword) && userPassword !== confirmPassword;
82
+ const restricted = print !== "full" || !extract || !modify;
83
+ const canProtect = Boolean(bytes) && !encrypted && !mismatch && (userPassword ? userPassword === confirmPassword : restricted);
84
+ const handleProtect = async (event) => {
85
+ event.preventDefault();
86
+ if (!canProtect) return;
87
+ setIsWorking(true);
88
+ setGeneratedOwner("");
89
+ setStatus({ tone: "info", text: "Encrypting your PDF..." });
90
+ try {
91
+ const result = await protectPdf(bytes, { userPassword, ownerPassword, print, extract, modify }, browserQpdf);
92
+ const blob = new Blob([result.bytes], { type: "application/pdf" });
93
+ downloadBlob(blob, `${toBaseName(file.name, "document")}-protected.pdf`);
94
+ if (!ownerPassword) setGeneratedOwner(result.ownerPassword);
95
+ setStatus({
96
+ tone: "success",
97
+ text: `Protected with AES-256 (${formatBytes(blob.size)}). Your download has started.`
98
+ });
99
+ } catch (error) {
100
+ setStatus({ tone: "error", text: error?.message || "Could not protect this PDF." });
101
+ } finally {
102
+ setIsWorking(false);
103
+ }
104
+ };
105
+ const handleUnlock = async (event) => {
106
+ event.preventDefault();
107
+ if (!bytes || !encrypted) return;
108
+ setIsWorking(true);
109
+ setStatus({ tone: "info", text: "Unlocking your PDF..." });
110
+ try {
111
+ const unlocked = await unlockPdf(bytes, unlockPassword, browserQpdf);
112
+ const blob = new Blob([unlocked], { type: "application/pdf" });
113
+ downloadBlob(blob, `${toBaseName(file.name, "document")}-unlocked.pdf`);
114
+ setNeedsPassword(false);
115
+ setStatus({
116
+ tone: "success",
117
+ text: `Unlocked (${formatBytes(blob.size)}). The password and restrictions are removed. Your download has started.`
118
+ });
119
+ } catch (error) {
120
+ if (error instanceof PasswordError) {
121
+ setNeedsPassword(true);
122
+ setStatus({ tone: "error", text: error.message });
123
+ } else {
124
+ setStatus({ tone: "error", text: error?.message || "Could not unlock this PDF." });
125
+ }
126
+ } finally {
127
+ setIsWorking(false);
128
+ }
129
+ };
130
+ const busy = isWorking;
131
+ return /* @__PURE__ */ jsx(
132
+ PdfToolPage,
133
+ {
134
+ title: "Protect / Unlock PDF",
135
+ description: "Add a password and permissions to a PDF with AES-256 encryption, or remove a password you know. Runs in your browser with qpdf; files and passwords never leave your device.",
136
+ stats: [
137
+ { value: "AES", label: "256-bit" },
138
+ { value: file ? encrypted ? "Locked" : "Open" : "-", label: "File" }
139
+ ],
140
+ children: /* @__PURE__ */ jsx(
141
+ PdfToolColumns,
142
+ {
143
+ main: /* @__PURE__ */ jsxs(Fragment, { children: [
144
+ file ? /* @__PURE__ */ jsx(
145
+ SelectedPdfCard,
146
+ {
147
+ file,
148
+ pageCount: 0,
149
+ detail: bytes ? encrypted ? "Password protected" : "Not protected" : "reading...",
150
+ onRemove: clearFile,
151
+ disabled: busy
152
+ }
153
+ ) : /* @__PURE__ */ jsx(
154
+ FileDropzone,
155
+ {
156
+ accept: ".pdf,application/pdf",
157
+ disabled: busy,
158
+ onFiles: selectFiles,
159
+ icon: "fa-solid fa-lock",
160
+ title: "Drag and drop a PDF here",
161
+ hint: "Protected PDFs open in Unlock mode automatically."
162
+ }
163
+ ),
164
+ bytes && !encrypted ? /* @__PURE__ */ jsxs("form", { onSubmit: handleProtect, className: `mt-5 ${cardClass}`, children: [
165
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-slate-900 dark:text-slate-100", children: "Protect" }),
166
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-4 sm:grid-cols-2", children: [
167
+ /* @__PURE__ */ jsx(
168
+ PasswordInput,
169
+ {
170
+ id: "user-password",
171
+ label: "Password to open",
172
+ value: userPassword,
173
+ onChange: setUserPassword,
174
+ disabled: busy,
175
+ hint: userPassword && userPassword.length < 8 ? "Short passwords are easy to guess. Use 8+ characters." : "Leave empty to only restrict printing, copying, or editing."
176
+ }
177
+ ),
178
+ /* @__PURE__ */ jsx(
179
+ PasswordInput,
180
+ {
181
+ id: "confirm-password",
182
+ label: "Confirm password",
183
+ value: confirmPassword,
184
+ onChange: setConfirmPassword,
185
+ disabled: busy || !userPassword,
186
+ hint: mismatch ? "Passwords don't match." : ""
187
+ }
188
+ )
189
+ ] }),
190
+ /* @__PURE__ */ jsxs("fieldset", { className: "mt-5 grid gap-3 sm:grid-cols-3", disabled: busy, children: [
191
+ /* @__PURE__ */ jsx("legend", { className: `${fieldLabelClass} mb-2`, children: "Permissions" }),
192
+ /* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
193
+ "Printing",
194
+ /* @__PURE__ */ jsxs("select", { value: print, onChange: (event) => setPrint(event.target.value), className: `mt-2 ${inputClass}`, children: [
195
+ /* @__PURE__ */ jsx("option", { value: "full", children: "Allowed" }),
196
+ /* @__PURE__ */ jsx("option", { value: "low", children: "Low quality only" }),
197
+ /* @__PURE__ */ jsx("option", { value: "none", children: "Not allowed" })
198
+ ] })
199
+ ] }),
200
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 self-end text-sm text-slate-700 dark:text-slate-300", children: [
201
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: extract, onChange: (event) => setExtract(event.target.checked), className: "h-4 w-4 accent-[#1F8CFF]" }),
202
+ "Allow copying text"
203
+ ] }),
204
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 self-end text-sm text-slate-700 dark:text-slate-300", children: [
205
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: modify, onChange: (event) => setModify(event.target.checked), className: "h-4 w-4 accent-[#1F8CFF]" }),
206
+ "Allow editing"
207
+ ] })
208
+ ] }),
209
+ restricted ? /* @__PURE__ */ jsx("div", { className: "mt-5", children: /* @__PURE__ */ jsx(
210
+ PasswordInput,
211
+ {
212
+ id: "owner-password",
213
+ label: "Owner password (to change permissions later)",
214
+ value: ownerPassword,
215
+ onChange: setOwnerPassword,
216
+ disabled: busy,
217
+ hint: "Optional. If empty, a strong random one is generated and shown after saving."
218
+ }
219
+ ) }) : null,
220
+ /* @__PURE__ */ jsx(StatusBanner, { status }),
221
+ generatedOwner ? /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100", children: [
222
+ /* @__PURE__ */ jsx("p", { className: "font-semibold", children: "Generated owner password, save it if you may want to change permissions:" }),
223
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
224
+ /* @__PURE__ */ jsx("code", { className: "select-all break-all rounded bg-white/80 px-2 py-1 text-sm text-slate-900 dark:bg-slate-900 dark:text-slate-100", children: generatedOwner }),
225
+ /* @__PURE__ */ jsxs(
226
+ "button",
227
+ {
228
+ type: "button",
229
+ onClick: () => navigator.clipboard?.writeText(generatedOwner),
230
+ className: secondaryButtonClass,
231
+ children: [
232
+ /* @__PURE__ */ jsx("i", { className: "fa-solid fa-copy" }),
233
+ "Copy"
234
+ ]
235
+ }
236
+ )
237
+ ] })
238
+ ] }) : null,
239
+ /* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-wrap gap-3", children: [
240
+ /* @__PURE__ */ jsxs("button", { type: "submit", disabled: busy || !canProtect, className: primaryButtonClass, children: [
241
+ /* @__PURE__ */ jsx("i", { className: `fa-solid ${isWorking ? "fa-spinner fa-spin" : "fa-lock"}` }),
242
+ isWorking ? "Protecting..." : "Protect & Download"
243
+ ] }),
244
+ /* @__PURE__ */ jsx(BackToPdfTools, {})
245
+ ] })
246
+ ] }) : null,
247
+ bytes && encrypted ? /* @__PURE__ */ jsxs("form", { onSubmit: handleUnlock, className: `mt-5 ${cardClass}`, children: [
248
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-slate-900 dark:text-slate-100", children: "Unlock" }),
249
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-slate-600 dark:text-slate-300", children: needsPassword ? "This PDF needs its password to open. Enter it to remove the protection." : "Files that only restrict printing or copying unlock without a password. Otherwise, enter the password." }),
250
+ /* @__PURE__ */ jsx("div", { className: "mt-4 max-w-md", children: /* @__PURE__ */ jsx(
251
+ PasswordInput,
252
+ {
253
+ id: "unlock-password",
254
+ label: "Password",
255
+ value: unlockPassword,
256
+ onChange: setUnlockPassword,
257
+ disabled: busy,
258
+ autoComplete: "off",
259
+ inputRef: unlockInputRef
260
+ }
261
+ ) }),
262
+ /* @__PURE__ */ jsx(StatusBanner, { status }),
263
+ /* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-wrap gap-3", children: [
264
+ /* @__PURE__ */ jsxs("button", { type: "submit", disabled: busy, className: primaryButtonClass, children: [
265
+ /* @__PURE__ */ jsx("i", { className: `fa-solid ${isWorking ? "fa-spinner fa-spin" : "fa-lock-open"}` }),
266
+ isWorking ? "Unlocking..." : "Unlock & Download"
267
+ ] }),
268
+ /* @__PURE__ */ jsx(BackToPdfTools, {})
269
+ ] })
270
+ ] }) : null,
271
+ !bytes ? /* @__PURE__ */ jsxs(Fragment, { children: [
272
+ /* @__PURE__ */ jsx(StatusBanner, { status }),
273
+ /* @__PURE__ */ jsx("div", { className: "mt-5 flex flex-wrap gap-3", children: /* @__PURE__ */ jsx(BackToPdfTools, {}) })
274
+ ] }) : null
275
+ ] }),
276
+ side: /* @__PURE__ */ jsxs(Fragment, { children: [
277
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-slate-900 dark:text-slate-100", children: "How it works" }),
278
+ /* @__PURE__ */ jsx(
279
+ SideTips,
280
+ {
281
+ tips: [
282
+ "Encryption uses AES-256 (PDF 2.0 standard), supported by all modern viewers.",
283
+ "The open password can't be recovered if you forget it. Keep a copy somewhere safe.",
284
+ "Permissions are honoured by viewers but are not a hard lock: anyone who can open the file could remove them.",
285
+ "Unlock only works with a password you know; it doesn't crack passwords."
286
+ ]
287
+ }
288
+ ),
289
+ /* @__PURE__ */ jsx("p", { className: "mt-4 text-[11px] text-slate-500 dark:text-slate-400", children: "Powered by qpdf (Apache-2.0) compiled to WebAssembly. The engine (~1.3 MB) loads the first time you use this tool." })
290
+ ] })
291
+ }
292
+ )
293
+ }
294
+ );
295
+ }
296
+
297
+ export { ProtectPdf };
298
+ //# sourceMappingURL=chunk-PRBKSX3A.js.map
299
+ //# sourceMappingURL=chunk-PRBKSX3A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protect/ProtectPdf.tsx"],"names":[],"mappings":"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","file":"chunk-PRBKSX3A.js","sourcesContent":["import React, { useEffect, useRef, useState } from \"react\";\nimport { downloadBlob, formatBytes, isPdfFile, toBaseName } from \"../shared/files\";\nimport {\n BackToPdfTools,\n FileDropzone,\n PdfToolColumns,\n PdfToolPage,\n SelectedPdfCard,\n SideTips,\n StatusBanner,\n cardClass,\n fieldLabelClass,\n inputClass,\n primaryButtonClass,\n secondaryButtonClass,\n} from \"../shared/ToolLayout\";\nimport { PasswordInput } from \"../shared/PasswordInput\";\nimport { browserQpdf } from \"./browserQpdf\";\nimport { PasswordError, isEncrypted, protectPdf, unlockPdf, type PrintPermission } from \"./protectPdfCore\";\n\nconst MAX_BYTES = 200 * 1024 * 1024;\n\nexport default function ProtectPdf() {\n const [file, setFile] = useState(null);\n const [bytes, setBytes] = useState(null);\n const [encrypted, setEncrypted] = useState(false);\n const [mode, setMode] = useState(\"protect\");\n const [userPassword, setUserPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [ownerPassword, setOwnerPassword] = useState(\"\");\n const [print, setPrint] = useState<PrintPermission>(\"full\");\n const [extract, setExtract] = useState(true);\n const [modify, setModify] = useState(true);\n const [unlockPassword, setUnlockPassword] = useState(\"\");\n const [needsPassword, setNeedsPassword] = useState(false);\n const [generatedOwner, setGeneratedOwner] = useState(\"\");\n const [isWorking, setIsWorking] = useState(false);\n const [status, setStatus] = useState({\n tone: \"info\",\n text: \"Add a PDF to protect it with a password, or to remove a password you know.\",\n });\n const unlockInputRef = useRef(null);\n const requestRef = useRef(0);\n\n useEffect(() => {\n document.title = \"Protect / Unlock PDF | Apps\";\n }, []);\n\n // After a password error, put the cursor in the password box once the form\n // is enabled again (a disabled input can't take focus).\n useEffect(() => {\n if (needsPassword && !isWorking) unlockInputRef.current?.focus();\n }, [needsPassword, isWorking, status]);\n\n const resetSecrets = () => {\n setUserPassword(\"\");\n setConfirmPassword(\"\");\n setOwnerPassword(\"\");\n setUnlockPassword(\"\");\n setGeneratedOwner(\"\");\n setNeedsPassword(false);\n };\n\n const selectFiles = async (files) => {\n const next = files.find(isPdfFile);\n if (!next) {\n if (files.length) setStatus({ tone: \"error\", text: \"Only PDF files are supported.\" });\n return;\n }\n if (next.size > MAX_BYTES) {\n setStatus({ tone: \"error\", text: \"This PDF is larger than 200 MB.\" });\n return;\n }\n const request = ++requestRef.current;\n resetSecrets();\n setFile(next);\n setBytes(null);\n try {\n const data = new Uint8Array(await next.arrayBuffer());\n const locked = await isEncrypted(data);\n if (request !== requestRef.current) return;\n setBytes(data);\n setEncrypted(locked);\n setMode(locked ? \"unlock\" : \"protect\");\n setStatus(\n locked\n ? { tone: \"info\", text: `${next.name} is protected. Enter its password if it has one, then unlock.` }\n : { tone: \"success\", text: `${next.name} loaded. Choose a password and permissions.` }\n );\n } catch (error) {\n if (request !== requestRef.current) return;\n setFile(null);\n setStatus({ tone: \"error\", text: error?.message || \"Could not read this PDF.\" });\n }\n };\n\n const clearFile = () => {\n requestRef.current += 1;\n setFile(null);\n setBytes(null);\n setEncrypted(false);\n resetSecrets();\n };\n\n const mismatch = Boolean(confirmPassword) && userPassword !== confirmPassword;\n const restricted = print !== \"full\" || !extract || !modify;\n const canProtect = Boolean(bytes) && !encrypted && !mismatch && (userPassword ? userPassword === confirmPassword : restricted);\n\n const handleProtect = async (event) => {\n event.preventDefault();\n if (!canProtect) return;\n setIsWorking(true);\n setGeneratedOwner(\"\");\n setStatus({ tone: \"info\", text: \"Encrypting your PDF...\" });\n try {\n const result = await protectPdf(bytes, { userPassword, ownerPassword, print, extract, modify }, browserQpdf);\n const blob = new Blob([result.bytes as BlobPart], { type: \"application/pdf\" });\n downloadBlob(blob, `${toBaseName(file.name, \"document\")}-protected.pdf`);\n if (!ownerPassword) setGeneratedOwner(result.ownerPassword);\n setStatus({\n tone: \"success\",\n text: `Protected with AES-256 (${formatBytes(blob.size)}). Your download has started.`,\n });\n } catch (error) {\n setStatus({ tone: \"error\", text: error?.message || \"Could not protect this PDF.\" });\n } finally {\n setIsWorking(false);\n }\n };\n\n const handleUnlock = async (event) => {\n event.preventDefault();\n if (!bytes || !encrypted) return;\n setIsWorking(true);\n setStatus({ tone: \"info\", text: \"Unlocking your PDF...\" });\n try {\n const unlocked = await unlockPdf(bytes, unlockPassword, browserQpdf);\n const blob = new Blob([unlocked as BlobPart], { type: \"application/pdf\" });\n downloadBlob(blob, `${toBaseName(file.name, \"document\")}-unlocked.pdf`);\n setNeedsPassword(false);\n setStatus({\n tone: \"success\",\n text: `Unlocked (${formatBytes(blob.size)}). The password and restrictions are removed. Your download has started.`,\n });\n } catch (error) {\n if (error instanceof PasswordError) {\n setNeedsPassword(true);\n setStatus({ tone: \"error\", text: error.message });\n } else {\n setStatus({ tone: \"error\", text: error?.message || \"Could not unlock this PDF.\" });\n }\n } finally {\n setIsWorking(false);\n }\n };\n\n const busy = isWorking;\n\n return (\n <PdfToolPage\n title=\"Protect / Unlock PDF\"\n description=\"Add a password and permissions to a PDF with AES-256 encryption, or remove a password you know. Runs in your browser with qpdf; files and passwords never leave your device.\"\n stats={[\n { value: \"AES\", label: \"256-bit\" },\n { value: file ? (encrypted ? \"Locked\" : \"Open\") : \"-\", label: \"File\" },\n ]}\n >\n <PdfToolColumns\n main={\n <>\n {file ? (\n <SelectedPdfCard\n file={file}\n pageCount={0}\n detail={bytes ? (encrypted ? \"Password protected\" : \"Not protected\") : \"reading...\"}\n onRemove={clearFile}\n disabled={busy}\n />\n ) : (\n <FileDropzone\n accept=\".pdf,application/pdf\"\n disabled={busy}\n onFiles={selectFiles}\n icon=\"fa-solid fa-lock\"\n title=\"Drag and drop a PDF here\"\n hint=\"Protected PDFs open in Unlock mode automatically.\"\n />\n )}\n\n {bytes && !encrypted ? (\n <form onSubmit={handleProtect} className={`mt-5 ${cardClass}`}>\n <h3 className=\"text-sm font-semibold text-slate-900 dark:text-slate-100\">Protect</h3>\n <div className=\"mt-4 grid gap-4 sm:grid-cols-2\">\n <PasswordInput\n id=\"user-password\"\n label=\"Password to open\"\n value={userPassword}\n onChange={setUserPassword}\n disabled={busy}\n hint={userPassword && userPassword.length < 8 ? \"Short passwords are easy to guess. Use 8+ characters.\" : \"Leave empty to only restrict printing, copying, or editing.\"}\n />\n <PasswordInput\n id=\"confirm-password\"\n label=\"Confirm password\"\n value={confirmPassword}\n onChange={setConfirmPassword}\n disabled={busy || !userPassword}\n hint={mismatch ? \"Passwords don't match.\" : \"\"}\n />\n </div>\n\n <fieldset className=\"mt-5 grid gap-3 sm:grid-cols-3\" disabled={busy}>\n <legend className={`${fieldLabelClass} mb-2`}>Permissions</legend>\n <label className={fieldLabelClass}>\n Printing\n <select value={print} onChange={(event) => setPrint(event.target.value as PrintPermission)} className={`mt-2 ${inputClass}`}>\n <option value=\"full\">Allowed</option>\n <option value=\"low\">Low quality only</option>\n <option value=\"none\">Not allowed</option>\n </select>\n </label>\n <label className=\"flex items-center gap-2 self-end text-sm text-slate-700 dark:text-slate-300\">\n <input type=\"checkbox\" checked={extract} onChange={(event) => setExtract(event.target.checked)} className=\"h-4 w-4 accent-[#1F8CFF]\" />\n Allow copying text\n </label>\n <label className=\"flex items-center gap-2 self-end text-sm text-slate-700 dark:text-slate-300\">\n <input type=\"checkbox\" checked={modify} onChange={(event) => setModify(event.target.checked)} className=\"h-4 w-4 accent-[#1F8CFF]\" />\n Allow editing\n </label>\n </fieldset>\n\n {restricted ? (\n <div className=\"mt-5\">\n <PasswordInput\n id=\"owner-password\"\n label=\"Owner password (to change permissions later)\"\n value={ownerPassword}\n onChange={setOwnerPassword}\n disabled={busy}\n hint=\"Optional. If empty, a strong random one is generated and shown after saving.\"\n />\n </div>\n ) : null}\n\n <StatusBanner status={status} />\n\n {generatedOwner ? (\n <div className=\"mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100\">\n <p className=\"font-semibold\">Generated owner password, save it if you may want to change permissions:</p>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <code className=\"select-all break-all rounded bg-white/80 px-2 py-1 text-sm text-slate-900 dark:bg-slate-900 dark:text-slate-100\">\n {generatedOwner}\n </code>\n <button\n type=\"button\"\n onClick={() => navigator.clipboard?.writeText(generatedOwner)}\n className={secondaryButtonClass}\n >\n <i className=\"fa-solid fa-copy\" />\n Copy\n </button>\n </div>\n </div>\n ) : null}\n\n <div className=\"mt-5 flex flex-wrap gap-3\">\n <button type=\"submit\" disabled={busy || !canProtect} className={primaryButtonClass}>\n <i className={`fa-solid ${isWorking ? \"fa-spinner fa-spin\" : \"fa-lock\"}`} />\n {isWorking ? \"Protecting...\" : \"Protect & Download\"}\n </button>\n <BackToPdfTools />\n </div>\n </form>\n ) : null}\n\n {bytes && encrypted ? (\n <form onSubmit={handleUnlock} className={`mt-5 ${cardClass}`}>\n <h3 className=\"text-sm font-semibold text-slate-900 dark:text-slate-100\">Unlock</h3>\n <p className=\"mt-1 text-xs text-slate-600 dark:text-slate-300\">\n {needsPassword\n ? \"This PDF needs its password to open. Enter it to remove the protection.\"\n : \"Files that only restrict printing or copying unlock without a password. Otherwise, enter the password.\"}\n </p>\n <div className=\"mt-4 max-w-md\">\n <PasswordInput\n id=\"unlock-password\"\n label=\"Password\"\n value={unlockPassword}\n onChange={setUnlockPassword}\n disabled={busy}\n autoComplete=\"off\"\n inputRef={unlockInputRef}\n />\n </div>\n\n <StatusBanner status={status} />\n\n <div className=\"mt-5 flex flex-wrap gap-3\">\n <button type=\"submit\" disabled={busy} className={primaryButtonClass}>\n <i className={`fa-solid ${isWorking ? \"fa-spinner fa-spin\" : \"fa-lock-open\"}`} />\n {isWorking ? \"Unlocking...\" : \"Unlock & Download\"}\n </button>\n <BackToPdfTools />\n </div>\n </form>\n ) : null}\n\n {!bytes ? (\n <>\n <StatusBanner status={status} />\n <div className=\"mt-5 flex flex-wrap gap-3\">\n <BackToPdfTools />\n </div>\n </>\n ) : null}\n </>\n }\n side={\n <>\n <h2 className=\"text-base font-semibold text-slate-900 dark:text-slate-100\">How it works</h2>\n <SideTips\n tips={[\n \"Encryption uses AES-256 (PDF 2.0 standard), supported by all modern viewers.\",\n \"The open password can't be recovered if you forget it. Keep a copy somewhere safe.\",\n \"Permissions are honoured by viewers but are not a hard lock: anyone who can open the file could remove them.\",\n \"Unlock only works with a password you know; it doesn't crack passwords.\",\n ]}\n />\n <p className=\"mt-4 text-[11px] text-slate-500 dark:text-slate-400\">\n Powered by qpdf (Apache-2.0) compiled to WebAssembly. The engine (~1.3 MB) loads the first time you use this tool.\n </p>\n </>\n }\n />\n </PdfToolPage>\n );\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import { visualSize, textOrigin } from './chunk-PMH7OWPF.js';
2
+ import { loadPdf } from './chunk-EKHEY3LI.js';
3
+ import { degrees } from 'pdf-lib';
4
+
5
+ function placementToVisual(placement, visualWidth, visualHeight) {
6
+ const width = placement.width * visualWidth;
7
+ const height = placement.height * visualHeight;
8
+ return {
9
+ vx: placement.x * visualWidth,
10
+ vy: visualHeight - placement.y * visualHeight - height,
11
+ width,
12
+ height
13
+ };
14
+ }
15
+ async function applySignatures(bytes, placements) {
16
+ if (!placements.length) {
17
+ throw new Error("Place at least one signature first.");
18
+ }
19
+ const doc = await loadPdf(bytes);
20
+ const pages = doc.getPages();
21
+ const embedded = /* @__PURE__ */ new Map();
22
+ for (const placement of placements) {
23
+ const page = pages[placement.pageIndex];
24
+ if (!page) {
25
+ throw new Error(`Page ${placement.pageIndex + 1} does not exist.`);
26
+ }
27
+ let image = embedded.get(placement.image);
28
+ if (!image) {
29
+ image = await doc.embedPng(placement.image);
30
+ embedded.set(placement.image, image);
31
+ }
32
+ const { visualWidth, visualHeight } = visualSize(page);
33
+ const box = placementToVisual(placement, visualWidth, visualHeight);
34
+ const origin = textOrigin(page, box.vx, box.vy);
35
+ page.drawImage(image, {
36
+ x: origin.x,
37
+ y: origin.y,
38
+ width: box.width,
39
+ height: box.height,
40
+ rotate: degrees(origin.angle)
41
+ });
42
+ }
43
+ return doc.save();
44
+ }
45
+
46
+ export { applySignatures, placementToVisual };
47
+ //# sourceMappingURL=chunk-SVPUX7AM.js.map
48
+ //# sourceMappingURL=chunk-SVPUX7AM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sign/signPdfCore.ts"],"names":[],"mappings":";;;;AAkBO,SAAS,iBAAA,CACd,SAAA,EACA,WAAA,EACA,YAAA,EACA;AACA,EAAA,MAAM,KAAA,GAAQ,UAAU,KAAA,GAAQ,WAAA;AAChC,EAAA,MAAM,MAAA,GAAS,UAAU,MAAA,GAAS,YAAA;AAClC,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,UAAU,CAAA,GAAI,WAAA;AAAA,IAClB,EAAA,EAAI,YAAA,GAAe,SAAA,CAAU,CAAA,GAAI,YAAA,GAAe,MAAA;AAAA,IAChD,KAAA;AAAA,IACA;AAAA,GACF;AACF;AAEA,eAAsB,eAAA,CACpB,OACA,UAAA,EACA;AACA,EAAA,IAAI,CAAC,WAAW,MAAA,EAAQ;AACtB,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACvD;AAEA,EAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,KAAK,CAAA;AAC/B,EAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAS;AAC3B,EAAA,MAAM,QAAA,uBAAe,GAAA,EAA0B;AAE/C,EAAA,KAAA,MAAW,aAAa,UAAA,EAAY;AAClC,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,SAAA,CAAU,SAAS,CAAA;AACtC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,KAAA,EAAQ,SAAA,CAAU,SAAA,GAAY,CAAC,CAAA,gBAAA,CAAkB,CAAA;AAAA,IACnE;AAEA,IAAA,IAAI,KAAA,GAAQ,QAAA,CAAS,GAAA,CAAI,SAAA,CAAU,KAAK,CAAA;AACxC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,KAAA,GAAQ,MAAM,GAAA,CAAI,QAAA,CAAS,SAAA,CAAU,KAAK,CAAA;AAC1C,MAAA,QAAA,CAAS,GAAA,CAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,EAAE,WAAA,EAAa,YAAA,EAAa,GAAI,WAAW,IAAI,CAAA;AACrD,IAAA,MAAM,GAAA,GAAM,iBAAA,CAAkB,SAAA,EAAW,WAAA,EAAa,YAAY,CAAA;AAClE,IAAA,MAAM,SAAS,UAAA,CAAW,IAAA,EAAM,GAAA,CAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AAC9C,IAAA,IAAA,CAAK,UAAU,KAAA,EAAO;AAAA,MACpB,GAAG,MAAA,CAAO,CAAA;AAAA,MACV,GAAG,MAAA,CAAO,CAAA;AAAA,MACV,OAAO,GAAA,CAAI,KAAA;AAAA,MACX,QAAQ,GAAA,CAAI,MAAA;AAAA,MACZ,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,KAAK;AAAA,KAC7B,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,IAAI,IAAA,EAAK;AAClB","file":"chunk-SVPUX7AM.js","sourcesContent":["import { PDFImage, degrees } from \"pdf-lib\";\nimport { loadPdf } from \"../shared/files\";\nimport { textOrigin, visualSize } from \"../shared/pageGeometry\";\n\n/**\n * A signature image placed on a page. x/y/width/height are fractions (0-1)\n * of the visible page, measured from its top-left corner, as on screen.\n */\nexport type SignaturePlacement = {\n pageIndex: number;\n image: Uint8Array;\n x: number;\n y: number;\n width: number;\n height: number;\n};\n\n/** Convert a top-left fractional box into a bottom-left box in visual points. */\nexport function placementToVisual(\n placement: Pick<SignaturePlacement, \"x\" | \"y\" | \"width\" | \"height\">,\n visualWidth: number,\n visualHeight: number\n) {\n const width = placement.width * visualWidth;\n const height = placement.height * visualHeight;\n return {\n vx: placement.x * visualWidth,\n vy: visualHeight - placement.y * visualHeight - height,\n width,\n height,\n };\n}\n\nexport async function applySignatures(\n bytes: ArrayBuffer | Uint8Array,\n placements: SignaturePlacement[]\n) {\n if (!placements.length) {\n throw new Error(\"Place at least one signature first.\");\n }\n\n const doc = await loadPdf(bytes);\n const pages = doc.getPages();\n const embedded = new Map<Uint8Array, PDFImage>();\n\n for (const placement of placements) {\n const page = pages[placement.pageIndex];\n if (!page) {\n throw new Error(`Page ${placement.pageIndex + 1} does not exist.`);\n }\n\n let image = embedded.get(placement.image);\n if (!image) {\n image = await doc.embedPng(placement.image);\n embedded.set(placement.image, image);\n }\n\n const { visualWidth, visualHeight } = visualSize(page);\n const box = placementToVisual(placement, visualWidth, visualHeight);\n const origin = textOrigin(page, box.vx, box.vy);\n page.drawImage(image, {\n x: origin.x,\n y: origin.y,\n width: box.width,\n height: box.height,\n rotate: degrees(origin.angle),\n });\n }\n\n return doc.save();\n}\n"]}