[{"data":1,"prerenderedAt":41},["ShallowReactive",2],{"$fJAYb5-rFw-FJ-9SYyVtAYP0cXs8kD-sG7Ni_Lh4YBJg":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"tools":13,"draft":15,"html":16,"toc":17,"minutes":40},"merging-pdfs-in-the-browser","How Merging PDFs Works in the Browser, and What Gets Lost","How ToolOnTap merges PDFs with pdf-lib's copyPages, previews them with pdf.js, refuses protected files, and why bookmarks and form fields don't survive.","2026-10-05",[9,10,11,12],"pdf","pdf-lib","pdf.js","javascript",[14],"merge-pdf",false,"\u003Cp>A visa application wants your bank statements, payslips and passport scan as one PDF. You have six files. Most online mergers want you to upload all six to a server you know nothing about, which is a strange thing to do with a bank statement. I built \u003Ca href=\"\u002Fmerge-pdf\">Merge PDF\u003C\u002Fa> so the whole job happens on your own device, and on the way I learned exactly what a merge keeps and what it quietly drops.\u003C\u002Fp>\n\u003Ch2 id=\"two-libraries-two-jobs\">Two libraries, two jobs\u003C\u002Fh2>\n\u003Cp>The tool uses two libraries, because no single one does both jobs well:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>pdf-lib\u003C\u002Fstrong> reads and writes the PDF structure. It does the actual merge, and it’s the one that can tell if a file is encrypted.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>pdf.js\u003C\u002Fstrong> (Mozilla’s PDF renderer) draws pages. It’s only used for the previews, so you can check you picked the right files.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Neither loads with the page. pdf-lib is imported the first time you pick a PDF, and pdf.js loads right after it, in the background, with one worker shared by every open file (by default pdf.js would start one per document).\u003C\u002Fp>\n\u003Cp>There’s a small catch in sharing the same bytes. pdf.js moves its data into the worker, which leaves the original buffer unusable on the main thread. So \u003Ccode>openPreview\u003C\u002Fcode> reads the file again and gives pdf.js its own copy, and pdf-lib keeps the one it parsed. Each file sits in memory twice, which is part of why there’s a size limit.\u003C\u002Fp>\n\u003Ch2 id=\"checking-a-file-before-trusting-it\">Checking a file before trusting it\u003C\u002Fh2>\n\u003Cp>Every file goes through the same checks, in this order:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Size.\u003C\u002Fstrong> Up to 100 MB per PDF. Bigger files get a message suggesting your PDF app’s “Reduce file size” option.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Header.\u003C\u002Fstrong> The bytes must contain \u003Ccode>%PDF-\u003C\u002Fcode>. The check looks anywhere in the first 1,024 bytes, not only at byte 0, because some generators put junk before the header. A file renamed to \u003Ccode>.pdf\u003C\u002Fcode> fails here, with a message that says so.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Parsing and encryption.\u003C\u002Fstrong> This is the interesting one:\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">PDFDocument\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> } \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> import\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'pdf-lib'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)  \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">\u002F\u002F only loaded once a PDF is picked\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">try\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">  \u002F\u002F ignoreEncryption so encrypted files load and can be reported, instead of throwing a generic error\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> doc\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> PDFDocument.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">load\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(bytes, { ignoreEncryption: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">true\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, updateMetadata: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">false\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> })\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (doc.isEncrypted) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { issue: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">pdfIssue\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(file.name, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'encrypted'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> pageCount\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> doc.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">getPageCount\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">pageCount) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { issue: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">pdfIssue\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(file.name, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'empty'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { doc, bytes, pageCount }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">catch\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (e) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { issue: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">pdfIssue\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(file.name, e \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> RangeError\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> ?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'memory'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> :\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'damaged'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>pdf-lib can’t decrypt PDFs. Without \u003Ccode>ignoreEncryption\u003C\u002Fcode>, an encrypted file throws an error that doesn’t say much. With it, the file loads, \u003Ccode>isEncrypted\u003C\u002Fcode> is true, and the tool can give a specific message. Copying pages out of an encrypted file would produce blank or garbled pages, so it’s refused.\u003C\u002Fp>\n\u003Cp>That includes PDFs you can open without a password. A file with only editing restrictions (no printing, no copying) is still encrypted inside, so it’s refused too. The message suggests the usual fix: print it to a new PDF from your PDF app, or save a copy without protection, and add that copy.\u003C\u002Fp>\n\u003Cp>A \u003Ccode>RangeError\u003C\u002Fcode> while parsing usually means the browser ran out of memory, so it gets the “close other tabs” message instead of “damaged”.\u003C\u002Fp>\n\u003Ch2 id=\"the-merge-itself\">The merge itself\u003C\u002Fh2>\n\u003Cp>Once the files are loaded, the merge is short:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">PDFDocument\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> } \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> import\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'pdf-lib'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> out\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> PDFDocument.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">create\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">out.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">setProducer\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'ToolOnTap'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">out.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">setCreator\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'ToolOnTap – toolontap.com'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> [\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">i\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">doc\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">of\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> docs.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">entries\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> pages\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> out.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">copyPages\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(doc, doc.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">getPageIndices\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">())\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  for\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> page\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> of\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> pages) out.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">addPage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(page)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">  onProgress\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">?.(i \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>copyPages\u003C\u002Fcode> does the heavy lifting. A PDF page is an object that points to other objects: its content stream (the drawing instructions), the fonts and images it uses, and its annotations. \u003Ccode>copyPages\u003C\u002Fcode> follows those links and copies everything the page needs into the new document. Nothing is drawn or converted, so text stays text you can select and search, and scanned images keep their original compression. Each page keeps its own size and orientation, so an A4 letter and a US Letter form can sit next to each other unchanged.\u003C\u002Fp>\n\u003Cp>The progress callback runs after each file, which is where the button’s “Merging file 2 of 6…” comes from. The result is the files in the order of the list, from top to bottom. You drag them, or use the arrow buttons, to change that order.\u003C\u002Fp>\n\u003Ch2 id=\"what-doesn-t-come-along\">What doesn’t come along\u003C\u002Fh2>\n\u003Cp>The merge copies pages. Anything that belongs to the document as a whole, and not to a page, stays behind. The code says it in one comment: “document-level parts (bookmarks, form data) don’t”.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>Part of the PDF\u003C\u002Fth>\n\u003Cth>Where it lives\u003C\u002Fth>\n\u003Cth>After the merge\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Text, images, vector drawings\u003C\u002Ftd>\n\u003Ctd>The page\u003C\u002Ftd>\n\u003Ctd>Kept exactly\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Web links on the page\u003C\u002Ftd>\n\u003Ctd>The page’s annotations\u003C\u002Ftd>\n\u003Ctd>Kept\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Page size and rotation\u003C\u002Ftd>\n\u003Ctd>The page\u003C\u002Ftd>\n\u003Ctd>Kept\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Bookmarks (the sidebar outline)\u003C\u002Ftd>\n\u003Ctd>The document catalog\u003C\u002Ftd>\n\u003Ctd>Dropped\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Form fields\u003C\u002Ftd>\n\u003Ctd>Widgets on the page, field tree in the catalog\u003C\u002Ftd>\n\u003Ctd>May stop being editable\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Title, author, keywords\u003C\u002Ftd>\n\u003Ctd>Document info\u003C\u002Ftd>\n\u003Ctd>Not copied; Producer is set to ToolOnTap\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>Form fields are the subtle case. What you see of a field (the box and any value drawn in it) is a widget annotation on the page, and it comes along. But the list that tells a PDF reader “these widgets are form fields” sits in the document catalog, and that doesn’t. So a merged form can look right and still not let you type in it. That’s why the tool’s tip says to fill in and save forms before merging. Your answers then travel as part of the page.\u003C\u002Fp>\n\u003Ch2 id=\"previews-without-rendering-every-page\">Previews without rendering every page\u003C\u002Fh2>\n\u003Cp>Each file in the list shows its first page, 44 pixels wide, and you can open any file to look through all its pages. A 300-page statement shouldn’t mean rendering 300 pages up front, so the preview component waits:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>It uses an \u003Ccode>IntersectionObserver\u003C\u002Fcode> and renders a page only when it comes within 300 pixels of the screen.\u003C\u002Fli>\n\u003Cli>It renders at twice the shown width, so previews stay sharp on high-density screens.\u003C\u002Fli>\n\u003Cli>It turns the finished canvas into a small JPEG (quality 0.8) and shows that as an image. A JPEG blob takes a fraction of the memory a live canvas would, which matters on long documents.\u003C\u002Fli>\n\u003Cli>It keeps every canvas under the browser’s size limit. iOS refuses a canvas that’s too big and simply draws nothing.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>If a preview fails, the file still merges. The preview is there to help you check the order, and it never blocks the job.\u003C\u002Fp>\n\u003Ch2 id=\"memory-and-the-100-mb-limit\">Memory and the 100 MB limit\u003C\u002Fh2>\n\u003Cp>There’s no limit on the number of files. The real limit is your device’s memory: each file is held by pdf-lib and pdf.js, and the merged result is built in memory too before you download it. That’s why each file is capped at 100 MB, and why very large merges work best on a computer rather than a phone.\u003C\u002Fp>\n\u003Cp>The merged PDF is about as big as all the files together, since nothing is re-compressed. If you’re sending it by email, keep in mind that email services usually cap attachments at 20 to 25 MB.\u003C\u002Fp>\n\u003Ch2 id=\"try-it\">Try it\u003C\u002Fh2>\n\u003Cp>\u003Ca href=\"\u002Fmerge-pdf\">Merge PDF\u003C\u002Fa> joins two or more PDFs in the order you choose, with a page count and a preview for each. If you need only part of a file first, \u003Ca href=\"\u002Fsplit-pdf\">Split PDF\u003C\u002Fa> can extract the pages you need, and \u003Ca href=\"\u002Frearrange-pdf-pages\">Rearrange PDF pages\u003C\u002Fa> changes their order. The rest are on the \u003Ca href=\"\u002Fpdf-tools\">PDF tools\u003C\u002Fa> page.\u003C\u002Fp>\n",[18,22,25,28,31,34,37],{"id":19,"text":20,"level":21},"two-libraries-two-jobs","Two libraries, two jobs",2,{"id":23,"text":24,"level":21},"checking-a-file-before-trusting-it","Checking a file before trusting it",{"id":26,"text":27,"level":21},"the-merge-itself","The merge itself",{"id":29,"text":30,"level":21},"what-doesn-t-come-along","What doesn’t come along",{"id":32,"text":33,"level":21},"previews-without-rendering-every-page","Previews without rendering every page",{"id":35,"text":36,"level":21},"memory-and-the-100-mb-limit","Memory and the 100 MB limit",{"id":38,"text":39,"level":21},"try-it","Try it",5,1792325801086]