[{"data":1,"prerenderedAt":43},["ShallowReactive",2],{"$fFH4lzAiqeCzkslD0BWEhIJBjI7l6uVDUP94V23d8_t4":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"tools":13,"draft":17,"html":18,"toc":19,"minutes":42},"reordering-rotating-deleting-pdf-pages","One Small Edit Model Behind Rotate, Delete and Rearrange PDF Pages","How ToolOnTap's PDF organizer stores each change as plain data, keeps 50 steps of undo, and saves rotation as a page setting instead of redrawing the page.","2026-09-27",[9,10,11,12],"pdf","state management","vue","typescript",[14,15,16],"rotate-pdf","delete-pdf-pages","rearrange-pdf-pages",false,"\u003Cp>A phone scan of a 30-page contract comes out with pages 7 and 19 upside down, a blank sheet after page 12, and the signature page at the end where the other side wants it first. Fixing that usually means three different tools, or one paid app. I wanted it to take one page and a minute.\u003C\u002Fp>\n\u003Cp>ToolOnTap has three pages for this: \u003Ca href=\"\u002Frotate-pdf\">Rotate PDF\u003C\u002Fa>, \u003Ca href=\"\u002Fdelete-pdf-pages\">Delete PDF pages\u003C\u002Fa> and \u003Ca href=\"\u002Frearrange-pdf-pages\">Rearrange PDF pages\u003C\u002Fa>. They’re one tool underneath. The page you land on only changes the hint text and the output name (\u003Ccode>contract-rotated.pdf\u003C\u002Fcode>, \u003Ccode>contract-edited.pdf\u003C\u002Fcode> or \u003Ccode>contract-reordered.pdf\u003C\u002Fcode>). Every action works on all three, because a real scan usually needs more than one kind of fix.\u003C\u002Fp>\n\u003Ch2 id=\"the-whole-document-is-a-short-list\">The whole document is a short list\u003C\u002Fh2>\n\u003Cp>The core idea is that the tool never edits the PDF while you work. It edits a list that describes the PDF you want:\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\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> PageEdit\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">source\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">rotation\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }  \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">\u002F\u002F rotation: extra degrees, 0\u002F90\u002F180\u002F270\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> initialPages\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">count\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\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:#6F42C1;--shiki-dark:#B392F0\"> PageEdit\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\"> Array.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">from\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ length: count }, (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">_\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">source\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\"> ({ source, rotation: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }))\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Each entry says “take page \u003Ccode>source\u003C\u002Fcode> of the original file and turn it \u003Ccode>rotation\u003C\u002Fcode> degrees more”. A 5-page file starts as five entries, \u003Ccode>{0, 0}\u003C\u002Fcode> to \u003Ccode>{4, 0}\u003C\u002Fcode>. Then each action is a small function on that list:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>Action\u003C\u002Fth>\n\u003Cth>What happens to the list\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Rotate\u003C\u002Ftd>\n\u003Ctd>the \u003Ccode>rotation\u003C\u002Fcode> of the chosen entries goes up or down by 90, wrapped to 0–270\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Delete\u003C\u002Ftd>\n\u003Ctd>the chosen entries are filtered out\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Drag a page\u003C\u002Ftd>\n\u003Ctd>one entry is moved to a new position\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Move earlier \u002F later\u003C\u002Ftd>\n\u003Ctd>every selected entry swaps one step with its neighbour\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>The position of an entry in the list is the page’s new position. Its \u003Ccode>source\u003C\u002Fcode> is its identity. Because pages are never duplicated in this tool, \u003Ccode>source\u003C\u002Fcode> stays unique even after any number of moves and deletions. That’s what lets the tool keep track of “page 12” while it travels to third place, and why each preview still shows its original number.\u003C\u002Fp>\n\u003Cp>Every function returns a new list and never changes the old one. Rotation, for example:\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:#57606a;--shiki-dark:#8b949e\">\u002F\u002F Rotate the given pages (or all of them when none are given) by ±90° steps\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> rotatePages\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">pages\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> PageEdit\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">[], \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">delta\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">sources\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Set\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">number\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:#6F42C1;--shiki-dark:#B392F0\"> PageEdit\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\"> pages.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">map\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">p\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =>\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\">sources?.size \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">||\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> sources.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">has\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(p.source) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?\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\">p, rotation: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">normalize\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(p.rotation \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> delta) } \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> p))\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>normalize\u003C\u002Fcode> turns any angle into 0, 90, 180 or 270, so rotating left from 0 gives 270, not -90. When no pages are selected, the toolbar rotates all of them. That’s the quickest fix for a whole scan that came out sideways.\u003C\u002Fp>\n\u003Ch2 id=\"undo-comes-almost-for-free\">Undo comes almost for free\u003C\u002Fh2>\n\u003Cp>Since no edit changes the old list, undo is just keeping the old lists. Every action goes through one function that pushes the current list onto a history before replacing it:\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\">function\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> edit\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">next\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> PageEdit\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">[], \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">message\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string\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\"> (next \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">===\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> pages.value) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  history.value \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\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\">history.value.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">slice\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:#005CC5;--shiki-dark:#79B8FF\">49\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">), pages.value]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  pages.value \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> next\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  announcement.value \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> message\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>Three small details are in there:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>slice(-49)\u003C\u002Fcode> caps the history at 50 steps. Each step is a list of small objects, so even a long document’s history takes little memory, but a cap keeps it bounded.\u003C\u002Fli>\n\u003Cli>\u003Ccode>if (next === pages.value) return\u003C\u002Fcode> skips actions that changed nothing. \u003Ccode>movePage\u003C\u002Fcode> returns the same list for an invalid move, so a drop onto the page’s own position doesn’t add an empty undo step.\u003C\u002Fli>\n\u003Cli>\u003Ccode>announcement\u003C\u002Fcode> is read out by screen readers through a hidden live region: “2 pages rotated right.”, “Page moved to position 3.”, “Last change undone.”\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>“Undo all” is the same \u003Ccode>edit\u003C\u002Fcode> with a fresh \u003Ccode>initialPages\u003C\u002Fcode> list, so even “Undo all” can be undone. The “Save PDF” button stays disabled until the list differs from the original, which \u003Ccode>isChanged\u003C\u002Fcode> checks by looking for any entry that moved, disappeared or has a rotation.\u003C\u002Fp>\n\u003Ch2 id=\"tap-to-select-on-phones-and-with-a-keyboard\">Tap to select, on phones and with a keyboard\u003C\u002Fh2>\n\u003Cp>Dragging works well with a mouse or a touchpad, but not with a finger on a long grid, or with a keyboard. So every page is also a toggle button: tap it to select it, tap again to deselect it. The toolbar then rotates, moves or deletes everything selected.\u003C\u002Fp>\n\u003Cp>The selection is stored by \u003Ccode>source\u003C\u002Fcode>, not by position. If you select pages 3 and 8 and move them earlier, they’re still selected afterwards, so you can press the arrow again. After an undo, the selection drops any page that no longer exists in the restored list.\u003C\u002Fp>\n\u003Cp>Moving several selected pages one step needed some care. Take pages 1 and 3 selected, and “Move earlier”. Page 1 is already first and can’t move. Page 3 should move to second place, but it must not jump over page 1. The rule in \u003Ccode>shiftPages\u003C\u002Fcode> is that a selected page only swaps with an unselected neighbour. The tests pin down both cases: moving pages 3 and 4 of 5 earlier gives the order 1, 3, 4, 2, 5, and moving pages 1 and 3 of 4 earlier gives 1, 3, 2, 4.\u003C\u002Fp>\n\u003Cp>One limit is checked everywhere: a PDF needs at least one page, so the delete buttons are disabled when only one page is left, and the toolbar won’t delete a selection that covers every page.\u003C\u002Fp>\n\u003Ch2 id=\"saving-copy-the-pages-set-the-angle\">Saving: copy the pages, set the angle\u003C\u002Fh2>\n\u003Cp>Nothing touches the PDF until “Save PDF”. Then pdf-lib builds a new document from the list in one pass:\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:#005CC5;--shiki-dark:#79B8FF\"> copied\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, pages.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">map\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">p\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =>\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> p.source))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">copied.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">forEach\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">((\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">page\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">i\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\"> {\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\"> extra\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> pages[i]\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.rotation\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\"> (extra) page.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">setRotation\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">degrees\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">((page.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">getRotation\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">().angle \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> extra) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">%\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 360\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\">addPage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(page)\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>Deleting is simply leaving a page out of \u003Ccode>copyPages\u003C\u002Fcode>. Reordering is the order of the indexes. Rotation is one line, and it’s the decision I care most about in this tool.\u003C\u002Fp>\n\u003Ch2 id=\"why-rotation-is-a-page-setting\">Why rotation is a page setting\u003C\u002Fh2>\n\u003Cp>Every PDF page has a \u003Ccode>\u002FRotate\u003C\u002Fcode> entry that tells a viewer to show the page turned by a multiple of 90 degrees. The page content itself, the text and drawing instructions, stays exactly as it was. There were two ways to rotate a page:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Render the page to an image, rotate the image, and put it back. Text would become pixels: not selectable, not searchable, blurry when you zoom in, and usually a bigger file.\u003C\u002Fli>\n\u003Cli>Change the \u003Ccode>\u002FRotate\u003C\u002Fcode> number. Nothing is redrawn and nothing is lost.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The tool does the second. It adds your turn to the page’s existing rotation, because a page can already carry a \u003Ccode>\u002FRotate\u003C\u002Fcode> of its own. A page stored with 90 that you turn another 90 right is saved with 180.\u003C\u002Fp>\n\u003Cp>That’s also why the number under each preview is the turn you’ve added, not the page’s total rotation. “180°” means “upside down compared to how it opens now”, which is what you see on screen. The previews do the same sum: pdf.js draws each thumbnail at the page’s own rotation plus the pending one, so what you see is what will be saved.\u003C\u002Fp>\n\u003Cp>It also explains a common question. Most PDF readers only rotate the view on your screen and don’t change the file, so the next person still gets it sideways. A saved \u003Ccode>\u002FRotate\u003C\u002Fcode> is part of the file, so it travels with it to whoever opens it next.\u003C\u002Fp>\n\u003Cp>What it can’t do is straighten a page scanned slightly crooked. \u003Ccode>\u002FRotate\u003C\u002Fcode> only allows quarter turns, so the tool only offers quarter turns.\u003C\u002Fp>\n\u003Ch2 id=\"what-changes-in-the-new-file\">What changes in the new file\u003C\u002Fh2>\n\u003Cp>Pages are copied as they are, with their fonts, images and size, so text stays selectable and links to websites keep working. The original file isn’t changed. Two things don’t carry over well, and the pages say so: links inside the document, like a table of contents, may point to the wrong page after a reorder, and a deleted page’s images can, in a few PDFs, still be in a shared resource that another page uses.\u003C\u002Fp>\n\u003Ch2 id=\"try-it\">Try it\u003C\u002Fh2>\n\u003Cp>Open a PDF in \u003Ca href=\"\u002Frotate-pdf\">Rotate PDF\u003C\u002Fa>, \u003Ca href=\"\u002Fdelete-pdf-pages\">Delete PDF pages\u003C\u002Fa> or \u003Ca href=\"\u002Frearrange-pdf-pages\">Rearrange PDF pages\u003C\u002Fa>; all three let you do everything above before you save. The rest of the PDF tools are on the \u003Ca href=\"\u002Fpdf-tools\">PDF tools\u003C\u002Fa> page.\u003C\u002Fp>\n",[20,24,27,30,33,36,39],{"id":21,"text":22,"level":23},"the-whole-document-is-a-short-list","The whole document is a short list",2,{"id":25,"text":26,"level":23},"undo-comes-almost-for-free","Undo comes almost for free",{"id":28,"text":29,"level":23},"tap-to-select-on-phones-and-with-a-keyboard","Tap to select, on phones and with a keyboard",{"id":31,"text":32,"level":23},"saving-copy-the-pages-set-the-angle","Saving: copy the pages, set the angle",{"id":34,"text":35,"level":23},"why-rotation-is-a-page-setting","Why rotation is a page setting",{"id":37,"text":38,"level":23},"what-changes-in-the-new-file","What changes in the new file",{"id":40,"text":41,"level":23},"try-it","Try it",6,1792325805318]