Skip to content

Commit a36bbb2

Browse files
committed
odf watermarker
1 parent 70799a4 commit a36bbb2

File tree

3 files changed

+306
-1
lines changed

3 files changed

+306
-1
lines changed

deno.jsonc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"Command": "https://deno.land/x/[email protected]/command/mod.ts",
3939
"datetime": "https://deno.land/[email protected]/datetime/format.ts",
4040
"lightningcss": "https://cdn.jsdelivr.net/npm/lightningcss-wasm/+esm",
41-
"@cloudflare/workers-types": "https://cdn.jsdelivr.net/npm/@cloudflare/workers-types@latest/index.ts"
41+
"@cloudflare/workers-types": "https://cdn.jsdelivr.net/npm/@cloudflare/workers-types@latest/index.ts",
42+
"pdf-lib": "https://unpkg.com/[email protected]/es/index.d.ts"
4243
}
4344
}

public/pdf-utils/index.html

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
<!DOCTYPE html>
2+
<html lang="zh-CN">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>PDF 水印添加工具</title>
8+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
9+
integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
10+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
11+
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
12+
<style>
13+
body {
14+
font-family: Arial, sans-serif;
15+
line-height: 1.6;
16+
margin: 0;
17+
padding: 20px;
18+
background-color: #f5f5f5;
19+
color: #333;
20+
}
21+
22+
label {
23+
display: block;
24+
font-weight: bold;
25+
margin-bottom: 5px;
26+
}
27+
28+
input[type="text"],
29+
input[type="file"] {
30+
width: 100%;
31+
padding: 10px;
32+
border: 1px solid #ddd;
33+
border-radius: 4px;
34+
box-sizing: border-box;
35+
}
36+
37+
input[type="color"] {
38+
height: 40px;
39+
width: 100%;
40+
}
41+
42+
input[type="range"] {
43+
width: 100%;
44+
}
45+
46+
.btn {
47+
background-color: #3498db;
48+
color: white;
49+
border: none;
50+
padding: 10px 20px;
51+
cursor: pointer;
52+
font-size: 16px;
53+
border-radius: 4px;
54+
display: block;
55+
margin: 20px auto;
56+
transition: background-color 0.3s;
57+
}
58+
59+
.btn:hover {
60+
background-color: #2980b9;
61+
}
62+
63+
.btn:disabled {
64+
background-color: #bdc3c7;
65+
cursor: not-allowed;
66+
}
67+
68+
.preview {
69+
margin-top: 30px;
70+
border: 1px solid #ddd;
71+
border-radius: 4px;
72+
padding: 10px;
73+
background-color: #f9f9f9;
74+
}
75+
76+
.preview iframe {
77+
width: 100%;
78+
height: 400px;
79+
border: none;
80+
}
81+
82+
.value-display {
83+
display: inline-block;
84+
min-width: 30px;
85+
text-align: right;
86+
}
87+
</style>
88+
</head>
89+
90+
<body>
91+
<div class="container-sm mx-auto p-4 bg-white rounded shadow-sm" style="max-width: 800px;">
92+
<h1 class="text-center mb-4" style="color: #2c3e50">PDF 水印添加工具</h1>
93+
94+
<div class="m-4">
95+
<label for="pdfFile">上传PDF文件:</label>
96+
<input type="file" id="pdfFile" accept="application/pdf">
97+
</div>
98+
99+
100+
<div class="d-flex gap-4 mb-4">
101+
<div class="flex-grow-1">
102+
<label for="watermarkText">水印文本:</label>
103+
<input type="text" id="watermarkText" value="CONFIDENTIAL" placeholder="输入水印文本">
104+
</div>
105+
<div class="flex-grow-1">
106+
<label for="watermarkColor">水印颜色:</label>
107+
<input type="color" id="watermarkColor" value="#FF0000">
108+
</div>
109+
</div>
110+
111+
<div class="d-flex gap-4 mb-4">
112+
<div class="flex-grow-1">
113+
<label for="opacity">不透明度: <span id="opacityValue" class="value-display">0.6</span></label>
114+
<input type="range" id="opacity" min="0.1" max="1" step="0.1" value="0.6">
115+
</div>
116+
<div class="flex-grow-1">
117+
<label for="fontSize">字体大小: <span id="fontSizeValue" class="value-display">60</span>px</label>
118+
<input type="range" id="fontSize" min="20" max="100" step="1" value="60">
119+
</div>
120+
<div class="flex-grow-1">
121+
<label for="rotation">旋转角度: <span id="rotationValue" class="value-display">45</span>°</label>
122+
<input type="range" id="rotation" min="0" max="360" step="5" value="45">
123+
</div>
124+
</div>
125+
126+
<button id="addWatermarkBtn" class="btn" disabled>添加水印</button>
127+
128+
<div id="progressContainer" class="w-100 rounded-1 my-2" style="display: none;">
129+
<div class="progress" style="height: 20px;">
130+
<div id="progressBar" class="progress-bar"></div>
131+
</div>
132+
<p id="progressText" style="text-align: center;">处理中: 0%</p>
133+
</div>
134+
135+
<div id="previewContainer" class="preview" style="display: none;">
136+
<h2>PDF 预览:</h2>
137+
<iframe id="pdfPreview" title="PDF Preview"></iframe>
138+
<p class="fs-6 mt-1" style="color: #7f8c8d;">注意: 预览显示的是原始PDF。添加水印后,将自动下载处理后的文件。</p>
139+
</div>
140+
141+
<button id="downloadBtn" class="btn" style="display: none;">下载 PDF</button>
142+
<div class="mt-4 p-4 rounded-1 fs-6" style="background-color: #f8f9fa;">
143+
<h3 class="mt-0">使用说明:</h3>
144+
<ol class="ps-xl-0">
145+
<li>上传PDF文件</li>
146+
<li>设置水印文本、颜色、不透明度、大小和角度</li>
147+
<li>点击"添加水印"按钮</li>
148+
<li>处理完成后,点击"下载 PDF"按钮下载带水印的文件</li>
149+
</ol>
150+
</div>
151+
</div>
152+
153+
<script type="module">
154+
// 获取DOM元素
155+
const pdfFileInput = document.getElementById('pdfFile');
156+
const watermarkTextInput = document.getElementById('watermarkText');
157+
const watermarkColorInput = document.getElementById('watermarkColor');
158+
const opacityInput = document.getElementById('opacity');
159+
const fontSizeInput = document.getElementById('fontSize');
160+
const rotationInput = document.getElementById('rotation');
161+
const addWatermarkBtn = document.getElementById('addWatermarkBtn');
162+
const previewContainer = document.getElementById('previewContainer');
163+
const pdfPreview = document.getElementById('pdfPreview');
164+
const progressContainer = document.getElementById('progressContainer');
165+
const progressBar = document.getElementById('progressBar');
166+
const progressText = document.getElementById('progressText');
167+
const downloadBtn = document.getElementById('downloadBtn')
168+
169+
// 显示当前值
170+
const opacityValue = document.getElementById('opacityValue');
171+
const fontSizeValue = document.getElementById('fontSizeValue');
172+
const rotationValue = document.getElementById('rotationValue');
173+
174+
// 更新显示值
175+
opacityInput.addEventListener('input', () => {
176+
opacityValue.textContent = opacityInput.value;
177+
});
178+
179+
fontSizeInput.addEventListener('input', () => {
180+
fontSizeValue.textContent = fontSizeInput.value;
181+
});
182+
183+
rotationInput.addEventListener('input', () => {
184+
rotationValue.textContent = rotationInput.value;
185+
});
186+
187+
// 文件选择处理
188+
pdfFileInput.addEventListener('change', (event) => {
189+
const file = event.target.files[0];
190+
if (file && file.type === 'application/pdf') {
191+
// 启用添加水印按钮
192+
addWatermarkBtn.disabled = false;
193+
194+
// 创建预览
195+
const fileURL = URL.createObjectURL(file);
196+
pdfPreview.src = fileURL;
197+
previewContainer.style.display = 'block';
198+
return;
199+
}
200+
201+
if (file && file.type !== 'application/pdf') {
202+
alert('请选择有效的PDF文件');
203+
pdfFileInput.value = '';
204+
addWatermarkBtn.disabled = true;
205+
previewContainer.style.display = 'none';
206+
}
207+
});
208+
209+
// 将十六进制颜色转换为RGB
210+
function hexToRgb(hex) {
211+
const r = parseInt(hex.slice(1, 3), 16) / 255;
212+
const g = parseInt(hex.slice(3, 5), 16) / 255;
213+
const b = parseInt(hex.slice(5, 7), 16) / 255;
214+
return { r, g, b };
215+
}
216+
217+
// 添加水印处理
218+
addWatermarkBtn.addEventListener('click', async () => {
219+
const file = pdfFileInput.files[0];
220+
if (!file) {
221+
alert('请先选择PDF文件');
222+
return;
223+
}
224+
225+
const watermarkText = watermarkTextInput.value || 'CONFIDENTIAL';
226+
const watermarkColor = watermarkColorInput.value;
227+
const opacity = parseFloat(opacityInput.value);
228+
const fontSize = parseInt(fontSizeInput.value);
229+
const rotation = parseInt(rotationInput.value);
230+
231+
// 显示进度条
232+
progressContainer.style.display = 'block';
233+
progressBar.style.width = '0%';
234+
progressText.textContent = '处理中: 0%';
235+
addWatermarkBtn.disabled = true;
236+
237+
try {
238+
// 加载PDF文档
239+
const arrayBuffer = await file.arrayBuffer();
240+
const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);
241+
242+
// 获取所有页面
243+
const pages = pdfDoc.getPages();
244+
const { r, g, b } = hexToRgb(watermarkColor);
245+
const font = await pdfDoc.embedFont(PDFLib.StandardFonts.HelveticaBold);
246+
247+
// 处理每个页面
248+
for (let i = 0; i < pages.length; i++) {
249+
const page = pages[i];
250+
const { width, height } = page.getSize();
251+
252+
// 计算水印位置
253+
const textWidth = font.widthOfTextAtSize(watermarkText, fontSize);
254+
const textHeight = font.heightAtSize(fontSize);
255+
256+
// 绘制水印
257+
page.drawText(watermarkText, {
258+
x: width / 2 - textWidth / 2,
259+
y: height / 2 - textHeight / 2,
260+
size: fontSize,
261+
font: font,
262+
color: PDFLib.rgb(r, g, b),
263+
opacity: opacity,
264+
rotate: PDFLib.degrees(rotation),
265+
});
266+
267+
// 更新进度
268+
const progress = Math.round(((i + 1) / pages.length) * 100);
269+
progressBar.style.width = `${progress}%`;
270+
progressText.textContent = `处理中: ${progress}%`;
271+
272+
// 允许UI更新
273+
await new Promise(resolve => setTimeout(resolve, 0));
274+
}
275+
276+
// 保存带水印的PDF
277+
const watermarkedPdfBytes = await pdfDoc.save();
278+
279+
// 创建Blob并下载
280+
const blob = new Blob([watermarkedPdfBytes], { type: 'application/pdf' });
281+
282+
// 预览水印文件
283+
const fileURL = URL.createObjectURL(blob);
284+
pdfPreview.src = fileURL;
285+
previewContainer.style.display = 'block';
286+
287+
// 下载水印文件
288+
downloadBtn.style.display = 'block';
289+
downloadBtn.addEventListener('click', () => {
290+
saveAs(blob, `watermarked_${file.name}`);
291+
});
292+
} catch (error) {
293+
console.error('PDF处理错误:', error);
294+
alert('添加水印时出错: ' + error.message);
295+
} finally {
296+
progressContainer.style.display = 'none';
297+
addWatermarkBtn.disabled = false;
298+
}
299+
});
300+
</script>
301+
</body>
302+
303+
</html>

src/util/header.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<div class="toy"><i class="fa-solid fa-gamepad fa-xs"></i> <span class="disappear">玩具</span>
1717
<nav class="toy-nav disappear">
1818
<a class="decoration-line" href="/public/resume/">我的简历</a>
19+
<a class="decoration-line" href="/public/pdf-utils/">pdf 水印工具</a>
1920
<a class="decoration-line" href="/public/write-css/index.html">学习 UI</a>
2021
</nav>
2122
</div>

0 commit comments

Comments
 (0)