Merger · No upload
Merge PDF files, privately
Add PDF files, put the rows in the order you want, and download one PDF. Every page is copied byte for byte — nothing is rendered or re-encoded — and the merge runs inside your browser: nothing is uploaded at any step, and the network meter below proves it live.
Refused by name: protected files (this page does not open protected files), cross-reference or object streams not in Flate (only Flate is read), no %PDF header or no readable page tree, past 50 files, 128 MB or 20,000 pages. Bookmarks are chained; links outside the merge are dropped and the row says how many; signatures do not carry over; tagging, the attachments panel and page labels are not carried. Takes .pdf.
The list is the page order, top to bottom. Drag a row to move it, or use its ↑ and ↓ buttons — the same job from the keyboard. That order is what Merge writes.
How this merger works
When you add a file, your browser opens it with its File API and a reader on this page walks it in memory — no transfer happens, because the file is already on your machine — from the header to the page tree. That is why each file gets a row with its page count and size before you press Merge, and why a file this page will not open gets a row saying why. Merge then writes one new file in the order the list shows. Every page, with its content, fonts and images copied byte for byte, /Filter untouched: nothing is decompressed, rendered or re-encoded, so a scan stays that scan. Form fields and annotations are carried too, but as dictionaries rewritten to the merged file's object numbers — and so is the structure around them, because two files' object numbers collide and a merged file needs one page tree. The header is the highest version any source declared, floored at 1.4; the trailer gets a fresh /ID; and there is no /Info dictionary and no document metadata stream, so nothing at the document level names a producer, author or date. An image's own embedded metadata travels inside its bytes.
The better instrument is not ours but your browser's. Press F12, open the Network tab, leave it visible, and merge: nothing of yours joins the list. On a first visit you may see this site being cached so the tools work offline; not one of those requests carries a byte of your PDF, and merging fetches nothing on any visit. That panel records what a page does rather than what it claims, and no code of ours can quiet it — which is why the meter above counts the requests this page makes from the moment your first file is read, and says so. This route is first-party throughout, so it works offline from the first page load and the meter's zero has no footnote. Compare notes with the methodology page.
The honest limits. A file that carries an encryption dictionary is refused by name the moment you add it — including one a viewer would have opened without asking anything — and a damaged file whose compressed object streams cannot be read is refused the same way, because this page cannot tell ciphertext from damage and will not rebuild either into pages. A damaged cross-reference is a different case: it is never the encrypted part, so the reader rebuilds around it and the file merges. The reader inflates only Flate, so a cross-reference or object stream under any other filter — LZW, ASCII85, a chain, a TIFF predictor — is refused naming it: a reader that cannot decode the map silently finds no pages. So is a file with no %PDF header in its first 1,024 bytes, no readable page tree or no pages. Forms are merged into one, and fields whose names collide between files are grouped under the file's name with the row saying how many; a script in one form that names a renamed field may then stop working. Named destinations are resolved, and a page whose content or resources point at nothing is counted and stated. The caps are about your device — up to 50 files and 128 MB, and 20,000 pages, each refused by name — because the whole merge sits in this tab's memory: measured in Chrome 151, thirty copies of a 4.4 MB, 126-page booklet (127 MB in, 3,780 pages) peaked at 455 MB of JavaScript heap and 849 MB of renderer memory with the finished file held for download.