PDF Compiler — Merge Multiple PDFs Into One

March 3, 2026 NEW

A lightweight PDF Compiler that lets you upload multiple PDF files, reorder them, and compile them into one combined PDF — all in the browser (nothing gets uploaded).

Install: Save as /tools/pdf-compiler/index.html and visit the folder in your browser.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>PDF Compiler — Merge PDFs</title>
<meta name="description" content="Compile multiple PDFs into one file in your browser. Reorder, remove, and download the merged PDF. No uploads, no database." />
<meta name="robots" content="index,follow" />
<link rel="canonical" href="https://your-site.com/tools/pdf-compiler/" />
<style>
  :root{
    --bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
    --text:#e8eef8; --muted:rgba(232,238,248,.72);
    --line:rgba(255,255,255,.10); --accent:#2a8cff;
    --shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
    --ok:#7CFF9A; --bad:#FF7C7C;
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
  .wrap{max-width:980px;margin:0 auto;padding:18px}
  .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
  .pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
  .muted{color:var(--muted)}
  .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
  input[type="text"]{
    width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
    background:var(--panel2);color:var(--text);outline:none;font-weight:800;
  }
  input[type="file"]{
    width:100%;
    padding:10px 12px;border-radius:12px;border:1px dashed rgba(255,255,255,.22);
    background:rgba(255,255,255,.03);color:var(--muted);
  }
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    border:0;border-radius:12px;padding:10px 12px;background:var(--accent);
    color:#06101a;font-weight:950;cursor:pointer;
  }
  .btn2{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:transparent;
    color:var(--text);font-weight:950;cursor:pointer;
  }
  .btn2[disabled], .btn[disabled]{opacity:.5;cursor:not-allowed}
  .list{display:grid;gap:10px;margin-top:10px}
  .item{
    padding:10px;border-radius:14px;border:1px solid rgba(255,255,255,.10);
    background:rgba(255,255,255,.03);
    display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;
  }
  .name{font-weight:950}
  .meta{font-size:12px;color:var(--muted);margin-top:4px}
  .actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
  .mini{
    border:1px solid rgba(255,255,255,.12);background:transparent;color:var(--text);
    border-radius:10px;padding:8px 10px;font-weight:950;cursor:pointer;
  }
  .hr{height:1px;background:var(--line);margin:12px 0}
  .log{
    padding:12px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);
    white-space:pre-wrap;word-break:break-word;
    font:650 12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    min-height:86px;
  }
  .ok{color:var(--ok);font-weight:950}
  .bad{color:var(--bad);font-weight:950}
</style>

<!-- PDF-Lib (client-side PDF merge) -->
<script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
</head>
<body>
<div class="wrap">

  <div class="card">
    <div class="pill">Tool</div>
    <h1 style="margin:10px 0 6px;font-size:22px">PDF Compiler</h1>
    <div class="muted">Add PDFs → reorder → compile → download (runs locally in your browser).</div>
  </div>

  <div class="card">
    <b>Add PDF files</b>
    <div class="muted" style="margin-top:6px">Nothing is uploaded. Your PDFs stay on your device.</div>
    <div class="hr"></div>

    <div class="row">
      <div style="flex:1;min-width:280px">
        <input id="file" type="file" accept="application/pdf" multiple />
        <div class="muted" style="margin-top:6px;font-size:12px">Tip: You can add more files later — they’ll append to the list.</div>
      </div>

      <div style="flex:1;min-width:280px">
        <div class="muted" style="font-size:12px;margin-bottom:6px">Output filename</div>
        <input id="outName" type="text" value="compiled.pdf" />
      </div>

      <div class="row" style="align-self:flex-end">
        <button class="btn" id="compile" type="button">🧩 Compile</button>
        <button class="btn2" id="clear" type="button">🧹 Clear</button>
      </div>
    </div>

    <div class="hr"></div>

    <b>Files</b>
    <div id="list" class="list"></div>

    <div class="hr"></div>

    <b>Status</b>
    <div id="log" class="log">Add PDFs to begin…</div>
  </div>

</div>

