To rearrange PDF pages for booklet printing or other physical output, you need more than swapping two slides in a presentation deck. While screen PDFs are arranged in sequential reader spreads (1, 2, 3, 4, 5, 6, 7, 8), duplex printers, saddle-stitch booklets, and single-pass document scanners operate on physical sheets where four logical pages share a single folded leaf or where odd and even sides are captured in separate passes.
How PDF Page Order and Page Labels Actually Work
In the PDF specification (ISO 32000-1, Section 7.7.3), pages are not ordered by their object ID numbers or byte positions in the file. Instead, the document catalog points to a /Pages tree whose /Kids array lists indirect object references (4 0 R, 9 0 R, etc.) in display sequence. When you rearrange pages in a PDF, a compliant tool only reorders that /Kids reference array or copies the /Page dictionaries into a new /Kids list—the underlying font subsets, vector paths, and compressed image streams are never re-encoded.
To verify how /Kids references and viewer page labels behave when pages move, we can create a 6-page US Letter (612 x 792 pt) manual (manual_6p.pdf) with Roman numeral front-matter labels (i, ii) on the first two pages and Arabic labels (1–4) on the remaining four pages. On both macOS (Homebrew Python) and Ubuntu/Debian Linux, PEP 668 blocks system-wide pip installs by default, so create a virtual environment first:
python3 -m venv .venv
.venv/bin/pip install pypdf pymupdf
Run inspect_page_tree.py to generate manual_6p.pdf, move Page 6 to the front in place using PyMuPDF (pymupdf), and compare the /Kids array and /PageLabels:
import pymupdf
from pypdf import PdfReader
doc = pymupdf.open()
titles = [
"Page 1: Front Cover",
"Page 2: Inside Front Cover & TOC",
"Page 3: Setup & Wiring Diagram",
"Page 4: Calibration Procedure",
"Page 5: Troubleshooting Table",
"Page 6: Back Cover & Support",
]
for i, title in enumerate(titles, start=1):
page = doc.new_page(width=612, height=792)
page.insert_text((72, 100), title, fontsize=22, fontname="helv")
page.insert_text((72, 140), f"Original sequential page {i} of 6 (612 x 792 pt US Letter)", fontsize=12, fontname="helv")
page.draw_rect(pymupdf.Rect(54, 54, 558, 738), color=(0.2, 0.2, 0.2), width=1.5)
doc.set_page_labels([
{"startpage": 0, "prefix": "", "style": "r", "firstpagenum": 1},
{"startpage": 2, "prefix": "", "style": "D", "firstpagenum": 1},
])
doc.save("manual_6p.pdf", garbage=4, deflate=True)
# Move index 5 (Page 6) to index 0 in-place
doc.move_page(5, 0)
doc.save("reordered_movepage.pdf", garbage=4, deflate=True)
doc.close()
for fname in ["manual_6p.pdf", "reordered_movepage.pdf"]:
r = PdfReader(fname)
kids = [f"{ref.idnum} 0 R" for ref in r.trailer["/Root"]["/Pages"]["/Kids"]]
print(f"{fname:24s} /Kids = [{', '.join(kids)}]")
r_move = PdfReader("reordered_movepage.pdf")
for i, p in enumerate(r_move.pages):
print(f" Slot {i + 1} (viewer label={r_move.page_labels[i]!r:<4}): {p.extract_text().splitlines()[0]}")
Output:
manual_6p.pdf /Kids = [4 0 R, 9 0 R, 12 0 R, 15 0 R, 18 0 R, 21 0 R]
reordered_movepage.pdf /Kids = [21 0 R, 4 0 R, 9 0 R, 12 0 R, 15 0 R, 18 0 R]
Slot 1 (viewer label='i' ): Page 6: Back Cover & Support
Slot 2 (viewer label='ii'): Page 1: Front Cover
Slot 3 (viewer label='1' ): Page 2: Inside Front Cover & TOC
Slot 4 (viewer label='2' ): Page 3: Setup & Wiring Diagram
Slot 5 (viewer label='3' ): Page 4: Calibration Procedure
Slot 6 (viewer label='4' ): Page 5: Troubleshooting Table
Look closely at the two details in the output above:
- Object IDs do not change: Page 6 is object
21 0 R. Moving it to the front simply shifts21 0 Rto index0of the/Kidsarray ([21 0 R, 4 0 R, 9 0 R, 12 0 R, 15 0 R, 18 0 R]). - The
/PageLabelstrap: Because the/PageLabelsnumber tree in the/Rootcatalog maps 0-based physical indices (0 -> 'i',1 -> 'ii',2 -> '1') rather than object references, moving21 0 Rto index0in place causes PDF viewers to labelPage 6: Back Cover & Supportas Roman numeral'i'andPage 2: Inside Front Cover & TOCas page'1'. Copying pages into a fresh PDF document (viapdf-liborpypdf’sPdfWriter) drops the stale/PageLabelstree so viewer page numbers match the new physical sequence (1–6).PyMuPDF’sdoc.select()reorders in place, but in PyMuPDF 1.28 it also removes/PageLabels, with the same result.
Reader Spreads vs. Printer Spreads: The Math of Booklet Imposition
When you fold a stack of duplex-printed sheets in half along the spine (saddle-stitch binding), each physical sheet of paper contains four booklet pages: two on the front side (recto) and two on the back side (verso).
Consequently, every saddle-stitch booklet obeys two structural rules:
- The total page count
Nmust be a multiple of 4: A 6-page PDF requires((6 + 3) // 4) * 4 = 8page slots across two physical sheets. - Outer and inner pages pair across the fold: On Sheet
k(fork = 0toN/4 - 1, 0-indexed), the 1-based page pairings are:- Front of sheet (Recto): Left half =
N - 2*k, Right half =1 + 2*k - Back of sheet (Verso): Left half =
2 + 2*k, Right half =N - 1 - 2*k
- Front of sheet (Recto): Left half =
For an 8-page booklet (N = 8), this produces the printer-spread sequence [8, 1, 2, 7, 6, 3, 4, 5]:
flowchart LR
subgraph Seq["Reader Order (1 to 8)"]
R["Pages 1, 2, 3, 4, 5, 6, 7, 8"]
end
subgraph S1["Physical Sheet 1 (Outer Leaf)"]
S1F["Front (Recto): Left = Page 8 | Right = Page 1"]
S1B["Back (Verso): Left = Page 2 | Right = Page 7"]
end
subgraph S2["Physical Sheet 2 (Inner Leaf, Nested & Folded)"]
S2F["Front (Recto): Left = Page 6 | Right = Page 3"]
S2B["Back (Verso): Left = Page 4 | Right = Page 5"]
end
R --> S1
R --> S2
Here is the exact printer-spread page sequence for standard 4-, 8-, 12-, and 16-page saddle-stitched booklets:
| Booklet Size | Duplex Sheets Required | Sheet 1 (Front / Back) | Sheet 2 (Front / Back) | Full Printer-Spread Sequence (1-Based) |
|---|---|---|---|---|
| 4 pages | 1 sheet | [4, 1] / [2, 3] | — | 4, 1, 2, 3 |
| 8 pages | 2 sheets | [8, 1] / [2, 7] | [6, 3] / [4, 5] | 8, 1, 2, 7, 6, 3, 4, 5 |
| 12 pages | 3 sheets | [12, 1] / [2, 11] | [10, 3] / [4, 9] (+ Sheet 3: [8, 5] / [6, 7]) | 12, 1, 2, 11, 10, 3, 4, 9, 8, 5, 6, 7 |
| 16 pages | 4 sheets | [16, 1] / [2, 15] | [14, 3] / [4, 13] (+ Sheets 3–4) | 16, 1, 2, 15, 14, 3, 4, 13, 12, 5, 6, 11, 10, 7, 8, 9 |
Method 1: Visual Page Reordering in the Browser
If you already have a 4-page or 8-page brochure, program, or packet where you need to move pages into printer-spread order—or if a cover page, appendix, or signature sheet is sitting in the wrong slot before duplex printing—you can reorder the pages visually in Organize PDF. The tool runs in your browser, so the PDF is not uploaded to a server.
Here is how the browser organizer works and what to expect:
- Thumbnail Grid with Source Page Badges: When you load a PDF,
pdf.jsrenders each page as a thumbnail. Each card shows its current position (#1,#2, etc.), and once a page has moved away from its original slot, an amberorig #Nbadge shows which page of the uploaded file it is. To arrange an 8-page booklet, reorder the cards until the badges read8, 1, 2, 7, 6, 3, 4, 5from left to right. - Drag-and-Drop and Arrow Controls: Drag a card to a new slot, or use its Move Left / Move Right buttons to shift it one position. Each card also has rotate and remove buttons, while the toolbar offers batch rotation, Reverse Order, Undo, and a Reset button that restores the initial page order and any removed pages.
- Blank Padding Pages: Add blank page appends an empty page, sized like the last page, to the end of the grid; drag it into position. For a 6-page document, add two blanks and drag them just before the back cover to get the
[1, 2, 3, 4, 5, Blank, Blank, 6]sequence explained in Method 2. - Page-Tree Rebuild via
pdf-lib: Clicking Download PDF creates a fresh PDF and copies the pages in their new order withcopyPages(), adding blank pages where you inserted them.
The browser tool outputs single pages in your chosen order rather than placing two pages onto one landscape sheet. For true 2-up imposition, use the PyMuPDF method below.
Method 2: Booklet Reordering and Back-Cover Padding in Python (pypdf)
If you plan to print using your printer driver’s “2 pages per sheet” setting, you only need a single-page PDF whose pages are reordered into the saddle-stitch sequence and padded to a multiple of 4.
There is a catch when padding a document like our 6-page manual_6p.pdf to 8 pages:
- End-padding (
[1, 2, 3, 4, 5, 6, Blank, Blank]): Appending two blank pages at the very end putsPage 8 (Blank)on the outside back cover (Sheet 1 Front, Left) and buries your realPage 6: Back Cover & Supportinside the booklet on Sheet 2 Front, Left (facingPage 3). - Back-cover-preserving padding (
[1, 2, 3, 4, 5, Blank, Blank, 6]): Inserting the two blank padding pages immediately before the final page keepsPage 6at logical slot 8 so it prints on the outside back cover oppositePage 1: Front Cover, while the two blank pages become the inside back cover (slot 7) and its facing page (slot 6).
Here is the pypdf script that pads a document while preserving its outside back cover and rearranges the pages into saddle-stitch printer spreads:
from pypdf import PdfReader, PdfWriter
reader = PdfReader("manual_6p.pdf")
n = len(reader.pages)
padded = ((n + 3) // 4) * 4
pad_count = padded - n
# Insert blank padding slots (None) right before the final back-cover page
logical_pages = list(reader.pages[:-1]) + [None] * pad_count + [reader.pages[-1]]
# Build 0-based saddle-stitch printer-spread order
order = []
for k in range(padded // 4):
order.extend([padded - 1 - 2 * k, 2 * k]) # Sheet k+1 Front (Recto)
order.extend([2 * k + 1, padded - 2 - 2 * k]) # Sheet k+1 Back (Verso)
width = float(reader.pages[0].mediabox.width)
height = float(reader.pages[0].mediabox.height)
writer = PdfWriter()
for idx in order:
page = logical_pages[idx]
if page is not None:
writer.add_page(page)
else:
writer.add_blank_page(width=width, height=height)
with open("manual_booklet_cover_preserved.pdf", "wb") as f:
writer.write(f)
check = PdfReader("manual_booklet_cover_preserved.pdf")
for s in range(0, len(check.pages), 2):
sheet_num = s // 4 + 1
side = "Front (Recto)" if (s // 2) % 2 == 0 else "Back (Verso)"
left_txt = (check.pages[s].extract_text().splitlines() or ["[Blank Inside Padding]"])[0]
right_txt = (check.pages[s + 1].extract_text().splitlines() or ["[Blank Inside Padding]"])[0]
print(f"Sheet {sheet_num} {side}: Left='{left_txt}' | Right='{right_txt}'")
Output:
Sheet 1 Front (Recto): Left='Page 6: Back Cover & Support' | Right='Page 1: Front Cover'
Sheet 1 Back (Verso): Left='Page 2: Inside Front Cover & TOC' | Right='[Blank Inside Padding]'
Sheet 2 Front (Recto): Left='[Blank Inside Padding]' | Right='Page 3: Setup & Wiring Diagram'
Sheet 2 Back (Verso): Left='Page 4: Calibration Procedure' | Right='Page 5: Troubleshooting Table'
Method 3: Building True 2-Up Imposed Landscape Sheets (PyMuPDF)
Relying on an operating system’s “2 pages per sheet” print dialog often introduces extra printer margins or flips the back side of each sheet along the wrong edge. To eliminate print-driver guesswork, you can use PyMuPDF’s Page.show_pdf_page() to place two portrait pages side by side onto true landscape sheets (792 x 612 pt for US Letter or 841.89 x 595.28 pt for A4) as vector Form XObjects. The script reuses Method 2’s back-cover-preserving padding, so the blank slots land inside the booklet:
import pymupdf
src = pymupdf.open("manual_6p.pdf")
n = len(src)
padded = ((n + 3) // 4) * 4
# Same back-cover-preserving padding as Method 2: blank slots go right before the last page
logical = list(range(n - 1)) + [None] * (padded - n) + [n - 1]
order = []
for k in range(padded // 4):
order.extend([padded - 1 - 2 * k, 2 * k])
order.extend([2 * k + 1, padded - 2 - 2 * k])
# Swap source width (612) and height (792) to create a 792x612 pt landscape sheet
sheet_w = src[0].rect.height
sheet_h = src[0].rect.width
half_w = sheet_w / 2.0
halves = [pymupdf.Rect(0, 0, half_w, sheet_h), pymupdf.Rect(half_w, 0, sheet_w, sheet_h)]
out = pymupdf.open()
for s_idx in range(0, len(order), 2):
sheet = out.new_page(width=sheet_w, height=sheet_h)
for rect, slot in zip(halves, order[s_idx:s_idx + 2]):
if logical[slot] is not None:
sheet.show_pdf_page(rect, src, logical[slot])
out.save("manual_booklet_2up.pdf", garbage=4, deflate=True)
out.close()
src.close()
check_2up = pymupdf.open("manual_booklet_2up.pdf")
print(f"2-up imposed PDF: {len(check_2up)} landscape sheets ({check_2up[0].rect.width:.0f}x{check_2up[0].rect.height:.0f} pt)")
for s_idx, sheet in enumerate(check_2up):
side = "Front (Recto)" if s_idx % 2 == 0 else "Back (Verso)"
left, right = ((sheet.get_text(clip=r).splitlines() or ["[Blank]"])[0] for r in halves)
print(f" Physical Sheet {s_idx // 2 + 1} {side}: Left='{left}' | Right='{right}'")
check_2up.close()
Output:
2-up imposed PDF: 4 landscape sheets (792x612 pt)
Physical Sheet 1 Front (Recto): Left='Page 6: Back Cover & Support' | Right='Page 1: Front Cover'
Physical Sheet 1 Back (Verso): Left='Page 2: Inside Front Cover & TOC' | Right='[Blank]'
Physical Sheet 2 Front (Recto): Left='[Blank]' | Right='Page 3: Setup & Wiring Diagram'
Physical Sheet 2 Back (Verso): Left='Page 4: Calibration Procedure' | Right='Page 5: Troubleshooting Table'
[!TIP] Short-Edge vs. Long-Edge Duplex Flip: Landscape 2-up sheets are meant to be printed with Two-Sided: Flip on Short Edge. If your office printer driver locks duplex printing to Flip on Long Edge, every verso sheet (
Physical Sheet 1 Back,Physical Sheet 2 Back, etc.) will print upside down. You can compensate directly in thePyMuPDFloop above by addingif (s_idx // 2) % 2 == 1: sheet.set_rotation(180)before saving, or by rotating every even sheet180°in Rotate PDF Pages.
Method 4: Re-Interleaving a Two-Pass Simplex ADF Scan
Another common reason to rearrange PDF pages before printing happens when you scan a double-sided packet on a single-sided (simplex) Automatic Document Feeder:
- Pass 1 scans the front (odd) sides in forward order:
1, 3, 5, 7. - You flip the stack over and feed it back through for Pass 2, which scans the back (even) sides in reverse order:
8, 6, 4, 2. - The scanner saves a single 8-page PDF ordered
[1, 3, 5, 7, 8, 6, 4, 2].
Instead of moving pages one by one, split the page array at the midpoint in pypdf, reverse the second half with [::-1], and interleave the pairs with zip():
import pymupdf
from pypdf import PdfReader, PdfWriter
# Simulate a two-pass simplex scan: fronts 1, 3, 5, 7, then backs 8, 6, 4, 2
scan = pymupdf.open()
for n in [1, 3, 5, 7, 8, 6, 4, 2]:
scan.new_page(width=612, height=792).insert_text((72, 100), f"Scanned Sheet Page {n}", fontsize=18)
scan.save("simplex_scan_8p.pdf")
reader = PdfReader("simplex_scan_8p.pdf")
half = len(reader.pages) // 2
odds = reader.pages[:half] # [Page 1, Page 3, Page 5, Page 7]
evens = reader.pages[half:][::-1] # [Page 8, Page 6, Page 4, Page 2] -> [2, 4, 6, 8]
writer = PdfWriter()
for odd_page, even_page in zip(odds, evens):
writer.add_page(odd_page)
writer.add_page(even_page)
with open("simplex_scan_reordered.pdf", "wb") as f:
writer.write(f)
fixed = PdfReader("simplex_scan_reordered.pdf")
print("Reordered simplex scan:", [p.extract_text().strip() for p in fixed.pages])
Output:
Reordered simplex scan: ['Scanned Sheet Page 1', 'Scanned Sheet Page 2', 'Scanned Sheet Page 3', 'Scanned Sheet Page 4', 'Scanned Sheet Page 5', 'Scanned Sheet Page 6', 'Scanned Sheet Page 7', 'Scanned Sheet Page 8']
You can compare the file sizes before and after rearranging and imposing using stat (stat -f "%N: %z bytes" on macOS BSD stat, or stat -c "%n: %s bytes" on Linux GNU coreutils):
# macOS (BSD stat)
stat -f "%N: %z bytes" manual_6p.pdf manual_booklet_cover_preserved.pdf manual_booklet_2up.pdf simplex_scan_8p.pdf simplex_scan_reordered.pdf
# Linux (GNU coreutils)
# stat -c "%n: %s bytes" manual_6p.pdf manual_booklet_cover_preserved.pdf manual_booklet_2up.pdf simplex_scan_8p.pdf simplex_scan_reordered.pdf
Output:
manual_6p.pdf: 4024 bytes
manual_booklet_cover_preserved.pdf: 5000 bytes
manual_booklet_2up.pdf: 5184 bytes
simplex_scan_8p.pdf: 3460 bytes
simplex_scan_reordered.pdf: 3581 bytes
Comparing Ways to Rearrange PDF Pages for Booklet Printing
| Method | Can Insert Blank Padding (N mod 4)? | Output Layout | Preserves /PageLabels & /Outlines? | Best Use Case |
|---|---|---|---|---|
Organize PDF (pdf-lib) | Yes (Add blank page, then drag into place) | 1-up single pages | No (rebuilds clean PDFDocument) | Visually reordering and padding short booklets or fixing misplaced cover/insert sheets |
Python pypdf (PdfWriter) | Yes (writer.add_blank_page()) | 1-up single pages in printer-spread order | No (resets to 1-based numbering) | Padding to a multiple of 4 ([8, 1, 2, 7, 6, 3, 4, 5]) or re-interleaving 2-pass simplex scans |
Python PyMuPDF (show_pdf_page()) | Yes (leaves empty half-sheets on padded slots) | True 2-up landscape sheets (792 x 612 pt) | No (new imposed document) | Full saddle-stitch imposition without relying on printer driver “2 pages per sheet” scaling |
Python PyMuPDF (doc.move_page() / select()) | No (new_page() required separately) | 1-up single pages | move_page() keeps raw /PageLabels indices; select() resets them | In-place page moves on large PDFs |
Before you impose or rearrange a document into printer spreads, make sure you stamp any visible footer numbers first while the PDF is still in sequential reader order (1, 2, 3...)—otherwise page numbers applied afterward will label your outside back cover as “Page 1.” You can add sequential footers with PDF Page Numbers (see our guide on PDF page number placement and cover pages). If you are going the opposite direction—converting a scanned two-up booklet back into individual sequential pages—see our guide on splitting two-page book scans into single PDF pages, or clean up stray sheets beforehand with Remove PDF Pages and our guide to rotating upside-down scanned PDF pages permanently.