@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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +229 -2
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/dist/chunk-2K5X7L5J.js +43 -0
- package/dist/chunk-2K5X7L5J.js.map +1 -0
- package/dist/chunk-4EAYBJ6G.js +362 -0
- package/dist/chunk-4EAYBJ6G.js.map +1 -0
- package/dist/chunk-4LBJTDV5.js +165 -0
- package/dist/chunk-4LBJTDV5.js.map +1 -0
- package/dist/chunk-4USCGC24.js +304 -0
- package/dist/chunk-4USCGC24.js.map +1 -0
- package/dist/chunk-5OIMR74G.js +273 -0
- package/dist/chunk-5OIMR74G.js.map +1 -0
- package/dist/chunk-6NFNAKWA.js +257 -0
- package/dist/chunk-6NFNAKWA.js.map +1 -0
- package/dist/chunk-7MTEMQLL.js +47 -0
- package/dist/chunk-7MTEMQLL.js.map +1 -0
- package/dist/chunk-7V4SWTUK.js +83 -0
- package/dist/chunk-7V4SWTUK.js.map +1 -0
- package/dist/chunk-7XH7MFZ4.js +295 -0
- package/dist/chunk-7XH7MFZ4.js.map +1 -0
- package/dist/chunk-BK3MYSUS.js +61 -0
- package/dist/chunk-BK3MYSUS.js.map +1 -0
- package/dist/chunk-BLHNKR3S.js +35 -0
- package/dist/chunk-BLHNKR3S.js.map +1 -0
- package/dist/chunk-CCJ43SIX.js +342 -0
- package/dist/chunk-CCJ43SIX.js.map +1 -0
- package/dist/chunk-CNEDWATL.js +210 -0
- package/dist/chunk-CNEDWATL.js.map +1 -0
- package/dist/chunk-CYTWDJE2.js +152 -0
- package/dist/chunk-CYTWDJE2.js.map +1 -0
- package/dist/chunk-DKVOICYZ.js +45 -0
- package/dist/chunk-DKVOICYZ.js.map +1 -0
- package/dist/chunk-DPEJU4P6.js +79 -0
- package/dist/chunk-DPEJU4P6.js.map +1 -0
- package/dist/chunk-DQPABZTU.js +22 -0
- package/dist/chunk-DQPABZTU.js.map +1 -0
- package/dist/chunk-EKHEY3LI.js +46 -0
- package/dist/chunk-EKHEY3LI.js.map +1 -0
- package/dist/chunk-G467LFKC.js +191 -0
- package/dist/chunk-G467LFKC.js.map +1 -0
- package/dist/chunk-GLYWSVSB.js +250 -0
- package/dist/chunk-GLYWSVSB.js.map +1 -0
- package/dist/chunk-GXBIMUO7.js +135 -0
- package/dist/chunk-GXBIMUO7.js.map +1 -0
- package/dist/chunk-H6EERRCR.js +180 -0
- package/dist/chunk-H6EERRCR.js.map +1 -0
- package/dist/chunk-HDTM3LXD.js +35 -0
- package/dist/chunk-HDTM3LXD.js.map +1 -0
- package/dist/chunk-IGMX56C3.js +180 -0
- package/dist/chunk-IGMX56C3.js.map +1 -0
- package/dist/chunk-J4IRICCJ.js +74 -0
- package/dist/chunk-J4IRICCJ.js.map +1 -0
- package/dist/chunk-JXKQ7S6W.js +331 -0
- package/dist/chunk-JXKQ7S6W.js.map +1 -0
- package/dist/chunk-KY6DXS6S.js +34 -0
- package/dist/chunk-KY6DXS6S.js.map +1 -0
- package/dist/chunk-NOFGCYGS.js +22 -0
- package/dist/chunk-NOFGCYGS.js.map +1 -0
- package/dist/chunk-OSHUJRKD.js +131 -0
- package/dist/chunk-OSHUJRKD.js.map +1 -0
- package/dist/chunk-PMH7OWPF.js +40 -0
- package/dist/chunk-PMH7OWPF.js.map +1 -0
- package/dist/chunk-PRBKSX3A.js +299 -0
- package/dist/chunk-PRBKSX3A.js.map +1 -0
- package/dist/chunk-SVPUX7AM.js +48 -0
- package/dist/chunk-SVPUX7AM.js.map +1 -0
- package/dist/chunk-TR6HJ4NO.js +659 -0
- package/dist/chunk-TR6HJ4NO.js.map +1 -0
- package/dist/chunk-TV3E4YUF.js +69 -0
- package/dist/chunk-TV3E4YUF.js.map +1 -0
- package/dist/chunk-U2ROTOPF.js +256 -0
- package/dist/chunk-U2ROTOPF.js.map +1 -0
- package/dist/chunk-VCTN77K2.js +112 -0
- package/dist/chunk-VCTN77K2.js.map +1 -0
- package/dist/chunk-WEOPDD3P.js +120 -0
- package/dist/chunk-WEOPDD3P.js.map +1 -0
- package/dist/chunk-ZBZ7OC6I.js +228 -0
- package/dist/chunk-ZBZ7OC6I.js.map +1 -0
- package/dist/config-CJYo46X9.d.ts +19 -0
- package/dist/index.d.ts +487 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/react/certificate-sign.d.ts +5 -0
- package/dist/react/certificate-sign.js +14 -0
- package/dist/react/certificate-sign.js.map +1 -0
- package/dist/react/compress.d.ts +5 -0
- package/dist/react/compress.js +8 -0
- package/dist/react/compress.js.map +1 -0
- package/dist/react/fill-form.d.ts +5 -0
- package/dist/react/fill-form.js +8 -0
- package/dist/react/fill-form.js.map +1 -0
- package/dist/react/images-to-pdf.d.ts +5 -0
- package/dist/react/images-to-pdf.js +7 -0
- package/dist/react/images-to-pdf.js.map +1 -0
- package/dist/react/merge.d.ts +5 -0
- package/dist/react/merge.js +8 -0
- package/dist/react/merge.js.map +1 -0
- package/dist/react/metadata.d.ts +5 -0
- package/dist/react/metadata.js +8 -0
- package/dist/react/metadata.js.map +1 -0
- package/dist/react/organize.d.ts +5 -0
- package/dist/react/organize.js +12 -0
- package/dist/react/organize.js.map +1 -0
- package/dist/react/pdf-to-images.d.ts +5 -0
- package/dist/react/pdf-to-images.js +13 -0
- package/dist/react/pdf-to-images.js.map +1 -0
- package/dist/react/protect.d.ts +5 -0
- package/dist/react/protect.js +8 -0
- package/dist/react/protect.js.map +1 -0
- package/dist/react/sign.d.ts +5 -0
- package/dist/react/sign.js +13 -0
- package/dist/react/sign.js.map +1 -0
- package/dist/react/split.d.ts +5 -0
- package/dist/react/split.js +11 -0
- package/dist/react/split.js.map +1 -0
- package/dist/react/verify-signatures.d.ts +5 -0
- package/dist/react/verify-signatures.js +8 -0
- package/dist/react/verify-signatures.js.map +1 -0
- package/dist/react/watermark.d.ts +5 -0
- package/dist/react/watermark.js +10 -0
- package/dist/react/watermark.js.map +1 -0
- package/dist/react.d.ts +40 -0
- package/dist/react.js +40 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/vite.d.ts +5 -0
- package/dist/vite.js +9 -0
- package/dist/vite.js.map +1 -0
- package/package.json +163 -4
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { formatPageNumber, stampPdf } from './chunk-VCTN77K2.js';
|
|
2
|
+
import { useSinglePdf } from './chunk-BK3MYSUS.js';
|
|
3
|
+
import { PdfToolPage, PdfToolColumns, SideTips, SelectedPdfCard, FileDropzone, cardClass, fieldLabelClass, inputClass, StatusBanner, primaryButtonClass, BackToPdfTools } from './chunk-H6EERRCR.js';
|
|
4
|
+
import { toBaseName, downloadBlob, formatBytes } from './chunk-EKHEY3LI.js';
|
|
5
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
6
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var NUMBER_FORMATS = ["{n}", "Page {n}", "Page {n} of {total}", "{n} / {total}"];
|
|
9
|
+
var POSITIONS = [
|
|
10
|
+
{ value: "bottom-center", label: "Bottom center" },
|
|
11
|
+
{ value: "bottom-right", label: "Bottom right" },
|
|
12
|
+
{ value: "bottom-left", label: "Bottom left" },
|
|
13
|
+
{ value: "top-center", label: "Top center" },
|
|
14
|
+
{ value: "top-right", label: "Top right" },
|
|
15
|
+
{ value: "top-left", label: "Top left" }
|
|
16
|
+
];
|
|
17
|
+
function Toggle({ checked, onChange, label, disabled = false }) {
|
|
18
|
+
return /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3 text-sm font-semibold text-slate-900 dark:text-slate-100", children: [
|
|
19
|
+
/* @__PURE__ */ jsx(
|
|
20
|
+
"input",
|
|
21
|
+
{
|
|
22
|
+
type: "checkbox",
|
|
23
|
+
checked,
|
|
24
|
+
onChange: (event) => onChange(event.target.checked),
|
|
25
|
+
disabled,
|
|
26
|
+
className: "h-4 w-4 accent-[#1F8CFF]"
|
|
27
|
+
}
|
|
28
|
+
),
|
|
29
|
+
label
|
|
30
|
+
] });
|
|
31
|
+
}
|
|
32
|
+
function StampPreview({ watermark, pageNumbers }) {
|
|
33
|
+
const [vertical, horizontal] = (pageNumbers?.position || "bottom-center").split("-");
|
|
34
|
+
const justify = { left: "justify-start", center: "justify-center", right: "justify-end" }[horizontal];
|
|
35
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto mt-4 flex aspect-[1/1.414] w-40 flex-col overflow-hidden rounded-md border border-slate-300 bg-white p-2 shadow-sm", children: [
|
|
36
|
+
pageNumbers && vertical === "top" ? /* @__PURE__ */ jsx("div", { className: `flex ${justify} text-[7px] text-slate-600`, children: formatPageNumber(pageNumbers.format, 1, 12) }) : null,
|
|
37
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 items-center justify-center", children: [
|
|
38
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-x-1 top-2 space-y-1", children: Array.from({ length: 9 }, (_, i) => /* @__PURE__ */ jsx("div", { className: "h-1 rounded bg-slate-200", style: { width: `${70 + i * 17 % 30}%` } }, i)) }),
|
|
39
|
+
watermark ? /* @__PURE__ */ jsx(
|
|
40
|
+
"span",
|
|
41
|
+
{
|
|
42
|
+
className: "relative whitespace-nowrap font-bold",
|
|
43
|
+
style: {
|
|
44
|
+
color: watermark.color,
|
|
45
|
+
opacity: watermark.opacity,
|
|
46
|
+
fontSize: `${Math.max(6, watermark.fontSize / 4)}px`,
|
|
47
|
+
transform: `rotate(${-watermark.rotation}deg)`
|
|
48
|
+
},
|
|
49
|
+
children: watermark.text
|
|
50
|
+
}
|
|
51
|
+
) : null
|
|
52
|
+
] }),
|
|
53
|
+
pageNumbers && vertical === "bottom" ? /* @__PURE__ */ jsx("div", { className: `flex ${justify} text-[7px] text-slate-600`, children: formatPageNumber(pageNumbers.format, 1, 12) }) : null
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
function WatermarkPdf() {
|
|
57
|
+
const [useWatermark, setUseWatermark] = useState(true);
|
|
58
|
+
const [watermarkText, setWatermarkText] = useState("CONFIDENTIAL");
|
|
59
|
+
const [watermarkSize, setWatermarkSize] = useState(60);
|
|
60
|
+
const [watermarkOpacity, setWatermarkOpacity] = useState(0.2);
|
|
61
|
+
const [watermarkRotation, setWatermarkRotation] = useState(45);
|
|
62
|
+
const [watermarkColor, setWatermarkColor] = useState("#e11d48");
|
|
63
|
+
const [useNumbers, setUseNumbers] = useState(false);
|
|
64
|
+
const [numberFormat, setNumberFormat] = useState("Page {n} of {total}");
|
|
65
|
+
const [numberPosition, setNumberPosition] = useState("bottom-center");
|
|
66
|
+
const [numberSize, setNumberSize] = useState(11);
|
|
67
|
+
const [startAt, setStartAt] = useState(1);
|
|
68
|
+
const [pages, setPages] = useState("");
|
|
69
|
+
const [outputName, setOutputName] = useState("");
|
|
70
|
+
const [isStamping, setIsStamping] = useState(false);
|
|
71
|
+
const [status, setStatus] = useState({
|
|
72
|
+
tone: "info",
|
|
73
|
+
text: "Add a PDF, then stamp a text watermark, page numbers, or both."
|
|
74
|
+
});
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
document.title = "Watermark & Page Numbers | Apps";
|
|
77
|
+
}, []);
|
|
78
|
+
const onError = useCallback((text) => setStatus({ tone: "error", text }), []);
|
|
79
|
+
const onLoaded = useCallback(({ file, pageCount }) => {
|
|
80
|
+
setOutputName(`${toBaseName(file.name, "document")}-stamped`);
|
|
81
|
+
setStatus({
|
|
82
|
+
tone: "success",
|
|
83
|
+
text: `${file.name} loaded with ${pageCount} page${pageCount === 1 ? "" : "s"}.`
|
|
84
|
+
});
|
|
85
|
+
}, []);
|
|
86
|
+
const pdf = useSinglePdf({ onError, onLoaded });
|
|
87
|
+
const watermark = useWatermark ? {
|
|
88
|
+
text: watermarkText,
|
|
89
|
+
fontSize: Number(watermarkSize),
|
|
90
|
+
opacity: Number(watermarkOpacity),
|
|
91
|
+
rotation: Number(watermarkRotation),
|
|
92
|
+
color: watermarkColor
|
|
93
|
+
} : null;
|
|
94
|
+
const pageNumbers = useNumbers ? {
|
|
95
|
+
format: numberFormat,
|
|
96
|
+
position: numberPosition,
|
|
97
|
+
fontSize: Number(numberSize),
|
|
98
|
+
startAt: Number(startAt)
|
|
99
|
+
} : null;
|
|
100
|
+
const hasStamp = Boolean(watermark && watermarkText.trim() || pageNumbers);
|
|
101
|
+
const handleStamp = async (event) => {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
if (!pdf.bytes || !hasStamp) return;
|
|
104
|
+
setIsStamping(true);
|
|
105
|
+
setStatus({ tone: "info", text: "Stamping your PDF..." });
|
|
106
|
+
try {
|
|
107
|
+
const bytes = await stampPdf(pdf.bytes, { pages, watermark, pageNumbers });
|
|
108
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
109
|
+
downloadBlob(blob, `${toBaseName(outputName, "stamped")}.pdf`);
|
|
110
|
+
setStatus({
|
|
111
|
+
tone: "success",
|
|
112
|
+
text: `Done (${formatBytes(blob.size)}). Your download has started.`
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
setStatus({ tone: "error", text: `Stamping failed: ${error?.message || "Please try again."}` });
|
|
116
|
+
} finally {
|
|
117
|
+
setIsStamping(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const busy = isStamping || pdf.isReading;
|
|
121
|
+
return /* @__PURE__ */ jsx(
|
|
122
|
+
PdfToolPage,
|
|
123
|
+
{
|
|
124
|
+
title: "Watermark & Page Numbers",
|
|
125
|
+
description: "Stamp a text watermark such as DRAFT or CONFIDENTIAL, and add page numbers, on every page or just the ones you pick. Runs in your browser.",
|
|
126
|
+
stats: [
|
|
127
|
+
{ value: pdf.pageCount || "-", label: "Pages" },
|
|
128
|
+
{ value: "PDF", label: "Output" }
|
|
129
|
+
],
|
|
130
|
+
children: /* @__PURE__ */ jsx(
|
|
131
|
+
PdfToolColumns,
|
|
132
|
+
{
|
|
133
|
+
main: /* @__PURE__ */ jsxs("form", { onSubmit: handleStamp, children: [
|
|
134
|
+
pdf.file ? /* @__PURE__ */ jsx(
|
|
135
|
+
SelectedPdfCard,
|
|
136
|
+
{
|
|
137
|
+
signed: pdf.signed,
|
|
138
|
+
file: pdf.file,
|
|
139
|
+
pageCount: pdf.pageCount,
|
|
140
|
+
onRemove: pdf.clear,
|
|
141
|
+
disabled: isStamping
|
|
142
|
+
}
|
|
143
|
+
) : /* @__PURE__ */ jsx(
|
|
144
|
+
FileDropzone,
|
|
145
|
+
{
|
|
146
|
+
accept: ".pdf,application/pdf",
|
|
147
|
+
disabled: busy,
|
|
148
|
+
onFiles: pdf.selectFiles,
|
|
149
|
+
title: "Drag and drop a PDF here",
|
|
150
|
+
hint: "or click to browse from your device"
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "mt-5 space-y-4", disabled: busy, children: [
|
|
154
|
+
/* @__PURE__ */ jsxs("div", { className: cardClass, children: [
|
|
155
|
+
/* @__PURE__ */ jsx(Toggle, { checked: useWatermark, onChange: setUseWatermark, label: "Text watermark" }),
|
|
156
|
+
useWatermark ? /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-4 sm:grid-cols-2", children: [
|
|
157
|
+
/* @__PURE__ */ jsxs("label", { className: `sm:col-span-2 ${fieldLabelClass}`, children: [
|
|
158
|
+
"Text",
|
|
159
|
+
/* @__PURE__ */ jsx(
|
|
160
|
+
"input",
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
value: watermarkText,
|
|
164
|
+
onChange: (event) => setWatermarkText(event.target.value),
|
|
165
|
+
maxLength: 80,
|
|
166
|
+
className: `mt-2 ${inputClass}`
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
] }),
|
|
170
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
171
|
+
"Size: ",
|
|
172
|
+
watermarkSize,
|
|
173
|
+
"pt",
|
|
174
|
+
/* @__PURE__ */ jsx(
|
|
175
|
+
"input",
|
|
176
|
+
{
|
|
177
|
+
type: "range",
|
|
178
|
+
min: 12,
|
|
179
|
+
max: 160,
|
|
180
|
+
value: watermarkSize,
|
|
181
|
+
onChange: (event) => setWatermarkSize(Number(event.target.value)),
|
|
182
|
+
className: "mt-2 w-full accent-[#1F8CFF]"
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
] }),
|
|
186
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
187
|
+
"Opacity: ",
|
|
188
|
+
Math.round(watermarkOpacity * 100),
|
|
189
|
+
"%",
|
|
190
|
+
/* @__PURE__ */ jsx(
|
|
191
|
+
"input",
|
|
192
|
+
{
|
|
193
|
+
type: "range",
|
|
194
|
+
min: 0.05,
|
|
195
|
+
max: 1,
|
|
196
|
+
step: 0.05,
|
|
197
|
+
value: watermarkOpacity,
|
|
198
|
+
onChange: (event) => setWatermarkOpacity(Number(event.target.value)),
|
|
199
|
+
className: "mt-2 w-full accent-[#1F8CFF]"
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
] }),
|
|
203
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
204
|
+
"Angle",
|
|
205
|
+
/* @__PURE__ */ jsxs(
|
|
206
|
+
"select",
|
|
207
|
+
{
|
|
208
|
+
value: watermarkRotation,
|
|
209
|
+
onChange: (event) => setWatermarkRotation(Number(event.target.value)),
|
|
210
|
+
className: `mt-2 ${inputClass}`,
|
|
211
|
+
children: [
|
|
212
|
+
/* @__PURE__ */ jsx("option", { value: 0, children: "Horizontal" }),
|
|
213
|
+
/* @__PURE__ */ jsx("option", { value: 45, children: "Diagonal (45\xB0)" }),
|
|
214
|
+
/* @__PURE__ */ jsx("option", { value: -45, children: "Diagonal (-45\xB0)" }),
|
|
215
|
+
/* @__PURE__ */ jsx("option", { value: 90, children: "Vertical" })
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
] }),
|
|
220
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
221
|
+
"Colour",
|
|
222
|
+
/* @__PURE__ */ jsx(
|
|
223
|
+
"input",
|
|
224
|
+
{
|
|
225
|
+
type: "color",
|
|
226
|
+
value: watermarkColor,
|
|
227
|
+
onChange: (event) => setWatermarkColor(event.target.value),
|
|
228
|
+
className: "mt-2 h-9 w-full cursor-pointer rounded-lg border border-slate-300 bg-white dark:border-slate-700 dark:bg-slate-950"
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
] })
|
|
232
|
+
] }) : null
|
|
233
|
+
] }),
|
|
234
|
+
/* @__PURE__ */ jsxs("div", { className: cardClass, children: [
|
|
235
|
+
/* @__PURE__ */ jsx(Toggle, { checked: useNumbers, onChange: setUseNumbers, label: "Page numbers" }),
|
|
236
|
+
useNumbers ? /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-4 sm:grid-cols-2", children: [
|
|
237
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
238
|
+
"Format",
|
|
239
|
+
/* @__PURE__ */ jsx(
|
|
240
|
+
"select",
|
|
241
|
+
{
|
|
242
|
+
value: numberFormat,
|
|
243
|
+
onChange: (event) => setNumberFormat(event.target.value),
|
|
244
|
+
className: `mt-2 ${inputClass}`,
|
|
245
|
+
children: NUMBER_FORMATS.map((format) => /* @__PURE__ */ jsx("option", { value: format, children: formatPageNumber(format, 1, 12) }, format))
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
250
|
+
"Position",
|
|
251
|
+
/* @__PURE__ */ jsx(
|
|
252
|
+
"select",
|
|
253
|
+
{
|
|
254
|
+
value: numberPosition,
|
|
255
|
+
onChange: (event) => setNumberPosition(event.target.value),
|
|
256
|
+
className: `mt-2 ${inputClass}`,
|
|
257
|
+
children: POSITIONS.map((position) => /* @__PURE__ */ jsx("option", { value: position.value, children: position.label }, position.value))
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
262
|
+
"Font size",
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"input",
|
|
265
|
+
{
|
|
266
|
+
type: "number",
|
|
267
|
+
min: 6,
|
|
268
|
+
max: 36,
|
|
269
|
+
value: numberSize,
|
|
270
|
+
onChange: (event) => setNumberSize(Number(event.target.value)),
|
|
271
|
+
className: `mt-2 ${inputClass}`
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
] }),
|
|
275
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
276
|
+
"Start numbering at",
|
|
277
|
+
/* @__PURE__ */ jsx(
|
|
278
|
+
"input",
|
|
279
|
+
{
|
|
280
|
+
type: "number",
|
|
281
|
+
min: 1,
|
|
282
|
+
value: startAt,
|
|
283
|
+
onChange: (event) => setStartAt(Number(event.target.value)),
|
|
284
|
+
className: `mt-2 ${inputClass}`
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
] })
|
|
288
|
+
] }) : null
|
|
289
|
+
] }),
|
|
290
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
|
|
291
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
292
|
+
"Apply to pages",
|
|
293
|
+
/* @__PURE__ */ jsx(
|
|
294
|
+
"input",
|
|
295
|
+
{
|
|
296
|
+
type: "text",
|
|
297
|
+
value: pages,
|
|
298
|
+
onChange: (event) => setPages(event.target.value),
|
|
299
|
+
placeholder: "All pages (e.g. 2-)",
|
|
300
|
+
className: `mt-2 ${inputClass}`
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
] }),
|
|
304
|
+
/* @__PURE__ */ jsxs("label", { className: fieldLabelClass, children: [
|
|
305
|
+
"Output file name",
|
|
306
|
+
/* @__PURE__ */ jsx(
|
|
307
|
+
"input",
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
value: outputName,
|
|
311
|
+
onChange: (event) => setOutputName(event.target.value),
|
|
312
|
+
placeholder: "stamped",
|
|
313
|
+
className: `mt-2 ${inputClass}`
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
] })
|
|
317
|
+
] })
|
|
318
|
+
] }),
|
|
319
|
+
/* @__PURE__ */ jsx(StatusBanner, { status }),
|
|
320
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-wrap gap-3", children: [
|
|
321
|
+
/* @__PURE__ */ jsxs(
|
|
322
|
+
"button",
|
|
323
|
+
{
|
|
324
|
+
type: "submit",
|
|
325
|
+
disabled: busy || !pdf.bytes || !hasStamp,
|
|
326
|
+
className: primaryButtonClass,
|
|
327
|
+
children: [
|
|
328
|
+
/* @__PURE__ */ jsx("i", { className: `fa-solid ${isStamping ? "fa-spinner fa-spin" : "fa-stamp"}` }),
|
|
329
|
+
isStamping ? "Stamping..." : "Apply & Download"
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
),
|
|
333
|
+
/* @__PURE__ */ jsx(BackToPdfTools, {})
|
|
334
|
+
] })
|
|
335
|
+
] }),
|
|
336
|
+
side: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
337
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-slate-900 dark:text-slate-100", children: "Preview" }),
|
|
338
|
+
/* @__PURE__ */ jsx(StampPreview, { watermark, pageNumbers }),
|
|
339
|
+
/* @__PURE__ */ jsx(
|
|
340
|
+
SideTips,
|
|
341
|
+
{
|
|
342
|
+
tips: [
|
|
343
|
+
"The preview is a rough guide to placement, not an exact render.",
|
|
344
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
345
|
+
"Skip a cover page with ",
|
|
346
|
+
/* @__PURE__ */ jsx("code", { children: "2-" }),
|
|
347
|
+
" in Apply to pages. Numbering counts only stamped pages."
|
|
348
|
+
] }),
|
|
349
|
+
"Text uses Helvetica, so it supports basic Latin characters only."
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
] })
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export { WatermarkPdf };
|
|
361
|
+
//# sourceMappingURL=chunk-4EAYBJ6G.js.map
|
|
362
|
+
//# sourceMappingURL=chunk-4EAYBJ6G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamp/WatermarkPdf.tsx"],"names":[],"mappings":"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","file":"chunk-4EAYBJ6G.js","sourcesContent":["import React, { useCallback, useEffect, useState } from \"react\";\nimport { downloadBlob, formatBytes, 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} from \"../shared/ToolLayout\";\nimport { useSinglePdf } from \"../shared/useSinglePdf\";\nimport { formatPageNumber, stampPdf, type NumberPosition } from \"./stampPdfCore\";\n\nconst NUMBER_FORMATS = [\"{n}\", \"Page {n}\", \"Page {n} of {total}\", \"{n} / {total}\"];\n\nconst POSITIONS = [\n { value: \"bottom-center\", label: \"Bottom center\" },\n { value: \"bottom-right\", label: \"Bottom right\" },\n { value: \"bottom-left\", label: \"Bottom left\" },\n { value: \"top-center\", label: \"Top center\" },\n { value: \"top-right\", label: \"Top right\" },\n { value: \"top-left\", label: \"Top left\" },\n];\n\nfunction Toggle({ checked, onChange, label, disabled = false }) {\n return (\n <label className=\"flex cursor-pointer items-center gap-3 text-sm font-semibold text-slate-900 dark:text-slate-100\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(event) => onChange(event.target.checked)}\n disabled={disabled}\n className=\"h-4 w-4 accent-[#1F8CFF]\"\n />\n {label}\n </label>\n );\n}\n\nfunction StampPreview({ watermark, pageNumbers }) {\n const [vertical, horizontal] = (pageNumbers?.position || \"bottom-center\").split(\"-\");\n const justify = { left: \"justify-start\", center: \"justify-center\", right: \"justify-end\" }[horizontal];\n\n return (\n <div className=\"mx-auto mt-4 flex aspect-[1/1.414] w-40 flex-col overflow-hidden rounded-md border border-slate-300 bg-white p-2 shadow-sm\">\n {pageNumbers && vertical === \"top\" ? (\n <div className={`flex ${justify} text-[7px] text-slate-600`}>\n {formatPageNumber(pageNumbers.format, 1, 12)}\n </div>\n ) : null}\n <div className=\"relative flex flex-1 items-center justify-center\">\n <div className=\"absolute inset-x-1 top-2 space-y-1\">\n {Array.from({ length: 9 }, (_, i) => (\n <div key={i} className=\"h-1 rounded bg-slate-200\" style={{ width: `${70 + ((i * 17) % 30)}%` }} />\n ))}\n </div>\n {watermark ? (\n <span\n className=\"relative whitespace-nowrap font-bold\"\n style={{\n color: watermark.color,\n opacity: watermark.opacity,\n fontSize: `${Math.max(6, watermark.fontSize / 4)}px`,\n transform: `rotate(${-watermark.rotation}deg)`,\n }}\n >\n {watermark.text}\n </span>\n ) : null}\n </div>\n {pageNumbers && vertical === \"bottom\" ? (\n <div className={`flex ${justify} text-[7px] text-slate-600`}>\n {formatPageNumber(pageNumbers.format, 1, 12)}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport default function WatermarkPdf() {\n const [useWatermark, setUseWatermark] = useState(true);\n const [watermarkText, setWatermarkText] = useState(\"CONFIDENTIAL\");\n const [watermarkSize, setWatermarkSize] = useState(60);\n const [watermarkOpacity, setWatermarkOpacity] = useState(0.2);\n const [watermarkRotation, setWatermarkRotation] = useState(45);\n const [watermarkColor, setWatermarkColor] = useState(\"#e11d48\");\n const [useNumbers, setUseNumbers] = useState(false);\n const [numberFormat, setNumberFormat] = useState(\"Page {n} of {total}\");\n const [numberPosition, setNumberPosition] = useState<NumberPosition>(\"bottom-center\");\n const [numberSize, setNumberSize] = useState(11);\n const [startAt, setStartAt] = useState(1);\n const [pages, setPages] = useState(\"\");\n const [outputName, setOutputName] = useState(\"\");\n const [isStamping, setIsStamping] = useState(false);\n const [status, setStatus] = useState({\n tone: \"info\",\n text: \"Add a PDF, then stamp a text watermark, page numbers, or both.\",\n });\n\n useEffect(() => {\n document.title = \"Watermark & Page Numbers | Apps\";\n }, []);\n\n const onError = useCallback((text) => setStatus({ tone: \"error\", text }), []);\n const onLoaded = useCallback(({ file, pageCount }) => {\n setOutputName(`${toBaseName(file.name, \"document\")}-stamped`);\n setStatus({\n tone: \"success\",\n text: `${file.name} loaded with ${pageCount} page${pageCount === 1 ? \"\" : \"s\"}.`,\n });\n }, []);\n const pdf = useSinglePdf({ onError, onLoaded });\n\n const watermark = useWatermark\n ? {\n text: watermarkText,\n fontSize: Number(watermarkSize),\n opacity: Number(watermarkOpacity),\n rotation: Number(watermarkRotation),\n color: watermarkColor,\n }\n : null;\n const pageNumbers = useNumbers\n ? {\n format: numberFormat,\n position: numberPosition,\n fontSize: Number(numberSize),\n startAt: Number(startAt),\n }\n : null;\n const hasStamp = Boolean((watermark && watermarkText.trim()) || pageNumbers);\n\n const handleStamp = async (event) => {\n event.preventDefault();\n if (!pdf.bytes || !hasStamp) return;\n\n setIsStamping(true);\n setStatus({ tone: \"info\", text: \"Stamping your PDF...\" });\n try {\n const bytes = await stampPdf(pdf.bytes, { pages, watermark, pageNumbers });\n const blob = new Blob([bytes as BlobPart], { type: \"application/pdf\" });\n downloadBlob(blob, `${toBaseName(outputName, \"stamped\")}.pdf`);\n setStatus({\n tone: \"success\",\n text: `Done (${formatBytes(blob.size)}). Your download has started.`,\n });\n } catch (error) {\n setStatus({ tone: \"error\", text: `Stamping failed: ${error?.message || \"Please try again.\"}` });\n } finally {\n setIsStamping(false);\n }\n };\n\n const busy = isStamping || pdf.isReading;\n\n return (\n <PdfToolPage\n title=\"Watermark & Page Numbers\"\n description=\"Stamp a text watermark such as DRAFT or CONFIDENTIAL, and add page numbers, on every page or just the ones you pick. Runs in your browser.\"\n stats={[\n { value: pdf.pageCount || \"-\", label: \"Pages\" },\n { value: \"PDF\", label: \"Output\" },\n ]}\n >\n <PdfToolColumns\n main={\n <form onSubmit={handleStamp}>\n {pdf.file ? (\n <SelectedPdfCard\n signed={pdf.signed}\n file={pdf.file}\n pageCount={pdf.pageCount}\n onRemove={pdf.clear}\n disabled={isStamping}\n />\n ) : (\n <FileDropzone\n accept=\".pdf,application/pdf\"\n disabled={busy}\n onFiles={pdf.selectFiles}\n title=\"Drag and drop a PDF here\"\n hint=\"or click to browse from your device\"\n />\n )}\n\n <fieldset className=\"mt-5 space-y-4\" disabled={busy}>\n <div className={cardClass}>\n <Toggle checked={useWatermark} onChange={setUseWatermark} label=\"Text watermark\" />\n {useWatermark ? (\n <div className=\"mt-4 grid gap-4 sm:grid-cols-2\">\n <label className={`sm:col-span-2 ${fieldLabelClass}`}>\n Text\n <input\n type=\"text\"\n value={watermarkText}\n onChange={(event) => setWatermarkText(event.target.value)}\n maxLength={80}\n className={`mt-2 ${inputClass}`}\n />\n </label>\n <label className={fieldLabelClass}>\n Size: {watermarkSize}pt\n <input\n type=\"range\"\n min={12}\n max={160}\n value={watermarkSize}\n onChange={(event) => setWatermarkSize(Number(event.target.value))}\n className=\"mt-2 w-full accent-[#1F8CFF]\"\n />\n </label>\n <label className={fieldLabelClass}>\n Opacity: {Math.round(watermarkOpacity * 100)}%\n <input\n type=\"range\"\n min={0.05}\n max={1}\n step={0.05}\n value={watermarkOpacity}\n onChange={(event) => setWatermarkOpacity(Number(event.target.value))}\n className=\"mt-2 w-full accent-[#1F8CFF]\"\n />\n </label>\n <label className={fieldLabelClass}>\n Angle\n <select\n value={watermarkRotation}\n onChange={(event) => setWatermarkRotation(Number(event.target.value))}\n className={`mt-2 ${inputClass}`}\n >\n <option value={0}>Horizontal</option>\n <option value={45}>Diagonal (45°)</option>\n <option value={-45}>Diagonal (-45°)</option>\n <option value={90}>Vertical</option>\n </select>\n </label>\n <label className={fieldLabelClass}>\n Colour\n <input\n type=\"color\"\n value={watermarkColor}\n onChange={(event) => setWatermarkColor(event.target.value)}\n className=\"mt-2 h-9 w-full cursor-pointer rounded-lg border border-slate-300 bg-white dark:border-slate-700 dark:bg-slate-950\"\n />\n </label>\n </div>\n ) : null}\n </div>\n\n <div className={cardClass}>\n <Toggle checked={useNumbers} onChange={setUseNumbers} label=\"Page numbers\" />\n {useNumbers ? (\n <div className=\"mt-4 grid gap-4 sm:grid-cols-2\">\n <label className={fieldLabelClass}>\n Format\n <select\n value={numberFormat}\n onChange={(event) => setNumberFormat(event.target.value)}\n className={`mt-2 ${inputClass}`}\n >\n {NUMBER_FORMATS.map((format) => (\n <option key={format} value={format}>\n {formatPageNumber(format, 1, 12)}\n </option>\n ))}\n </select>\n </label>\n <label className={fieldLabelClass}>\n Position\n <select\n value={numberPosition}\n onChange={(event) => setNumberPosition(event.target.value as NumberPosition)}\n className={`mt-2 ${inputClass}`}\n >\n {POSITIONS.map((position) => (\n <option key={position.value} value={position.value}>\n {position.label}\n </option>\n ))}\n </select>\n </label>\n <label className={fieldLabelClass}>\n Font size\n <input\n type=\"number\"\n min={6}\n max={36}\n value={numberSize}\n onChange={(event) => setNumberSize(Number(event.target.value))}\n className={`mt-2 ${inputClass}`}\n />\n </label>\n <label className={fieldLabelClass}>\n Start numbering at\n <input\n type=\"number\"\n min={1}\n value={startAt}\n onChange={(event) => setStartAt(Number(event.target.value))}\n className={`mt-2 ${inputClass}`}\n />\n </label>\n </div>\n ) : null}\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <label className={fieldLabelClass}>\n Apply to pages\n <input\n type=\"text\"\n value={pages}\n onChange={(event) => setPages(event.target.value)}\n placeholder=\"All pages (e.g. 2-)\"\n className={`mt-2 ${inputClass}`}\n />\n </label>\n <label className={fieldLabelClass}>\n Output file name\n <input\n type=\"text\"\n value={outputName}\n onChange={(event) => setOutputName(event.target.value)}\n placeholder=\"stamped\"\n className={`mt-2 ${inputClass}`}\n />\n </label>\n </div>\n </fieldset>\n\n <StatusBanner status={status} />\n\n <div className=\"mt-5 flex flex-wrap gap-3\">\n <button\n type=\"submit\"\n disabled={busy || !pdf.bytes || !hasStamp}\n className={primaryButtonClass}\n >\n <i className={`fa-solid ${isStamping ? \"fa-spinner fa-spin\" : \"fa-stamp\"}`} />\n {isStamping ? \"Stamping...\" : \"Apply & Download\"}\n </button>\n <BackToPdfTools />\n </div>\n </form>\n }\n side={\n <>\n <h2 className=\"text-base font-semibold text-slate-900 dark:text-slate-100\">Preview</h2>\n <StampPreview watermark={watermark} pageNumbers={pageNumbers} />\n <SideTips\n tips={[\n \"The preview is a rough guide to placement, not an exact render.\",\n <>\n Skip a cover page with <code>2-</code> in Apply to pages. Numbering counts only\n stamped pages.\n </>,\n \"Text uses Helvetica, so it supports basic Latin characters only.\",\n ]}\n />\n </>\n }\n />\n </PdfToolPage>\n );\n}\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { COMPRESSION_LEVELS, compressPdf, browserCodec } from './chunk-ZBZ7OC6I.js';
|
|
2
|
+
import { useSinglePdf } from './chunk-BK3MYSUS.js';
|
|
3
|
+
import { PdfToolPage, PdfToolColumns, SummaryList, SideTips, SelectedPdfCard, FileDropzone, fieldLabelClass, StatusBanner, primaryButtonClass, BackToPdfTools } from './chunk-H6EERRCR.js';
|
|
4
|
+
import { formatBytes, downloadBlob, toBaseName } from './chunk-EKHEY3LI.js';
|
|
5
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
6
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var LEVEL_HINTS = {
|
|
9
|
+
light: "Barely visible change. Best for print.",
|
|
10
|
+
balanced: "Good for email and sharing.",
|
|
11
|
+
strong: "Smallest file. Photos look softer."
|
|
12
|
+
};
|
|
13
|
+
function CompressPdf() {
|
|
14
|
+
const [level, setLevel] = useState("balanced");
|
|
15
|
+
const [result, setResult] = useState(null);
|
|
16
|
+
const [isCompressing, setIsCompressing] = useState(false);
|
|
17
|
+
const [status, setStatus] = useState({
|
|
18
|
+
tone: "info",
|
|
19
|
+
text: "Add a PDF to shrink it by re-compressing its images."
|
|
20
|
+
});
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
document.title = "Compress PDF | Apps";
|
|
23
|
+
}, []);
|
|
24
|
+
const onError = useCallback((text) => setStatus({ tone: "error", text }), []);
|
|
25
|
+
const onLoaded = useCallback(({ file }) => {
|
|
26
|
+
setResult(null);
|
|
27
|
+
setStatus({ tone: "success", text: `${file.name} loaded (${formatBytes(file.size)}). Choose a level and compress.` });
|
|
28
|
+
}, []);
|
|
29
|
+
const pdf = useSinglePdf({ onError, onLoaded });
|
|
30
|
+
const clearFile = () => {
|
|
31
|
+
pdf.clear();
|
|
32
|
+
setResult(null);
|
|
33
|
+
};
|
|
34
|
+
const handleCompress = async (event) => {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
if (!pdf.bytes) return;
|
|
37
|
+
setIsCompressing(true);
|
|
38
|
+
setResult(null);
|
|
39
|
+
try {
|
|
40
|
+
const { bytes, stats } = await compressPdf(
|
|
41
|
+
pdf.bytes,
|
|
42
|
+
COMPRESSION_LEVELS[level],
|
|
43
|
+
browserCodec,
|
|
44
|
+
(done, total) => setStatus({ tone: "info", text: total ? `Compressing images (${done} of ${total})...` : "Optimizing..." })
|
|
45
|
+
);
|
|
46
|
+
setResult(stats);
|
|
47
|
+
if (stats.newSize < stats.originalSize * 0.98) {
|
|
48
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
49
|
+
downloadBlob(blob, `${toBaseName(pdf.file.name, "document")}-compressed.pdf`);
|
|
50
|
+
const saved = Math.round((1 - stats.newSize / stats.originalSize) * 100);
|
|
51
|
+
setStatus({
|
|
52
|
+
tone: "success",
|
|
53
|
+
text: `${formatBytes(stats.originalSize)} \u2192 ${formatBytes(stats.newSize)} (${saved}% smaller). Your download has started.`
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
setStatus({
|
|
57
|
+
tone: "info",
|
|
58
|
+
text: stats.images ? "This PDF is already well optimized; a smaller version could not be made at this level. Try Strong, or keep your original." : "This PDF has no images to compress, so it cannot be made noticeably smaller. Text and vector content is already compact."
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
setStatus({ tone: "error", text: `Compression failed: ${error?.message || "Please try again."}` });
|
|
63
|
+
} finally {
|
|
64
|
+
setIsCompressing(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const busy = isCompressing || pdf.isReading;
|
|
68
|
+
return /* @__PURE__ */ jsx(
|
|
69
|
+
PdfToolPage,
|
|
70
|
+
{
|
|
71
|
+
title: "Compress PDF",
|
|
72
|
+
description: "Shrink PDFs that are heavy with photos or scans by re-compressing their images. Runs in your browser; files never leave your device.",
|
|
73
|
+
stats: [
|
|
74
|
+
{ value: pdf.file ? formatBytes(pdf.file.size) : "-", label: "Original" },
|
|
75
|
+
{ value: result ? formatBytes(result.newSize) : "-", label: "Result" }
|
|
76
|
+
],
|
|
77
|
+
children: /* @__PURE__ */ jsx(
|
|
78
|
+
PdfToolColumns,
|
|
79
|
+
{
|
|
80
|
+
main: /* @__PURE__ */ jsxs("form", { onSubmit: handleCompress, children: [
|
|
81
|
+
pdf.file ? /* @__PURE__ */ jsx(
|
|
82
|
+
SelectedPdfCard,
|
|
83
|
+
{
|
|
84
|
+
signed: pdf.signed,
|
|
85
|
+
file: pdf.file,
|
|
86
|
+
pageCount: pdf.pageCount,
|
|
87
|
+
onRemove: clearFile,
|
|
88
|
+
disabled: isCompressing
|
|
89
|
+
}
|
|
90
|
+
) : /* @__PURE__ */ jsx(
|
|
91
|
+
FileDropzone,
|
|
92
|
+
{
|
|
93
|
+
accept: ".pdf,application/pdf",
|
|
94
|
+
disabled: busy,
|
|
95
|
+
onFiles: pdf.selectFiles,
|
|
96
|
+
title: "Drag and drop a PDF here",
|
|
97
|
+
hint: "or click to browse from your device"
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "mt-5", disabled: busy || !pdf.bytes, children: [
|
|
101
|
+
/* @__PURE__ */ jsx("legend", { className: fieldLabelClass, children: "Compression level" }),
|
|
102
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 grid gap-2 sm:grid-cols-3", children: Object.entries(COMPRESSION_LEVELS).map(([id, option]) => /* @__PURE__ */ jsxs(
|
|
103
|
+
"label",
|
|
104
|
+
{
|
|
105
|
+
className: `cursor-pointer rounded-xl border p-3 text-sm transition ${level === id ? "border-[#1F8CFF] bg-blue-50 dark:bg-blue-500/10" : "border-slate-200 bg-white hover:border-slate-300 dark:border-slate-700 dark:bg-slate-900"}`,
|
|
106
|
+
children: [
|
|
107
|
+
/* @__PURE__ */ jsx(
|
|
108
|
+
"input",
|
|
109
|
+
{
|
|
110
|
+
type: "radio",
|
|
111
|
+
name: "compression-level",
|
|
112
|
+
value: id,
|
|
113
|
+
checked: level === id,
|
|
114
|
+
onChange: () => setLevel(id),
|
|
115
|
+
className: "sr-only"
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
/* @__PURE__ */ jsx("span", { className: "block font-semibold text-slate-900 dark:text-slate-100", children: option.label }),
|
|
119
|
+
/* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-slate-600 dark:text-slate-300", children: LEVEL_HINTS[id] })
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
id
|
|
123
|
+
)) })
|
|
124
|
+
] }),
|
|
125
|
+
/* @__PURE__ */ jsx(StatusBanner, { status }),
|
|
126
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-wrap gap-3", children: [
|
|
127
|
+
/* @__PURE__ */ jsxs("button", { type: "submit", disabled: busy || !pdf.bytes, className: primaryButtonClass, children: [
|
|
128
|
+
/* @__PURE__ */ jsx("i", { className: `fa-solid ${isCompressing ? "fa-spinner fa-spin" : "fa-file-zipper"}` }),
|
|
129
|
+
isCompressing ? "Compressing..." : "Compress PDF"
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsx(BackToPdfTools, {})
|
|
132
|
+
] })
|
|
133
|
+
] }),
|
|
134
|
+
side: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
135
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-slate-900 dark:text-slate-100", children: "Result" }),
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
SummaryList,
|
|
138
|
+
{
|
|
139
|
+
items: [
|
|
140
|
+
{ label: "Images found", value: result ? result.images : "-" },
|
|
141
|
+
{ label: "Images compressed", value: result ? result.compressed : "-" },
|
|
142
|
+
{ label: "New size", value: result ? formatBytes(result.newSize) : "-" }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
),
|
|
146
|
+
/* @__PURE__ */ jsx(
|
|
147
|
+
SideTips,
|
|
148
|
+
{
|
|
149
|
+
tips: [
|
|
150
|
+
"Works best on scans and photo-heavy PDFs.",
|
|
151
|
+
"Text stays sharp: only images are re-compressed.",
|
|
152
|
+
"Masks and CMYK images are left as they are."
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
] })
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { CompressPdf };
|
|
164
|
+
//# sourceMappingURL=chunk-4LBJTDV5.js.map
|
|
165
|
+
//# sourceMappingURL=chunk-4LBJTDV5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compress/CompressPdf.tsx"],"names":[],"mappings":"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","file":"chunk-4LBJTDV5.js","sourcesContent":["import React, { useCallback, useEffect, useState } from \"react\";\nimport { downloadBlob, formatBytes, toBaseName } from \"../shared/files\";\nimport {\n BackToPdfTools,\n FileDropzone,\n PdfToolColumns,\n PdfToolPage,\n SelectedPdfCard,\n SideTips,\n StatusBanner,\n SummaryList,\n fieldLabelClass,\n primaryButtonClass,\n} from \"../shared/ToolLayout\";\nimport { useSinglePdf } from \"../shared/useSinglePdf\";\nimport { browserCodec } from \"./browserCodec\";\nimport { COMPRESSION_LEVELS, compressPdf } from \"./compressPdfCore\";\n\nconst LEVEL_HINTS = {\n light: \"Barely visible change. Best for print.\",\n balanced: \"Good for email and sharing.\",\n strong: \"Smallest file. Photos look softer.\",\n};\n\nexport default function CompressPdf() {\n const [level, setLevel] = useState(\"balanced\");\n const [result, setResult] = useState(null);\n const [isCompressing, setIsCompressing] = useState(false);\n const [status, setStatus] = useState({\n tone: \"info\",\n text: \"Add a PDF to shrink it by re-compressing its images.\",\n });\n\n useEffect(() => {\n document.title = \"Compress PDF | Apps\";\n }, []);\n\n const onError = useCallback((text) => setStatus({ tone: \"error\", text }), []);\n const onLoaded = useCallback(({ file }) => {\n setResult(null);\n setStatus({ tone: \"success\", text: `${file.name} loaded (${formatBytes(file.size)}). Choose a level and compress.` });\n }, []);\n const pdf = useSinglePdf({ onError, onLoaded });\n\n const clearFile = () => {\n pdf.clear();\n setResult(null);\n };\n\n const handleCompress = async (event) => {\n event.preventDefault();\n if (!pdf.bytes) return;\n\n setIsCompressing(true);\n setResult(null);\n try {\n const { bytes, stats } = await compressPdf(pdf.bytes, COMPRESSION_LEVELS[level], browserCodec, (done, total) =>\n setStatus({ tone: \"info\", text: total ? `Compressing images (${done} of ${total})...` : \"Optimizing...\" })\n );\n setResult(stats);\n\n if (stats.newSize < stats.originalSize * 0.98) {\n const blob = new Blob([bytes as BlobPart], { type: \"application/pdf\" });\n downloadBlob(blob, `${toBaseName(pdf.file.name, \"document\")}-compressed.pdf`);\n const saved = Math.round((1 - stats.newSize / stats.originalSize) * 100);\n setStatus({\n tone: \"success\",\n text: `${formatBytes(stats.originalSize)} → ${formatBytes(stats.newSize)} (${saved}% smaller). Your download has started.`,\n });\n } else {\n setStatus({\n tone: \"info\",\n text: stats.images\n ? \"This PDF is already well optimized; a smaller version could not be made at this level. Try Strong, or keep your original.\"\n : \"This PDF has no images to compress, so it cannot be made noticeably smaller. Text and vector content is already compact.\",\n });\n }\n } catch (error) {\n setStatus({ tone: \"error\", text: `Compression failed: ${error?.message || \"Please try again.\"}` });\n } finally {\n setIsCompressing(false);\n }\n };\n\n const busy = isCompressing || pdf.isReading;\n\n return (\n <PdfToolPage\n title=\"Compress PDF\"\n description=\"Shrink PDFs that are heavy with photos or scans by re-compressing their images. Runs in your browser; files never leave your device.\"\n stats={[\n { value: pdf.file ? formatBytes(pdf.file.size) : \"-\", label: \"Original\" },\n { value: result ? formatBytes(result.newSize) : \"-\", label: \"Result\" },\n ]}\n >\n <PdfToolColumns\n main={\n <form onSubmit={handleCompress}>\n {pdf.file ? (\n <SelectedPdfCard\n signed={pdf.signed} file={pdf.file} pageCount={pdf.pageCount} onRemove={clearFile} disabled={isCompressing} />\n ) : (\n <FileDropzone\n accept=\".pdf,application/pdf\"\n disabled={busy}\n onFiles={pdf.selectFiles}\n title=\"Drag and drop a PDF here\"\n hint=\"or click to browse from your device\"\n />\n )}\n\n <fieldset className=\"mt-5\" disabled={busy || !pdf.bytes}>\n <legend className={fieldLabelClass}>Compression level</legend>\n <div className=\"mt-2 grid gap-2 sm:grid-cols-3\">\n {Object.entries(COMPRESSION_LEVELS).map(([id, option]) => (\n <label\n key={id}\n className={`cursor-pointer rounded-xl border p-3 text-sm transition ${\n level === id\n ? \"border-[#1F8CFF] bg-blue-50 dark:bg-blue-500/10\"\n : \"border-slate-200 bg-white hover:border-slate-300 dark:border-slate-700 dark:bg-slate-900\"\n }`}\n >\n <input\n type=\"radio\"\n name=\"compression-level\"\n value={id}\n checked={level === id}\n onChange={() => setLevel(id)}\n className=\"sr-only\"\n />\n <span className=\"block font-semibold text-slate-900 dark:text-slate-100\">{option.label}</span>\n <span className=\"mt-1 block text-xs text-slate-600 dark:text-slate-300\">{LEVEL_HINTS[id]}</span>\n </label>\n ))}\n </div>\n </fieldset>\n\n <StatusBanner status={status} />\n\n <div className=\"mt-5 flex flex-wrap gap-3\">\n <button type=\"submit\" disabled={busy || !pdf.bytes} className={primaryButtonClass}>\n <i className={`fa-solid ${isCompressing ? \"fa-spinner fa-spin\" : \"fa-file-zipper\"}`} />\n {isCompressing ? \"Compressing...\" : \"Compress PDF\"}\n </button>\n <BackToPdfTools />\n </div>\n </form>\n }\n side={\n <>\n <h2 className=\"text-base font-semibold text-slate-900 dark:text-slate-100\">Result</h2>\n <SummaryList\n items={[\n { label: \"Images found\", value: result ? result.images : \"-\" },\n { label: \"Images compressed\", value: result ? result.compressed : \"-\" },\n { label: \"New size\", value: result ? formatBytes(result.newSize) : \"-\" },\n ]}\n />\n <SideTips\n tips={[\n \"Works best on scans and photo-heavy PDFs.\",\n \"Text stays sharp: only images are re-compressed.\",\n \"Masks and CMYK images are left as they are.\",\n ]}\n />\n </>\n }\n />\n </PdfToolPage>\n );\n}\n"]}
|