</> pdf-merge.php

288 行9791 字节.php
📥 下载源码 🔙 返回工具
1<?php
2class=class="string">"keyword">header(class="string">'Content-Type: text/html; charset=utf-class="number">8');
3
4class=class="string">"keyword">class=class="string">"comment">// Handle merge request via local Node.js server
5class=class="string">"keyword">if ($_SERVER[class="string">'REQUEST_METHOD'] === class="string">'POST' && class=class="string">"keyword">isset($_FILES[class="string">'pdfs'])) {
6 $files = $_FILES[class="string">'pdfs'];
7 $fileCount = is_array($files[class="string">'name']) ? count($files[class="string">'name']) : class="number">0;
8
9 class=class="string">"keyword">if ($fileCount < class="number">2) {
10 http_response_code(class="number">400);
11 class=class="string">"keyword">exit(class="string">'请至少上传class="number">2个PDF文件');
12 }
13
14 class=class="string">"keyword">class=class="string">"comment">// Build curl multipart request
15 $ch = curl_init(class="string">'http:class="keyword">class="comment">//class="number">127.0.class="number">0.1:class="number">3001/pdf-merge');
16 curl_setopt_array($ch, [
17 CURLOPT_POST => true,
18 CURLOPT_RETURNTRANSFER => true,
19 CURLOPT_TIMEOUT => class="number">30,
20 ]);
21
22 $postFields = [];
23 class=class="string">"keyword">for ($i = class="number">0; $i < $fileCount; $i++) {
24 class=class="string">"keyword">if ($files[class="string">'error'][$i] !== UPLOAD_ERR_OK) class=class="string">"keyword">continue;
25 $ext = strtolower(pathinfo($files[class="string">'name'][$i], PATHINFO_EXTENSION));
26 class=class="string">"keyword">if ($ext !== class="string">'pdf') class=class="string">"keyword">continue;
27
28 class=class="string">"keyword">class=class="string">"comment">// Attach actual file path class=class="string">"keyword">for curl CURLFile
29 $postFields[class="string">'pdfs_' . $i] = class=class="string">"keyword">new CURLFile(
30 $files[class="string">'tmp_name'][$i],
31 $files[class="string">'type'][$i],
32 $files[class="string">'name'][$i]
33 );
34 }
35
36 class=class="string">"keyword">if (count($postFields) < class="number">2) {
37 http_response_code(class="number">400);
38 class=class="string">"keyword">exit(class="string">'有效的PDF文件不足class="number">2个');
39 }
40
41 curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
42 $result = curl_exec($ch);
43 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
44 curl_close($ch);
45
46 class=class="string">"keyword">if ($httpCode !== class="number">200 || class=class="string">"keyword">empty($result)) {
47 http_response_code(class="number">500);
48 class=class="string">"keyword">exit(class="string">'PDF合并失败,请确认文件格式正确');
49 }
50
51 class=class="string">"keyword">header(class="string">'Content-Type: application/pdf');
52 class=class="string">"keyword">header(class="string">'Content-Disposition: attachment; filename="merged.pdf"');
53 class=class="string">"keyword">header(class="string">'Content-Length: ' . strlen($result));
54 class=class="string">"keyword">echo $result;
55 class=class="string">"keyword">exit;
56}
57?>
58<!DOCTYPE html>
59<class="tag">html lang="zh-CN">
60<class="tag">head>
61<class="tag">meta charset="UTF-class="number">8">
62<class="tag">meta name="viewport" content="width=device-width, initial-scale=class="number">1.0">
63<class="tag">meta property="og:title" content="PDF 合并 - η空间">
64<class="tag">meta name="description" content="将多个 PDF 文件合并成一个,免费在线工具,无需上传到服务器">
65<class="tag">meta name="keywords" content="PDF合并,在线PDF工具,免费PDF合并,PDF合并工具">
66<class="tag">meta property="og:description" content="将多个 PDF 文件合并成一个,免费在线工具">
67<class="tag">meta property="og:type" content="website">
68<class="tag">title>PDF 合并 - η空间</class="tag">title>
69<class="tag">link rel="stylesheet" href="/tools/share.css">
70 <class="tag">link rel="stylesheet" href="/assets/pet-cat.css">
71<class="tag">style>
72* { margin: class="number">0; padding: class="number">0; box-sizing: border-box; }
73body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: class=class="string">"keyword">class=class="string">"comment">#f0f2f5; color: #class="number">333; line-height: class="number">1.6; }
74class=class="string">"keyword">header {
75 background: linear-gradient(135deg, class=class="string">"keyword">class=class="string">"comment">#4facfe class="number">0%, #00f2fe class="number">100%);
76 color: class=class="string">"keyword">class=class="string">"comment">#fff; padding: 30px 20px; text-align: center;
77}
78class=class="string">"keyword">header h1 { font-size: 24px; margin-bottom: 4px; }
79class=class="string">"keyword">header p { opacity: class="number">0.85; font-size: 14px; }
80.container { max-width: 600px; margin: class="number">0 auto; padding: 24px 16px; }
81.back-link { display: inline-block; margin-bottom: 16px; color:class=class="string">"keyword">class=class="string">"comment">#4facfe; text-decoration: none; font-size: 14px; }
82.card { background: class=class="string">"keyword">class=class="string">"comment">#fff; border-radius: 12px; padding: 24px; box-shadow: class="number">0 2px 8px rgba(class="number">0,class="number">0,class="number">0,class="number">0.06); margin-bottom: 16px; }
83.upload-zone {
84 border: 2px dashed class=class="string">"keyword">class=class="string">"comment">#ccc;
85 border-radius: 10px;
86 padding: 40px 20px;
87 text-align: center;
88 cursor: pointer;
89 transition: border-color class="number">0.3s, background class="number">0.3s;
90}
91.upload-zone:hover { border-color: class=class="string">"keyword">class=class="string">"comment">#4facfe; background: #f0f8ff; }
92.upload-zone .icon { font-size: 48px; margin-bottom: 12px; }
93.upload-zone p { color: class=class="string">"keyword">class=class="string">"comment">#class="number">999; font-size: 14px; }
94.upload-zone input { display: none; }
95.file-list { margin-top: 16px; }
96.file-item {
97 display: flex; align-items: center; justify-content: space-between;
98 padding: 10px 14px; background: class=class="string">"keyword">class=class="string">"comment">#f8f9fa; border-radius: 8px; margin-bottom: 8px;
99 font-size: 14px;
100}
101.file-item .name { flex: class="number">1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102.file-item .size { color: class=class="string">"keyword">class=class="string">"comment">#class="number">999; margin: class="number">0 12px; white-space: nowrap; }
103.file-item .remove {
104 color: class=class="string">"keyword">class=class="string">"comment">#ef4444; cursor: pointer; font-size: 18px; line-height: class="number">1;
105 padding: class="number">0 4px;
106}
107.file-item .remove:hover { color: class=class="string">"keyword">class=class="string">"comment">#dc2626; }
108.file-info { font-size: 13px; color: class=class="string">"keyword">class=class="string">"comment">#class="number">666; margin-top: 4px; }
109.btn {
110 display: inline-block; padding: 10px 24px; border-radius: 8px;
111 border: none; font-size: 15px; cursor: pointer;
112 background: class=class="string">"keyword">class=class="string">"comment">#4facfe; color: #fff; transition: opacity class="number">0.2s; width: class="number">100%;
113}
114.btn:hover { opacity: class="number">0.85; }
115.btn:disabled { opacity: class="number">0.4; cursor: not-allowed; }
116.result-box { display: none; margin-top: 16px; text-align: center; padding: 20px; background: class=class="string">"keyword">class=class="string">"comment">#f0f9ff; border-radius: 8px; }
117.result-box .success-icon { font-size: 36px; margin-bottom: 8px; }
118.download-link { display: inline-block; margin-top: 12px; padding: 10px 24px; background: class=class="string">"keyword">class=class="string">"comment">#4facfe; color: #fff; border-radius: 8px; text-decoration: none; }
119.spinner { display: none; margin: 20px auto; text-align: center; }
120.spinner::after {
121 content: class="string">''; display: inline-block; width: 32px; height: 32px;
122 border: 3px solid class=class="string">"keyword">class=class="string">"comment">#ddd; border-top-color: #4facfe; border-radius: class="number">50%;
123 animation: spin class="number">0.8s linear infinite;
124}
125@keyframes spin { to { transform: rotate(360deg); } }
126footer { text-align: center; padding: 24px; color: class=class="string">"keyword">class=class="string">"comment">#class="number">999; font-size: 13px; }
127</class="tag">style>
128</class="tag">head>
129<class="tag">body>
130
131<class=class="string">"keyword">header>
132 <class="tag">h1>📄 PDF 合并</class="tag">h1>
133 <class="tag">p>将多个 PDF 文件合并成一个,上传后自动处理</class="tag">p>
134</class=class="string">"keyword">header>
135
136<class="tag">div class=class="string">"keyword">class="container">
137 <class="tag">a href="index.html" class=class="string">"keyword">class="back-link">← 返回工具列表</class="tag">a> · <class="tag">a href="/download/pdf-merge.zip" style="color:class=class="string">"keyword">class=class="string">"comment">#667eea;font-size:13px;text-decoration:none;">📦 源码下载</class="tag">a>
138
139 <class="tag">div class=class="string">"keyword">class="card">
140 <class="tag">div class=class="string">"keyword">class="upload-zone" id="uploadZone">
141 <class="tag">div class=class="string">"keyword">class="icon">📁</class="tag">div>
142 <class="tag">p>点击选择多个 PDF 文件</class="tag">p>
143 <class="tag">input type="file" id="fileInput" accept=".pdf" multiple>
144 </class="tag">div>
145
146 <class="tag">div class=class="string">"keyword">class="file-list" id="fileList"></class="tag">div>
147
148 <class="tag">button class=class="string">"keyword">class="btn" id="mergeBtn" disabled style="margin-top:16px;">合并 PDF</class="tag">button>
149
150 <class="tag">div class=class="string">"keyword">class="spinner" id="spinner"></class="tag">div>
151
152 <class="tag">div class=class="string">"keyword">class="result-box" id="resultBox">
153 <class="tag">div class=class="string">"keyword">class="success-icon">✅</class="tag">div>
154 <class="tag">p id="resultMsg">合并成功!</class="tag">p>
155 <class="tag">a class=class="string">"keyword">class="download-link" id="downloadLink">⬇ 下载合并后的 PDF</class="tag">a>
156 </class="tag">div>
157 </class="tag">div>
158</class="tag">div>
159
160<class="tag">footer>
161 <class="tag">p>上传的 PDF 文件合并后自动删除,不会保留</class="tag">p>
162</class="tag">footer>
163
164<class="tag">script>
165class=class="string">"keyword">const uploadZone = document.class="func">getElementById(class="string">'uploadZone');
166class=class="string">"keyword">const fileInput = document.class="func">getElementById(class="string">'fileInput');
167class=class="string">"keyword">const fileList = document.class="func">getElementById(class="string">'fileList');
168class=class="string">"keyword">const mergeBtn = document.class="func">getElementById(class="string">'mergeBtn');
169class=class="string">"keyword">const spinner = document.class="func">getElementById(class="string">'spinner');
170class=class="string">"keyword">const resultBox = document.class="func">getElementById(class="string">'resultBox');
171class=class="string">"keyword">const resultMsg = document.class="func">getElementById(class="string">'resultMsg');
172class=class="string">"keyword">const downloadLink = document.class="func">getElementById(class="string">'downloadLink');
173
174class=class="string">"keyword">let selectedFiles = [];
175
176uploadZone.class="func">addEventListener(class="string">'click', () => fileInput.click());
177uploadZone.class="func">addEventListener(class="string">'dragover', (e) => {
178 e.preventDefault();
179 uploadZone.style.borderColor = class="string">'class="keyword">class="comment">#4facfe';
180 uploadZone.style.background = class="string">'class="keyword">class="comment">#f0f8ff';
181});
182uploadZone.class="func">addEventListener(class="string">'dragleave', () => {
183 uploadZone.style.borderColor = class="string">'class="keyword">class="comment">#ccc';
184 uploadZone.style.background = class="string">'';
185});
186uploadZone.class="func">addEventListener(class="string">'drop', (e) => {
187 e.preventDefault();
188 uploadZone.style.borderColor = class="string">'class="keyword">class="comment">#ccc';
189 uploadZone.style.background = class="string">'';
190 addFiles(e.dataTransfer.files);
191});
192
193fileInput.class="func">addEventListener(class="string">'change', () => {
194 class=class="string">"keyword">if (fileInput.files.length > class="number">0) addFiles(fileInput.files);
195 fileInput.value = class="string">'';
196});
197
198class=class="string">"keyword">function addFiles(files) {
199 class=class="string">"keyword">for (class=class="string">"keyword">const file of files) {
200 class=class="string">"keyword">if (file.type !== class="string">'application/pdf') class=class="string">"keyword">continue;
201 class=class="string">"keyword">if (!selectedFiles.find(f => f.name === file.name && f.size === file.size)) {
202 selectedFiles.push(file);
203 }
204 }
205 renderFileList();
206}
207
208class=class="string">"keyword">function removeFile(index) {
209 selectedFiles.splice(index, class="number">1);
210 renderFileList();
211}
212
213class=class="string">"keyword">function formatSize(bytes) {
214 class=class="string">"keyword">if (bytes < class="number">1024) class=class="string">"keyword">return bytes + class="string">'B';
215 class=class="string">"keyword">if (bytes < class="number">1048576) class=class="string">"keyword">return (bytes / class="number">1024).toFixed(class="number">1) + class="string">'KB';
216 class=class="string">"keyword">return (bytes / class="number">1048576).toFixed(class="number">1) + class="string">'MB';
217}
218
219class=class="string">"keyword">function renderFileList() {
220 fileList.innerHTML = class="string">'';
221 class=class="string">"keyword">if (selectedFiles.length === class="number">0) {
222 mergeBtn.disabled = true;
223 class=class="string">"keyword">return;
224 }
225
226 selectedFiles.forEach((file, i) => {
227 class=class="string">"keyword">const div = document.createElement(class="string">'div');
228 div.className = class="string">'file-item';
229 div.innerHTML = `
230 <class="tag">span class=class="string">"keyword">class="name">${file.name}</class="tag">span>
231 <class="tag">span class=class="string">"keyword">class="size">${formatSize(file.size)}</class="tag">span>
232 <class="tag">span class=class="string">"keyword">class="remove" onclick="removeFile(${i})">×</class="tag">span>
233 `;
234 fileList.appendChild(div);
235 });
236
237 class=class="string">"keyword">const info = document.createElement(class="string">'div');
238 info.className = class="string">'file-info';
239 info.textContent = `共 ${selectedFiles.length} 个文件,合并后按上传顺序排列`;
240 fileList.appendChild(info);
241
242 mergeBtn.disabled = selectedFiles.length < class="number">2;
243}
244
245mergeBtn.class="func">addEventListener(class="string">'click', async () => {
246 class=class="string">"keyword">if (selectedFiles.length < class="number">2) {
247 class="func">alert(class="string">'请至少选择class="number">2个 PDF 文件');
248 class=class="string">"keyword">return;
249 }
250
251 spinner.style.display = class="string">'block';
252 resultBox.style.display = class="string">'none';
253 mergeBtn.disabled = true;
254
255 try {
256 class=class="string">"keyword">const formData = class=class="string">"keyword">new FormData();
257 class=class="string">"keyword">for (class=class="string">"keyword">const file of selectedFiles) {
258 formData.append(class="string">'pdfs[]', file);
259 }
260
261 class=class="string">"keyword">const res = await class="func">fetch(class="string">'pdf-merge.php', {
262 method: class="string">'POST',
263 body: formData
264 });
265
266 class=class="string">"keyword">if (!res.ok) {
267 class=class="string">"keyword">const text = await res.text();
268 throw class=class="string">"keyword">new Error(text || class="string">'合并失败');
269 }
270
271 class=class="string">"keyword">const blob = await res.blob();
272 class=class="string">"keyword">const url = URL.createObjectURL(blob);
273
274 resultMsg.textContent = `合并成功!共 ${selectedFiles.length} 个文件合并为 class="number">1 个 PDF (${formatSize(blob.size)})`;
275 downloadLink.href = url;
276 resultBox.style.display = class="string">'block';
277 } class="func">catch(err) {
278 class="func">alert(class="string">'合并失败: ' + err.message);
279 } finally {
280 spinner.style.display = class="string">'none';
281 mergeBtn.disabled = false;
282 }
283});
284</class="tag">script>
285<class="tag">script src="/tools/share.js"></class="tag">script>
286<class="tag">script src="/assets/pet-cat.js"></class="tag">script>
287</class="tag">body>
288</class="tag">html>

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/273059_cn/tools/source-viewer.php:69) in /www/wwwroot/273059_cn/tools/source-viewer.php on line 72

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/273059_cn/tools/source-viewer.php:69) in /www/wwwroot/273059_cn/tools/source-viewer.php on line 73
true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $postFields = []; for ($i = 0; $i < $fileCount; $i++) { if ($files['error'][$i] !== UPLOAD_ERR_OK) continue; $ext = strtolower(pathinfo($files['name'][$i], PATHINFO_EXTENSION)); if ($ext !== 'pdf') continue; // Attach actual file path for curl CURLFile $postFields['pdfs_' . $i] = new CURLFile( $files['tmp_name'][$i], $files['type'][$i], $files['name'][$i] ); } if (count($postFields) < 2) { http_response_code(400); exit('有效的PDF文件不足2个'); } curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200 || empty($result)) { http_response_code(500); exit('PDF合并失败,请确认文件格式正确'); } header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="merged.pdf"'); header('Content-Length: ' . strlen($result)); echo $result; exit; } ?> PDF 合并 - η空间

📄 PDF 合并

将多个 PDF 文件合并成一个,上传后自动处理

← 返回工具列表 · 📦 源码下载
📁

点击选择多个 PDF 文件

合并成功!

⬇ 下载合并后的 PDF

上传的 PDF 文件合并后自动删除,不会保留