{"id":1607,"date":"2026-03-28T13:58:32","date_gmt":"2026-03-28T13:58:32","guid":{"rendered":"https:\/\/primetoolhub.com\/?page_id=1607"},"modified":"2026-08-08T12:54:39","modified_gmt":"2026-08-08T12:54:39","slug":"pdf-merge-and-split-studio","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/pdf-merge-and-split-studio\/","title":{"rendered":"Advanced PDF Merge and Split Studio (100% Offline Tool)"},"content":{"rendered":"<div class=\"pth-hero-section\">\n<div class=\"pth-hero-content\">\n<h2><b data-path-to-node=\"3,0,0\" data-index-in-node=\"0\">Merge &amp; Split PDFs Instantly \u2014 100% Offline<\/b> \u2014 Process sensitive files locally with total peace of mind<\/h2>\n<p>Use our advanced PDF Merge and Split Studio to combine, organize, split, and edit PDF files directly in your browser. 100% offline and secure.<\/p>\n<div id=\"pth-toc-placeholder\">\u00a0<\/div>\n<\/div>\n<div class=\"pth-hero-image\"><img decoding=\"async\" src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/03\/PDF-Merge-and-Split-Studio-1024x572.jpeg\" alt=\"PDF Merge and Split Studio\" width=\"450\" height=\"253\" data-no-lazy=\"1\" \/><\/div>\n<\/div>\n\n\n<div class=\"wp-block-rank-math-toc-block\" id=\"rank-math-toc\"><h2>Table of Contents<\/h2><nav><ul><li><a href=\"#\ud83d\udfe2-a-workspace-for-many-files-at-once\">\ud83d\udfe2 A Workspace For Many Files At Once<\/a><\/li><li><a href=\"#\ud83d\udfe1-the-duplex-scan-problem-solved-properly\">\ud83d\udfe1 The Duplex Scan Problem, Solved Properly<\/a><\/li><li><a href=\"#\ud83d\udfe2-where-the-page-grid-earns-its-place\">\ud83d\udfe2 Where The Page Grid Earns Its Place<\/a><\/li><li><a href=\"#\ud83d\udfe1-three-real-jobs\">\ud83d\udfe1 Three Real Jobs<\/a><ul><li><a href=\"#monthly-invoices-into-one-file\">Monthly invoices into one file<\/a><\/li><li><a href=\"#breaking-a-long-report-into-chapters\">Breaking a long report into chapters<\/a><\/li><li><a href=\"#preparing-a-set-of-documents-to-send-out\">Preparing a set of documents to send out<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udfe2-a-sensible-working-order\">\ud83d\udfe2 A Sensible Working Order<\/a><\/li><li><a href=\"#\ud83d\udd34-common-mistakes-and-real-limits\">\ud83d\udd34 Common Mistakes and Real Limits<\/a><\/li><li><a href=\"#\ud83d\udfe1-what-this-page-does-not-do\">\ud83d\udfe1 What This Page Does Not Do<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/2.16.105\/pdf.min.js\" data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\"><\/script>\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf-lib\/1.17.1\/pdf-lib.min.js\" data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\"><\/script>\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/Sortable\/1.15.0\/Sortable.min.js\" data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\"><\/script>\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jszip\/3.10.1\/jszip.min.js\" data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\"><\/script>\r\n\r\n<style>\r\n\/* STRICTLY SCOPED CSS - PTH PDF MERGE & ORGANIZE STUDIO *\/\r\n#pth-pdf-org-wrapper {\r\n  --blu: #2563eb; --grn: #047857; --red: #dc2626; --amb: #b45309; --pur: #6d28d9;\r\n  --bg1: #ffffff; --bg2: #f8fafc; --bg3: #f1f5f9;\r\n  --bd1: #cbd5e1; --bd2: #e2e8f0;\r\n  --tx1: #0f172a; --tx2: #475569;\r\n  font-family: 'Inter', system-ui, sans-serif;\r\n  max-width: 1400px; margin: 0 auto; box-sizing: border-box;\r\n  background: var(--bg3); padding: 20px; border-radius: 16px; color: var(--tx1); opacity: 1;\r\n}\r\n#pth-pdf-org-wrapper * { box-sizing: border-box; min-width: 0; }\r\n\r\n#pth-pdf-org-wrapper .pth-org-intro { background: var(--bg1); border: 1px solid var(--bd2); border-radius: 12px; padding: 22px; margin-bottom: 18px; }\r\n#pth-pdf-org-wrapper .pth-org-title { font-size: 27px; font-weight: 800; color: var(--blu); margin: 0 0 10px 0; line-height: 1.25; }\r\n#pth-pdf-org-wrapper .pth-org-intro p { font-size: 14px; color: var(--tx2); margin: 0; line-height: 1.65; }\r\n#pth-pdf-org-wrapper .pth-badges { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }\r\n#pth-pdf-org-wrapper .pth-badge-top { font-size: 12px; font-weight: 600; padding: 4px 12px; border-radius: 20px; }\r\n#pth-pdf-org-wrapper .pth-version-badge { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }\r\n#pth-pdf-org-wrapper .pth-status-offline { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }\r\n#pth-pdf-org-wrapper .pth-badge-ora { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }\r\n\r\n#pth-pdf-org-wrapper .pth-actions-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }\r\n#pth-pdf-org-wrapper .layout { display: grid; grid-template-columns: 1.8fr 1fr; gap: 16px; align-items: start; }\r\n#pth-pdf-org-wrapper .layout.fw { grid-template-columns: 1fr; }\r\n#pth-pdf-org-wrapper .layout.fw .sidebar { display: none; }\r\n\r\n#pth-pdf-org-wrapper .card { background: var(--bg1); border: 1px solid var(--bd2); border-radius: 12px; overflow: hidden; }\r\n#pth-pdf-org-wrapper .card-pad { padding: 14px; }\r\n#pth-pdf-org-wrapper .hdr { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--bd2); background: var(--bg2); flex-wrap: wrap; gap: 8px; }\r\n#pth-pdf-org-wrapper .hdr-title { font-size: 15px; font-weight: 700; color: var(--tx1); margin: 0; }\r\n#pth-pdf-org-wrapper label { font-size: 12px; color: var(--tx2); font-weight: 600; }\r\n\r\n#pth-pdf-org-wrapper .btn { padding: 8px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: 1px solid var(--bd1); transition: background .15s; display: inline-flex; align-items: center; gap: 6px; font-family: inherit; background: var(--bg1); color: var(--tx1); }\r\n#pth-pdf-org-wrapper .btn:hover { background: var(--bg2); }\r\n#pth-pdf-org-wrapper .btn:disabled { color: #94a3b8; cursor: not-allowed; background: var(--bg3); }\r\n#pth-pdf-org-wrapper .btn-primary { background: var(--blu); color: #fff; border-color: var(--blu); }\r\n#pth-pdf-org-wrapper .btn-primary:hover { background: #1d4ed8; }\r\n#pth-pdf-org-wrapper .btn-grn { background: var(--grn); color: #fff; border-color: var(--grn); }\r\n#pth-pdf-org-wrapper .btn-grn:hover { background: #065f46; }\r\n#pth-pdf-org-wrapper .btn-red { background: var(--red); color: #fff; border-color: var(--red); }\r\n#pth-pdf-org-wrapper .btn-sm { padding: 5px 10px; font-size: 12px; }\r\n\r\n#pth-pdf-org-wrapper .drop { border: 2px dashed var(--bd1); border-radius: 12px; background: var(--bg2); padding: 42px 20px; text-align: center; cursor: pointer; }\r\n#pth-pdf-org-wrapper .drop.over { border-color: var(--blu); background: #eff6ff; }\r\n#pth-pdf-org-wrapper .drop .big { font-size: 34px; margin-bottom: 8px; }\r\n#pth-pdf-org-wrapper .drop .t1 { font-size: 15px; font-weight: 700; color: var(--tx1); margin-bottom: 4px; }\r\n#pth-pdf-org-wrapper .drop .t2 { font-size: 12px; color: var(--tx2); }\r\n\r\n#pth-pdf-org-wrapper .tabs { display: flex; gap: 4px; padding: 8px 8px 0; background: var(--bg2); border-bottom: 1px solid var(--bd2); flex-wrap: wrap; }\r\n#pth-pdf-org-wrapper .tab { padding: 8px 12px; border-radius: 8px 8px 0 0; border: 1px solid transparent; background: transparent; color: var(--tx2); font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; }\r\n#pth-pdf-org-wrapper .tab:hover { background: var(--bg3); }\r\n#pth-pdf-org-wrapper .tab.on { background: var(--bg1); color: var(--blu); border-color: var(--bd2); border-bottom-color: var(--bg1); margin-bottom: -1px; }\r\n#pth-pdf-org-wrapper .panel { display: none; padding: 14px; }\r\n#pth-pdf-org-wrapper .panel.on { display: block; }\r\n#pth-pdf-org-wrapper .panel h4 { font-size: 12px; font-weight: 800; color: var(--tx2); margin: 0 0 8px; text-transform: uppercase; letter-spacing: .5px; }\r\n#pth-pdf-org-wrapper .row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; align-items: center; }\r\n#pth-pdf-org-wrapper .fld { margin-bottom: 12px; }\r\n#pth-pdf-org-wrapper .fld label { display: block; margin-bottom: 4px; }\r\n#pth-pdf-org-wrapper input[type=\"text\"], #pth-pdf-org-wrapper input[type=\"number\"], #pth-pdf-org-wrapper select {\r\n  width: 100%; padding: 7px 9px; border: 1px solid var(--bd1); border-radius: 7px; font-size: 13px; font-family: inherit; color: var(--tx1); background: var(--bg1);\r\n}\r\n#pth-pdf-org-wrapper input[type=\"range\"] { width: 100%; }\r\n\r\n#pth-pdf-org-wrapper .flist { display: flex; flex-direction: column; gap: 8px; max-height: 420px; overflow-y: auto; }\r\n#pth-pdf-org-wrapper .fitem { display: flex; align-items: center; gap: 10px; border: 1px solid var(--bd2); border-radius: 9px; padding: 9px 11px; background: var(--bg2); }\r\n#pth-pdf-org-wrapper .fitem.sortable-ghost { border-color: var(--blu); background: #eff6ff; }\r\n#pth-pdf-org-wrapper .fitem .grip { cursor: grab; color: #94a3b8; font-weight: 800; font-size: 15px; user-select: none; }\r\n#pth-pdf-org-wrapper .fitem .fmeta { flex: 1; min-width: 0; }\r\n#pth-pdf-org-wrapper .fitem .fname { font-size: 13px; font-weight: 700; color: var(--tx1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\r\n#pth-pdf-org-wrapper .fitem .fsub { font-size: 11px; color: var(--tx2); font-weight: 600; }\r\n#pth-pdf-org-wrapper .fitem input[type=\"text\"] { width: 118px; padding: 5px 7px; font-size: 12px; }\r\n#pth-pdf-org-wrapper .fitem button { border: 1px solid var(--bd1); background: var(--bg1); border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer; font-family: inherit; color: var(--red); font-weight: 700; }\r\n\r\n#pth-pdf-org-wrapper .pgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 9px; max-height: 560px; overflow-y: auto; padding: 4px; }\r\n#pth-pdf-org-wrapper .pcell { border: 2px solid var(--bd2); border-radius: 8px; padding: 5px; background: var(--bg2); cursor: grab; position: relative; }\r\n#pth-pdf-org-wrapper .pcell.sortable-ghost { border-color: var(--blu); background: #eff6ff; }\r\n#pth-pdf-org-wrapper .pcell.sel { border-color: var(--blu); background: #eff6ff; }\r\n#pth-pdf-org-wrapper .pcell canvas { width: 100%; height: auto; display: block; border-radius: 3px; background: #fff; }\r\n#pth-pdf-org-wrapper .pcell .pl { font-size: 10px; font-weight: 700; color: var(--tx2); text-align: center; padding-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\r\n#pth-pdf-org-wrapper .pcell .pa { display: flex; gap: 3px; justify-content: center; padding-top: 4px; }\r\n#pth-pdf-org-wrapper .pcell .pa button { border: 1px solid var(--bd1); background: var(--bg1); border-radius: 5px; font-size: 10px; padding: 2px 5px; cursor: pointer; font-family: inherit; color: var(--tx1); }\r\n#pth-pdf-org-wrapper .pcell .src { position: absolute; top: 4px; left: 4px; background: var(--pur); color: #fff; font-size: 9px; font-weight: 800; border-radius: 4px; padding: 1px 5px; }\r\n\r\n#pth-pdf-org-wrapper .itbl { width: 100%; border-collapse: collapse; font-size: 12px; }\r\n#pth-pdf-org-wrapper .itbl th, #pth-pdf-org-wrapper .itbl td { border: 1px solid var(--bd2); padding: 7px 9px; text-align: left; color: var(--tx1); }\r\n#pth-pdf-org-wrapper .itbl th { background: var(--bg2); font-weight: 700; }\r\n#pth-pdf-org-wrapper .itbl td.ok { color: var(--grn); font-weight: 700; }\r\n#pth-pdf-org-wrapper .itbl td.no { color: var(--amb); font-weight: 700; }\r\n\r\n#pth-pdf-org-wrapper .note { background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px; padding: 10px 12px; font-size: 12px; color: #78350f; line-height: 1.6; margin-bottom: 12px; }\r\n#pth-pdf-org-wrapper .note a { color: #1d4ed8; font-weight: 700; }\r\n#pth-pdf-org-wrapper .empty { font-size: 12px; color: var(--tx2); text-align: center; padding: 22px 10px; }\r\n#pth-pdf-org-wrapper .stats { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; font-weight: 700; color: var(--tx2); }\r\n#pth-pdf-org-wrapper .stats b { color: var(--blu); }\r\n\r\n#pth-pdf-org-wrapper .prog { height: 8px; background: var(--bg3); border-radius: 5px; overflow: hidden; margin-bottom: 10px; display: none; }\r\n#pth-pdf-org-wrapper .prog.on { display: block; }\r\n#pth-pdf-org-wrapper .prog i { display: block; height: 100%; width: 0; background: var(--blu); }\r\n\r\n#pth-pdf-org-wrapper #pth-org-toast { position: fixed; bottom: 20px; right: 20px; background: #0f172a; color: #fff; padding: 12px 18px; border-radius: 10px; font-size: 13px; font-weight: 600; z-index: 999999; display: none; max-width: 330px; line-height: 1.5; }\r\n#pth-pdf-org-wrapper #pth-org-toast.show { display: block; }\r\n#pth-pdf-org-wrapper #pth-org-toast.err { background: #991b1b; }\r\n#pth-pdf-org-wrapper #pth-org-toast.ok { background: #065f46; }\r\n\r\n@media (max-width: 900px) {\r\n  #pth-pdf-org-wrapper { padding: 14px; }\r\n  #pth-pdf-org-wrapper .layout { grid-template-columns: 1fr; }\r\n}\r\n<\/style>\r\n\r\n<div id=\"pth-pdf-org-wrapper\">\r\n\r\n  <div class=\"pth-org-intro\">\r\n    <h2 class=\"pth-org-title\">PDF Merge &amp; Organize Studio - Work Across Many Files<\/h2>\r\n    <div class=\"pth-badges\">\r\n      <span class=\"pth-badge-top pth-version-badge\">v5.0<\/span>\r\n      <span class=\"pth-badge-top pth-status-offline\">100% Offline<\/span>\r\n      <span class=\"pth-badge-top pth-badge-ora\">Zero Server Uploads<\/span>\r\n    <\/div>\r\n    <p>Load several PDFs at once and treat them as one workspace. Merge chosen page ranges, interleave two scans, split a long file into parts, drag pages between documents in a single grid, or run the same change across every file you loaded. Everything happens in your browser.<\/p>\r\n  <\/div>\r\n\r\n  <div class=\"pth-actions-bar\">\r\n    <button type=\"button\" class=\"btn\" id=\"pth-org-sample\">Load Sample<\/button>\r\n    <button type=\"button\" class=\"btn\" id=\"pth-org-fw\">Full Width<\/button>\r\n    <button type=\"button\" class=\"btn\" id=\"pth-org-clear\">Clear<\/button>\r\n    <button type=\"button\" class=\"btn btn-sm\" id=\"pth-org-add\">Add More Files<\/button>\r\n  <\/div>\r\n\r\n  <div class=\"layout\" id=\"pth-org-layout\">\r\n\r\n    <div class=\"card\">\r\n      <div class=\"hdr\">\r\n        <h3 class=\"hdr-title\">Loaded Files<\/h3>\r\n        <div class=\"stats\">\r\n          <span>Files: <b id=\"pth-org-nfiles\">0<\/b><\/span>\r\n          <span>Pages: <b id=\"pth-org-npages\">0<\/b><\/span>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"card-pad\" id=\"pth-org-dropwrap\">\r\n        <div class=\"drop\" id=\"pth-org-drop\" role=\"button\" tabindex=\"0\" aria-label=\"Click or drop PDF files to load\">\r\n          <div class=\"big\" aria-hidden=\"true\">&#128193;<\/div>\r\n          <div class=\"t1\">Select or drop PDF files here<\/div>\r\n          <div class=\"t2\">Load as many as you need - nothing is uploaded anywhere<\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"card-pad\" id=\"pth-org-listwrap\" style=\"display:none\">\r\n        <div class=\"note\">Drag the handle to change file order. The page range box limits which pages of that file are used when merging - leave it empty to take the whole file.<\/div>\r\n        <div class=\"flist\" id=\"pth-org-flist\"><\/div>\r\n      <\/div>\r\n\r\n      <div class=\"card-pad\" id=\"pth-org-gridwrap\" style=\"display:none; border-top:1px solid #e2e8f0;\">\r\n        <div class=\"hdr\" style=\"border:none;background:transparent;padding:0 0 10px;\">\r\n          <h3 class=\"hdr-title\">Cross-File Page Grid<\/h3>\r\n          <div class=\"row\" style=\"margin:0\">\r\n            <button type=\"button\" class=\"btn btn-sm\" id=\"pth-grid-build\">Load Pages<\/button>\r\n            <button type=\"button\" class=\"btn btn-sm\" id=\"pth-grid-selall\">Select All<\/button>\r\n            <button type=\"button\" class=\"btn btn-sm\" id=\"pth-grid-selnone\">Clear Selection<\/button>\r\n            <button type=\"button\" class=\"btn btn-sm btn-red\" id=\"pth-grid-delsel\">Delete Selected<\/button>\r\n          <\/div>\r\n        <\/div>\r\n        <div class=\"note\">Every page from every loaded file appears here with a colour tag showing which file it came from. Drag pages into any order, including across files, then build one new PDF from the result.<\/div>\r\n        <div class=\"prog\" id=\"pth-grid-prog\"><i id=\"pth-grid-bar\"><\/i><\/div>\r\n        <div class=\"pgrid\" id=\"pth-org-pgrid\"><div class=\"empty\">Press Load Pages to build the grid.<\/div><\/div>\r\n        <div class=\"row\" style=\"margin-top:12px\">\r\n          <button type=\"button\" class=\"btn btn-grn\" id=\"pth-grid-save\" disabled>Build PDF From Grid<\/button>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <input type=\"file\" id=\"pth-org-input\" accept=\"application\/pdf\" multiple style=\"display:none\" aria-label=\"Choose PDF files\">\r\n    <\/div>\r\n\r\n    <div class=\"card sidebar\">\r\n      <div class=\"tabs\" role=\"tablist\">\r\n        <button type=\"button\" class=\"tab on\" data-tab=\"merge\" role=\"tab\">Merge<\/button>\r\n        <button type=\"button\" class=\"tab\" data-tab=\"split\" role=\"tab\">Split<\/button>\r\n        <button type=\"button\" class=\"tab\" data-tab=\"batch\" role=\"tab\">Batch<\/button>\r\n        <button type=\"button\" class=\"tab\" data-tab=\"inspect\" role=\"tab\">Inspect<\/button>\r\n      <\/div>\r\n\r\n      <div class=\"panel on\" data-panel=\"merge\">\r\n        <h4>Merge Mode<\/h4>\r\n        <div class=\"fld\">\r\n          <select id=\"pth-merge-mode\" aria-label=\"Merge mode\">\r\n            <option value=\"seq\">Sequential - one file after another<\/option>\r\n            <option value=\"alt\">Interleave - alternate pages of two files<\/option>\r\n            <option value=\"ins\">Insert one file inside another<\/option>\r\n          <\/select>\r\n        <\/div>\r\n\r\n        <div id=\"pth-merge-alt\" style=\"display:none\">\r\n          <div class=\"note\">Made for duplex scanning: scan the odd sides into one file, the even sides into another, then interleave them back together.<\/div>\r\n          <div class=\"fld\">\r\n            <label for=\"pth-alt-a\">First file (odd pages)<\/label>\r\n            <select id=\"pth-alt-a\" aria-label=\"First file for interleave\"><\/select>\r\n          <\/div>\r\n          <div class=\"fld\">\r\n            <label for=\"pth-alt-b\">Second file (even pages)<\/label>\r\n            <select id=\"pth-alt-b\" aria-label=\"Second file for interleave\"><\/select>\r\n          <\/div>\r\n          <div class=\"fld\">\r\n            <label style=\"display:flex;align-items:center;gap:6px;\">\r\n              <input type=\"checkbox\" id=\"pth-alt-rev\" style=\"width:auto\"> Second file was scanned in reverse order\r\n            <\/label>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div id=\"pth-merge-ins\" style=\"display:none\">\r\n          <div class=\"fld\">\r\n            <label for=\"pth-ins-base\">Base file<\/label>\r\n            <select id=\"pth-ins-base\" aria-label=\"Base file\"><\/select>\r\n          <\/div>\r\n          <div class=\"fld\">\r\n            <label for=\"pth-ins-add\">File to insert<\/label>\r\n            <select id=\"pth-ins-add\" aria-label=\"File to insert\"><\/select>\r\n          <\/div>\r\n          <div class=\"fld\">\r\n            <label for=\"pth-ins-at\">Insert after page number<\/label>\r\n            <input type=\"number\" id=\"pth-ins-at\" value=\"1\" min=\"0\">\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"fld\">\r\n          <label for=\"pth-merge-name\">Output file name<\/label>\r\n          <input type=\"text\" id=\"pth-merge-name\" value=\"merged.pdf\">\r\n        <\/div>\r\n        <div class=\"row\">\r\n          <button type=\"button\" class=\"btn btn-grn\" id=\"pth-do-merge\" disabled>Merge Files<\/button>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"panel\" data-panel=\"split\">\r\n        <h4>Split Method<\/h4>\r\n        <div class=\"fld\">\r\n          <label for=\"pth-split-file\">File to split<\/label>\r\n          <select id=\"pth-split-file\" aria-label=\"File to split\"><\/select>\r\n        <\/div>\r\n        <div class=\"fld\">\r\n          <select id=\"pth-split-mode\" aria-label=\"Split method\">\r\n            <option value=\"every\">Every N pages<\/option>\r\n            <option value=\"parts\">Into N equal parts<\/option>\r\n            <option value=\"range\">Custom ranges<\/option>\r\n            <option value=\"burst\">Burst - one file per page<\/option>\r\n          <\/select>\r\n        <\/div>\r\n        <div class=\"fld\" id=\"pth-split-nwrap\">\r\n          <label for=\"pth-split-n\" id=\"pth-split-nlab\">Pages per file<\/label>\r\n          <input type=\"number\" id=\"pth-split-n\" value=\"2\" min=\"1\">\r\n        <\/div>\r\n        <div class=\"fld\" id=\"pth-split-rwrap\" style=\"display:none\">\r\n          <label for=\"pth-split-r\">Ranges, one output file per group<\/label>\r\n          <input type=\"text\" id=\"pth-split-r\" placeholder=\"1-3, 4-8, 9-12\">\r\n        <\/div>\r\n        <div class=\"note\">Results arrive as a single ZIP file so nothing gets lost between downloads.<\/div>\r\n        <div class=\"prog\" id=\"pth-split-prog\"><i id=\"pth-split-bar\"><\/i><\/div>\r\n        <div class=\"row\">\r\n          <button type=\"button\" class=\"btn btn-grn\" id=\"pth-do-split\" disabled>Split and Download ZIP<\/button>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"panel\" data-panel=\"batch\">\r\n        <h4>Apply To Every Loaded File<\/h4>\r\n        <div class=\"fld\">\r\n          <label style=\"display:flex;align-items:center;gap:6px;\">\r\n            <input type=\"checkbox\" id=\"pth-b-rot\" style=\"width:auto\"> Rotate every page\r\n          <\/label>\r\n          <select id=\"pth-b-rotdeg\" aria-label=\"Rotation amount\" style=\"margin-top:6px\">\r\n            <option value=\"90\">90 degrees clockwise<\/option>\r\n            <option value=\"180\">180 degrees<\/option>\r\n            <option value=\"270\">90 degrees anticlockwise<\/option>\r\n          <\/select>\r\n        <\/div>\r\n        <div class=\"fld\">\r\n          <label style=\"display:flex;align-items:center;gap:6px;\">\r\n            <input type=\"checkbox\" id=\"pth-b-meta\" style=\"width:auto\"> Clear author, title and producer details\r\n          <\/label>\r\n        <\/div>\r\n        <div class=\"fld\">\r\n          <label style=\"display:flex;align-items:center;gap:6px;\">\r\n            <input type=\"checkbox\" id=\"pth-b-wm\" style=\"width:auto\"> Stamp a watermark on every page\r\n          <\/label>\r\n          <input type=\"text\" id=\"pth-b-wmtext\" placeholder=\"CONFIDENTIAL\" style=\"margin-top:6px\">\r\n          <select id=\"pth-b-wmpos\" aria-label=\"Watermark position\" style=\"margin-top:6px\">\r\n            <option value=\"diag\">Diagonal across the page<\/option>\r\n            <option value=\"bc\">Bottom centre<\/option>\r\n            <option value=\"tc\">Top centre<\/option>\r\n          <\/select>\r\n        <\/div>\r\n        <div class=\"fld\">\r\n          <label style=\"display:flex;align-items:center;gap:6px;\">\r\n            <input type=\"checkbox\" id=\"pth-b-shrink\" style=\"width:auto\"> Reduce file size by flattening pages to images\r\n          <\/label>\r\n          <label for=\"pth-b-q\" style=\"margin-top:6px\">Image quality: <span id=\"pth-b-qv\">70<\/span>%<\/label>\r\n          <input type=\"range\" id=\"pth-b-q\" min=\"30\" max=\"95\" value=\"70\">\r\n        <\/div>\r\n        <div class=\"note\">Flattening genuinely shrinks image-heavy scans, but the page becomes a picture, so its text stops being selectable. Leave it off for documents people need to search.<\/div>\r\n        <div class=\"fld\">\r\n          <label for=\"pth-b-name\">Rename pattern - use {n} for the number and {name} for the original<\/label>\r\n          <input type=\"text\" id=\"pth-b-name\" value=\"{name}\" placeholder=\"invoice-{n}\">\r\n        <\/div>\r\n        <div class=\"prog\" id=\"pth-batch-prog\"><i id=\"pth-batch-bar\"><\/i><\/div>\r\n        <div class=\"row\">\r\n          <button type=\"button\" class=\"btn btn-grn\" id=\"pth-do-batch\" disabled>Run Batch and Download ZIP<\/button>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"panel\" data-panel=\"inspect\">\r\n        <h4>File Inspector<\/h4>\r\n        <div class=\"note\">A quick look at what you loaded, so you know which files carry a form, which are scans with no selectable text, and which are locked before you plan any splitting.<\/div>\r\n        <div class=\"row\">\r\n          <button type=\"button\" class=\"btn btn-sm\" id=\"pth-do-inspect\" disabled>Inspect Files<\/button>\r\n        <\/div>\r\n        <div id=\"pth-inspect-out\"><div class=\"empty\">Load files, then press Inspect Files.<\/div><\/div>\r\n        <div class=\"note\" style=\"margin-top:14px\">Need to annotate, sign or permanently redact inside one document? That is a different job - use the <a href=\"https:\/\/schoolict.net\/tools\/free-offline-pdf-reader-and-editor\/\">offline PDF editor<\/a>, which also handles text extraction.<\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div id=\"pth-org-toast\" role=\"status\" aria-live=\"polite\"><\/div>\r\n<\/div>\r\n\r\n<script data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\">\r\nwindow.pthPdfOrganizeApp = (function () {\r\n  'use strict';\r\n\r\n  var W = null, booted = false;\r\n  var S = { files: [], grid: [], seq: 1, sortFiles: null, sortGrid: null };\r\n  var COLORS = ['#6d28d9', '#0369a1', '#047857', '#b45309', '#be123c', '#4338ca', '#0f766e', '#a16207'];\r\n\r\n  function $(id) { return document.getElementById(id); }\r\n  function qa(sel) { return W ? W.querySelectorAll(sel) : []; }\r\n\r\n  var tTimer = null;\r\n  function toast(msg, kind) {\r\n    var t = $('pth-org-toast');\r\n    if (!t) { return; }\r\n    t.className = 'show' + (kind ? ' ' + kind : '');\r\n    t.textContent = msg;\r\n    if (tTimer) { clearTimeout(tTimer); }\r\n    tTimer = setTimeout(function () { t.className = ''; }, 3200);\r\n  }\r\n\r\n  function esc(s) {\r\n    return String(s).replace(\/&\/g, '&amp;').replace(\/<\/g, '&lt;')\r\n      .replace(\/>\/g, '&gt;').replace(\/\"\/g, '&quot;');\r\n  }\r\n\r\n  function fileById(id) {\r\n    var i;\r\n    for (i = 0; i < S.files.length; i++) { if (S.files[i].id === id) { return S.files[i]; } }\r\n    return null;\r\n  }\r\n\r\n  \/* ---------- Page range parsing ---------- *\/\r\n  function parseRange(txt, max) {\r\n    if (!txt || !String(txt).replace(\/\\s\/g, '')) {\r\n      var all = [], i;\r\n      for (i = 1; i <= max; i++) { all.push(i); }\r\n      return all;\r\n    }\r\n    var out = [], parts = String(txt).split(','), k, p, m, a, b, n;\r\n    for (k = 0; k < parts.length; k++) {\r\n      p = parts[k].replace(\/\\s\/g, '');\r\n      if (!p) { continue; }\r\n      m = p.match(\/^(\\d+)-(\\d+)$\/);\r\n      if (m) {\r\n        a = parseInt(m[1], 10); b = parseInt(m[2], 10);\r\n        if (a > b) { n = a; a = b; b = n; }\r\n        for (n = a; n <= b; n++) { if (n >= 1 && n <= max && out.indexOf(n) < 0) { out.push(n); } }\r\n      } else if (\/^\\d+$\/.test(p)) {\r\n        n = parseInt(p, 10);\r\n        if (n >= 1 && n <= max && out.indexOf(n) < 0) { out.push(n); }\r\n      }\r\n    }\r\n    return out;\r\n  }\r\n\r\n  function fmtSize(b) {\r\n    if (b < 1024) { return b + ' B'; }\r\n    if (b < 1048576) { return (b \/ 1024).toFixed(1) + ' KB'; }\r\n    return (b \/ 1048576).toFixed(2) + ' MB';\r\n  }\r\n\r\n  \/* ---------- Loading ---------- *\/\r\n  function addFiles(list) {\r\n    var i, arr = [];\r\n    for (i = 0; i < list.length; i++) { arr.push(list[i]); }\r\n    var step = function (k) {\r\n      if (k >= arr.length) { afterLoad(); return; }\r\n      var f = arr[k];\r\n      if (f.type !== 'application\/pdf' && !\/\\.pdf$\/i.test(f.name)) {\r\n        toast('Skipped ' + f.name + ' - not a PDF.', 'err');\r\n        step(k + 1); return;\r\n      }\r\n      var fr = new FileReader();\r\n      fr.onload = function (ev) {\r\n        var buf = ev.target.result;\r\n        window.pdfjsLib.getDocument({ data: buf.slice(0) }).promise.then(function (doc) {\r\n          S.files.push({\r\n            id: 'f' + (S.seq++), name: f.name, bytes: buf, size: f.size,\r\n            pages: doc.numPages, doc: doc, range: '',\r\n            color: COLORS[S.files.length % COLORS.length]\r\n          });\r\n          step(k + 1);\r\n        })['catch'](function () {\r\n          S.files.push({\r\n            id: 'f' + (S.seq++), name: f.name, bytes: buf, size: f.size,\r\n            pages: 0, doc: null, range: '', locked: true,\r\n            color: COLORS[S.files.length % COLORS.length]\r\n          });\r\n          toast(f.name + ' looks locked or damaged.', 'err');\r\n          step(k + 1);\r\n        });\r\n      };\r\n      fr.onerror = function () { step(k + 1); };\r\n      fr.readAsArrayBuffer(f);\r\n    };\r\n    step(0);\r\n  }\r\n\r\n  function afterLoad() {\r\n    renderFiles();\r\n    fillSelects();\r\n    updateStats();\r\n    var has = S.files.length > 0;\r\n    $('pth-org-dropwrap').style.display = has ? 'none' : 'block';\r\n    $('pth-org-listwrap').style.display = has ? 'block' : 'none';\r\n    $('pth-org-gridwrap').style.display = has ? 'block' : 'none';\r\n    $('pth-do-merge').disabled = S.files.length < 2;\r\n    $('pth-do-split').disabled = !has;\r\n    $('pth-do-batch').disabled = !has;\r\n    $('pth-do-inspect').disabled = !has;\r\n    if (has) { toast(S.files.length + ' file(s) loaded.', 'ok'); }\r\n  }\r\n\r\n  function updateStats() {\r\n    var p = 0, i;\r\n    for (i = 0; i < S.files.length; i++) { p += S.files[i].pages; }\r\n    $('pth-org-nfiles').textContent = String(S.files.length);\r\n    $('pth-org-npages').textContent = String(p);\r\n  }\r\n\r\n  function renderFiles() {\r\n    var box = $('pth-org-flist');\r\n    box.innerHTML = '';\r\n    var i;\r\n    for (i = 0; i < S.files.length; i++) {\r\n      (function (f) {\r\n        var d = document.createElement('div');\r\n        d.className = 'fitem';\r\n        d.setAttribute('data-id', f.id);\r\n\r\n        var grip = document.createElement('span');\r\n        grip.className = 'grip';\r\n        grip.textContent = '\\u2261';\r\n        grip.setAttribute('aria-hidden', 'true');\r\n        d.appendChild(grip);\r\n\r\n        var dot = document.createElement('span');\r\n        dot.style.cssText = 'width:10px;height:10px;border-radius:3px;flex-shrink:0;background:' + f.color;\r\n        d.appendChild(dot);\r\n\r\n        var meta = document.createElement('div');\r\n        meta.className = 'fmeta';\r\n        var nm = document.createElement('div');\r\n        nm.className = 'fname';\r\n        nm.textContent = f.name;\r\n        var sub = document.createElement('div');\r\n        sub.className = 'fsub';\r\n        sub.textContent = (f.locked ? 'Locked file' : f.pages + ' pages') + ' - ' + fmtSize(f.size);\r\n        meta.appendChild(nm); meta.appendChild(sub);\r\n        d.appendChild(meta);\r\n\r\n        var rng = document.createElement('input');\r\n        rng.type = 'text';\r\n        rng.placeholder = 'All pages';\r\n        rng.value = f.range;\r\n        rng.setAttribute('aria-label', 'Page range for ' + f.name);\r\n        rng.addEventListener('input', function () { f.range = this.value; });\r\n        d.appendChild(rng);\r\n\r\n        var rm = document.createElement('button');\r\n        rm.type = 'button';\r\n        rm.textContent = 'Remove';\r\n        rm.setAttribute('aria-label', 'Remove ' + f.name);\r\n        rm.addEventListener('click', function () {\r\n          var k;\r\n          for (k = 0; k < S.files.length; k++) {\r\n            if (S.files[k].id === f.id) { S.files.splice(k, 1); break; }\r\n          }\r\n          S.grid = [];\r\n          $('pth-org-pgrid').innerHTML = '<div class=\"empty\">Press Load Pages to build the grid.<\/div>';\r\n          $('pth-grid-save').disabled = true;\r\n          afterLoad();\r\n        });\r\n        d.appendChild(rm);\r\n\r\n        box.appendChild(d);\r\n      })(S.files[i]);\r\n    }\r\n\r\n    if (S.sortFiles) { try { S.sortFiles.destroy(); } catch (e) { } }\r\n    if (window.Sortable && S.files.length) {\r\n      S.sortFiles = window.Sortable.create(box, {\r\n        handle: '.grip', animation: 150,\r\n        onEnd: function () {\r\n          var ids = [], nodes = box.children, k, arr = [];\r\n          for (k = 0; k < nodes.length; k++) { ids.push(nodes[k].getAttribute('data-id')); }\r\n          for (k = 0; k < ids.length; k++) { arr.push(fileById(ids[k])); }\r\n          S.files = arr;\r\n          fillSelects();\r\n          toast('File order updated.');\r\n        }\r\n      });\r\n    }\r\n  }\r\n\r\n  function fillSelects() {\r\n    var ids = ['pth-alt-a', 'pth-alt-b', 'pth-ins-base', 'pth-ins-add', 'pth-split-file'];\r\n    var i, k, sel, html = '';\r\n    for (k = 0; k < S.files.length; k++) {\r\n      html += '<option value=\"' + S.files[k].id + '\">' + esc(S.files[k].name) + '<\/option>';\r\n    }\r\n    for (i = 0; i < ids.length; i++) {\r\n      sel = $(ids[i]);\r\n      if (!sel) { continue; }\r\n      var prev = sel.value;\r\n      sel.innerHTML = html;\r\n      if (prev) { sel.value = prev; }\r\n    }\r\n    if (S.files.length > 1) {\r\n      if ($('pth-alt-b').selectedIndex === 0) { $('pth-alt-b').selectedIndex = 1; }\r\n      if ($('pth-ins-add').selectedIndex === 0) { $('pth-ins-add').selectedIndex = 1; }\r\n    }\r\n  }\r\n\r\n  \/* ---------- Cross-file page grid ---------- *\/\r\n  function buildGrid() {\r\n    if (!S.files.length) { return; }\r\n    var total = 0, i;\r\n    for (i = 0; i < S.files.length; i++) { total += S.files[i].pages; }\r\n    if (total > 300) {\r\n      toast('That is ' + total + ' pages - the grid may take a while.', 'err');\r\n    }\r\n    S.grid = [];\r\n    for (i = 0; i < S.files.length; i++) {\r\n      if (S.files[i].locked) { continue; }\r\n      var p;\r\n      for (p = 1; p <= S.files[i].pages; p++) {\r\n        S.grid.push({ gid: 'g' + (S.seq++), fid: S.files[i].id, page: p, rot: 0, sel: false });\r\n      }\r\n    }\r\n    renderGrid();\r\n  }\r\n\r\n  function renderGrid() {\r\n    var box = $('pth-org-pgrid');\r\n    box.innerHTML = '';\r\n    if (!S.grid.length) {\r\n      box.innerHTML = '<div class=\"empty\">No pages to show.<\/div>';\r\n      $('pth-grid-save').disabled = true;\r\n      return;\r\n    }\r\n    $('pth-grid-save').disabled = false;\r\n    var prog = $('pth-grid-prog'), bar = $('pth-grid-bar');\r\n    prog.className = 'prog on';\r\n    var done = 0, i;\r\n\r\n    for (i = 0; i < S.grid.length; i++) {\r\n      (function (g, idx) {\r\n        var f = fileById(g.fid);\r\n        var cell = document.createElement('div');\r\n        cell.className = 'pcell' + (g.sel ? ' sel' : '');\r\n        cell.setAttribute('data-gid', g.gid);\r\n\r\n        var tag = document.createElement('span');\r\n        tag.className = 'src';\r\n        tag.style.background = f ? f.color : '#64748b';\r\n        tag.textContent = f ? f.name.substring(0, 8) : '?';\r\n        cell.appendChild(tag);\r\n\r\n        var cv = document.createElement('canvas');\r\n        cv.width = 104; cv.height = 136;\r\n        cell.appendChild(cv);\r\n\r\n        var lab = document.createElement('div');\r\n        lab.className = 'pl';\r\n        lab.textContent = 'p' + g.page;\r\n        cell.appendChild(lab);\r\n\r\n        var acts = document.createElement('div');\r\n        acts.className = 'pa';\r\n        var rb = document.createElement('button');\r\n        rb.type = 'button'; rb.textContent = 'Rot';\r\n        rb.setAttribute('aria-label', 'Rotate this page');\r\n        rb.addEventListener('click', function (ev) {\r\n          ev.stopPropagation();\r\n          g.rot = (g.rot + 90) % 360;\r\n          paintCell(cv, g);\r\n          toast('Page rotated in the grid.');\r\n        });\r\n        var db = document.createElement('button');\r\n        db.type = 'button'; db.textContent = 'Del';\r\n        db.setAttribute('aria-label', 'Remove this page from the grid');\r\n        db.addEventListener('click', function (ev) {\r\n          ev.stopPropagation();\r\n          var k;\r\n          for (k = 0; k < S.grid.length; k++) { if (S.grid[k].gid === g.gid) { S.grid.splice(k, 1); break; } }\r\n          renderGrid();\r\n        });\r\n        acts.appendChild(rb); acts.appendChild(db);\r\n        cell.appendChild(acts);\r\n\r\n        cell.addEventListener('click', function () {\r\n          g.sel = !g.sel;\r\n          cell.className = 'pcell' + (g.sel ? ' sel' : '');\r\n        });\r\n\r\n        box.appendChild(cell);\r\n        paintCell(cv, g, function () {\r\n          done++;\r\n          bar.style.width = Math.round((done \/ S.grid.length) * 100) + '%';\r\n          if (done >= S.grid.length) { setTimeout(function () { prog.className = 'prog'; }, 400); }\r\n        });\r\n      })(S.grid[i], i);\r\n    }\r\n\r\n    if (S.sortGrid) { try { S.sortGrid.destroy(); } catch (e) { } }\r\n    if (window.Sortable) {\r\n      S.sortGrid = window.Sortable.create(box, {\r\n        animation: 150,\r\n        onEnd: function () {\r\n          var nodes = box.children, k, arr = [], gid, j;\r\n          for (k = 0; k < nodes.length; k++) {\r\n            gid = nodes[k].getAttribute('data-gid');\r\n            for (j = 0; j < S.grid.length; j++) {\r\n              if (S.grid[j].gid === gid) { arr.push(S.grid[j]); break; }\r\n            }\r\n          }\r\n          if (arr.length === S.grid.length) { S.grid = arr; }\r\n          toast('Page order updated.');\r\n        }\r\n      });\r\n    }\r\n  }\r\n\r\n  function paintCell(cv, g, cb) {\r\n    var f = fileById(g.fid);\r\n    var cx = cv.getContext('2d');\r\n    cx.fillStyle = '#ffffff';\r\n    cx.fillRect(0, 0, cv.width, cv.height);\r\n    if (!f || !f.doc) { if (cb) { cb(); } return; }\r\n    f.doc.getPage(g.page).then(function (pg) {\r\n      var rot = (pg.rotate + g.rot) % 360;\r\n      var base = pg.getViewport({ scale: 1, rotation: rot });\r\n      var sc = Math.min(cv.width \/ base.width, 136 \/ base.height);\r\n      var vp = pg.getViewport({ scale: sc, rotation: rot });\r\n      cv.height = Math.max(40, Math.floor(vp.height));\r\n      cx.fillStyle = '#ffffff';\r\n      cx.fillRect(0, 0, cv.width, cv.height);\r\n      return pg.render({ canvasContext: cx, viewport: vp }).promise.then(function () { if (cb) { cb(); } });\r\n    })['catch'](function () { if (cb) { cb(); } });\r\n  }\r\n\r\n  \/* ---------- Output helpers ---------- *\/\r\n  function dl(blob, name) {\r\n    var u = URL.createObjectURL(blob);\r\n    var a = document.createElement('a');\r\n    a.href = u; a.download = name;\r\n    document.body.appendChild(a); a.click(); document.body.removeChild(a);\r\n    setTimeout(function () { URL.revokeObjectURL(u); }, 1200);\r\n  }\r\n\r\n  function loadLib(f) {\r\n    return window.PDFLib.PDFDocument.load(f.bytes.slice(0), { ignoreEncryption: false });\r\n  }\r\n\r\n  \/* ---------- Merge ---------- *\/\r\n  function doMerge() {\r\n    var mode = $('pth-merge-mode').value;\r\n    var PDFLib = window.PDFLib;\r\n    var name = $('pth-merge-name').value || 'merged.pdf';\r\n    if (!\/\\.pdf$\/i.test(name)) { name += '.pdf'; }\r\n    var btn = $('pth-do-merge');\r\n    btn.disabled = true; btn.textContent = 'Merging...';\r\n\r\n    PDFLib.PDFDocument.create().then(function (out) {\r\n      var plan = [];\r\n      if (mode === 'seq') {\r\n        var i;\r\n        for (i = 0; i < S.files.length; i++) {\r\n          if (S.files[i].locked) { continue; }\r\n          var pages = parseRange(S.files[i].range, S.files[i].pages), k;\r\n          for (k = 0; k < pages.length; k++) { plan.push({ f: S.files[i], p: pages[k] }); }\r\n        }\r\n      } else if (mode === 'alt') {\r\n        var fa = fileById($('pth-alt-a').value), fb = fileById($('pth-alt-b').value);\r\n        if (!fa || !fb || fa.id === fb.id) { throw new Error('pick two different files'); }\r\n        var pa = parseRange(fa.range, fa.pages), pb = parseRange(fb.range, fb.pages);\r\n        if ($('pth-alt-rev').checked) { pb = pb.slice(0).reverse(); }\r\n        var n = Math.max(pa.length, pb.length), j;\r\n        for (j = 0; j < n; j++) {\r\n          if (j < pa.length) { plan.push({ f: fa, p: pa[j] }); }\r\n          if (j < pb.length) { plan.push({ f: fb, p: pb[j] }); }\r\n        }\r\n      } else {\r\n        var fbase = fileById($('pth-ins-base').value), fadd = fileById($('pth-ins-add').value);\r\n        if (!fbase || !fadd || fbase.id === fadd.id) { throw new Error('pick two different files'); }\r\n        var at = parseInt($('pth-ins-at').value, 10);\r\n        if (isNaN(at) || at < 0) { at = 0; }\r\n        var bp = parseRange(fbase.range, fbase.pages), ap = parseRange(fadd.range, fadd.pages), m;\r\n        for (m = 0; m < bp.length; m++) {\r\n          plan.push({ f: fbase, p: bp[m] });\r\n          if (m + 1 === at) {\r\n            for (j = 0; j < ap.length; j++) { plan.push({ f: fadd, p: ap[j] }); }\r\n          }\r\n        }\r\n        if (at === 0) {\r\n          var pre = [];\r\n          for (j = 0; j < ap.length; j++) { pre.push({ f: fadd, p: ap[j] }); }\r\n          plan = pre.concat(plan);\r\n        } else if (at >= bp.length) {\r\n          for (j = 0; j < ap.length; j++) { plan.push({ f: fadd, p: ap[j] }); }\r\n        }\r\n      }\r\n\r\n      if (!plan.length) { throw new Error('nothing selected'); }\r\n\r\n      var cache = {};\r\n      var step = function (k) {\r\n        if (k >= plan.length) { return out.save(); }\r\n        var item = plan[k];\r\n        var got = cache[item.f.id] ? Promise.resolve(cache[item.f.id]) : loadLib(item.f).then(function (d) { cache[item.f.id] = d; return d; });\r\n        return got.then(function (src) {\r\n          return out.copyPages(src, [item.p - 1]).then(function (cp) {\r\n            out.addPage(cp[0]);\r\n            return step(k + 1);\r\n          });\r\n        });\r\n      };\r\n      return step(0);\r\n    }).then(function (bytes) {\r\n      dl(new Blob([bytes], { type: 'application\/pdf' }), name);\r\n      btn.disabled = false; btn.textContent = 'Merge Files';\r\n      toast('Merged file saved.', 'ok');\r\n    })['catch'](function (err) {\r\n      btn.disabled = false; btn.textContent = 'Merge Files';\r\n      var m = err && err.message ? err.message : '';\r\n      if (m.indexOf('different files') >= 0) { toast('Choose two different files.', 'err'); }\r\n      else if (m.indexOf('nothing selected') >= 0) { toast('No pages selected - check your page ranges.', 'err'); }\r\n      else { toast('Merge failed. One of the files may be password protected.', 'err'); }\r\n    });\r\n  }\r\n\r\n  \/* ---------- Split ---------- *\/\r\n  function doSplit() {\r\n    var f = fileById($('pth-split-file').value);\r\n    if (!f) { toast('Choose a file to split.', 'err'); return; }\r\n    if (f.locked) { toast('That file is locked and cannot be split.', 'err'); return; }\r\n    var mode = $('pth-split-mode').value;\r\n    var n = parseInt($('pth-split-n').value, 10) || 1;\r\n    var groups = [], i, k;\r\n\r\n    if (mode === 'burst') {\r\n      for (i = 1; i <= f.pages; i++) { groups.push([i]); }\r\n    } else if (mode === 'every') {\r\n      var cur = [];\r\n      for (i = 1; i <= f.pages; i++) {\r\n        cur.push(i);\r\n        if (cur.length === n) { groups.push(cur); cur = []; }\r\n      }\r\n      if (cur.length) { groups.push(cur); }\r\n    } else if (mode === 'parts') {\r\n      var per = Math.ceil(f.pages \/ Math.max(1, n));\r\n      var c2 = [];\r\n      for (i = 1; i <= f.pages; i++) {\r\n        c2.push(i);\r\n        if (c2.length === per) { groups.push(c2); c2 = []; }\r\n      }\r\n      if (c2.length) { groups.push(c2); }\r\n    } else {\r\n      var parts = String($('pth-split-r').value).split(',');\r\n      for (k = 0; k < parts.length; k++) {\r\n        var g = parseRange(parts[k], f.pages);\r\n        if (g.length) { groups.push(g); }\r\n      }\r\n    }\r\n\r\n    if (!groups.length) { toast('Nothing to split - check your settings.', 'err'); return; }\r\n\r\n    var btn = $('pth-do-split');\r\n    btn.disabled = true; btn.textContent = 'Splitting...';\r\n    var prog = $('pth-split-prog'), bar = $('pth-split-bar');\r\n    prog.className = 'prog on';\r\n\r\n    var zip = new window.JSZip();\r\n    var base = f.name.replace(\/\\.pdf$\/i, '');\r\n\r\n    loadLib(f).then(function (src) {\r\n      var step = function (gi) {\r\n        if (gi >= groups.length) { return Promise.resolve(); }\r\n        return window.PDFLib.PDFDocument.create().then(function (out) {\r\n          var idx = [], j;\r\n          for (j = 0; j < groups[gi].length; j++) { idx.push(groups[gi][j] - 1); }\r\n          return out.copyPages(src, idx).then(function (cp) {\r\n            var j2;\r\n            for (j2 = 0; j2 < cp.length; j2++) { out.addPage(cp[j2]); }\r\n            return out.save();\r\n          }).then(function (bytes) {\r\n            zip.file(base + '-part-' + (gi + 1) + '.pdf', bytes);\r\n            bar.style.width = Math.round(((gi + 1) \/ groups.length) * 100) + '%';\r\n            return step(gi + 1);\r\n          });\r\n        });\r\n      };\r\n      return step(0);\r\n    }).then(function () {\r\n      return zip.generateAsync({ type: 'blob' });\r\n    }).then(function (blob) {\r\n      dl(blob, base + '-split.zip');\r\n      btn.disabled = false; btn.textContent = 'Split and Download ZIP';\r\n      prog.className = 'prog';\r\n      toast(groups.length + ' file(s) saved in a ZIP.', 'ok');\r\n    })['catch'](function () {\r\n      btn.disabled = false; btn.textContent = 'Split and Download ZIP';\r\n      prog.className = 'prog';\r\n      toast('Split failed. The file may be password protected.', 'err');\r\n    });\r\n  }\r\n\r\n  \/* ---------- Grid build ---------- *\/\r\n  function buildFromGrid() {\r\n    if (!S.grid.length) { toast('The grid is empty.', 'err'); return; }\r\n    var btn = $('pth-grid-save');\r\n    btn.disabled = true; btn.textContent = 'Building...';\r\n    var PDFLib = window.PDFLib;\r\n\r\n    PDFLib.PDFDocument.create().then(function (out) {\r\n      var cache = {};\r\n      var step = function (k) {\r\n        if (k >= S.grid.length) { return out.save(); }\r\n        var g = S.grid[k], f = fileById(g.fid);\r\n        if (!f) { return step(k + 1); }\r\n        var got = cache[f.id] ? Promise.resolve(cache[f.id]) : loadLib(f).then(function (d) { cache[f.id] = d; return d; });\r\n        return got.then(function (src) {\r\n          return out.copyPages(src, [g.page - 1]).then(function (cp) {\r\n            var pg = out.addPage(cp[0]);\r\n            if (g.rot) {\r\n              try { pg.setRotation(PDFLib.degrees((pg.getRotation().angle + g.rot) % 360)); } catch (e) { }\r\n            }\r\n            return step(k + 1);\r\n          });\r\n        });\r\n      };\r\n      return step(0);\r\n    }).then(function (bytes) {\r\n      dl(new Blob([bytes], { type: 'application\/pdf' }), 'combined-from-grid.pdf');\r\n      btn.disabled = false; btn.textContent = 'Build PDF From Grid';\r\n      toast('New PDF built from ' + S.grid.length + ' pages.', 'ok');\r\n    })['catch'](function () {\r\n      btn.disabled = false; btn.textContent = 'Build PDF From Grid';\r\n      toast('Could not build the PDF from the grid.', 'err');\r\n    });\r\n  }\r\n\r\n  \/* ---------- Batch ---------- *\/\r\n  function rasterizeDoc(f, quality) {\r\n    var PDFLib = window.PDFLib;\r\n    return PDFLib.PDFDocument.create().then(function (out) {\r\n      var step = function (p) {\r\n        if (p > f.pages) { return out.save(); }\r\n        return f.doc.getPage(p).then(function (pg) {\r\n          var vp = pg.getViewport({ scale: 1.6 });\r\n          var cv = document.createElement('canvas');\r\n          cv.width = Math.floor(vp.width); cv.height = Math.floor(vp.height);\r\n          var cx = cv.getContext('2d');\r\n          cx.fillStyle = '#ffffff'; cx.fillRect(0, 0, cv.width, cv.height);\r\n          return pg.render({ canvasContext: cx, viewport: vp }).promise.then(function () {\r\n            return new Promise(function (res) {\r\n              cv.toBlob(function (b) {\r\n                var fr = new FileReader();\r\n                fr.onload = function (ev) { res(ev.target.result); };\r\n                fr.readAsArrayBuffer(b);\r\n              }, 'image\/jpeg', quality \/ 100);\r\n            });\r\n          }).then(function (buf) {\r\n            return out.embedJpg(buf).then(function (img) {\r\n              var page = out.addPage([vp.width, vp.height]);\r\n              page.drawImage(img, { x: 0, y: 0, width: vp.width, height: vp.height });\r\n              return step(p + 1);\r\n            });\r\n          });\r\n        });\r\n      };\r\n      return step(1);\r\n    });\r\n  }\r\n\r\n  function doBatch() {\r\n    var rot = $('pth-b-rot').checked, deg = parseInt($('pth-b-rotdeg').value, 10);\r\n    var meta = $('pth-b-meta').checked;\r\n    var wm = $('pth-b-wm').checked, wmt = $('pth-b-wmtext').value || 'CONFIDENTIAL', wmp = $('pth-b-wmpos').value;\r\n    var shrink = $('pth-b-shrink').checked, qv = parseInt($('pth-b-q').value, 10);\r\n    var pattern = $('pth-b-name').value || '{name}';\r\n\r\n    if (!rot && !meta && !wm && !shrink && pattern === '{name}') {\r\n      toast('Tick at least one action first.', 'err'); return;\r\n    }\r\n\r\n    var btn = $('pth-do-batch');\r\n    btn.disabled = true; btn.textContent = 'Working...';\r\n    var prog = $('pth-batch-prog'), bar = $('pth-batch-bar');\r\n    prog.className = 'prog on';\r\n    var zip = new window.JSZip();\r\n    var PDFLib = window.PDFLib;\r\n\r\n    var step = function (i) {\r\n      if (i >= S.files.length) { return Promise.resolve(); }\r\n      var f = S.files[i];\r\n      if (f.locked) { bar.style.width = Math.round(((i + 1) \/ S.files.length) * 100) + '%'; return step(i + 1); }\r\n\r\n      var basePromise = shrink && f.doc ? rasterizeDoc(f, qv).then(function (b) { return PDFLib.PDFDocument.load(b); }) : loadLib(f);\r\n\r\n      return basePromise.then(function (doc) {\r\n        var pages = doc.getPages(), k;\r\n        if (rot) {\r\n          for (k = 0; k < pages.length; k++) {\r\n            try { pages[k].setRotation(PDFLib.degrees((pages[k].getRotation().angle + deg) % 360)); } catch (e) { }\r\n          }\r\n        }\r\n        if (meta) {\r\n          try {\r\n            doc.setTitle(''); doc.setAuthor(''); doc.setSubject('');\r\n            doc.setKeywords([]); doc.setProducer(''); doc.setCreator('');\r\n          } catch (e2) { }\r\n        }\r\n        if (wm) {\r\n          return doc.embedFont(PDFLib.StandardFonts.HelveticaBold).then(function (font) {\r\n            var j, pg, w, h, size, tw;\r\n            for (j = 0; j < pages.length; j++) {\r\n              pg = pages[j]; w = pg.getWidth(); h = pg.getHeight();\r\n              size = wmp === 'diag' ? Math.max(24, Math.min(64, w \/ (wmt.length * 0.62))) : 15;\r\n              tw = font.widthOfTextAtSize(wmt, size);\r\n              if (wmp === 'diag') {\r\n                pg.drawText(wmt, {\r\n                  x: (w - tw * 0.72) \/ 2, y: h \/ 2 - size,\r\n                  size: size, font: font, color: PDFLib.rgb(0.55, 0.58, 0.64),\r\n                  opacity: 0.28, rotate: PDFLib.degrees(38)\r\n                });\r\n              } else {\r\n                pg.drawText(wmt, {\r\n                  x: (w - tw) \/ 2, y: wmp === 'bc' ? 24 : h - 34,\r\n                  size: size, font: font, color: PDFLib.rgb(0.42, 0.45, 0.52), opacity: 0.72\r\n                });\r\n              }\r\n            }\r\n            return doc.save();\r\n          });\r\n        }\r\n        return doc.save();\r\n      }).then(function (bytes) {\r\n        var nm = pattern.replace(\/\\{n\\}\/g, String(i + 1)).replace(\/\\{name\\}\/g, f.name.replace(\/\\.pdf$\/i, ''));\r\n        if (!\/\\.pdf$\/i.test(nm)) { nm += '.pdf'; }\r\n        zip.file(nm, bytes);\r\n        bar.style.width = Math.round(((i + 1) \/ S.files.length) * 100) + '%';\r\n        return step(i + 1);\r\n      })['catch'](function () {\r\n        bar.style.width = Math.round(((i + 1) \/ S.files.length) * 100) + '%';\r\n        return step(i + 1);\r\n      });\r\n    };\r\n\r\n    step(0).then(function () {\r\n      return zip.generateAsync({ type: 'blob' });\r\n    }).then(function (blob) {\r\n      dl(blob, 'batch-output.zip');\r\n      btn.disabled = false; btn.textContent = 'Run Batch and Download ZIP';\r\n      prog.className = 'prog';\r\n      toast('Batch finished - ZIP downloaded.', 'ok');\r\n    })['catch'](function () {\r\n      btn.disabled = false; btn.textContent = 'Run Batch and Download ZIP';\r\n      prog.className = 'prog';\r\n      toast('Batch run failed.', 'err');\r\n    });\r\n  }\r\n\r\n  \/* ---------- Inspect ---------- *\/\r\n  function doInspect() {\r\n    var box = $('pth-inspect-out');\r\n    box.innerHTML = '<div class=\"empty\">Checking files...<\/div>';\r\n    var rows = [];\r\n\r\n    var step = function (i) {\r\n      if (i >= S.files.length) { return Promise.resolve(); }\r\n      var f = S.files[i];\r\n      var r = { name: f.name, pages: f.pages, size: fmtSize(f.size), locked: !!f.locked, form: false, text: false };\r\n      if (f.locked) { rows.push(r); return step(i + 1); }\r\n\r\n      return window.PDFLib.PDFDocument.load(f.bytes.slice(0)).then(function (doc) {\r\n        try { r.form = doc.getForm().getFields().length > 0; } catch (e) { r.form = false; }\r\n        return f.doc.getPage(1).then(function (pg) { return pg.getTextContent(); }).then(function (tc) {\r\n          r.text = tc.items.length > 3;\r\n          rows.push(r); return step(i + 1);\r\n        });\r\n      })['catch'](function () { rows.push(r); return step(i + 1); });\r\n    };\r\n\r\n    step(0).then(function () {\r\n      var html = '<table class=\"itbl\"><thead><tr><th>File<\/th><th>Pages<\/th><th>Size<\/th><th>Form<\/th><th>Text<\/th><\/tr><\/thead><tbody>';\r\n      var i, r;\r\n      for (i = 0; i < rows.length; i++) {\r\n        r = rows[i];\r\n        html += '<tr><td>' + esc(r.name) + (r.locked ? ' <b>(locked)<\/b>' : '') + '<\/td>'\r\n          + '<td>' + (r.locked ? '-' : r.pages) + '<\/td>'\r\n          + '<td>' + r.size + '<\/td>'\r\n          + '<td class=\"' + (r.form ? 'ok' : 'no') + '\">' + (r.form ? 'Yes' : 'No') + '<\/td>'\r\n          + '<td class=\"' + (r.text ? 'ok' : 'no') + '\">' + (r.text ? 'Selectable' : 'Scan') + '<\/td><\/tr>';\r\n      }\r\n      html += '<\/tbody><\/table>';\r\n      box.innerHTML = html;\r\n      toast('Inspection finished.', 'ok');\r\n    });\r\n  }\r\n\r\n  \/* ---------- Sample ---------- *\/\r\n  function loadSample() {\r\n    if (!window.PDFLib) { toast('Engine still loading, try again in a moment.', 'err'); return; }\r\n    var PDFLib = window.PDFLib;\r\n    var made = [];\r\n\r\n    function makeDoc(title, labels) {\r\n      return PDFLib.PDFDocument.create().then(function (doc) {\r\n        return doc.embedFont(PDFLib.StandardFonts.HelveticaBold).then(function (bold) {\r\n          return doc.embedFont(PDFLib.StandardFonts.Helvetica).then(function (reg) {\r\n            var i;\r\n            for (i = 0; i < labels.length; i++) {\r\n              var p = doc.addPage([595, 842]);\r\n              p.drawText(title, { x: 56, y: 770, size: 20, font: bold, color: PDFLib.rgb(0.15, 0.23, 0.42) });\r\n              p.drawText(labels[i], { x: 56, y: 700, size: 34, font: bold, color: PDFLib.rgb(0.2, 0.3, 0.5) });\r\n              p.drawText('Load several of these, then try merging, splitting', { x: 56, y: 650, size: 12, font: reg, color: PDFLib.rgb(0.2, 0.25, 0.33) });\r\n              p.drawText('or dragging pages between files in the grid.', { x: 56, y: 632, size: 12, font: reg, color: PDFLib.rgb(0.2, 0.25, 0.33) });\r\n            }\r\n            return doc.save();\r\n          });\r\n        });\r\n      });\r\n    }\r\n\r\n    makeDoc('Sample A - odd sides', ['Page 1', 'Page 3', 'Page 5']).then(function (a) {\r\n      made.push({ name: 'sample-A-odd.pdf', bytes: a });\r\n      return makeDoc('Sample B - even sides', ['Page 2', 'Page 4', 'Page 6']);\r\n    }).then(function (b) {\r\n      made.push({ name: 'sample-B-even.pdf', bytes: b });\r\n      var step = function (i) {\r\n        if (i >= made.length) { afterLoad(); return; }\r\n        var ab = new ArrayBuffer(made[i].bytes.length);\r\n        new Uint8Array(ab).set(made[i].bytes);\r\n        window.pdfjsLib.getDocument({ data: ab.slice(0) }).promise.then(function (doc) {\r\n          S.files.push({\r\n            id: 'f' + (S.seq++), name: made[i].name, bytes: ab, size: ab.byteLength,\r\n            pages: doc.numPages, doc: doc, range: '',\r\n            color: COLORS[S.files.length % COLORS.length]\r\n          });\r\n          step(i + 1);\r\n        })['catch'](function () { step(i + 1); });\r\n      };\r\n      step(0);\r\n    })['catch'](function () { toast('Could not build the sample files.', 'err'); });\r\n  }\r\n\r\n  \/* ---------- Wiring ---------- *\/\r\n  function bind() {\r\n    var i;\r\n\r\n    $('pth-org-drop').addEventListener('click', function () { $('pth-org-input').click(); });\r\n    $('pth-org-drop').addEventListener('keydown', function (e) {\r\n      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); $('pth-org-input').click(); }\r\n    });\r\n    $('pth-org-add').addEventListener('click', function () { $('pth-org-input').click(); });\r\n    $('pth-org-input').addEventListener('change', function () {\r\n      if (this.files && this.files.length) { addFiles(this.files); }\r\n      this.value = '';\r\n    });\r\n\r\n    var dz = $('pth-org-drop');\r\n    dz.addEventListener('dragover', function (e) { e.preventDefault(); dz.classList.add('over'); });\r\n    dz.addEventListener('dragleave', function () { dz.classList.remove('over'); });\r\n    dz.addEventListener('drop', function (e) {\r\n      e.preventDefault(); dz.classList.remove('over');\r\n      if (e.dataTransfer.files && e.dataTransfer.files.length) { addFiles(e.dataTransfer.files); }\r\n    });\r\n\r\n    $('pth-org-sample').addEventListener('click', loadSample);\r\n    $('pth-org-fw').addEventListener('click', function () { $('pth-org-layout').classList.toggle('fw'); });\r\n    $('pth-org-clear').addEventListener('click', function () {\r\n      S.files = []; S.grid = [];\r\n      $('pth-org-flist').innerHTML = '';\r\n      $('pth-org-pgrid').innerHTML = '<div class=\"empty\">Press Load Pages to build the grid.<\/div>';\r\n      $('pth-inspect-out').innerHTML = '<div class=\"empty\">Load files, then press Inspect Files.<\/div>';\r\n      $('pth-grid-save').disabled = true;\r\n      afterLoad();\r\n      toast('Workspace cleared.');\r\n    });\r\n\r\n    var tabs = qa('.tab');\r\n    for (i = 0; i < tabs.length; i++) {\r\n      tabs[i].addEventListener('click', function () {\r\n        var name = this.getAttribute('data-tab'), k;\r\n        var all = qa('.tab'), pans = qa('.panel');\r\n        for (k = 0; k < all.length; k++) { all[k].classList.remove('on'); }\r\n        for (k = 0; k < pans.length; k++) { pans[k].classList.remove('on'); }\r\n        this.classList.add('on');\r\n        var p = W.querySelector('.panel[data-panel=\"' + name + '\"]');\r\n        if (p) { p.classList.add('on'); }\r\n      });\r\n    }\r\n\r\n    $('pth-merge-mode').addEventListener('change', function () {\r\n      $('pth-merge-alt').style.display = this.value === 'alt' ? 'block' : 'none';\r\n      $('pth-merge-ins').style.display = this.value === 'ins' ? 'block' : 'none';\r\n    });\r\n    $('pth-do-merge').addEventListener('click', doMerge);\r\n\r\n    $('pth-split-mode').addEventListener('change', function () {\r\n      var v = this.value;\r\n      $('pth-split-nwrap').style.display = (v === 'every' || v === 'parts') ? 'block' : 'none';\r\n      $('pth-split-rwrap').style.display = v === 'range' ? 'block' : 'none';\r\n      $('pth-split-nlab').textContent = v === 'parts' ? 'Number of parts' : 'Pages per file';\r\n    });\r\n    $('pth-do-split').addEventListener('click', doSplit);\r\n\r\n    $('pth-grid-build').addEventListener('click', buildGrid);\r\n    $('pth-grid-save').addEventListener('click', buildFromGrid);\r\n    $('pth-grid-selall').addEventListener('click', function () {\r\n      var k;\r\n      for (k = 0; k < S.grid.length; k++) { S.grid[k].sel = true; }\r\n      renderGrid();\r\n    });\r\n    $('pth-grid-selnone').addEventListener('click', function () {\r\n      var k;\r\n      for (k = 0; k < S.grid.length; k++) { S.grid[k].sel = false; }\r\n      renderGrid();\r\n    });\r\n    $('pth-grid-delsel').addEventListener('click', function () {\r\n      var kept = [], k, n = 0;\r\n      for (k = 0; k < S.grid.length; k++) {\r\n        if (S.grid[k].sel) { n++; } else { kept.push(S.grid[k]); }\r\n      }\r\n      if (!n) { toast('No pages selected. Click pages to select them.', 'err'); return; }\r\n      if (!kept.length) { toast('That would delete every page.', 'err'); return; }\r\n      S.grid = kept;\r\n      renderGrid();\r\n      toast(n + ' page(s) removed from the grid.');\r\n    });\r\n\r\n    $('pth-b-q').addEventListener('input', function () { $('pth-b-qv').textContent = this.value; });\r\n    $('pth-do-batch').addEventListener('click', doBatch);\r\n    $('pth-do-inspect').addEventListener('click', doInspect);\r\n  }\r\n\r\n  function boot() {\r\n    if (booted) { return; }\r\n    W = document.getElementById('pth-pdf-org-wrapper');\r\n    if (!W) { return; }\r\n    if (!window.pdfjsLib || !window.PDFLib || !window.JSZip) { return; }\r\n    booted = true;\r\n    window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/2.16.105\/pdf.worker.min.js';\r\n    bind();\r\n  }\r\n\r\n  boot();\r\n  if (document.readyState === 'loading') {\r\n    document.addEventListener('DOMContentLoaded', boot);\r\n  }\r\n  window.addEventListener('load', function () { setTimeout(boot, 50); });\r\n  if (!booted && window.MutationObserver) {\r\n    var mo = new MutationObserver(function () { boot(); if (booted) { mo.disconnect(); } });\r\n    try { mo.observe(document.documentElement, { childList: true, subtree: true }); } catch (e) { }\r\n    setTimeout(function () { try { mo.disconnect(); } catch (e2) { } }, 10000);\r\n  }\r\n\r\n  return { boot: boot };\r\n})();\r\n<\/script>\n\n\n\n<style>\n.pth-c32 { max-width: 900px; margin: 38px auto 0; font-family: 'Inter', system-ui, sans-serif; color: #1e293b; line-height: 1.75; }\n.pth-c32 h2 { font-size: 25px; font-weight: 800; color: #0f172a; margin: 32px 0 12px; }\n.pth-c32 h3 { font-size: 18px; font-weight: 700; color: #1e293b; margin: 22px 0 8px; }\n.pth-c32 h4 { font-size: 15px; font-weight: 700; color: #334155; margin: 16px 0 6px; }\n.pth-c32 ul, .pth-c32 ol { margin: 0 0 15px; padding-left: 22px; }\n.pth-c32 li { margin-bottom: 7px; color: #1e293b; font-weight: 500; }\n.pth-c32 a { color: #1d4ed8; font-weight: 600; text-decoration: none; }\n.pth-c32 a:hover { text-decoration: underline; }\n.pth-c32 code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 14px; color: #be123c; }\n.pth-c32-feat { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; margin: 18px 0; }\n.pth-c32-card { border: 1px solid #e2e8f0; border-radius: 10px; padding: 15px 17px; background: #f8fafc; }\n.pth-c32-card b { display: block; color: #0f172a; font-size: 15px; margin-bottom: 4px; }\n.pth-c32-card span { font-size: 14px; color: #475569; font-weight: 500; }\n.pth-c32-steps { counter-reset: pthc; list-style: none; padding-left: 0; }\n.pth-c32-steps li { counter-increment: pthc; position: relative; padding: 5px 0 5px 42px; }\n.pth-c32-steps li::before { content: counter(pthc); position: absolute; left: 0; top: 5px; width: 27px; height: 27px; background: #2563eb; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 13px; }\n.pth-c32-tbl { width: 100%; border-collapse: collapse; margin: 0 0 18px; font-size: 14px; }\n.pth-c32-tbl th, .pth-c32-tbl td { border: 1px solid #e2e8f0; padding: 9px 12px; text-align: left; color: #1e293b; }\n.pth-c32-tbl th { background: #f8fafc; font-weight: 700; color: #0f172a; }\n.pth-c32-warn { border-left: 4px solid #b45309; background: #fffbeb; border-radius: 0 8px 8px 0; padding: 13px 17px; margin: 0 0 18px; color: #78350f; font-weight: 500; }\n.pth-c32-links { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 11px; margin: 16px 0; }\n.pth-c32-links a { display: block; border: 1px solid #e2e8f0; border-radius: 10px; padding: 13px 15px; background: #f8fafc; font-weight: 700; }\n.pth-c32-links a:hover { border-color: #2563eb; background: #eff6ff; text-decoration: none; }\n.pth-c32-links a em { display: block; font-style: normal; font-weight: 500; font-size: 13px; color: #475569; margin-top: 3px; }\n.pth-c32-more { max-height: 100%; overflow: visible; }\n@media (max-width: 640px) {\n  .pth-c32-more { max-height: 420px; overflow: hidden; position: relative; }\n  .pth-c32-more.pth-open { max-height: 100%; }\n  .pth-c32-more:not(.pth-open)::after { content: \"\"; position: absolute; left: 0; right: 0; bottom: 0; height: 70px; background: linear-gradient(rgba(255,255,255,0), #ffffff); }\n  .pth-c32-btn { display: block; width: 100%; margin: 10px 0 0; padding: 11px; border: 1px solid #cbd5e1; border-radius: 9px; background: #f1f5f9; color: #0f172a; font-weight: 700; font-size: 14px; cursor: pointer; font-family: inherit; }\n}\n@media (min-width: 641px) { .pth-c32-btn { display: none; } }\n<\/style>\n \n<div class=\"pth-c32\">\n \n<!-- HERO IMAGE PLACEHOLDER - paste the tag below once the image is uploaded to Media Library\n<img decoding=\"async\" src=\"MEDIA_URL_HERE\" alt=\"Several PDF files being combined and split into new documents inside a browser workspace\" width=\"1200\" height=\"630\" loading=\"eager\" fetchpriority=\"high\" data-no-lazy=\"1\">\n-->\n \n<div class=\"pth-c32-feat\">\n  <div class=\"pth-c32-card\"><b>Merge with page ranges<\/b><span>Take pages 1-3 from one file and page 7 from another instead of the whole document.<\/span><\/div>\n  <div class=\"pth-c32-card\"><b>Interleave two scans<\/b><span>Odd sides in one file, even sides in another, zipped back into the right order.<\/span><\/div>\n  <div class=\"pth-c32-card\"><b>Four ways to split<\/b><span>Every N pages, into equal parts, by custom ranges, or one file per page.<\/span><\/div>\n  <div class=\"pth-c32-card\"><b>Cross-file page grid<\/b><span>Every page of every loaded file in one grid &#8211; drag pages between documents freely.<\/span><\/div>\n  <div class=\"pth-c32-card\"><b>Batch the boring parts<\/b><span>Rotate, watermark, clear metadata and rename every loaded file in one pass.<\/span><\/div>\n  <div class=\"pth-c32-card\"><b>Results as a ZIP<\/b><span>Split and batch jobs arrive in one archive, so nothing is lost between downloads.<\/span><\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Last Updated: August 2026<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-a-workspace-for-many-files-at-once\" class=\"wp-block-heading\">\ud83d\udfe2 A Workspace For Many Files At Once<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most PDF sites handle one operation on one file. Drop in a document, get a document back, start again for the next one. That falls apart the moment you have eleven scanned invoices, or a report split across four exports, or a duplex scan sitting in two useless halves.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This page loads as many PDFs as you want and treats them as one workspace. Files stay in the list while you work, so you can merge some, split another, then run a batch change across all of them without reloading anything. Nothing is uploaded &#8211; your browser reads the files directly from your disk.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Tab<\/th><th>What it handles<\/th><\/tr><\/thead><tbody><tr><td>Merge<\/td><td>Sequential joining with per-file page ranges, interleaving two files, inserting one file inside another<\/td><\/tr><tr><td>Split<\/td><td>Every N pages, equal parts, custom ranges, or burst into single pages &#8211; delivered as a ZIP<\/td><\/tr><tr><td>Page Grid<\/td><td>Every page of every file in one grid, draggable across file boundaries<\/td><\/tr><tr><td>Batch<\/td><td>Rotate, watermark, clear metadata, shrink and rename across all loaded files<\/td><\/tr><tr><td>Inspect<\/td><td>Pages, size, locked status, whether a file has a form or is a scan with no selectable text<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 id=\"\ud83d\udfe1-the-duplex-scan-problem-solved-properly\" class=\"wp-block-heading\">\ud83d\udfe1 The Duplex Scan Problem, Solved Properly<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the job that sends people looking for a tool in the first place, so it is worth walking through.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You have a sheet-feed scanner with no duplex unit. You scan the stack face up and get pages 1, 3, 5, 7. You flip the stack and scan again, which gives you 8, 6, 4, 2 &#8211; the even sides in reverse, because flipping reverses the order.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Load both files, open the Merge tab, choose&nbsp;<strong>Interleave<\/strong>, pick the odd file first and the even file second, then tick&nbsp;<strong>Second file was scanned in reverse order<\/strong>. The result comes out 1, 2, 3, 4, 5, 6, 7, 8. Press Load Sample first if you want to try the whole sequence on generated files before touching a real scan.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-where-the-page-grid-earns-its-place\" class=\"wp-block-heading\">\ud83d\udfe2 Where The Page Grid Earns Its Place<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Merging joins whole files in order. Sometimes that is not the shape of the problem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Say you are assembling a submission: the cover letter is page 1 of one file, the signed form is pages 2-3 of a second, and two supporting photos sit on pages 5 and 9 of a third. Working that out with merge ranges is possible but fiddly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Press&nbsp;<strong>Load Pages<\/strong>&nbsp;instead. Every page of every loaded file appears as a thumbnail with a coloured tag showing which file it came from. Drag them into the order you want &#8211; across files, not just within one &#8211; delete the ones you do not need, rotate anything sideways, then press Build PDF From Grid. You see the finished document before you create it.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-three-real-jobs\" class=\"wp-block-heading\">\ud83d\udfe1 Three Real Jobs<\/h2>\n\n\n\n<h3 id=\"monthly-invoices-into-one-file\" class=\"wp-block-heading\">Monthly invoices into one file<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Load the month&#8217;s invoices, drag the file list into date order, and merge. If some files have a cover sheet you do not want, type&nbsp;<code>2-<\/code>&nbsp;style ranges in each file&#8217;s page range box to skip page 1.<\/p>\n\n\n\n<h3 id=\"breaking-a-long-report-into-chapters\" class=\"wp-block-heading\">Breaking a long report into chapters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Open the Split tab, choose&nbsp;<strong>Custom ranges<\/strong>, and type the chapter boundaries:&nbsp;<code>1-12, 13-28, 29-40<\/code>. You get three PDFs inside one ZIP, named after the original file.<\/p>\n\n\n\n<h3 id=\"preparing-a-set-of-documents-to-send-out\" class=\"wp-block-heading\">Preparing a set of documents to send out<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Load everything, open Batch, tick the metadata cleaner, add a&nbsp;<code>DRAFT<\/code>&nbsp;watermark, set the rename pattern to&nbsp;<code>tender-{n}<\/code>, and run it. Every file comes back stamped, stripped of author details, and numbered in one archive.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-a-sensible-working-order\" class=\"wp-block-heading\">\ud83d\udfe2 A Sensible Working Order<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Load every file you plan to work with, so you only choose them once.<\/li>\n\n\n\n<li>Open Inspect first on unfamiliar files &#8211; it tells you page counts, which are scans, and which are locked before you plan anything.<\/li>\n\n\n\n<li>Fix page order and rotation, either by dragging files in the list or by using the Page Grid for finer control.<\/li>\n\n\n\n<li>Do the structural work: merge or split.<\/li>\n\n\n\n<li>Run Batch last, since watermarks and metadata cleaning should apply to the finished documents rather than the parts.<\/li>\n<\/ol>\n\n\n\n<h2 id=\"\ud83d\udd34-common-mistakes-and-real-limits\" class=\"wp-block-heading\">\ud83d\udd34 Common Mistakes and Real Limits<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\ud83d\udd35&nbsp;<strong>Equal parts does not always give that many files.<\/strong>&nbsp;Splitting 12 pages into 5 parts puts 3 pages in each, which produces 4 files. The setting controls pages per file through rounding, so use custom ranges when the exact number of outputs matters.<\/li>\n\n\n\n<li>\ud83d\udfe0&nbsp;<strong>Forgetting the reverse tick on duplex scans.<\/strong>&nbsp;Without it the second half comes out backwards. If the result looks wrong, that tick box is almost always why.<\/li>\n\n\n\n<li>\ud83d\udfe3&nbsp;<strong>Expecting merging to shrink anything.<\/strong>&nbsp;Joining files adds their contents together. If the combined file needs to be smaller, that is the shrink option in Batch, and it works by flattening pages to images.<\/li>\n\n\n\n<li>\ud83d\udd35&nbsp;<strong>Shrinking text documents.<\/strong>&nbsp;Flattening genuinely helps image-heavy scans and hurts text documents, because the words stop being selectable. Leave it off for anything people need to search.<\/li>\n\n\n\n<li>\ud83d\udfe0&nbsp;<strong>Password-protected files.<\/strong>&nbsp;Locked PDFs load and appear in the list marked as locked, but merge, split and batch skip them. Remove the password first in the software that applied it.<\/li>\n\n\n\n<li>\ud83d\udfe3&nbsp;<strong>Very large page grids.<\/strong>&nbsp;Building thumbnails for several hundred pages takes time and memory. You will see a warning past three hundred pages &#8211; split the work into smaller sessions on a modest machine.<\/li>\n<\/ul>\n\n\n\n<h2 id=\"\ud83d\udfe1-what-this-page-does-not-do\" class=\"wp-block-heading\">\ud83d\udfe1 What This Page Does Not Do<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This tool works across files. Editing the inside of a single document is a different job and lives elsewhere: annotating, signing, filling forms, extracting text and permanently redacting private details are all handled by the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/free-offline-pdf-reader-and-editor\/\">offline PDF reader and editor<\/a>. If you are creating a document rather than reorganising existing ones, write it in&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/markdown-to-pdf-converter\/\">Markdown Studio Pro<\/a>&nbsp;and export a PDF from there. The theory behind lossless page assembly is covered in&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/how-pdf-merging-works\/\">how PDF merging works<\/a>.<\/p>\n\n\n\n<style>\n.pth-faq-wrap { max-width: 900px; margin: 30px auto 0; font-family: 'Inter', system-ui, sans-serif; }\n.pth-faq-title { font-size: 25px; font-weight: 800; color: #0f172a; margin: 0 0 16px; }\n.pth-faq-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 13px; }\n.pth-faq-card { border: 1px solid #e2e8f0; border-radius: 11px; padding: 16px 18px; background: #f8fafc; }\n.pth-faq-q { font-size: 15px; font-weight: 800; color: #0f172a; margin: 0 0 7px; line-height: 1.45; }\n.pth-faq-a { font-size: 14px; font-weight: 500; color: #334155; margin: 0; line-height: 1.65; }\n@media (max-width: 900px) { .pth-faq-grid { grid-template-columns: repeat(2, 1fr); } }\n@media (max-width: 600px) { .pth-faq-grid { grid-template-columns: 1fr; } }\n<\/style>\n \n<div class=\"pth-faq-wrap\">\n  <p class=\"pth-faq-title\">Frequently Asked Questions<\/p>\n  <div class=\"pth-faq-grid\">\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Are my files uploaded to a server?<\/p>\n      <p class=\"pth-faq-a\">No. Every file is read and rewritten by your own browser. You can disconnect from the internet once the page has loaded and keep working.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">How many PDFs can I merge at once?<\/p>\n      <p class=\"pth-faq-a\">There is no fixed limit. The practical ceiling is your device&#8217;s memory, so several hundred pages will feel slow on an older laptop.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Can I merge only some pages from a file?<\/p>\n      <p class=\"pth-faq-a\">Yes. Each file in the list has a page range box. Type something like 1-3, 7 to use those pages only, or leave it empty to take the whole file.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">How do I fix a two-sided scan that came out in two files?<\/p>\n      <p class=\"pth-faq-a\">Choose Interleave in the Merge tab, set the odd file first and the even file second, then tick the reverse order box because flipping the stack reverses the second pass.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Does merging reduce quality?<\/p>\n      <p class=\"pth-faq-a\">No. Pages are copied across intact rather than re-encoded, so text stays text and images keep their original data. The merged file is roughly the sum of its parts.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Why did splitting into 5 parts give me 4 files?<\/p>\n      <p class=\"pth-faq-a\">Pages per file are worked out by rounding up, so 12 pages across 5 parts becomes 3 pages each and fills only 4 files. Use custom ranges when you need an exact number of outputs.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Can I move pages between two different documents?<\/p>\n      <p class=\"pth-faq-a\">Yes, that is what the Page Grid is for. Press Load Pages, then drag any page into any position regardless of which file it came from, and build one new PDF from the result.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Why can I not open my password-protected file?<\/p>\n      <p class=\"pth-faq-a\">Encrypted PDFs appear in the list marked as locked and are skipped by every operation. Remove the password in the program that applied it, then load the unlocked copy.<\/p>\n    <\/div>\n \n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">How do I annotate or sign a document here?<\/p>\n      <p class=\"pth-faq-a\">You do not &#8211; this page handles operations across whole files. Annotation, signatures, form filling, text extraction and redaction live in the offline PDF reader and editor.<\/p>\n    <\/div>\n \n  <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Merge &amp; Split PDFs Instantly \u2014 100% Offline \u2014 Process sensitive files locally with total peace of mind Use our advanced PDF Merge and Split Studio to combine, organize, split, and edit PDF files directly in your browser. 100% offline and secure. \u00a0 Merge with page rangesTake pages 1-3 from one file and page 7 &#8230; <a title=\"Advanced PDF Merge and Split Studio (100% Offline Tool)\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/pdf-merge-and-split-studio\/\" aria-label=\"Read more about Advanced PDF Merge and Split Studio (100% Offline Tool)\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":1629,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-1607","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/1607","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/comments?post=1607"}],"version-history":[{"count":3,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/1607\/revisions"}],"predecessor-version":[{"id":6986,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/1607\/revisions\/6986"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media\/1629"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=1607"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}