File size: 19,116 Bytes
3fcdbb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="theme-color" content="#f7f8ff" />
  <meta name="description" content="Merge PDF files quickly and privately in your browser with PDFix." />
  <title>Merge PDF Files Online — PDFix</title>
  <style>
    :root {
      --bg: #f7f8ff; --surface: #fff; --ink: #17182d; --muted: #71738a;
      --line: #e7e8f2; --primary: #635bff; --primary-dark: #4c43e8;
      --soft: #efedff; --green: #119b82; --danger: #d94765;
      --shadow: 0 18px 55px rgba(47,43,112,.09);
    }
    * { box-sizing: border-box; }
    body {
      margin: 0; min-height: 100vh; color: var(--ink);
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: radial-gradient(circle at 85% 5%, rgba(177,171,255,.19), transparent 25rem),
                  radial-gradient(circle at 4% 35%, rgba(115,221,211,.12), transparent 22rem), var(--bg);
      line-height: 1.5;
    }
    a { color: inherit; text-decoration: none; }
    button, input { font: inherit; }
    button { cursor: pointer; }
    .container { width: min(1040px, calc(100% - 32px)); margin: 0 auto; }
    header { padding: 20px 0; }
    .nav { display:flex; align-items:center; justify-content:space-between; gap:18px; }
    .brand { display:inline-flex; align-items:center; gap:10px; font-size:1.4rem; font-weight:850; letter-spacing:-1px; }
    .brand-mark { width:39px; height:39px; display:grid; place-items:center; border-radius:13px; color:#fff; background:linear-gradient(145deg,#766dff,#4c43e8); box-shadow:0 8px 20px rgba(99,91,255,.25); }
    .brand-mark svg { width:23px; height:23px; }
    .brand-name span { color:var(--primary); }
    .back-link { color:#65677d; font-weight:650; font-size:.92rem; }
    .back-link:hover { color:var(--primary); }
    main { padding: 42px 0 70px; }
    .intro { text-align:center; max-width:700px; margin:0 auto 28px; }
    .eyebrow { display:inline-flex; gap:8px; align-items:center; padding:7px 12px; border:1px solid #e4e1ff; border-radius:99px; background:rgba(255,255,255,.8); color:#5a52e9; font-size:.82rem; font-weight:750; }
    h1 { margin:18px 0 12px; font-size:clamp(2.4rem,5vw,4rem); line-height:1.06; letter-spacing:-2.7px; }
    h1 span { color:var(--primary); }
    .intro p { margin:0; color:var(--muted); font-size:1.05rem; }
    .workspace { max-width:820px; margin:0 auto; border:1px solid rgba(99,91,255,.14); border-radius:24px; padding:12px; background:rgba(255,255,255,.9); box-shadow:var(--shadow); }
    .dropzone { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:220px; padding:26px 18px; text-align:center; border:2px dashed #cbc8ff; border-radius:17px; background:linear-gradient(135deg,rgba(238,237,255,.72),rgba(239,253,251,.5)); transition:.2s ease; }
    .dropzone.dragover { border-color:var(--primary); background:#efedff; transform:scale(.995); }
    .upload-icon { width:54px; height:54px; display:grid; place-items:center; margin-bottom:13px; border-radius:17px; color:var(--primary); background:#fff; box-shadow:0 7px 18px rgba(71,64,160,.1); }
    .upload-icon svg { width:28px; height:28px; }
    .dropzone strong { font-size:1.1rem; }
    .dropzone p { margin:5px 0 16px; color:var(--muted); font-size:.9rem; }
    .btn { border:0; display:inline-flex; align-items:center; justify-content:center; gap:9px; padding:12px 18px; border-radius:13px; font-weight:750; transition:.2s ease; }
    .btn-primary { color:#fff; background:var(--primary); box-shadow:0 8px 18px rgba(99,91,255,.22); }
    .btn-primary:hover:not(:disabled) { background:var(--primary-dark); transform:translateY(-1px); }
    .btn-light { color:#4b4d64; background:#fff; border:1px solid var(--line); }
    .btn-light:hover { border-color:#c9c5ff; color:var(--primary); }
    button:disabled { opacity:.5; cursor:not-allowed; box-shadow:none; }
    input[type=file] { display:none; }
    .file-panel { display:none; padding:22px 10px 8px; }
    .file-panel.visible { display:block; }
    .panel-head { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:13px; }
    .panel-head h2 { margin:0; font-size:1.12rem; }
    .count { color:var(--muted); font-size:.86rem; }
    .file-list { display:grid; gap:10px; }
    .file-row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; padding:13px; border:1px solid var(--line); border-radius:14px; background:#fff; }
    .file-icon { width:42px; height:46px; display:grid; place-items:center; border-radius:11px; color:#e6536a; background:#fff0f2; }
    .file-icon svg { width:23px; height:23px; }
    .file-meta { min-width:0; }
    .file-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:700; font-size:.92rem; }
    .file-size { color:var(--muted); font-size:.8rem; margin-top:3px; }
    .file-actions { display:flex; align-items:center; gap:5px; }
    .icon-btn { width:34px; height:34px; display:grid; place-items:center; border:1px solid var(--line); border-radius:10px; color:#696b80; background:#fff; }
    .icon-btn:hover:not(:disabled) { color:var(--primary); border-color:#c9c5ff; }
    .icon-btn.remove:hover { color:var(--danger); border-color:#ffc7d1; }
    .icon-btn svg { width:17px; height:17px; }
    .panel-actions { display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-top:17px; }
    .privacy { display:flex; align-items:center; gap:7px; color:var(--muted); font-size:.82rem; }
    .privacy svg { width:17px; height:17px; color:var(--green); flex-shrink:0; }
    .status { display:none; margin-top:17px; padding:13px 15px; border-radius:12px; font-size:.9rem; }
    .status.show { display:block; }
    .status.info { background:#efedff; color:#4c43c9; }
    .status.success { background:#e6fbf3; color:#087d68; }
    .status.error { background:#fff0f2; color:#b52b49; }
    .tips { display:grid; grid-template-columns:repeat(3,1fr); gap:15px; max-width:820px; margin:24px auto 0; }
    .tip { padding:18px; border:1px solid var(--line); border-radius:17px; background:rgba(255,255,255,.72); }
    .tip b { display:block; margin-bottom:5px; font-size:.92rem; }
    .tip p { margin:0; color:var(--muted); font-size:.84rem; }
    footer { border-top:1px solid var(--line); padding:24px 0; color:var(--muted); font-size:.85rem; }
    .footer-inner { display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; }
    .toast { position:fixed; left:50%; bottom:22px; transform:translate(-50%,18px); opacity:0; pointer-events:none; padding:12px 17px; border-radius:13px; background:#20213a; color:#fff; box-shadow:var(--shadow); z-index:10; transition:.22s ease; max-width:calc(100% - 28px); text-align:center; font-size:.9rem; }
    .toast.show { opacity:1; transform:translate(-50%,0); }
    @media(max-width:650px) {
      main { padding-top:30px; }
      h1 { letter-spacing:-1.8px; }
      .tips { grid-template-columns:1fr; }
      .file-row { grid-template-columns:auto minmax(0,1fr); }
      .file-actions { grid-column:2; justify-content:flex-start; }
      .panel-head { align-items:flex-start; flex-direction:column; }
      .panel-actions { align-items:stretch; flex-direction:column; }
      .panel-actions .btn { width:100%; }
    }
  </style>
</head>
<body>
  <header>
    <div class="container nav">
      <a class="brand" href="index.html" aria-label="PDFix homepage">
        <span class="brand-mark"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M7 3.5h7l4 4V20a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 20V5A1.5 1.5 0 0 1 7.5 3.5Z" stroke="currentColor" stroke-width="1.8"/><path d="M14 3.8V8h4M8.5 12h7M8.5 15.5h7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>
        <span class="brand-name">PDF<span>ix</span></span>
      </a>
      <a class="back-link" href="index.html">← Back to all tools</a>
    </div>
  </header>

  <main>
    <div class="container">
      <div class="intro">
        <div class="eyebrow">✦ PDFix PDF tools</div>
        <h1>Merge <span>PDF files</span></h1>
        <p>Bring multiple documents together into one PDF. Add your files, arrange their order, and download the merged result.</p>
      </div>

      <div class="workspace">
        <div class="dropzone" id="dropzone" tabindex="0" role="button" aria-label="Choose or drop PDF files">
          <div class="upload-icon"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M5 14.5v4A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5v-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
          <strong>Drop PDF files here</strong>
          <p>Choose two or more PDFs to combine them into one file.</p>
          <button type="button" class="btn btn-primary" id="chooseFiles">Select PDF files <span aria-hidden="true">↑</span></button>
          <input id="fileInput" type="file" accept=".pdf,application/pdf" multiple />
        </div>

        <div class="file-panel" id="filePanel">
          <div class="panel-head">
            <div><h2>Your PDF files</h2><div class="count" id="fileCount">0 files selected</div></div>
            <button type="button" class="btn btn-light" id="addMore">+ Add more files</button>
          </div>
          <div class="file-list" id="fileList"></div>
          <div class="panel-actions">
            <div class="privacy"><svg viewBox="0 0 24 24" fill="none"><path d="M12 3 19 6v5c0 4.5-2.9 7.7-7 10-4.1-2.3-7-5.5-7-10V6l7-3Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="m9 12 2 2 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg> Files are processed in your browser.</div>
            <button type="button" class="btn btn-primary" id="mergeBtn" disabled>Merge PDFs <span aria-hidden="true">→</span></button>
          </div>
          <div class="status" id="status" role="status" aria-live="polite"></div>
        </div>
      </div>

      <div class="tips">
        <article class="tip"><b>① Add your files</b><p>Select at least two PDF documents or drag them into the upload area.</p></article>
        <article class="tip"><b>② Arrange the order</b><p>Use the arrows beside each file to choose the order of pages in the final PDF.</p></article>
        <article class="tip"><b>③ Merge and download</b><p>PDFix combines your files locally, then saves the new PDF to your device.</p></article>
      </div>
    </div>
  </main>

  <footer><div class="container footer-inner"><a class="brand" href="index.html"><span class="brand-name">PDF<span>ix</span></span></a><span>© <span id="year"></span> PDFix · Your documents, made simple.</span></div></footer>
  <div class="toast" id="toast" role="status" aria-live="polite"></div>

  <!-- pdf-lib runs in the browser; no PDF files are sent to a server. -->
  <script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
  <script>
    const fileInput = document.getElementById('fileInput');
    const dropzone = document.getElementById('dropzone');
    const filePanel = document.getElementById('filePanel');
    const fileList = document.getElementById('fileList');
    const fileCount = document.getElementById('fileCount');
    const mergeBtn = document.getElementById('mergeBtn');
    const statusBox = document.getElementById('status');
    const toast = document.getElementById('toast');
    let selectedFiles = [];
    let toastTimer;

    document.getElementById('year').textContent = new Date().getFullYear();

    function showToast(message) {
      toast.textContent = message;
      toast.classList.add('show');
      clearTimeout(toastTimer);
      toastTimer = setTimeout(() => toast.classList.remove('show'), 3000);
    }
    function setStatus(message, type = 'info') {
      statusBox.textContent = message;
      statusBox.className = `status show ${type}`;
    }
    function clearStatus() {
      statusBox.textContent = '';
      statusBox.className = 'status';
    }
    function formatSize(bytes) {
      if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
      return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    }
    function pdfIcon() {
      return '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6.5 3.5h8l4 4V20a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 5.5 20V5A1.5 1.5 0 0 1 7 3.5Z" stroke="currentColor" stroke-width="1.7"/><path d="M14.5 3.8V8h4M8 12h8M8 15h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>';
    }
    function addFiles(fileCollection) {
      const incoming = Array.from(fileCollection || []);
      if (!incoming.length) return;
      const invalid = incoming.filter(file => !file.name.toLowerCase().endsWith('.pdf') && file.type !== 'application/pdf');
      if (invalid.length) {
        showToast('Please select PDF files only. Non-PDF files were skipped.');
      }
      const valid = incoming.filter(file => file.name.toLowerCase().endsWith('.pdf') || file.type === 'application/pdf');
      valid.forEach(file => {
        const duplicate = selectedFiles.some(existing => existing.name === file.name && existing.size === file.size && existing.lastModified === file.lastModified);
        if (!duplicate) selectedFiles.push(file);
      });
      renderFiles();
      fileInput.value = '';
      if (valid.length) clearStatus();
    }
    function renderFiles() {
      fileList.innerHTML = '';
      filePanel.classList.toggle('visible', selectedFiles.length > 0);
      fileCount.textContent = `${selectedFiles.length} ${selectedFiles.length === 1 ? 'file' : 'files'} selected`;
      mergeBtn.disabled = selectedFiles.length < 2;
      selectedFiles.forEach((file, index) => {
        const row = document.createElement('div');
        row.className = 'file-row';
        const icon = document.createElement('div');
        icon.className = 'file-icon';
        icon.innerHTML = pdfIcon();
        const meta = document.createElement('div');
        meta.className = 'file-meta';
        const name = document.createElement('div');
        name.className = 'file-name';
        name.textContent = file.name;
        name.title = file.name;
        const size = document.createElement('div');
        size.className = 'file-size';
        size.textContent = formatSize(file.size);
        meta.append(name, size);

        const actions = document.createElement('div');
        actions.className = 'file-actions';
        actions.append(
          makeIconButton('↑', 'Move file up', index === 0, () => moveFile(index, -1)),
          makeIconButton('↓', 'Move file down', index === selectedFiles.length - 1, () => moveFile(index, 1)),
          makeIconButton('×', 'Remove file', false, () => removeFile(index), true)
        );
        row.append(icon, meta, actions);
        fileList.append(row);
      });
    }
    function makeIconButton(label, ariaLabel, disabled, action, isRemove = false) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = `icon-btn${isRemove ? ' remove' : ''}`;
      button.textContent = label;
      button.setAttribute('aria-label', ariaLabel);
      button.title = ariaLabel;
      button.disabled = disabled;
      button.addEventListener('click', action);
      return button;
    }
    function moveFile(index, direction) {
      const target = index + direction;
      if (target < 0 || target >= selectedFiles.length) return;
      [selectedFiles[index], selectedFiles[target]] = [selectedFiles[target], selectedFiles[index]];
      renderFiles();
      clearStatus();
    }
    function removeFile(index) {
      selectedFiles.splice(index, 1);
      renderFiles();
      clearStatus();
    }

    document.getElementById('chooseFiles').addEventListener('click', event => {
      event.stopPropagation();
      fileInput.click();
    });
    document.getElementById('addMore').addEventListener('click', () => fileInput.click());
    dropzone.addEventListener('click', event => {
      if (event.target.closest('button')) return;
      fileInput.click();
    });
    dropzone.addEventListener('keydown', event => {
      if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        fileInput.click();
      }
    });
    fileInput.addEventListener('change', () => addFiles(fileInput.files));
    ['dragenter', 'dragover'].forEach(type => dropzone.addEventListener(type, event => {
      event.preventDefault();
      dropzone.classList.add('dragover');
    }));
    ['dragleave', 'drop'].forEach(type => dropzone.addEventListener(type, event => {
      event.preventDefault();
      dropzone.classList.remove('dragover');
    }));
    dropzone.addEventListener('drop', event => addFiles(event.dataTransfer.files));

    mergeBtn.addEventListener('click', async () => {
      if (selectedFiles.length < 2) {
        setStatus('Please add at least two PDF files to merge.', 'error');
        return;
      }
      if (!window.PDFLib) {
        setStatus('The PDF library could not load. Check your internet connection and reload this page.', 'error');
        return;
      }
      const originalLabel = mergeBtn.innerHTML;
      mergeBtn.disabled = true;
      mergeBtn.textContent = 'Merging PDFs…';
      setStatus('Merging your files. Please keep this tab open.', 'info');
      try {
        const { PDFDocument } = window.PDFLib;
        const mergedPdf = await PDFDocument.create();
        for (let i = 0; i < selectedFiles.length; i++) {
          const file = selectedFiles[i];
          setStatus(`Reading file ${i + 1} of ${selectedFiles.length}: ${file.name}`, 'info');
          const bytes = await file.arrayBuffer();
          const sourcePdf = await PDFDocument.load(bytes);
          const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices());
          copiedPages.forEach(page => mergedPdf.addPage(page));
        }
        const outputBytes = await mergedPdf.save();
        const blob = new Blob([outputBytes], { type: 'application/pdf' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = 'pdfix-merged.pdf';
        document.body.appendChild(link);
        link.click();
        link.remove();
        setTimeout(() => URL.revokeObjectURL(url), 1500);
        setStatus('Success! Your merged PDF has been downloaded as pdfix-merged.pdf.', 'success');
      } catch (error) {
        console.error(error);
        setStatus('We could not merge one of these files. It may be damaged, password-protected, or use an unsupported PDF feature. Please check the files and try again.', 'error');
      } finally {
        mergeBtn.disabled = selectedFiles.length < 2;
        mergeBtn.innerHTML = originalLabel;
      }
    });
  </script>
</body>
</html>