<script>
(function(){
  var files = []; // {file:File, id:string}
  var $ = function(id){ return document.getElementById(id); };

  function fmtBytes(n){
    n = Number(n||0);
    if (n < 1024) return n + " B";
    var kb = n / 1024;
    if (kb < 1024) return kb.toFixed(1) + " KB";
    var mb = kb / 1024;
    if (mb < 1024) return mb.toFixed(2) + " MB";
    var gb = mb / 1024;
    return gb.toFixed(2) + " GB";
  }

  function log(msg, cls){
    var el = $("log");
    el.innerHTML = (cls ? '<span class="'+cls+'">' : '') + escapeHtml(msg) + (cls ? '</span>' : '');
  }

  function appendLog(msg){
    var el = $("log");
    el.textContent = (el.textContent ? el.textContent + "\n" : "") + msg;
  }

  function escapeHtml(s){
    return String(s||"").replace(/[&<>"]/g, function(c){
      return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]);
    });
  }

  function uid(){
    return Math.random().toString(16).slice(2) + Date.now().toString(16);
  }

  function render(){
    var host = $("list");
    host.innerHTML = "";

    if (!files.length){
      host.innerHTML = '<div class="muted" style="font-size:13px">No PDFs added yet.</div>';
      return;
    }

    files.forEach(function(x, idx){
      var div = document.createElement("div");
      div.className = "item";

      var left = document.createElement("div");
      left.innerHTML =
        '<div class="name">' + escapeHtml((idx+1) + ". " + x.file.name) + '</div>' +
        '<div class="meta">Size: <b>' + escapeHtml(fmtBytes(x.file.size)) + '</b></div>';

      var actions = document.createElement("div");
      actions.className = "actions";

      var up = document.createElement("button");
      up.className = "mini";
      up.type = "button";
      up.textContent = "↑";
      up.disabled = idx === 0;
      up.addEventListener("click", function(){
        var tmp = files[idx-1]; files[idx-1] = files[idx]; files[idx] = tmp;
        render();
      });

      var down = document.createElement("button");
      down.className = "mini";
      down.type = "button";
      down.textContent = "↓";
      down.disabled = idx === files.length - 1;
      down.addEventListener("click", function(){
        var tmp = files[idx+1]; files[idx+1] = files[idx]; files[idx] = tmp;
        render();
      });

      var del = document.createElement("button");
      del.className = "mini";
      del.type = "button";
      del.textContent = "✕";
      del.addEventListener("click", function(){
        files = files.filter(function(y){ return y.id !== x.id; });
        render();
      });

      actions.appendChild(up);
      actions.appendChild(down);
      actions.appendChild(del);

      div.appendChild(left);
      div.appendChild(actions);
      host.appendChild(div);
    });
  }

  function safeOutName(){
    var n = $("outName").value.trim() || "compiled.pdf";
    if (!/\.pdf$/i.test(n)) n += ".pdf";
    n = n.replace(/[^\w\.\-\(\)\s]+/g, "_");
    return n;
  }

  async function compile(){
    if (!files.length) { log("Add at least one PDF first.", "bad"); return; }
    if (!window.PDFLib || !window.PDFLib.PDFDocument) {
      log("PDF-Lib failed to load. Check your connection (CDN).", "bad");
      return;
    }

    $("compile").disabled = true;
    $("clear").disabled = true;
    log("Compiling…");

    try{
      var PDFDocument = window.PDFLib.PDFDocument;
      var merged = await PDFDocument.create();

      for (var i=0;i<files.length;i++){
        var f = files[i].file;
        appendLog("Loading: " + f.name);
        var buf = await f.arrayBuffer();
        var src = await PDFDocument.load(buf, { ignoreEncryption: true });

        var pages = await merged.copyPages(src, src.getPageIndices());
        pages.forEach(function(p){ merged.addPage(p); });

        appendLog("Added pages: " + pages.length);
      }

      appendLog("Saving merged PDF…");
      var bytes = await merged.save();

      var blob = new Blob([bytes], {type:"application/pdf"});
      var url = URL.createObjectURL(blob);

      var a = document.createElement("a");
      a.href = url;
      a.download = safeOutName();
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);

      log("Done! Download started: " + a.download, "ok");
    } catch(e){
      log("Error: " + (e && e.message ? e.message : String(e)), "bad");
    } finally {
      $("compile").disabled = false;
      $("clear").disabled = false;
    }
  }

  $("file").addEventListener("change", function(){
    var picked = Array.prototype.slice.call($("file").files || []);
    if (!picked.length) return;

    picked.forEach(function(f){
      if (!/\.pdf$/i.test(f.name) && f.type !== "application/pdf") return;
      files.push({file:f, id:uid()});
    });

    $("file").value = "";
    render();
    log("Added " + picked.length + " file(s). Reorder if needed, then Compile.");
  });

  $("compile").addEventListener("click", compile);

  $("clear").addEventListener("click", function(){
    if (!confirm("Clear the file list?")) return;
    files = [];
    render();
    log("Cleared. Add PDFs to begin…");
  });

  render();
})();
</script>
</body>
</html>

Comments (0)

No comments yet — be the first.

← Back to all scripts